Skip to content

Commit

Permalink
Implements animated favicon gif builder
Browse files Browse the repository at this point in the history
closes jh3y#31
  • Loading branch information
aurium committed Oct 11, 2021
1 parent 8de79b7 commit a3c3436
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 2 deletions.
1 change: 0 additions & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
margin: 0;
padding: 0;
min-height: 100vh;

}

header {
Expand Down
25 changes: 25 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -108,6 +122,17 @@ const App = () => {
<button disabled={loading} onClick={selectRandomWhirl}>
Lucky Dip!
</button>
<div className="select-wrapper">
<label>
Favicon:&nbsp;
<select onChange={selectLoadingFavicon}>
<option value="">[ none ]</option>
<option>basic</option>
<option>ring-of-dots</option>
</select>
</label>
</div>
<div id="favicon-debug" ref={faviconDebug}></div>
</div>
<a
target="_blank"
Expand Down
146 changes: 146 additions & 0 deletions src/favicon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
const { floor, sin, cos, PI } = Math
const turn = 2 * PI
const iconDefaults = { primary: '#F80', secondary: '#FFF' }

const icons = {
basic(ctx, t, opts) {
const { primary, secondary, scale } = { ...iconDefaults, ...opts }
ctx.fillStyle = opts.transparent
ctx.lineWidth = 2 * scale
let mid = 8 * scale
ctx.fillRect(0, 0, 16 * scale, 16 * scale)
//ctx.ellipse(8*scale, 8*scale, 7*t*scale, 7*t*scale, 0, 0, 2*Math.PI)

ctx.strokeStyle = secondary
ctx.beginPath()
ctx.ellipse(mid, mid, 7 * scale, 7 * scale, 0, 0, turn)
ctx.stroke()

ctx.strokeStyle = primary
ctx.beginPath()
ctx.ellipse(mid, mid, 7 * scale, 7 * scale, 0, turn * t, turn * t + 1.5)
ctx.stroke()
},

'ring-of-dots': (ctx, t, opts) => {
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()
9 changes: 8 additions & 1 deletion src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -92,6 +92,13 @@ button {

}

#favicon-debug {
margin-left: 10px;
width: 32px;
height: 32px;
background-size: 32px;
}

#root {
display: grid;
grid-gap: 1rem;
Expand Down

0 comments on commit a3c3436

Please sign in to comment.