import type { InterpolationFunction, SignalValue, TimingFunction, } from '@revideo/core'; import {capitalize, deepLerp, SignalContext, useLogger} from '@revideo/core'; import {makeSignalExtensions} from '../utils/makeSignalExtensions'; import {addInitializer, initialize} from './initializers'; export interface PropertyMetadata { default?: T; interpolationFunction?: InterpolationFunction; parser?: (value: any) => T; getter?: () => T; setter?: (value: any) => void; tweener?: ( value: T, duration: number, timingFunction: TimingFunction, interpolationFunction: InterpolationFunction, ) => void; cloneable?: boolean; inspectable?: boolean; compoundParent?: string; compound?: boolean; compoundEntries: [string, string][]; } const PROPERTIES = Symbol.for('@revideo/2d/decorators/properties'); export function getPropertyMeta( object: any, key: string | symbol, ): PropertyMetadata | null { return object[PROPERTIES]?.[key] ?? null; } export function getPropertyMetaOrCreate( object: any, key: string | symbol, ): PropertyMetadata { let lookup: Record>; if (!object[PROPERTIES]) { object[PROPERTIES] = lookup = {}; } else if ( object[PROPERTIES] && !Object.prototype.hasOwnProperty.call(object, PROPERTIES) ) { object[PROPERTIES] = lookup = Object.fromEntries>( Object.entries( >>object[PROPERTIES], ).map(([key, meta]) => [key, {...meta}]), ); } else { lookup = object[PROPERTIES]; } lookup[key] ??= { cloneable: true, inspectable: true, compoundEntries: [], }; return lookup[key]; } export function getPropertiesOf( value: any, ): Record> { if (value && typeof value === 'object') { return value[PROPERTIES] ?? {}; } return {}; } export function initializeSignals(instance: any, props: Record) { initialize(instance); for (const [key, meta] of Object.entries(getPropertiesOf(instance))) { const signal = instance[key]; signal.reset(); if (props[key] !== undefined) { signal(props[key]); } if (meta.compoundEntries !== undefined) { for (const [key, property] of meta.compoundEntries) { if (property in props) { signal[key](props[property]); } } } } } /** * Create a signal decorator. * * @remarks * This decorator turns the given property into a signal. * * The class using this decorator can implement the following methods: * - `get[PropertyName]` - A property getter. * - `get[PropertyName]` - A property setter. * - `tween[PropertyName]` - A tween provider. * * @example * ```ts * class Example { * \@property() * public declare length: Signal; * } * ``` */ export function signal(): PropertyDecorator { return (target: any, key) => { // FIXME property metadata is not inherited // Consider retrieving it inside the initializer using the instance and not // the class. const meta = getPropertyMetaOrCreate(target, key); addInitializer(target, (instance: any) => { let initial: SignalValue = meta.default!; const defaultMethod = instance[`getDefault${capitalize(key as string)}`]; if (defaultMethod) { initial = () => defaultMethod.call(instance, meta.default); } const signal = new SignalContext( initial, meta.interpolationFunction ?? deepLerp, instance, meta.parser?.bind(instance), makeSignalExtensions(meta, instance, key), ); instance[key] = signal.toSignal(); }); }; } /** * Create an initial signal value decorator. * * @remarks * This decorator specifies the initial value of a property. * * Must be specified before the {@link signal} decorator. * * @example * ```ts * class Example { * \@initial(1) * \@property() * public declare length: Signal; * } * ``` * * @param value - The initial value of the property. */ export function initial(value: T): PropertyDecorator { return (target: any, key) => { const meta = getPropertyMeta(target, key); if (!meta) { useLogger().error(`Missing property decorator for "${key.toString()}"`); return; } meta.default = value; }; } /** * Create a signal interpolation function decorator. * * @remarks * This decorator specifies the interpolation function of a property. * The interpolation function is used when tweening between different values. * * Must be specified before the {@link signal} decorator. * * @example * ```ts * class Example { * \@interpolation(textLerp) * \@property() * public declare text: Signal; * } * ``` * * @param value - The interpolation function for the property. */ export function interpolation( value: InterpolationFunction, ): PropertyDecorator { return (target: any, key) => { const meta = getPropertyMeta(target, key); if (!meta) { useLogger().error(`Missing property decorator for "${key.toString()}"`); return; } meta.interpolationFunction = value; }; } /** * Create a signal parser decorator. * * @remarks * This decorator specifies the parser of a property. * Instead of returning the raw value, its passed as the first parameter to the * parser and the resulting value is returned. * * If the wrapper class has a method called `lerp` it will be set as the * default interpolation function for the property. * * Must be specified before the {@link signal} decorator. * * @example * ```ts * class Example { * \@wrapper(Vector2) * \@property() * public declare offset: Signal; * } * ``` * * @param value - The wrapper class for the property. */ export function parser(value: (value: any) => T): PropertyDecorator { return (target: any, key) => { const meta = getPropertyMeta(target, key); if (!meta) { useLogger().error(`Missing property decorator for "${key.toString()}"`); return; } meta.parser = value; }; } /** * Create a signal wrapper decorator. * * @remarks * This is a shortcut decorator for setting both the {@link parser} and * {@link interpolation}. * * The interpolation function will be set only if the wrapper class has a method * called `lerp`, which will be used as said function. * * Must be specified before the {@link signal} decorator. * * @example * ```ts * class Example { * \@wrapper(Vector2) * \@property() * public declare offset: Signal; * * // same as: * \@parser(value => new Vector2(value)) * \@interpolation(Vector2.lerp) * \@property() * public declare offset: Signal; * } * ``` * * @param value - The wrapper class for the property. */ export function wrapper( value: (new (value: any) => T) & {lerp?: InterpolationFunction}, ): PropertyDecorator { return (target: any, key) => { const meta = getPropertyMeta(target, key); if (!meta) { useLogger().error(`Missing property decorator for "${key.toString()}"`); return; } meta.parser = raw => new value(raw); if ('lerp' in value) { meta.interpolationFunction ??= value.lerp; } }; } /** * Create a cloneable property decorator. * * @remarks * This decorator specifies whether the property should be copied over when * cloning the node. * * By default, any property is cloneable. * * Must be specified before the {@link signal} decorator. * * @example * ```ts * class Example { * \@clone(false) * \@property() * public declare length: Signal; * } * ``` * * @param value - Whether the property should be cloneable. */ export function cloneable(value = true): PropertyDecorator { return (target: any, key) => { const meta = getPropertyMeta(target, key); if (!meta) { useLogger().error(`Missing property decorator for "${key.toString()}"`); return; } meta.cloneable = value; }; } /** * Create an inspectable property decorator. * * @remarks * This decorator specifies whether the property should be visible in the * inspector. * * By default, any property is inspectable. * * Must be specified before the {@link signal} decorator. * * @example * ```ts * class Example { * \@inspectable(false) * \@property() * public declare hiddenLength: Signal; * } * ``` * * @param value - Whether the property should be inspectable. */ export function inspectable(value = true): PropertyDecorator { return (target: any, key) => { const meta = getPropertyMeta(target, key); if (!meta) { useLogger().error(`Missing property decorator for "${key.toString()}"`); return; } meta.inspectable = value; }; }