import type { GeometrySnapshot } from './snapshots.js' import type { StateMaterializer } from './states.js' // --------------------------------------------------------------------------- // Transition Sampling Configuration // --------------------------------------------------------------------------- /** Mode that determines how a transition is sampled. */ export type TransitionMode = 'keyframes' | 'range' | 'transition' /** * Configuration for sampling a transition or animation. * * The materializer uses this to decide which time slices to capture. */ export interface TransitionSampleConfig { /** Sampling strategy. */ mode: TransitionMode /** Target element selector for the transition. */ selector: string /** Explicit keyframe timestamps in ms (keyframes mode). */ keyframes?: number[] /** Start of the sampled range in ms (range mode). */ from?: number /** End of the sampled range in ms (range mode). */ to?: number /** Total duration of the transition in ms (transition mode). */ duration?: number /** Number of evenly-spaced samples (transition mode). */ sampleCount?: number } // --------------------------------------------------------------------------- // Transition Sampler // --------------------------------------------------------------------------- /** * Contract for sampling snapshots across a temporal range. * * The sampler receives a state materializer and a capture callback. * It is responsible for scrubbing or waiting to each sample time and * invoking the callback to produce a geometry snapshot. */ export interface TransitionSampler { /** * Sample the transition according to `config`. * * @param config - Sampling configuration. * @param materializer - State materializer used to set up the initial state. * @param capture - Callback that captures a single geometry snapshot. * @returns Array of snapshots in chronological order. */ sample( config: TransitionSampleConfig, materializer: StateMaterializer, capture: () => Promise ): Promise } // --------------------------------------------------------------------------- // Sampling Helpers // --------------------------------------------------------------------------- /** * Resolve the list of sample times (in ms) from a configuration. * * - keyframes: returns the provided keyframe array. * - range: returns [from, to]. * - transition: returns evenly-spaced samples across duration. */ export function resolveSampleTimes(config: TransitionSampleConfig): number[] { switch (config.mode) { case 'keyframes': { return config.keyframes?.length ? config.keyframes : [0] } case 'range': { const from = config.from ?? 0 const to = config.to ?? from return from === to ? [from] : [from, to] } case 'transition': { const duration = config.duration ?? 300 const count = Math.max(2, config.sampleCount ?? 3) const step = duration / (count - 1) return Array.from({ length: count }, (_, i) => Math.round(i * step)) } default: { // Exhaustiveness fallback. return [0] } } } // --------------------------------------------------------------------------- // Default Transition Sampler // --------------------------------------------------------------------------- /** * Creates a transition sampler that captures a snapshot at each sample time. * * This is a simplified implementation suitable for browser-backed * measurement. A production sampler may use CDP animation scrubbing * or requestAnimationFrame timing for higher precision. */ export function createTransitionSampler(): TransitionSampler { return { async sample(config, _materializer, capture) { const times = resolveSampleTimes(config) const snapshots: GeometrySnapshot[] = [] for (const time of times) { // In a full implementation this would seek the animation to `time` // before capturing. Here we capture sequentially and annotate the // snapshot metadata with the intended sample time. const snapshot = await capture() if (typeof snapshot.metadata.durationMs === 'number') { snapshot.metadata.durationMs = time } else { snapshot.metadata.durationMs = time } snapshots.push(snapshot) } return snapshots }, } }