UNPKG

@vtbag/utensil-drawer

Version:

Pull out just what you need to craft seamless transitions. The Utensil Drawer holds reusable functions to help you build websites with view transitions. It is a bit sparse right now, but like the one in your kitchen, it is bound to fill up over time.

97 lines (96 loc) 4.18 kB
let currentViewTransition; const chained = []; /* One version of startViewTransition() for all browsers. Without native view transition support just calls the update function and returns a view transition object with promises. Calling this while a transition is active won't cancel the ongoing transition but stack no transitions into a single one to follow the current one. Cranks up speed if frequently interrupted. */ export function mayStartViewTransition(param, extensions = { chaining: false, speedUpWhenChained: 1 }, scope = document) { if (extensions?.chaining && currentViewTransition) { const transition = chain(param instanceof Function ? param : param?.update, param instanceof Function ? [] : (param?.types ?? [])); if (extensions?.speedUpWhenChained !== 1) { document.getAnimations().forEach((a) => { a.effect?.pseudoElement?.startsWith('::view-transition') && ((a.playbackRate *= extensions.speedUpWhenChained), console.log(a.playbackRate)); }); } return transition; } const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (param === undefined || param instanceof Function) { if (scope.startViewTransition && !reducedMotion) return resilient(scope.startViewTransition(param)); return fallback(param, []); } if (scope.startViewTransition && !reducedMotion) try { return resilient(scope.startViewTransition(param)); } catch (e) { return resilient(scope.startViewTransition(param.update)); } return fallback(param && typeof param === 'object' ? param.update : param, param.types ?? []); } function fallback(update = () => { }, types) { const updateCallbackDone = Promise.resolve(update()); const ready = Promise.resolve(updateCallbackDone); const finished = Promise.resolve(ready); return resilient({ updateCallbackDone, ready, finished, skipTransition: () => { }, types: new Set(types), }); } function chain(update = () => { }, types) { let updateResolve, updateReject, readyResolve, readyReject, finishResolve, finishReject; const updateCallbackDone = new Promise((res, rej) => ((updateResolve = res), (updateReject = rej))); const ready = new Promise((res, rej) => ((readyResolve = res), (readyReject = rej))); const finished = new Promise((res, rej) => ((finishResolve = res), (finishReject = rej))); const transition = { chained: true, skipped: false, updateResolve, updateReject, readyResolve, readyReject, finishResolve, finishReject, update, updateCallbackDone, ready, finished, skipTransition() { this.skipped = true; }, types, }; chained.push(transition); return transition; } function resilient(transition) { transition.finished.then(() => { currentViewTransition = undefined; if (chained.length === 0) return; const copied = [...chained]; chained.length = 0; const transition = mayStartViewTransition({ update: async () => { copied.forEach(async (update) => update.update && (await update.update())); }, types: copied[copied.length - 1].types, }); copied.find((update) => update.skipped) && transition.skipTransition(); transition.updateCallbackDone.then(() => copied.forEach((update) => update.updateResolve())); transition.updateCallbackDone.catch(() => copied.forEach((update) => update.updateReject())); transition.ready.then(() => copied[copied.length - 1].readyResolve()); // only the last one transition.ready.catch(() => copied.forEach((update) => update.readyReject())); transition.finished.then(() => copied.forEach((update) => update.finishResolve())); transition.finished.catch(() => copied.forEach((update) => update.finishReject())); }); return (currentViewTransition = transition); }