import { Binding, createBinding, joinBindings } from "@rbxts/react"; import { lerp } from "./math"; export interface BindingApi { subscribe: (callback: (newValue: T) => void) => () => void; update: (newValue: T) => void; getValue: () => T; } export interface Lerpable { Lerp: (this: T, to: T, alpha: number) => T; } export type BindingOrValue = Binding | T; type Bindable = Binding | NonNullable; type ComposeBindings = { [K in keyof T]: T[K] extends Bindable ? U : T[K]; }; type BindingCombiner = (...values: ComposeBindings) => U; /** * Returns whether the given value is a binding. * @param value The value to check. * @returns Whether the value is a binding. */ export function isBinding(value: T | Binding): value is Binding; export function isBinding(value: unknown): value is Binding; export function isBinding(value: unknown): value is Binding { return typeIs(value, "table") && "getValue" in value && "map" in value; } /** * Converts a value to a binding. If the given value is already a binding, it * will be returned as-is. * @param value The value to convert. * @returns The converted binding. */ export function toBinding(value: T | Binding): Binding { if (isBinding(value)) { return value; } else { const [result] = createBinding(value); return result; } } /** * Returns the value of a binding. If the given value is not a binding, it will * be returned as-is. * @param binding The binding to get the value of. * @returns The value of the binding. */ export function getBindingValue(binding: T | Binding): T { if (isBinding(binding)) { return binding.getValue(); } else { return binding; } } /** * Maps a binding to a new binding. If the given value is not a binding, it will * be passed to the mapper function and returned as a new binding. * @param binding The binding to map. * @param callback The mapper function. * @returns The mapped binding. */ export function mapBinding(binding: T | Binding, callback: (value: T) => U): Binding { if (isBinding(binding)) { return binding.map(callback); } else { const [result] = createBinding(callback(binding as T)); return result; } } /** * Joins a map of bindings into a single binding. If any of the given values * are not bindings, they will be wrapped in a new binding. * @param bindings The bindings to join. * @returns The joined binding. */ export function joinAnyBindings>>( bindings: T, ): Binding<{ [K in keyof T]: T[K] extends BindingOrValue ? U : T[K] }>; export function joinAnyBindings( bindings: readonly [...T], ): Binding<{ [K in keyof T]: T[K] extends BindingOrValue ? U : T[K] }>; export function joinAnyBindings(bindings: object): Binding { const bindingsToMap = {} as Record>; for (const [k, v] of pairs(bindings)) { bindingsToMap[k as keyof object] = toBinding(v); } return joinBindings(bindingsToMap); } /** * Gets the internal API of a binding. This is a hacky way to get access to the * `BindingInternalApi` object of a binding, which is not exposed by React. * @param binding The binding to get the internal API of. * @returns The binding's API. */ export function getBindingApi(binding: Binding) { for (const [key, value] of pairs(binding)) { const name = tostring(key); if (name === "Symbol(BindingImpl)" || name.sub(1, 12) === "RoactBinding") { return value as unknown as BindingApi; } } } /** * Returns a binding that lerps between two values using the given binding as * the alpha. * @param binding The binding to use as the alpha. * @param from The value to lerp from. * @param to The value to lerp to. * @returns A binding that lerps between two values. */ export function lerpBinding>( binding: Binding | number, from: T, to: T, ): Binding { return mapBinding(binding, (alpha) => { if (typeIs(from, "number")) { return lerp(from, to as number, alpha); } else { return from.Lerp(to, alpha); } }); } /** * Composes multiple bindings or values together into a single binding. * Calls the combiner function with the values of the bindings when any * of the bindings change. * @param ...bindings A list of bindings or values. * @param combiner The function that maps the bindings to a new value. * @returns A binding that returns the result of the combiner. */ export function composeBindings(...bindings: [...T, BindingCombiner]): Binding; export function composeBindings(...values: [...Bindable[], BindingCombiner]): Binding { const combiner = values.pop() as BindingCombiner; const bindings = values.map(toBinding); return joinBindings(bindings).map((bindings) => combiner(...bindings)); }