Files
openmaic/OpenMAIC/components/slide-renderer/Editor/motion-descriptor-adapter.ts
2026-08-16 14:58:47 +08:00

187 lines
5.9 KiB
TypeScript

import type { Target, TargetAndTransition, Transition } from 'motion/react';
import type { AnimatableValue, AnimationDescriptor, GeometryRef, Track } from '@/lib/choreography';
export interface ResolvedMotionLayer {
initial: Target;
animate: Target;
exit?: TargetAndTransition;
transition: Transition;
staticProps: Record<string, string | number>;
}
type MotionValue = string | number;
type MotionTarget = Record<string, MotionValue | MotionValue[]>;
type Params = Record<string, string | number>;
function hasOwn(record: object, property: string): boolean {
return Object.prototype.hasOwnProperty.call(record, property);
}
function setOwn<T extends Record<string, unknown>>(
record: T,
property: string,
value: T[string],
): void {
Object.defineProperty(record, property, {
configurable: true,
enumerable: true,
value,
writable: true,
});
}
function requireGeometry(ref: GeometryRef, geometry: Partial<Record<GeometryRef, number>>): number {
const value = geometry[ref];
if (value === undefined) {
throw new Error(`Missing geometry field "${ref}"`);
}
return value;
}
function resolveValue(
value: AnimatableValue,
geometry: Partial<Record<GeometryRef, number>>,
params: Params,
descriptorId: string,
): MotionValue {
if (typeof value === 'number') {
return value;
}
if (typeof value === 'string') {
return resolveParameterizedValue(value, params, descriptorId);
}
if ('ref' in value) {
return requireGeometry(value.ref, geometry) * (value.scale ?? 1) + (value.offset ?? 0);
}
return requireGeometry(value.axis, geometry) > value.threshold
? value.whenAbove
: value.whenBelow;
}
function applyUnit(value: MotionValue, unit: string | undefined): MotionValue {
return unit !== undefined && typeof value === 'number' ? `${value}${unit}` : value;
}
function resolveTransition(track: Track): Transition {
const transition: Record<string, unknown> = {};
if (track.durationMs !== undefined) transition.duration = track.durationMs / 1000;
if (track.delayMs !== undefined) transition.delay = track.delayMs / 1000;
if (track.easing?.type === 'cubicBezier') {
transition.ease = track.easing.points;
} else if (track.easing?.type === 'named') {
transition.ease = track.easing.name;
} else if (track.easing?.type === 'spring') {
transition.type = 'spring';
transition.stiffness = track.easing.stiffness;
transition.damping = track.easing.damping;
if (track.easing.mass !== undefined) transition.mass = track.easing.mass;
}
if (track.repeat !== undefined) {
transition.repeat = track.repeat === 'infinite' ? Infinity : track.repeat;
}
if (track.repeatDelayMs !== undefined) {
transition.repeatDelay = track.repeatDelayMs / 1000;
}
return transition as Transition;
}
function resolveParameterizedValue(value: string | number, params: Params, descriptorId: string) {
if (typeof value === 'number') return value;
const exactMatch = /^\{([^{}]+)\}$/.exec(value);
if (exactMatch) {
return requireParameter(exactMatch[1], params, descriptorId);
}
return value.replace(/\{([^{}]+)\}/g, (_placeholder, name: string) =>
String(requireParameter(name, params, descriptorId)),
);
}
function requireParameter(name: string, params: Params, descriptorId: string): string | number {
const value = params[name];
if (!hasOwn(params, name) || value === undefined) {
throw new Error(`Unresolved parameter "${name}" in descriptor "${descriptorId}"`);
}
return value;
}
export function resolveMotionLayer(
descriptor: AnimationDescriptor,
layerId: string,
options?: {
geometry?: Partial<Record<GeometryRef, number>>;
params?: Record<string, string | number | undefined>;
units?: Partial<Record<string, string>>;
},
): ResolvedMotionLayer {
const layer = descriptor.layers.find((candidate) => candidate.id === layerId);
if (!layer) {
throw new Error(`Layer "${layerId}" was not found in descriptor "${descriptor.id}"`);
}
const params: Params = {};
for (const [name, value] of Object.entries(descriptor.params ?? {})) {
setOwn(params, name, value);
}
for (const [name, value] of Object.entries(options?.params ?? {})) {
if (value !== undefined) setOwn(params, name, value);
}
const geometry = options?.geometry ?? {};
const initial: MotionTarget = {};
const animate: MotionTarget = {};
const transition: Record<string, Transition> = {};
const exitTarget: MotionTarget = {};
const exitTransition: Record<string, Transition> = {};
let hasExit = false;
for (const track of layer.tracks) {
const units = options?.units;
const unit = units && hasOwn(units, track.property) ? units[track.property] : undefined;
const from = applyUnit(resolveValue(track.from, geometry, params, descriptor.id), unit);
const to = applyUnit(resolveValue(track.to, geometry, params, descriptor.id), unit);
const propertyTransition = resolveTransition(track);
if ((track.phase ?? 'enter') === 'exit') {
hasExit = true;
setOwn(exitTarget, track.property, to);
setOwn(exitTransition, track.property, propertyTransition);
} else {
if (track.repeat !== undefined) {
setOwn(animate, track.property, [from, to]);
} else {
setOwn(initial, track.property, from);
setOwn(animate, track.property, to);
}
setOwn(transition, track.property, propertyTransition);
}
}
const staticProps: Record<string, string | number> = {};
for (const [property, value] of Object.entries(layer.staticProps ?? {})) {
setOwn(staticProps, property, resolveParameterizedValue(value, params, descriptor.id));
}
return {
initial: initial as Target,
animate: animate as Target,
...(hasExit
? {
exit: {
...exitTarget,
transition: exitTransition as Transition,
} as TargetAndTransition,
}
: {}),
transition: transition as Transition,
staticProps,
};
}