import { GroupAnimation, NativeAnimation, NativeAnimationWrapper, applyGeneratorOptions, getValueTransition, getViewAnimationLayerInfo, getViewAnimations, mapEasingToNativeEasing } from "motion-dom"; import { secondsToMilliseconds } from "motion-utils"; //#region src/components/animate-view/animate-view-layers.ts /** * Animate the pseudo-element layers the browser has generated for a named * view transition boundary, either retiming the browser's animations or * replacing its crossfade with custom keyframes. * * Ported from framer-motion's AnimateView (`animate-view-layers.mjs`). */ function animateViewLayers(name, animationType, { transition, onAnimationStart, onAnimationComplete, ...props }, types) { const layerAnimations = []; const definition = props[animationType]; const { transition: typeTransition, ...values } = (typeof definition === "function" ? definition(types) : definition) || {}; const hasValues = Object.keys(values).length > 0; let hasBrowserLayerAnimation = false; for (const viewAnimation of getViewAnimations()) { if (viewAnimation.playState === "finished") continue; const { effect } = viewAnimation; if (typeof KeyframeEffect === "undefined" || !(effect instanceof KeyframeEffect) || !effect.pseudoElement) continue; const info = getViewAnimationLayerInfo(effect.pseudoElement); if (!info || info.layer !== name) continue; hasBrowserLayerAnimation = true; /** * Custom values replace the browser's crossfade (old/new layers). * The group layer carries the position/size morph, so keep it running * with Motion's timing. */ if (hasValues && info.type !== "group") viewAnimation.cancel(); else { const transitionName = info.type === "group" ? "layout" : ""; let options = { ...getValueTransition(transition, transitionName), ...getValueTransition(typeTransition, transitionName) }; options.duration = secondsToMilliseconds(options.duration ?? .3); options = applyGeneratorOptions(options); effect.updateTiming({ delay: secondsToMilliseconds(options.delay ?? 0), duration: options.duration, easing: mapEasingToNativeEasing(options.ease, options.duration) }); layerAnimations.push(new NativeAnimationWrapper(viewAnimation)); } } if (hasValues && hasBrowserLayerAnimation) layerAnimations.push(...createLayerAnimations(name, animationType, values, transition, typeTransition)); const animation = new GroupAnimation(layerAnimations); let active = true; onAnimationStart?.(animation, animationType); if (onAnimationComplete) animation.finished.then(() => { if (active) onAnimationComplete(animationType); }); return () => { active = false; animation.cancel(); }; } /** * Run each custom value as a `NativeAnimation` against the snapshot * pseudo-element that stays visible for this animation type: the `new` * snapshot when entering, the `old` snapshot when exiting or sharing. * The browser's own old/new crossfade has already been cancelled by the * caller; the group layer (size/position morph) is untouched. */ function createLayerAnimations(layerName, animationType, values, baseTransition, typeTransition) { const animations = []; for (const [name, value] of Object.entries(values)) { let keyframes = value; const options = { ...getValueTransition(baseTransition, name), ...getValueTransition(typeTransition, name) }; options.duration && (options.duration = secondsToMilliseconds(options.duration)); options.delay && (options.delay = secondsToMilliseconds(options.delay)); /** * A single opacity value is a target, not a keyframe pair — backfill * the starting frame from the snapshot's natural state: entering * elements fade in from 0, exiting/shared ones fade out from 1. */ if (name === "opacity" && !Array.isArray(keyframes)) keyframes = [animationType === "enter" ? 0 : 1, keyframes]; animations.push(new NativeAnimation({ ...options, element: document.documentElement, name, pseudoElement: `::view-transition-${animationType === "enter" ? "new" : "old"}(${layerName})`, keyframes })); } return animations; } //#endregion export { animateViewLayers };