diff --git a/css/main.css b/css/main.css index c93c1aa..a024266 100644 --- a/css/main.css +++ b/css/main.css @@ -10,6 +10,37 @@ body { font-size: 16px; } +button { + padding: 8px 16px; + font-size: 14px; + border: none; + border-radius: 4px; + cursor: pointer; + color: #fff; + background-color: #555; + transition: background-color 0.2s; +} + +button:hover { + background-color: #333; +} + +button#download { + background-color: #2b7a2b; +} + +button#download:hover { + background-color: #216121; +} + +button#reset { + background-color: #b33a3a; +} + +button#reset:hover { + background-color: #902d2d; +} + p { text-align: center; } diff --git a/index.html b/index.html index 53680e6..b423994 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,7 @@

Thumbnail Generator

Generating YouTube video thumbnails for GitHub READMEs.

- +
@@ -44,6 +44,10 @@

Thumbnail Generator

+
+ + +
diff --git a/js/index.js b/js/index.js index ead6ed2..4c1d784 100644 --- a/js/index.js +++ b/js/index.js @@ -2,7 +2,7 @@ function getElementByIdOrDie(elementId) { var element = document.getElementById(elementId); if (element === null) { - throw new Error("Could not find element with id '" + elementId + "'"); + throw new Error("Could not find element with id '".concat(elementId, "'")); } return element; } @@ -17,7 +17,7 @@ function renderThumbnail(ctx, ytThumb, state) { gradient.addColorStop(1.0, '#00000000'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, state.width, height); - ctx.font = state.fontSize + "px LibreBaskerville"; + ctx.font = "".concat(state.fontSize, "px LibreBaskerville"); ctx.fillStyle = 'white'; ctx.fillText(state.title, state.pad, height - state.pad); } @@ -31,6 +31,26 @@ function updateUrl(state, defaultState) { } window.history.replaceState(null, "", "?" + new URLSearchParams(diff).toString()); } +function slugify(str) { + return str + .toLowerCase() + .trim() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, ""); +} +function downloadThumbnail(title, canvas) { + canvas.toBlob(function (blob) { + if (!blob) + throw new Error("Could not convert the canvas to Blob"); + var link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = slugify(title); + link.click(); + URL.revokeObjectURL(link.href); + }); +} window.onload = function () { var ytLink = getElementByIdOrDie("yt-link"); var ytError = getElementByIdOrDie("yt-error"); @@ -43,6 +63,8 @@ window.onload = function () { var ytFontDisplay = getElementByIdOrDie("yt-font-display"); var ytPad = getElementByIdOrDie("yt-pad"); var ytPadDisplay = getElementByIdOrDie("yt-pad-display"); + var downloadBtn = getElementByIdOrDie("download"); + var resetBtn = getElementByIdOrDie("reset"); var ctx = ytCanvas.getContext('2d'); if (ctx === null) throw new Error("Could not initialize 2d context"); @@ -70,18 +92,26 @@ window.onload = function () { if (link) ytLink.value = link; ytWidthDisplay.value = ytWidth.value; - ytFontDisplay.value = ytFont.value + "px"; + ytFontDisplay.value = "".concat(ytFont.value, "px"); ytPadDisplay.value = ytPad.value; - var state = { - title: ytTitle.value, - width: Number(ytWidth.value), - fontSize: Number(ytFont.value), - pad: Number(ytPad.value), - link: ytLink.value, - }; + var state = Object.assign({}, defaultState); var json = JSON.stringify(state); console.log(json); console.log(btoa(json)); + downloadBtn.addEventListener("click", function () { return downloadThumbnail(ytTitle.value, ytCanvas); }); + resetBtn.addEventListener("click", function () { + ytTitle.value = defaultState.title; + ytWidth.value = defaultState.width.toString(); + ytFont.value = defaultState.fontSize.toString(); + ytPad.value = defaultState.pad.toString(); + ytLink.value = defaultState.link; + ytWidthDisplay.value = defaultState.width.toString(); + ytFontDisplay.value = "".concat(defaultState.fontSize.toString(), "px"); + ytPadDisplay.value = ytPad.value; + state = Object.assign({}, defaultState); + updateUrl(state, defaultState); + renderThumbnail(ctx, ytThumb, state); + }); ytWidth.onchange = function () { return updateUrl(state, defaultState); }; ytWidth.oninput = function () { ytWidthDisplay.value = ytWidth.value; @@ -90,7 +120,7 @@ window.onload = function () { }; ytFont.onchange = function () { return updateUrl(state, defaultState); }; ytFont.oninput = function () { - ytFontDisplay.value = ytFont.value + "px"; + ytFontDisplay.value = "".concat(ytFont.value, "px"); state.fontSize = Number(ytFont.value); renderThumbnail(ctx, ytThumb, state); }; @@ -116,7 +146,7 @@ window.onload = function () { var ytHostRegexp = new RegExp('^(.+\.)?youtube\.com$'); if (ytHostRegexp.test(url.hostname)) { var ytVideoId = url.searchParams.getAll('v').join(''); - ytThumb.src = "http://i3.ytimg.com/vi/" + ytVideoId + "/maxresdefault.jpg"; + ytThumb.src = "http://i3.ytimg.com/vi/".concat(ytVideoId, "/maxresdefault.jpg"); } else { throw new Error('Only YouTube Links are supported'); diff --git a/ts/index.ts b/ts/index.ts index 5147072..1378f0a 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -19,7 +19,7 @@ function renderThumbnail(ctx: CanvasRenderingContext2D, ytThumb: HTMLImageElemen const aspect = ytThumb.height / ytThumb.width; const height = aspect * state.width; - ctx.canvas.width = state.width; + ctx.canvas.width = state.width; ctx.canvas.height = height; ctx.drawImage(ytThumb, 0, 0, state.width, height); @@ -46,18 +46,42 @@ function updateUrl(state: State, defaultState: State) { window.history.replaceState(null, "", "?" + new URLSearchParams(diff).toString()); } +function slugify(str: string) { + return str + .toLowerCase() + .trim() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, ""); +} + +function downloadThumbnail(title: string, canvas: HTMLCanvasElement): void { + canvas.toBlob(blob => { + if (!blob) throw new Error("Could not convert the canvas to Blob"); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = slugify(title); + link.click(); + URL.revokeObjectURL(link.href); + }); +} + + window.onload = () => { - const ytLink = getElementByIdOrDie("yt-link") as HTMLInputElement; - const ytError = getElementByIdOrDie("yt-error") as HTMLElement; - const ytThumb = getElementByIdOrDie("yt-thumb") as HTMLImageElement; - const ytCanvas = getElementByIdOrDie("yt-canvas") as HTMLCanvasElement; - const ytTitle = getElementByIdOrDie("yt-title") as HTMLInputElement; - const ytWidth = getElementByIdOrDie("yt-width") as HTMLInputElement; + const ytLink = getElementByIdOrDie("yt-link") as HTMLInputElement; + const ytError = getElementByIdOrDie("yt-error") as HTMLElement; + const ytThumb = getElementByIdOrDie("yt-thumb") as HTMLImageElement; + const ytCanvas = getElementByIdOrDie("yt-canvas") as HTMLCanvasElement; + const ytTitle = getElementByIdOrDie("yt-title") as HTMLInputElement; + const ytWidth = getElementByIdOrDie("yt-width") as HTMLInputElement; const ytWidthDisplay = getElementByIdOrDie("yt-width-display") as HTMLOutputElement; - const ytFont = getElementByIdOrDie("yt-font") as HTMLInputElement; - const ytFontDisplay = getElementByIdOrDie("yt-font-display") as HTMLOutputElement; - const ytPad = getElementByIdOrDie("yt-pad") as HTMLInputElement; - const ytPadDisplay = getElementByIdOrDie("yt-pad-display") as HTMLOutputElement; + const ytFont = getElementByIdOrDie("yt-font") as HTMLInputElement; + const ytFontDisplay = getElementByIdOrDie("yt-font-display") as HTMLOutputElement; + const ytPad = getElementByIdOrDie("yt-pad") as HTMLInputElement; + const ytPadDisplay = getElementByIdOrDie("yt-pad-display") as HTMLOutputElement; + const downloadBtn = getElementByIdOrDie("download") as HTMLButtonElement; + const resetBtn = getElementByIdOrDie("reset") as HTMLButtonElement; const ctx = ytCanvas.getContext('2d'); if (ctx === null) throw new Error(`Could not initialize 2d context`); @@ -71,28 +95,40 @@ window.onload = () => { }; const params = new URLSearchParams(window.location.search); - const title = params.get("title"); if (title) ytTitle.value = title; - const width = params.get("width"); if (width) ytWidth.value = width; - const fontSize = params.get("fontSize"); if (fontSize) ytFont.value = fontSize; - const pad = params.get("pad"); if (pad) ytPad.value = pad; - const link = params.get("link"); if (link) ytLink.value = link; + const title = params.get("title"); if (title) ytTitle.value = title; + const width = params.get("width"); if (width) ytWidth.value = width; + const fontSize = params.get("fontSize"); if (fontSize) ytFont.value = fontSize; + const pad = params.get("pad"); if (pad) ytPad.value = pad; + const link = params.get("link"); if (link) ytLink.value = link; ytWidthDisplay.value = ytWidth.value; - ytFontDisplay.value = `${ytFont.value}px`; - ytPadDisplay.value = ytPad.value; + ytFontDisplay.value = `${ytFont.value}px`; + ytPadDisplay.value = ytPad.value; - const state = { - title: ytTitle.value, - width: Number(ytWidth.value), - fontSize: Number(ytFont.value), - pad: Number(ytPad.value), - link: ytLink.value, - }; + let state = Object.assign({}, defaultState); const json = JSON.stringify(state); console.log(json) console.log(btoa(json)); + downloadBtn.addEventListener("click", () => downloadThumbnail(ytTitle.value, ytCanvas)); + + resetBtn.addEventListener("click", () => { + ytTitle.value = defaultState.title; + ytWidth.value = defaultState.width.toString(); + ytFont.value = defaultState.fontSize.toString(); + ytPad.value = defaultState.pad.toString(); + ytLink.value = defaultState.link; + + ytWidthDisplay.value = defaultState.width.toString(); + ytFontDisplay.value = `${defaultState.fontSize.toString()}px`; + ytPadDisplay.value = ytPad.value; + + state = Object.assign({}, defaultState) + updateUrl(state, defaultState); + renderThumbnail(ctx, ytThumb, state); + }); + ytWidth.onchange = () => updateUrl(state, defaultState); ytWidth.oninput = () => { ytWidthDisplay.value = ytWidth.value; @@ -131,7 +167,7 @@ window.onload = () => { } else { throw new Error('Only YouTube Links are supported'); } - } catch(e) { + } catch (e) { ytError.innerText = (e as Error).message; } };