Created
May 13, 2023 21:15
-
-
Save acorn1010/2a155a769d744de2cfcce8442af1b88b to your computer and use it in GitHub Desktop.
useAnimation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** A set that can have multiple entries of the same value. Thanks, ChatGPT! */ | |
export class Multiset<T> { | |
private readonly elements = new Map<T, number>(); | |
/** Total number of elements in the Multiset. */ | |
private _size = 0; | |
/** Returns the number of elements in the set after adding `element`. */ | |
add(element: T): number { | |
const count = 1 + (this.elements.get(element) || 0); | |
this.elements.set(element, count); | |
++this._size; | |
return count; | |
} | |
/** Returns `true` if the element was removed from the set. */ | |
remove(element: T): boolean { | |
const currentCount = this.elements.get(element) || 0; | |
if (currentCount < 1) { | |
return false; | |
} | |
--this._size; | |
if (currentCount === 1) { | |
this.elements.delete(element); | |
} else { | |
this.elements.set(element, currentCount - 1); | |
} | |
return true; | |
} | |
/** Returns `true` if `element` is in the Multiset */ | |
has(element: T): boolean { | |
return this.elements.has(element); | |
} | |
/** | |
* Returns the total number of elements in the Multiset (e.g. adding the same key 3 times counts | |
* as 3 elements). | |
*/ | |
size() { | |
return this._size; | |
} | |
/** Returns an iterator over the values in the Multiset. */ | |
*[Symbol.iterator](): Iterator<T> { | |
for (const value of this.elements.keys()) { | |
yield value; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) | |
* | |
* @author <a href="mailto:[email protected]">Gary Court</a> | |
* @see http://github.com/garycourt/murmurhash-js | |
* @author <a href="mailto:[email protected]">Austin Appleby</a> | |
* @see http://sites.google.com/site/murmurhash/ | |
* | |
* @param {string} key ASCII only | |
* @param {number} seed Positive integer only | |
* @return {number} 32-bit positive integer hash | |
*/ | |
export function weakHash(key: string, seed: number) { | |
let remainder, bytes, h1, h1b, c1, c2, k1, i; | |
remainder = key.length & 3; // key.length % 4 | |
bytes = key.length - remainder; | |
h1 = Math.abs(seed); | |
c1 = 0xcc9e2d51; | |
c2 = 0x1b873593; | |
i = 0; | |
while (i < bytes) { | |
k1 = | |
((key.charCodeAt(i) & 0xff)) | | |
((key.charCodeAt(++i) & 0xff) << 8) | | |
((key.charCodeAt(++i) & 0xff) << 16) | | |
((key.charCodeAt(++i) & 0xff) << 24); | |
++i; | |
k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff; | |
k1 = (k1 << 15) | (k1 >>> 17); | |
k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff; | |
h1 ^= k1; | |
h1 = (h1 << 13) | (h1 >>> 19); | |
h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff; | |
h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16)); | |
} | |
k1 = 0; | |
// Unwrapped switch statement because TypeScript was complaining about case fallthrough. | |
if (remainder === 3) { | |
k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; | |
} | |
if (remainder >= 2) { | |
k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; | |
} | |
if (remainder >= 1) { | |
k1 ^= (key.charCodeAt(i) & 0xff); | |
} | |
k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; | |
k1 = (k1 << 15) | (k1 >>> 17); | |
k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; | |
h1 ^= k1; | |
h1 ^= key.length; | |
h1 ^= h1 >>> 16; | |
h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; | |
h1 ^= h1 >>> 13; | |
h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff; | |
h1 ^= h1 >>> 16; | |
return h1 >>> 0; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {CSSProperties, useEffect, useRef} from 'react'; | |
import {Helmet} from 'react-helmet'; | |
import { Multiset } from '@shared/collections/Multiset'; | |
import { weakHash } from '@shared/utils/StringUtils'; | |
import {createGlobalStore} from '~/state/createGlobalStore'; | |
import {isEqual} from 'moderndash'; | |
/** An animation involving multiple "frames" / images. Each frame will appear for `durationMs` / `frames.length`. */ | |
type AnimationFrames = {type: 'frames', frames: string[], durationMs: number}; | |
/** An animation that animation from `from` props to `to`. */ | |
type AnimationCss = {type: 'css', from: CSSProperties, to: CSSProperties, durationMs: number}; | |
type AnimationType = AnimationFrames | AnimationCss; | |
const store = createGlobalStore({} as {[hash: number]: AnimationType}); | |
/** | |
* List of frames that have been preloaded. This reduces flicker by ensuring | |
* that frames are loaded before they need to be rendered by the keyframe. | |
*/ | |
const loadedFrames = new Set<string>(); | |
/** | |
* Provides the animations for the `useAnimation` hook. This must be included | |
* _once_ (and only once) in the DOM in order to inject the CSS keyframes needed | |
* by `useAnimation`. | |
*/ | |
export function AnimationProvider() { | |
const animations = store.useAll(); | |
const animationKeyframes: string[] = []; | |
for (const [hash, animation] of Object.entries(animations)) { | |
animationKeyframes.push(makeKeyframe(hash, animation)); | |
} | |
return <Helmet><style type='text/css'>{animationKeyframes.join('\n')}</style></Helmet>; | |
} | |
/** Returns a CSS keyframe representing the given `animation`. */ | |
function makeKeyframe(key: string, animation: AnimationType): string { | |
switch (animation.type) { | |
case 'frames': return makeAnimationKeyframe(key, animation); | |
case 'css': return makeCssKeyframe(key, animation); | |
} | |
} | |
function makeCssKeyframe(key: string, {from, to, durationMs}: AnimationCss) { | |
const fromRows = Object.entries(from).map(([key, value]) => ` ${key}: ${value};`); | |
const toRows = Object.entries(to).map(([key, value]) => ` ${key}: ${value};`); | |
return ` | |
@keyframes animation-${key} { | |
from { | |
${fromRows} | |
} | |
to { | |
${toRows} | |
} | |
} | |
.animation-${key} { | |
animation-name: animation-${key}; | |
animation-duration: ${durationMs / 1_000}s; | |
animation-iteration-count: 1; | |
animation-fill-mode: forwards; | |
} | |
`; | |
} | |
function makeAnimationKeyframe(key: string, {frames, durationMs}: AnimationFrames) { | |
// Preload frames | |
for (const frame of frames) { | |
if (!loadedFrames.has(frame)) { | |
loadedFrames.add(frame); | |
const image = new Image(); | |
image.src = frame; | |
} | |
} | |
const keyframes = | |
frames.map( | |
(frame, idx) => | |
` ${(100 * idx) / frames.length}% { background-image: url('${frame}'); }`); | |
return ` | |
@keyframes animation-${key} { | |
${keyframes.join('\n')} | |
100% { background-image: none; display: none; } | |
} | |
.animation-${key} { | |
animation-name: animation-${key}; | |
animation-duration: ${durationMs / 1_000}s; | |
animation-iteration-count: 1; | |
animation-fill-mode: forwards; | |
} | |
`; | |
} | |
/** Maps a unique key to the animation that needs to be added for that key. */ | |
const animationSet = new Multiset<number>(); | |
/** | |
* Given a list of frames and a `durationMs` per frame, returns a class that | |
* will apply the given animation as CSS keyframes. The animation class returned | |
* is deduped on the `frames-durationMs` key. | |
*/ | |
export function useAnimation(animation: AnimationType) { | |
const key = weakHash(JSON.stringify(animation), 0); | |
const animationRef = useRef(animation); | |
if (!isEqual(animationRef.current, animation)) { | |
animationRef.current = animation; | |
} | |
const value = animationRef.current; | |
useEffect(() => { | |
animationSet.add(key); | |
// This animation isn't added to the DOM yet, so add it | |
if (!store.has(key)) { | |
store.set(key, value); | |
} | |
return () => { | |
animationSet.remove(key); | |
if (!animationSet.has(key)) { | |
// Animation no longer exists, so delete it. | |
store.delete(key); | |
} | |
}; | |
}, [key, value]); | |
return `animation-${key}`; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment