@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
JavaScript
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);
}