diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea2a866 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +._* +Desktop.ini +Thumbs.db + +/node_modules +/dist \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a7f7173 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Brandon Bennett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd43c1c --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Art-Gen + +An app to generate thumbnails for YouTube Art Tracks! \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..f082c2e --- /dev/null +++ b/index.html @@ -0,0 +1,27 @@ + + + + + +Art Gen + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..784cc30 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "art-gen", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "art-gen", + "devDependencies": { + "@types/jsmediatags": "^3.9.3" + } + }, + "node_modules/@types/jsmediatags": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@types/jsmediatags/-/jsmediatags-3.9.3.tgz", + "integrity": "sha512-oNEPG+SI5E/VWK0x9JTWwkU+LmmOgW4tisCE4IrxiyNfzIRyg9kspNjaoqknpN9HUIexDvbD2/wbViw6TGIFgw==", + "dev": true + } + }, + "dependencies": { + "@types/jsmediatags": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@types/jsmediatags/-/jsmediatags-3.9.3.tgz", + "integrity": "sha512-oNEPG+SI5E/VWK0x9JTWwkU+LmmOgW4tisCE4IrxiyNfzIRyg9kspNjaoqknpN9HUIexDvbD2/wbViw6TGIFgw==", + "dev": true + } + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..9d0e6ba --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "name": "art-gen", + "devDependencies": { + "@types/jsmediatags": "^3.9.3" + } +} \ No newline at end of file diff --git a/src/app.mjs b/src/app.mjs new file mode 100644 index 0000000..cddac58 --- /dev/null +++ b/src/app.mjs @@ -0,0 +1,52 @@ +import { readTags, fromPicture } from "./jsmediatags.mjs"; + +const demo = await fetch("../test/26.m4a") +.then(response => response.blob()); + +const thumbnail = await generateThumbnail(demo); + +/** + * Generates a video thumbnail for a given song file. + * + * @param { Blob } song +*/ +export async function generateThumbnail(song){ + const canvas = document.querySelector("canvas"); + if (canvas === null){ + throw new ReferenceError("Cannot find canvas"); + } + + const ctx = canvas.getContext("2d"); + if (ctx === null){ + throw new SyntaxError("Cannot create canvas context"); + } + + const tags = await readTags(song); + console.log(tags); + + if (typeof tags.picture === "undefined"){ + throw new TypeError("Cannot load artwork from song"); + } + + const image = await fromPicture(tags.picture); + + const { naturalHeight } = image; + const { width, height } = canvas; + + ctx.translate(0,height / 2); + ctx.translate(0,-naturalHeight / 2); + + ctx.filter = "blur(30px)"; + + for (let i = 0; i < 16; i++){ + ctx.drawImage(image,0,0,width,naturalHeight); + } + + ctx.resetTransform(); + ctx.filter = "none"; + + ctx.fillStyle = "rgb(0 0 0 / 0.7)"; + ctx.fillRect(0,0,width,height); + + ctx.drawImage(image,135,135,810,810); +} \ No newline at end of file diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000..4ec4c0e --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,7 @@ +declare global { + interface Window { + jsmediatags: typeof import("jsmediatags"); + } +} + +export {}; \ No newline at end of file diff --git a/src/image.mjs b/src/image.mjs new file mode 100644 index 0000000..0307048 --- /dev/null +++ b/src/image.mjs @@ -0,0 +1,14 @@ +/** + * Loads an image from a specified URL. + * + * @param { string } src + * @param { HTMLImageElement } image +*/ +export async function loadImage(src,image = new Image()){ + await new Promise((resolve,reject) => { + image.addEventListener("load",resolve,{ once: true }); + image.addEventListener("error",reject,{ once: true }); + image.src = src; + }); + return image; +} \ No newline at end of file diff --git a/src/jsmediatags.mjs b/src/jsmediatags.mjs new file mode 100644 index 0000000..57e92f7 --- /dev/null +++ b/src/jsmediatags.mjs @@ -0,0 +1,39 @@ +import { loadImage } from "./image.mjs"; +const { jsmediatags } = window; + +/** + * @typedef { import("jsmediatags/types").Tags } Tags + * @typedef { import("jsmediatags/types").PictureType } PictureType +*/ + +/** + * Reads the media tags for a given song file. + * + * @param { string | Blob } data Accepts either a URL string or a Blob object. + * @returns { Promise } Resolves to a Tags object. +*/ +export async function readTags(data){ + return new Promise((resolve,reject) => { + jsmediatags.read(data,{ + onSuccess: ({ tags }) => resolve(tags), + onError: reject + }); + }); +} + +/** + * Reads a Picture tag, and returns an equivalent Image object for that data. + * + * @param { PictureType } picture +*/ +export async function fromPicture(picture){ + try { + const { format: type = "", data = [] } = picture; + const media = new Blob([new Uint8Array(data)],{ type }); + const source = window.URL.createObjectURL(media); + + return loadImage(source); + } catch { + throw new TypeError("Could not construct an image from the picture data"); + } +} \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..375344a --- /dev/null +++ b/styles.css @@ -0,0 +1,22 @@ +*, *::before, *::after { + box-sizing: border-box; +} + +html { + width: 100%; + height: 100%; +} + +body { + margin: 0; + width: 100%; + height: 100%; + font-family: system-ui, sans-serif; +} + +canvas { + width: 100%; + height: 100%; + display: block; + object-fit: contain; +} \ No newline at end of file diff --git a/test/26.m4a b/test/26.m4a new file mode 100644 index 0000000..e3ac6f7 Binary files /dev/null and b/test/26.m4a differ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8dcb5ae --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "rootDir": "./src", + "outDir": "./dist", + "module": "ESNext", + "moduleResolution": "Node", + "target": "ESNext", + "noEmit": true, + "strict": true + } +} \ No newline at end of file