import { h, Fragment, cloneElement } from 'preact'; import { useState, useEffect, useRef, useMemo } from 'preact/hooks'; import ColorHash from 'color-hash'; import css from './ui/css.js'; css` html { overflow: hidden; } `; const colors = new ColorHash({ lightness: 0.8 }); const abs = Math.abs; const clamp = (val) => Math.max(0, Math.min(val, 1)); const smoothstep = (min, max, value) => { const x = clamp((value - min)/(max - min)); return x*x*(3 - 2*x); }; const lag = (value, t) => { const a = Math.abs(value); if (a < t) return 0; if (a > 1) return value; const i = a - t; const res = i * (1 + a - i); return res * Math.sign(value); }; const keys = (a, b) => ( new Set([ ...Object.keys(a), ...Object.keys(b) ]) ); const addState = (a, b) => { if (!b) return a; const res = {}; for (const key of keys(a, b)) { const sum = (a[key] ?? 0) + (b[key] ?? 0) if (sum) res[key] = sum; } return res; }; const lerpState = (a, b, coeff) => { if (a === b) return a; const res = {}; for (const key of keys(a, b)) { const aval = a[key] ?? 0; const delta = (b[key] ?? 0) - aval; if (Math.abs(delta) < 0.01) res[key] = b[key]; else res[key] = aval + delta * coeff; } return res; }; const AnimationRoot = ({ children, fallback, commit, interactionRef }) => { children = Array.isArray(children) ? children : [children]; const interactionIndex = useRef(0); const [offsets, setOffsets] = useState({}); const [interactions, setInteractions] = useState({}); interactionRef.current = { start: (key) => { const id = interactionIndex.current++; setInteractions((i) => ({ ...i, [id]: { key, value: 0 } })); return id; }, update: (id, value) => { setInteractions((i) => ({ ...i, [id]: { ...i[id], value } })); }, end: (id, value) => { // move offset by nearerst integer, // subtract from interaction to allow fade-out const key = interactions[id].key; const target = Math.round(value); setOffsets((o) => addState(o, { [key]: target })); setInteractions((i) => ({ ...i, [id]: { ...i[id], value: value - target, from: value - target, expiry: 300 } })); }, }; useEffect(() => { if (!Object.values(interactions).find(int => int.expiry)) return; const t = document.timeline.currentTime; requestAnimationFrame((nt) => { const dt = nt - t; setInteractions((i) => { const res = {}; for (const id of Object.keys(i)) { const interaction = i[id]; if (!interaction.expiry) { res[id] = interaction; continue; } let expiry = interaction.expiry; expiry -= dt; if (expiry <= 0) continue; const t = expiry / 300; res[id] = { ...interaction, value: interaction.from * t * t, expiry }; } return res; }); }); }, [interactions]); const state = useMemo(() => { let state = Object.assign({}, fallback, offsets); for (const {key, value} of Object.values(interactions)) { state[key] = (state[key] ?? 0) + value; } return state; }, [fallback, offsets, interactions]); useEffect(() => { let delta = {}; for (const key in state) { if (state[key] > 0.7) { delta[key] = -1; } else if (state[key] < -0.7) { delta[key] = 1; } } if (Object.keys(delta).length) { // @TODO: make limits work delta = commit(delta); if (delta) setOffsets((o) => addState(o, delta)); } }, [state]); return children.map((child) => child && cloneElement(child, { state })); }; const Animatable = ({ state, offset, swipeI, color, children }) => { const scroll = lag(state.scroll, 0.05) + offset.scroll; const active = smoothstep(0.9, 0.1, abs(scroll)) * 2; const activeB = offset.scroll === 0; const swipe = lag(state[['swipe', swipeI]] ?? 0, 0.05) + offset.swipe; return (