import type { ParamValues } from "@/params"; import { clamp } from "@/utils/math"; import { TICKS_PER_SECOND } from "@/wasm"; import type { Transform } from "./index"; export type TransitionKind = | "fade" | "push-left" | "push-right" | "zoom" | "zoom-out" | "spin"; export interface VisualTransition { kind: TransitionKind; inDuration: number; outDuration: number; } export interface TransitionState { opacityMultiplier: number; transform: Transform; } export const TRANSITION_KIND_OPTIONS: Array<{ value: TransitionKind; label: string; }> = [ { value: "fade", label: "Soft fade" }, { value: "push-left", label: "Slide left" }, { value: "push-right", label: "Slide right" }, { value: "zoom", label: "Zoom in" }, { value: "zoom-out", label: "Zoom out" }, { value: "spin", label: "Spin dissolve" }, ]; export function isTransitionKind(value: string): value is TransitionKind { return TRANSITION_KIND_OPTIONS.some((option) => option.value === value); } export function withVisualTransitionParams({ params, transitionKind, inDuration, outDuration, }: { params: ParamValues; transitionKind: TransitionKind; inDuration: number; outDuration: number; }): ParamValues { return { ...params, "transition.kind": transitionKind, "transition.inDuration": inDuration, "transition.outDuration": outDuration, }; } export function buildTransitionFromParams({ params, }: { params: ParamValues; }): VisualTransition { const kindValue = params["transition.kind"]; const inDurationValue = params["transition.inDuration"]; const outDurationValue = params["transition.outDuration"]; return { kind: typeof kindValue === "string" && isTransitionKind(kindValue) ? kindValue : "fade", inDuration: readDuration({ value: inDurationValue }), outDuration: readDuration({ value: outDurationValue }), }; } export function applyVisualTransition({ baseTransform, transition, localTime, duration, canvasWidth, }: { baseTransform: Transform; transition?: VisualTransition; localTime: number; duration: number; canvasWidth: number; }): TransitionState { if (!transition || duration <= 0) { return { opacityMultiplier: 1, transform: baseTransform }; } const inProgress = progressWithin({ elapsed: localTime, duration: transition.inDuration, }); const outProgress = progressWithin({ elapsed: duration - localTime, duration: transition.outDuration, }); const progress = Math.min(inProgress, outProgress); const isSlide = transition.kind === "push-left" || transition.kind === "push-right"; const opacityMultiplier = isSlide ? 1 : ease({ progress }); const transform = applyTransitionTransform({ transform: baseTransform, transition, inProgress, outProgress, canvasWidth, }); return { opacityMultiplier, transform }; } function readDuration({ value }: { value: unknown }): number { const seconds = typeof value === "number" && Number.isFinite(value) ? clamp({ value, min: 0, max: 5 }) : 0; return seconds * TICKS_PER_SECOND; } function progressWithin({ elapsed, duration, }: { elapsed: number; duration: number; }): number { if (duration <= 0) { return 1; } return clamp({ value: elapsed / duration, min: 0, max: 1 }); } function ease({ progress }: { progress: number }): number { return progress * progress * (3 - 2 * progress); } function applyTransitionTransform({ transform, transition, inProgress, outProgress, canvasWidth, }: { transform: Transform; transition: VisualTransition; inProgress: number; outProgress: number; canvasWidth: number; }): Transform { if (transition.kind === "fade") { return transform; } if (transition.kind === "zoom" || transition.kind === "zoom-out") { const progress = Math.min(inProgress, outProgress); const eased = ease({ progress }); const scale = transition.kind === "zoom" ? 0.96 + eased * 0.04 : 1.04 - eased * 0.04; return { ...transform, scaleX: transform.scaleX * scale, scaleY: transform.scaleY * scale, }; } if (transition.kind === "spin") { const progress = Math.min(inProgress, outProgress); const eased = ease({ progress }); const scale = 0.96 + eased * 0.04; return { ...transform, scaleX: transform.scaleX * scale, scaleY: transform.scaleY * scale, rotate: transform.rotate - (1 - eased) * 6, }; } const direction = transition.kind === "push-right" ? -1 : 1; const enteringOffset = (1 - ease({ progress: inProgress })) * canvasWidth * direction; const leavingOffset = (ease({ progress: outProgress }) - 1) * canvasWidth * direction; return { ...transform, position: { x: transform.position.x + enteringOffset + leavingOffset, y: transform.position.y, }, }; }