import * as MachineAnimationPlayerModule from './MachineAnimationPlayer'; type AnyRecord = Record; export type AnimationBridgeStatus = | 'idle' | 'playing' | 'paused' | 'stopped' | 'complete' | 'stepping'; export interface AnimationBridgeSnapshot { machineId: string; animationId: string; status: AnimationBridgeStatus; isPlaying: boolean; isPaused: boolean; rpm: number; timeScale: number; loop: boolean; elapsedSeconds: number; deltaSeconds: number; cycleDurationSeconds: number; cycleTime: number; normalizedTime: number; progress: number; cycleIndex: number; stepIndex: number; stepCount: number; direction: 1 | -1; reducedMotion: boolean; partPoses: Record; raw: unknown; } export interface MachineAnimationPlayerAdapterOptions { machineId?: string; animationId?: string; initialRpm?: number; initialTimeScale?: number; initialLoop?: boolean; reducedMotion?: boolean; stepCount?: number; respectReducedMotion?: boolean; allowReducedMotionPlayback?: boolean; [key: string]: unknown; } export interface MachineAnimationPlayerAdapter { readonly rawPlayer: unknown; readonly module: unknown; getSnapshot(): AnimationBridgeSnapshot; subscribe(listener: (snapshot: AnimationBridgeSnapshot) => void): () => void; play(): void; pause(): void; resume(): void; restart(): void; stop(): void; toggle(): void; stepForward(steps?: number): void; stepBackward(steps?: number): void; seek(normalizedTime: number): void; setRpm(rpm: number): void; setTimeScale(timeScale: number): void; setLoop(loop: boolean): void; setReducedMotion(reducedMotion: boolean): void; dispose(): void; } const PLAYER_EXPORT_CANDIDATES = [ 'MachineAnimationPlayer', 'AnimationPlayer', 'MechanicalAnimationPlayer', 'default', ]; const SNAPSHOT_METHOD_CANDIDATES = [ 'getSnapshot', 'snapshot', 'getState', 'getCurrentState', 'getCurrentSnapshot', ]; const PLAY_METHOD_CANDIDATES = ['play', 'start', 'resume']; const PAUSE_METHOD_CANDIDATES = ['pause', 'suspend']; const RESUME_METHOD_CANDIDATES = ['resume', 'play', 'start']; const RESTART_METHOD_CANDIDATES = ['restart', 'resetAndPlay', 'replay']; const STOP_METHOD_CANDIDATES = ['stop', 'cancel', 'halt']; const STEP_FORWARD_METHOD_CANDIDATES = ['stepForward', 'step', 'advanceStep', 'nextStep']; const STEP_BACKWARD_METHOD_CANDIDATES = ['stepBackward', 'previousStep', 'prevStep']; const SEEK_METHOD_CANDIDATES = ['seek', 'setProgress', 'setNormalizedTime', 'setCycleTime']; const SET_RPM_METHOD_CANDIDATES = ['setRpm', 'setRPM', 'updateRpm', 'setSpeed']; const SET_TIME_SCALE_METHOD_CANDIDATES = [ 'setTimeScale', 'setPlaybackRate', 'setRate', 'setSpeedMultiplier', ]; const SET_LOOP_METHOD_CANDIDATES = ['setLoop', 'setLooping', 'setLoopEnabled']; const SET_REDUCED_MOTION_METHOD_CANDIDATES = [ 'setReducedMotion', 'setPrefersReducedMotion', 'setReduceMotion', ]; function isRecord(value: unknown): value is AnyRecord { return (typeof value === 'object' && value !== null) || typeof value === 'function'; } function asRecord(value: unknown): AnyRecord { return isRecord(value) ? (value as AnyRecord) : {}; } function toFiniteNumber(value: unknown, fallback: number): number { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string' && value.trim() !== '') { const parsed = Number(value); if (Number.isFinite(parsed)) { return parsed; } } return fallback; } function toBoolean(value: unknown, fallback: boolean): boolean { if (typeof value === 'boolean') { return value; } if (typeof value === 'string') { const normalized = value.trim().toLowerCase(); if (['1', 'true', 'yes', 'on', 'loop'].includes(normalized)) { return true; } if (['0', 'false', 'no', 'off', 'once'].includes(normalized)) { return false; } } return fallback; } function clamp01(value: number): number { if (!Number.isFinite(value)) { return 0; } return Math.min(1, Math.max(0, value)); } function positiveModulo(value: number, divisor: number): number { return ((value % divisor) + divisor) % divisor; } function getStringCandidate( records: AnyRecord[], keys: string[], fallback: string, ): string { for (const record of records) { for (const key of keys) { const value = record[key]; if (typeof value === 'string' && value.trim() !== '') { return value; } } } return fallback; } function getNumberCandidate( records: AnyRecord[], keys: string[], fallback: number, ): number { for (const record of records) { for (const key of keys) { const value = record[key]; const numberValue = toFiniteNumber(value, Number.NaN); if (Number.isFinite(numberValue)) { return numberValue; } } } return fallback; } function getNestedRecord(record: AnyRecord, key: string): AnyRecord { return asRecord(record[key]); } function callFirstMethod( target: unknown, methodNames: string[], args: unknown[] = [], ): boolean { const record = asRecord(target); for (const methodName of methodNames) { const method = record[methodName]; if (typeof method !== 'function') { continue; } try { method.apply(target, args); return true; } catch { // Continue trying compatibility aliases. The adapter intentionally supports // the public method names from all milestone animation player revisions. } } return false; } function readRawSnapshot(rawPlayer: unknown): unknown { const record = asRecord(rawPlayer); for (const methodName of SNAPSHOT_METHOD_CANDIDATES) { const member = record[methodName]; if (typeof member === 'function') { try { return member.call(rawPlayer); } catch { continue; } } if (member !== undefined) { return member; } } return record; } function extractPartPoses(snapshotRecord: AnyRecord): Record { const candidates = [ snapshotRecord.partPoses, snapshotRecord.poses, snapshotRecord.partTransforms, snapshotRecord.transforms, snapshotRecord.parts, ]; for (const candidate of candidates) { if (isRecord(candidate) && !Array.isArray(candidate)) { return candidate as Record; } } return {}; } function mapStatus(rawStatus: unknown, isPlayingHint: boolean): AnimationBridgeStatus { const normalized = typeof rawStatus === 'string' ? rawStatus.toLowerCase() : ''; if (isPlayingHint || ['play', 'playing', 'running', 'started'].includes(normalized)) { return 'playing'; } if (['pause', 'paused', 'suspended'].includes(normalized)) { return 'paused'; } if (['stop', 'stopped', 'cancelled', 'canceled'].includes(normalized)) { return 'stopped'; } if (['complete', 'completed', 'finished', 'done'].includes(normalized)) { return 'complete'; } if (['step', 'stepping'].includes(normalized)) { return 'stepping'; } return 'idle'; } export function normalizeAnimationSnapshot( rawSnapshot: unknown, animationModule: unknown, options: MachineAnimationPlayerAdapterOptions = {}, ): AnimationBridgeSnapshot { const snapshotRecord = asRecord(rawSnapshot); const moduleRecord = asRecord(animationModule); const settingsRecord = { ...asRecord(snapshotRecord.settings), ...asRecord(snapshotRecord.config), }; const records = [snapshotRecord, settingsRecord, moduleRecord, asRecord(options)]; const rpm = Math.max( 0, getNumberCandidate(records, ['rpm', 'RPM', 'speedRpm', 'defaultRpm'], options.initialRpm ?? 60), ); const timeScale = getNumberCandidate( records, ['timeScale', 'playbackRate', 'rate', 'speedMultiplier'], options.initialTimeScale ?? 1, ); const elapsedSeconds = Math.max( 0, getNumberCandidate(records, ['elapsedSeconds', 'elapsed', 'time', 't'], 0), ); const deltaSeconds = Math.max( 0, getNumberCandidate(records, ['deltaSeconds', 'delta', 'dt'], 0), ); const cycleDurationSeconds = Math.max( 0.001, getNumberCandidate( records, ['cycleDurationSeconds', 'cycleDuration', 'durationSeconds', 'duration'], rpm > 0 ? 60 / rpm : 1, ), ); const explicitNormalizedTime = getNumberCandidate( records, ['normalizedTime', 'cycleProgress', 'progress', 'cycleTime', 'phase'], Number.NaN, ); const normalizedTime = clamp01( Number.isFinite(explicitNormalizedTime) ? positiveModulo(explicitNormalizedTime, 1) : positiveModulo(elapsedSeconds / cycleDurationSeconds, 1), ); const stepCount = Math.max( 1, Math.round( getNumberCandidate(records, ['stepCount', 'steps', 'cycleSteps'], options.stepCount ?? 16), ), ); const stepIndex = Math.min( stepCount - 1, Math.max( 0, Math.floor( getNumberCandidate(records, ['stepIndex', 'activeStepIndex'], normalizedTime * stepCount), ), ), ); const cycleIndex = Math.max( 0, Math.floor(getNumberCandidate(records, ['cycleIndex', 'loopIndex'], elapsedSeconds / cycleDurationSeconds)), ); const isPlayingHint = toBoolean( snapshotRecord.isPlaying ?? snapshotRecord.playing ?? snapshotRecord.running, false, ); const status = mapStatus( snapshotRecord.status ?? snapshotRecord.playState ?? snapshotRecord.state ?? snapshotRecord.phaseName, isPlayingHint, ); const isPlaying = status === 'playing'; const isPaused = status === 'paused' || toBoolean(snapshotRecord.isPaused ?? snapshotRecord.paused ?? snapshotRecord.suspended, false); return { machineId: getStringCandidate( records, ['machineId', 'machine', 'slug'], options.machineId ?? 'unknown-machine', ), animationId: getStringCandidate( records, ['animationId', 'id', 'name'], options.animationId ?? getStringCandidate([moduleRecord], ['id', 'name'], 'unknown-animation'), ), status, isPlaying, isPaused, rpm, timeScale, loop: toBoolean( snapshotRecord.loop ?? snapshotRecord.looping ?? snapshotRecord.isLooping ?? settingsRecord.loop, options.initialLoop ?? true, ), elapsedSeconds, deltaSeconds, cycleDurationSeconds, cycleTime: normalizedTime, normalizedTime, progress: normalizedTime, cycleIndex, stepIndex, stepCount, direction: getNumberCandidate(records, ['direction'], 1) < 0 ? -1 : 1, reducedMotion: toBoolean( snapshotRecord.reducedMotion ?? snapshotRecord.prefersReducedMotion ?? settingsRecord.reducedMotion, options.reducedMotion ?? false, ), partPoses: extractPartPoses(snapshotRecord), raw: rawSnapshot, }; } export function createIdleAnimationBridgeSnapshot( machineId = 'unknown-machine', animationId = 'idle-animation', ): AnimationBridgeSnapshot { return normalizeAnimationSnapshot( { machineId, animationId, status: 'idle', elapsedSeconds: 0, deltaSeconds: 0, normalizedTime: 0, stepIndex: 0, stepCount: 1, partPoses: {}, }, undefined, { machineId, animationId }, ); } function resolvePlayerConstructorOrFactory(): unknown { const moduleRecord = MachineAnimationPlayerModule as AnyRecord; for (const exportName of PLAYER_EXPORT_CANDIDATES) { const candidate = moduleRecord[exportName]; if (typeof candidate === 'function') { return candidate; } } for (const candidate of Object.values(moduleRecord)) { if (typeof candidate === 'function') { return candidate; } } return undefined; } function instantiatePlayer( animationModule: unknown, options: MachineAnimationPlayerAdapterOptions, ): unknown { const constructorOrFactory = resolvePlayerConstructorOrFactory(); if (typeof constructorOrFactory !== 'function') { return new LocalAnimationPlayer(animationModule, options); } const factoryRecord = asRecord(constructorOrFactory); const attempts: Array<() => unknown> = [ () => new (constructorOrFactory as new (...args: unknown[]) => unknown)(animationModule, options), () => new (constructorOrFactory as new (...args: unknown[]) => unknown)({ ...options, module: animationModule, animationModule, animation: animationModule, }), () => (constructorOrFactory as (...args: unknown[]) => unknown)(animationModule, options), ]; const createMember = factoryRecord.create; if (typeof createMember === 'function') { attempts.push(() => createMember.call(constructorOrFactory, animationModule, options)); attempts.push(() => createMember.call(constructorOrFactory, { ...options, module: animationModule, animationModule, animation: animationModule, }), ); } for (const attempt of attempts) { try { const player = attempt(); if (player !== undefined && player !== null) { return player; } } catch { // Try the next known constructor shape. } } return new LocalAnimationPlayer(animationModule, options); } function subscribeToRawPlayer( rawPlayer: unknown, listener: (rawSnapshot: unknown) => void, ): () => void { const record = asRecord(rawPlayer); const subscriptionMethodNames = ['subscribe', 'onSnapshot', 'addSnapshotListener', 'listen']; for (const methodName of subscriptionMethodNames) { const method = record[methodName]; if (typeof method !== 'function') { continue; } try { const unsubscribe = method.call(rawPlayer, listener); if (typeof unsubscribe === 'function') { return unsubscribe as () => void; } if (isRecord(unsubscribe) && typeof unsubscribe.unsubscribe === 'function') { return () => { (unsubscribe.unsubscribe as () => void).call(unsubscribe); }; } if (isRecord(unsubscribe) && typeof unsubscribe.dispose === 'function') { return () => { (unsubscribe.dispose as () => void).call(unsubscribe); }; } return () => { callFirstMethod(rawPlayer, ['unsubscribe', 'removeSnapshotListener', 'unlisten'], [listener]); }; } catch { // Try another listener API. } } if (typeof record.on === 'function') { try { record.on.call(rawPlayer, 'snapshot', listener); return () => { const off = record.off ?? record.removeListener; if (typeof off === 'function') { off.call(rawPlayer, 'snapshot', listener); } }; } catch { // Fall through to RAF polling. } } let disposed = false; let frameHandle: number | undefined; const tick = () => { if (disposed) { return; } listener(readRawSnapshot(rawPlayer)); frameHandle = requestFrame(tick); }; frameHandle = requestFrame(tick); return () => { disposed = true; if (frameHandle !== undefined) { cancelFrame(frameHandle); } }; } function requestFrame(callback: FrameRequestCallback): number { if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') { return window.requestAnimationFrame(callback); } return setTimeout(() => callback(Date.now()), 16) as unknown as number; } function cancelFrame(handle: number): void { if (typeof window !== 'undefined' && typeof window.cancelAnimationFrame === 'function') { window.cancelAnimationFrame(handle); return; } clearTimeout(handle as unknown as ReturnType); } function callModuleLifecycle( module: unknown, methodNames: string[], context: AnyRecord, ): unknown { const record = asRecord(module); for (const methodName of methodNames) { const method = record[methodName]; if (typeof method !== 'function') { continue; } try { return method.call(module, context); } catch { return undefined; } } return undefined; } class LocalAnimationPlayer { private readonly module: unknown; private readonly options: MachineAnimationPlayerAdapterOptions; private readonly listeners = new Set<(snapshot: unknown) => void>(); private frameHandle: number | undefined; private lastTimestamp = 0; private disposed = false; private snapshot: AnimationBridgeSnapshot; constructor(animationModule: unknown, options: MachineAnimationPlayerAdapterOptions) { this.module = animationModule; this.options = options; this.snapshot = createIdleAnimationBridgeSnapshot( options.machineId ?? getStringCandidate([asRecord(animationModule)], ['machineId'], 'local-machine'), options.animationId ?? getStringCandidate([asRecord(animationModule)], ['id', 'name'], 'local-animation'), ); this.snapshot = { ...this.snapshot, rpm: options.initialRpm ?? this.snapshot.rpm, timeScale: options.initialTimeScale ?? this.snapshot.timeScale, loop: options.initialLoop ?? this.snapshot.loop, reducedMotion: options.reducedMotion ?? false, stepCount: options.stepCount ?? this.snapshot.stepCount, }; } getSnapshot(): AnimationBridgeSnapshot { return this.snapshot; } subscribe(listener: (snapshot: unknown) => void): () => void { this.listeners.add(listener); listener(this.snapshot); return () => { this.listeners.delete(listener); }; } play(): void { if (this.disposed || this.snapshot.isPlaying) { return; } callModuleLifecycle(this.module, ['onStart', 'start'], { snapshot: this.snapshot }); this.snapshot = { ...this.snapshot, status: 'playing', isPlaying: true, isPaused: false, }; this.emit(); this.lastTimestamp = performanceNow(); this.frameHandle = requestFrame(this.tick); } pause(): void { if (this.disposed) { return; } if (this.frameHandle !== undefined) { cancelFrame(this.frameHandle); this.frameHandle = undefined; } callModuleLifecycle(this.module, ['onPause', 'pause'], { snapshot: this.snapshot }); this.snapshot = { ...this.snapshot, status: 'paused', isPlaying: false, isPaused: true, }; this.emit(); } resume(): void { this.play(); } restart(): void { callModuleLifecycle(this.module, ['onRestart', 'restart', 'reset'], { snapshot: this.snapshot }); this.snapshot = { ...this.snapshot, elapsedSeconds: 0, deltaSeconds: 0, normalizedTime: 0, progress: 0, cycleTime: 0, cycleIndex: 0, stepIndex: 0, status: 'playing', isPlaying: true, isPaused: false, }; this.emit(); if (this.frameHandle !== undefined) { cancelFrame(this.frameHandle); } this.lastTimestamp = performanceNow(); this.frameHandle = requestFrame(this.tick); } stop(): void { if (this.frameHandle !== undefined) { cancelFrame(this.frameHandle); this.frameHandle = undefined; } callModuleLifecycle(this.module, ['onStop', 'stop'], { snapshot: this.snapshot }); this.snapshot = { ...this.snapshot, status: 'stopped', isPlaying: false, isPaused: false, }; this.emit(); } stepForward(steps = 1): void { this.step(Math.max(1, steps)); } stepBackward(steps = 1): void { this.step(-Math.max(1, steps)); } seek(normalizedTime: number): void { const progress = clamp01(normalizedTime); this.snapshot = { ...this.snapshot, normalizedTime: progress, cycleTime: progress, progress, elapsedSeconds: this.snapshot.cycleIndex * this.snapshot.cycleDurationSeconds + progress * this.snapshot.cycleDurationSeconds, stepIndex: Math.min(this.snapshot.stepCount - 1, Math.floor(progress * this.snapshot.stepCount)), }; this.tickModule(0); this.emit(); } setRpm(rpm: number): void { this.snapshot = { ...this.snapshot, rpm: Math.max(0, rpm), cycleDurationSeconds: rpm > 0 ? 60 / rpm : this.snapshot.cycleDurationSeconds, }; this.emit(); } setTimeScale(timeScale: number): void { this.snapshot = { ...this.snapshot, timeScale, }; this.emit(); } setLoop(loop: boolean): void { this.snapshot = { ...this.snapshot, loop, }; this.emit(); } setReducedMotion(reducedMotion: boolean): void { this.snapshot = { ...this.snapshot, reducedMotion, }; this.emit(); } dispose(): void { this.disposed = true; if (this.frameHandle !== undefined) { cancelFrame(this.frameHandle); this.frameHandle = undefined; } callModuleLifecycle(this.module, ['onDispose', 'dispose'], { snapshot: this.snapshot }); this.listeners.clear(); } private readonly tick = (timestamp: number): void => { if (this.disposed || !this.snapshot.isPlaying) { return; } const deltaSeconds = Math.max(0, (timestamp - this.lastTimestamp) / 1000); this.lastTimestamp = timestamp; const scaledDelta = deltaSeconds * this.snapshot.timeScale; const elapsedSeconds = this.snapshot.elapsedSeconds + scaledDelta; const cycleIndex = Math.floor(elapsedSeconds / this.snapshot.cycleDurationSeconds); const rawProgress = elapsedSeconds / this.snapshot.cycleDurationSeconds; const normalizedTime = this.snapshot.loop ? positiveModulo(rawProgress, 1) : clamp01(rawProgress); const reachedEnd = !this.snapshot.loop && rawProgress >= 1; this.snapshot = { ...this.snapshot, elapsedSeconds, deltaSeconds: scaledDelta, normalizedTime, cycleTime: normalizedTime, progress: normalizedTime, cycleIndex, stepIndex: Math.min( this.snapshot.stepCount - 1, Math.floor(normalizedTime * this.snapshot.stepCount), ), status: reachedEnd ? 'complete' : 'playing', isPlaying: !reachedEnd, isPaused: false, }; this.tickModule(scaledDelta); this.emit(); if (!reachedEnd) { this.frameHandle = requestFrame(this.tick); } }; private step(stepDelta: number): void { this.pause(); const nextStepIndex = positiveModulo( this.snapshot.stepIndex + stepDelta, this.snapshot.stepCount, ); this.seek(nextStepIndex / this.snapshot.stepCount); callModuleLifecycle(this.module, ['onStep', 'step'], { snapshot: this.snapshot, stepDelta, stepIndex: nextStepIndex, }); this.snapshot = { ...this.snapshot, status: 'stepping', isPlaying: false, isPaused: false, }; this.emit(); } private tickModule(deltaSeconds: number): void { const context = { snapshot: this.snapshot, deltaSeconds, elapsedSeconds: this.snapshot.elapsedSeconds, normalizedTime: this.snapshot.normalizedTime, rpm: this.snapshot.rpm, timeScale: this.snapshot.timeScale, }; const returnedPoses = callModuleLifecycle(this.module, ['onTick', 'tick', 'update'], context) ?? callModuleLifecycle(this.module, ['getPoses', 'getPartPoses'], context); if (isRecord(returnedPoses)) { this.snapshot = { ...this.snapshot, partPoses: returnedPoses as Record, }; } } private emit(): void { for (const listener of this.listeners) { listener(this.snapshot); } } } function performanceNow(): number { if (typeof performance !== 'undefined' && typeof performance.now === 'function') { return performance.now(); } return Date.now(); } export function createMachineAnimationPlayerAdapter( animationModule: unknown, options: MachineAnimationPlayerAdapterOptions = {}, ): MachineAnimationPlayerAdapter { const rawPlayer = instantiatePlayer(animationModule, options); const listeners = new Set<(snapshot: AnimationBridgeSnapshot) => void>(); let rawUnsubscribe: (() => void) | undefined; let disposed = false; let lastSnapshot = normalizeAnimationSnapshot(readRawSnapshot(rawPlayer), animationModule, options); const emitSnapshot = (rawSnapshot: unknown = readRawSnapshot(rawPlayer)) => { lastSnapshot = normalizeAnimationSnapshot(rawSnapshot, animationModule, options); for (const listener of listeners) { listener(lastSnapshot); } }; const ensureRawSubscription = () => { if (rawUnsubscribe || disposed) { return; } rawUnsubscribe = subscribeToRawPlayer(rawPlayer, emitSnapshot); }; const invokeAndEmit = (methodNames: string[], args: unknown[] = []) => { callFirstMethod(rawPlayer, methodNames, args); emitSnapshot(); }; const adapter: MachineAnimationPlayerAdapter = { rawPlayer, module: animationModule, getSnapshot() { lastSnapshot = normalizeAnimationSnapshot(readRawSnapshot(rawPlayer), animationModule, options); return lastSnapshot; }, subscribe(listener) { listeners.add(listener); listener(lastSnapshot); ensureRawSubscription(); return () => { listeners.delete(listener); if (listeners.size === 0 && rawUnsubscribe) { rawUnsubscribe(); rawUnsubscribe = undefined; } }; }, play() { if ( options.respectReducedMotion && !options.allowReducedMotionPlayback && lastSnapshot.reducedMotion ) { invokeAndEmit(PAUSE_METHOD_CANDIDATES); return; } invokeAndEmit(PLAY_METHOD_CANDIDATES); }, pause() { invokeAndEmit(PAUSE_METHOD_CANDIDATES); }, resume() { if ( options.respectReducedMotion && !options.allowReducedMotionPlayback && lastSnapshot.reducedMotion ) { invokeAndEmit(PAUSE_METHOD_CANDIDATES); return; } invokeAndEmit(RESUME_METHOD_CANDIDATES); }, restart() { invokeAndEmit(RESTART_METHOD_CANDIDATES); }, stop() { invokeAndEmit(STOP_METHOD_CANDIDATES); }, toggle() { if (lastSnapshot.isPlaying) { adapter.pause(); } else { adapter.play(); } }, stepForward(steps = 1) { if (!callFirstMethod(rawPlayer, STEP_FORWARD_METHOD_CANDIDATES, [steps])) { const next = lastSnapshot.normalizedTime + steps / Math.max(1, lastSnapshot.stepCount); adapter.seek(positiveModulo(next, 1)); } emitSnapshot(); }, stepBackward(steps = 1) { if (!callFirstMethod(rawPlayer, STEP_BACKWARD_METHOD_CANDIDATES, [steps])) { const next = lastSnapshot.normalizedTime - steps / Math.max(1, lastSnapshot.stepCount); adapter.seek(positiveModulo(next, 1)); } emitSnapshot(); }, seek(normalizedTime: number) { invokeAndEmit(SEEK_METHOD_CANDIDATES, [clamp01(normalizedTime)]); }, setRpm(rpm: number) { invokeAndEmit(SET_RPM_METHOD_CANDIDATES, [Math.max(0, rpm)]); }, setTimeScale(timeScale: number) { invokeAndEmit(SET_TIME_SCALE_METHOD_CANDIDATES, [timeScale]); }, setLoop(loop: boolean) { invokeAndEmit(SET_LOOP_METHOD_CANDIDATES, [loop]); }, setReducedMotion(reducedMotion: boolean) { invokeAndEmit(SET_REDUCED_MOTION_METHOD_CANDIDATES, [reducedMotion]); }, dispose() { disposed = true; if (rawUnsubscribe) { rawUnsubscribe(); rawUnsubscribe = undefined; } callFirstMethod(rawPlayer, ['dispose', 'destroy', 'teardown']); listeners.clear(); }, }; return adapter; }