Rewrite the core-v2 internals without changing the public API or runtime behavior. The four large files (types, scene, motion, gesture) become layered modules with one responsibility each: - shared/: geometry and environment helpers - view/: originView recipes - choreography/: effect types, presets, and composeEffects compositing - actions/: forward/back/replace/above/under via one overload resolver - scene/: SceneGraph storage, transitions (history rules), compositor (styles and interactivity), spring (pure stepper + rAF driver), and createOriginScene, which only wires them and owns async timing - composables/: useOrigin and the node-scope injection key - gesture/: builder, definition->policy normalization, start admission, DirectionalTracker, and a GestureRecognizer session state machine replacing fifteen loose closure variables - components/: renderers and gesture hosts, plus their prop types Verification (packages/core-v2/verify): differential fuzzing drives main and this branch with identical random operation sequences and pointer streams and requires identical scene state, styles, contexts, callbacks, and promise results at every step. It caught every planted mutation that applied. Benchmarks are at parity. Also fix a pre-existing test that called the defineProps macro inside a runtime defineComponent, and correct API.md's description of the flick commit threshold to match the runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Faarh2Ts6StJPcz2mRiHiR
81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
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 };
|
|
}
|