import type { CSSProperties } from "vue"; import { clamp01 } from "../shared/geometry"; import type { OriginEffect } from "./types"; /** * Normalize one contribution before the compositor combines it with effects * inherited from earlier origin frames. * * Most applications should return plain effects from a choreography and let * the scene call this function. It is exported for custom compositors and * diagnostics. * * @param effect - Optional effect to normalize. * @param fallbackLayer - Relative layer contribution added to `effect.layer`. * @returns A defined effect with a numeric layer. */ export function normalizedEffect( effect: OriginEffect | undefined, fallbackLayer = 0, ): OriginEffect { return { ...effect, layer: (effect?.layer ?? 0) + fallbackLayer, }; } /** Whether a transform contributes anything to a composed transform list. */ function isComposable(transform: unknown): transform is string { return ( typeof transform === "string" && transform !== "" && transform !== "none" ); } /** Result of collapsing an ordered list of effects into one style. */ export interface ComposedEffects { /** Merged style with the composed `transform` and `opacity` applied. */ readonly style: CSSProperties; /** Sum of every effect's relative layer contribution. */ readonly layer: number; } /** * Collapse independent effect layers, ordered from the outermost inherited * frame to the most local one, into a single style. * * Transforms are concatenated and opacities multiplied, including `transform` * and numeric `opacity` supplied through `effect.style`. Every other `style` * property uses local-last precedence over `base` and earlier effects. */ export function composeEffects( effects: readonly OriginEffect[], base: CSSProperties = {}, ): ComposedEffects { const style: CSSProperties = { ...base }; const transforms: string[] = []; let opacity = 1; let layer = 0; for (const effect of effects) { if (isComposable(effect.transform)) transforms.push(effect.transform); if (effect.opacity !== undefined) opacity *= effect.opacity; layer += effect.layer ?? 0; if (effect.style) { Object.assign(style, effect.style); if (isComposable(effect.style.transform)) transforms.push(effect.style.transform); if (effect.style.opacity !== undefined) { const styleOpacity = Number(effect.style.opacity); if (Number.isFinite(styleOpacity)) opacity *= styleOpacity; } } } // Assign the composed properties last so no single effect's `style` can // replace transform work inherited from other frames. style.transform = transforms.length ? transforms.join(" ") : "none"; style.opacity = String(clamp01(opacity)); return { style, layer }; }