From a3c3436bbecb7192155bc7c5a92217c51f45da00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lio=20A=2E=20Heckert?= Date: Mon, 11 Oct 2021 16:31:15 -0300 Subject: [PATCH] Implements animated favicon gif builder closes #31 --- public/index.html | 1 - src/App.js | 25 ++++++++ src/favicon.js | 146 ++++++++++++++++++++++++++++++++++++++++++++++ src/index.scss | 9 ++- 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 src/favicon.js diff --git a/public/index.html b/public/index.html index 017dda4..aab56ba 100755 --- a/public/index.html +++ b/public/index.html @@ -38,7 +38,6 @@ margin: 0; padding: 0; min-height: 100vh; - } header { diff --git a/src/App.js b/src/App.js index f4da435..491ffea 100755 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,8 @@ import React, { useEffect, useState, useRef, Fragment } from 'react' +import favicon from './favicon.js' import config from './whirl.config.json' const whirls = config.whirls.filter(w => w.active) + /** * Create rendering groups for the dropdown */ @@ -50,10 +52,22 @@ const App = () => { whirls[Math.floor(Math.random() * whirls.length)] ) const select = useRef(null) + const faviconDebug = useRef(null) const selectRandomWhirl = () => setSelected(getSelection(selected)) const selectWhirl = () => setSelected(whirls.filter(w => w.name === select.current.value)[0]) + const selectLoadingFavicon = async ev => { + faviconDebug.current.title = ev.target.value + if (ev.target.value) { + const url = await favicon.loading(ev.target.value) + faviconDebug.current.style.backgroundImage = `url(${url})` + } else { + favicon.stop() + faviconDebug.current.style.backgroundImage = 'none' + } + } + // When the selection changes, update the selected whirl useEffect(() => { setLoading(true) @@ -108,6 +122,17 @@ const App = () => { +
+ +
+
{ + const { primary, secondary, scale } = { ...iconDefaults, ...opts } + ctx.fillStyle = opts.transparent + ctx.lineWidth = 2 * scale + let mid = 8 * scale + let turn = Math.PI * 2 + ctx.fillRect(0, 0, 16 * scale, 16 * scale) + + for (let i = 0; i < turn; i += turn / 7) { + if (floor((i * 7) / turn) == floor(t * 7)) { + ctx.fillStyle = primary + } else { + ctx.fillStyle = secondary + } + ctx.beginPath() + ctx.ellipse( + mid + sin(i) * 6 * scale, + mid + cos(i) * 6 * scale, + 2 * scale, + 2 * scale, + 0, + 0, + turn + ) + ctx.fill() + } + }, +} + +const faviconSelector = 'link[rel*=shortcut][rel*="icon"], link[rel*="icon"]' + +function getFavicon() { + return document.querySelector(faviconSelector).href +} + +function setFavicon(val) { + document.querySelector(faviconSelector).setAttribute('href', val) +} + +async function mkGif(iconFunc, iconOpts) { + iconOpts = { + speed: 1, + scale: 4, + frames: 20, + transparent: '#7A7B7C', + ...iconOpts, + } + await import(/* webpackIgnore: true */ '/gif.js/gif.js') + var gif = new window.GIF({ + quality: 1, + repeat: 0, + workers: 1, + background: iconOpts.transparent, + width: 16 * iconOpts.scale, + height: 16 * iconOpts.scale, + transparent: parseInt(iconOpts.transparent.replace(/#/, ''), 16), + debug: false, + workerScript: '/gif.js/gif.worker.js', + }) + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + + for (var t = 0; t < 1; t += 1 / iconOpts.frames) { + iconFunc(ctx, t, iconOpts) + let delay = 1000 / iconOpts.frames / iconOpts.speed + gif.addFrame(ctx, { copy: true, delay }) + } + + return new Promise((resolve, reject) => { + gif.on('finished', function(blob) { + resolve(URL.createObjectURL(blob)) + }) + gif.on('abort', reject) + gif.render() + }) +} + +let originalFaviconURL +let loadingActive = false + +class Favicon { + /** + * Start favicon animation + * @param {string} iconName the preset animation. + * @param {object} iconOpts + * @param {number} iconOpts.speed frame delay divisor. + * @param {number} iconOpts.scale icon square size multiplyer. (base size: 16) + * @param {number} iconOpts.frames number of frames for this animation. + * The iconOpts accept other iconName related options. + * It will throw if iconName is not found. + */ + loading(iconName = 'basic', iconOpts) { + if (!icons[iconName]) { + return Promise.reject( + Error(`Favicon animation "${iconName}" does not exist.`) + ) + } + if (!originalFaviconURL) originalFaviconURL = getFavicon() + loadingActive = true + const start = Date.now() + return mkGif(icons[iconName], iconOpts).then(url => { + const buildTime = (Date.now() - start) / 1000 + // eslint-disable-next-line no-console + console.debug('GIF Done!', buildTime.toFixed(2) + 'secs', url) + if (!loadingActive) return false + setFavicon(url) + return url + }) + } + + /** + * Stop favicon animation and recover original icon. + */ + stop() { + if (!loadingActive) return false + loadingActive = false + setFavicon(originalFaviconURL) + return true + } +} + +export default new Favicon() diff --git a/src/index.scss b/src/index.scss index 5b3ae61..02fd182 100755 --- a/src/index.scss +++ b/src/index.scss @@ -78,7 +78,7 @@ button { cursor: pointer; background: $white; font-size: 1rem; - margin: 0 0 0 10px; + margin: 0 50px 0 10px; padding: 15px; &:hover { @@ -92,6 +92,13 @@ button { } +#favicon-debug { + margin-left: 10px; + width: 32px; + height: 32px; + background-size: 32px; +} + #root { display: grid; grid-gap: 1rem;