128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
|
|
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<GeometrySnapshot>
|
||
|
|
): Promise<GeometrySnapshot[]>
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// 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
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|