Skip to content

Instantly share code, notes, and snippets.

@trezy
Last active January 17, 2023 20:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trezy/989f978cfa5809929c33966b7410ccad to your computer and use it in GitHub Desktop.
Save trezy/989f978cfa5809929c33966b7410ccad to your computer and use it in GitHub Desktop.

useAnimatedFavicon

useAnimatedFavicon loads a spritesheet and converts it to an animated favicon!

How does it work?

First, make sure you have a <link> element on your page with rel="icon". You can even set the href of the element to a static favicon. useAnimatedFavicon will just overwrite the favicon when it's loaded.

  1. Load the spritesheet
  2. Start a render loop, rendering the image to an invisible <canvas> element
  3. Generate a data URI from the <canvas>
  4. Inject the data URI into the favicon's <link> element

Options

Option Name Type Required Description
frameCount integer Yes How many frames are in the spritesheet
frameRate integer Nope The frame rate (in milliseconds) at which the favicon will be updated
imageURL string Deffo The URL from which the spritesheet will be loaded
spritesheetDirection horizontal or vertical Nah Whether the spritesheet is a horizontal or vertical strip.
// Local imports
import { useAnimatedFavicon } from './useAnimatedFavicon.js'
export function AnimatedFavicon() {
useAnimatedFavicon({
imageURL: '/favicon-spritesheet.png',
frameCount: 4,
})
return (
<link
href={'/favicon.gif'}
rel={'icon'}
type={'image/gif'} />
)
}
// Module imports
import {
useCallback,
useEffect,
useRef,
useState,
} from 'react'
/**
* Renders a spritesheet into the favicon!
*
* @param {object} options All options.
* @param {number} options.frameCount How many frames are in your animation.
* @param {number} [options.frameRate = 100] The frame rate (in milliseconds) at which the sprite will be rendered.
* @param {number} options.imageURL URL for the favicon spritesheet.
* @param {'horizontal' | 'vertical'} [options.spritesheetDirection = 'horizontal'] Whether the spritesheet is a horizontal or vertical strip.
*/
export function useAnimatedFavicon(options) {
const {
frameCount,
frameRate = 100,
imageURL,
spritesheetDirection = 'horizontal',
} = options
const canvasRef = useRef(null)
const currentFrameRef = useRef(0)
const imageRef = useRef(null)
if (typeof window !== 'undefined') {
if (canvasRef.current === null) {
canvasRef.current = document.createElement('canvas')
}
if (imageRef.current === null) {
imageRef.current = document.createElement('img')
}
}
const [isImageLoaded, setIsImageLoaded] = useState(false)
const updateCanvas = useCallback(() => {
const canvasElement = canvasRef.current
const currentFrame = currentFrameRef.current
const imageElement = imageRef.current
if (!canvasElement) {
return
}
if (!isImageLoaded) {
return
}
canvasElement.height = 16
canvasElement.width = 16
const context = canvasElement.getContext('2d')
context.clearRect(0, 0, canvasElement.width, canvasElement.height)
const sourceX = (spritesheetDirection === 'horizontal') ? currentFrame * canvasElement.width : 0
const sourceY = (spritesheetDirection === 'vertical') ? currentFrame * canvasElement.height : 0
context.drawImage(
imageElement,
sourceX,
sourceY,
canvasElement.width,
canvasElement.height,
0,
0,
canvasElement.width,
canvasElement.height,
)
currentFrameRef.current += 1
if (currentFrameRef.current > (frameCount - 1)) {
currentFrameRef.current = 0
}
document
.querySelector('[rel="icon"]')
.setAttribute('href', canvasElement.toDataURL('image/png'))
}, [isImageLoaded])
useEffect(() => {
setInterval(updateCanvas, frameRate)
}, [
isImageLoaded,
updateCanvas,
])
useEffect(() => {
const imageElement = imageRef.current
if (!imageElement) {
return
}
imageElement.src = imageURL
imageElement
.decode()
.then(() => setIsImageLoaded(true))
}, [
imageURL,
setIsImageLoaded,
])
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment