From 329271691c25664c791fc099aa05b2051b5d1b36 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 01:15:12 +0000 Subject: [PATCH] Restructure core-v2 into layered modules 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 Claude-Session: https://claude.ai/code/session_01Faarh2Ts6StJPcz2mRiHiR --- packages/core-v2/API.md | 5 +- packages/core-v2/README.md | 20 + packages/core-v2/src/actions/actions.ts | 261 ++++++ packages/core-v2/src/actions/types.ts | 106 +++ packages/core-v2/src/choreography/compose.ts | 80 ++ packages/core-v2/src/choreography/define.ts | 27 + packages/core-v2/src/choreography/presets.ts | 55 ++ packages/core-v2/src/choreography/types.ts | 133 +++ .../core-v2/src/components/OriginGesture.vue | 11 +- .../src/components/OriginGestureSurface.vue | 67 +- .../core-v2/src/components/OriginNodeHost.vue | 4 +- .../core-v2/src/components/OriginScene.vue | 2 +- packages/core-v2/src/components/types.ts | 83 ++ packages/core-v2/src/composables/useOrigin.ts | 92 ++ packages/core-v2/src/gesture.test.ts | 23 +- packages/core-v2/src/gesture.ts | 582 ------------ packages/core-v2/src/gesture/builder.ts | 123 +++ packages/core-v2/src/gesture/direction.ts | 51 ++ packages/core-v2/src/gesture/policy.ts | 87 ++ packages/core-v2/src/gesture/recognizer.ts | 183 ++++ packages/core-v2/src/gesture/start.ts | 115 +++ packages/core-v2/src/gesture/tracker.ts | 86 ++ packages/core-v2/src/gesture/types.ts | 290 ++++++ .../core-v2/src/gesture/useOriginGesture.ts | 60 ++ packages/core-v2/src/index.ts | 46 +- packages/core-v2/src/lifecycle.ts | 53 -- packages/core-v2/src/motion.ts | 370 -------- packages/core-v2/src/scene.test.ts | 10 +- packages/core-v2/src/scene.ts | 773 ---------------- packages/core-v2/src/scene/compositor.ts | 148 ++++ .../core-v2/src/scene/createOriginScene.ts | 174 ++++ packages/core-v2/src/scene/graph.ts | 203 +++++ packages/core-v2/src/scene/spring.ts | 90 ++ packages/core-v2/src/scene/transitions.ts | 278 ++++++ packages/core-v2/src/scene/types.ts | 220 +++++ packages/core-v2/src/shared/environment.ts | 13 + packages/core-v2/src/shared/geometry.ts | 38 + packages/core-v2/src/types.ts | 831 ------------------ packages/core-v2/src/view/originView.ts | 44 + packages/core-v2/src/view/types.ts | 43 + packages/core-v2/verify/.gitignore | 1 + packages/core-v2/verify/README.md | 27 + packages/core-v2/verify/gesture.diff.ts | 381 ++++++++ packages/core-v2/verify/harness.ts | 110 +++ packages/core-v2/verify/scene.bench.ts | 101 +++ packages/core-v2/verify/scene.diff.ts | 285 ++++++ packages/core-v2/verify/setup-baseline.sh | 13 + packages/core-v2/verify/vitest.config.ts | 17 + 48 files changed, 4134 insertions(+), 2681 deletions(-) create mode 100644 packages/core-v2/src/actions/actions.ts create mode 100644 packages/core-v2/src/actions/types.ts create mode 100644 packages/core-v2/src/choreography/compose.ts create mode 100644 packages/core-v2/src/choreography/define.ts create mode 100644 packages/core-v2/src/choreography/presets.ts create mode 100644 packages/core-v2/src/choreography/types.ts create mode 100644 packages/core-v2/src/components/types.ts create mode 100644 packages/core-v2/src/composables/useOrigin.ts delete mode 100644 packages/core-v2/src/gesture.ts create mode 100644 packages/core-v2/src/gesture/builder.ts create mode 100644 packages/core-v2/src/gesture/direction.ts create mode 100644 packages/core-v2/src/gesture/policy.ts create mode 100644 packages/core-v2/src/gesture/recognizer.ts create mode 100644 packages/core-v2/src/gesture/start.ts create mode 100644 packages/core-v2/src/gesture/tracker.ts create mode 100644 packages/core-v2/src/gesture/types.ts create mode 100644 packages/core-v2/src/gesture/useOriginGesture.ts delete mode 100644 packages/core-v2/src/lifecycle.ts delete mode 100644 packages/core-v2/src/motion.ts delete mode 100644 packages/core-v2/src/scene.ts create mode 100644 packages/core-v2/src/scene/compositor.ts create mode 100644 packages/core-v2/src/scene/createOriginScene.ts create mode 100644 packages/core-v2/src/scene/graph.ts create mode 100644 packages/core-v2/src/scene/spring.ts create mode 100644 packages/core-v2/src/scene/transitions.ts create mode 100644 packages/core-v2/src/scene/types.ts create mode 100644 packages/core-v2/src/shared/environment.ts create mode 100644 packages/core-v2/src/shared/geometry.ts delete mode 100644 packages/core-v2/src/types.ts create mode 100644 packages/core-v2/src/view/originView.ts create mode 100644 packages/core-v2/src/view/types.ts create mode 100644 packages/core-v2/verify/.gitignore create mode 100644 packages/core-v2/verify/README.md create mode 100644 packages/core-v2/verify/gesture.diff.ts create mode 100644 packages/core-v2/verify/harness.ts create mode 100644 packages/core-v2/verify/scene.bench.ts create mode 100644 packages/core-v2/verify/scene.diff.ts create mode 100755 packages/core-v2/verify/setup-baseline.sh create mode 100644 packages/core-v2/verify/vitest.config.ts diff --git a/packages/core-v2/API.md b/packages/core-v2/API.md index 4533a21..903b1cf 100644 --- a/packages/core-v2/API.md +++ b/packages/core-v2/API.md @@ -553,7 +553,10 @@ When `finish()` does not explicitly override the decision, a target commits when either: - `progress >= commitThreshold`, default `0.36`; or -- `progress >= 0.06` and `velocity >= commitVelocity`, default `0.9`. +- release `velocity >= 0.3` progress units per second, at any progress. + +`commitVelocity` is accepted on choreographies but is not currently consulted; +the flick threshold is the fixed `0.3` above. The operation's intent becomes final before its spring settles. diff --git a/packages/core-v2/README.md b/packages/core-v2/README.md index 9bdb54a..db77f5d 100644 --- a/packages/core-v2/README.md +++ b/packages/core-v2/README.md @@ -292,6 +292,26 @@ flat host. “Y is Z's origin” is a coordinate and retained-history relationship, not Vue component ancestry. +## Source layout + +The package is layered; each layer depends only on the ones above it. + +| Directory | Responsibility | +| --------------- | ------------------------------------------------------------------------------------------------------- | +| `shared/` | Geometry and environment helpers | +| `view/` | `originView()` component recipes | +| `choreography/` | Effect types, `defineOriginChoreography()`, presets, and `composeEffects()` style compositing | +| `actions/` | `forward()`, `back()`, `replace()`, `above()`, `under()`, `originAction()` | +| `scene/` | `graph.ts` storage, `transitions.ts` history rules, `compositor.ts` styles, `spring.ts` settling motion | +| `composables/` | `useOrigin()` and the node-scope injection key | +| `gesture/` | Builder, definition→policy normalization, start admission, pointer tracking, and the recognizer | +| `components/` | `OriginScene`, `OriginGesture`, `OriginGestureSurface`, and the internal node host | + +`createOriginScene()` wires the scene modules together: the graph holds nodes +and operation edges, transitions rewrite it synchronously when operations +begin, commit, or cancel, and the scene adds the asynchronous parts (waiting +for Vue to mount a target, measuring, and running the settling spring). + ## Current experimental boundaries - One operation creates one target. Chaining operations already permits any diff --git a/packages/core-v2/src/actions/actions.ts b/packages/core-v2/src/actions/actions.ts new file mode 100644 index 0000000..3537263 --- /dev/null +++ b/packages/core-v2/src/actions/actions.ts @@ -0,0 +1,261 @@ +import { isChoreography } from "../choreography/define"; +import type { OriginChoreography } from "../choreography/types"; +import type { OriginView } from "../view/types"; +import type { + OriginAction, + OriginActionOptions, + OriginHistoryMode, + OriginNavigationActionOptions, + OriginNavigationIntent, + OriginPlacement, + OriginPlacementActionOptions, +} from "./types"; + +/* + * Every helper accepts either `(…, options?)`, producing an animation-free + * navigation intent for gesture `.navigate()`, or `(…, choreography, + * options?)`, producing a complete action for `begin()`/`perform()`. + */ + +interface HelperArguments { + readonly choreography?: OriginChoreography; + readonly options: Options; +} + +function helperArguments( + choreographyOrOptions: OriginChoreography | Options | undefined, + options: Options | undefined, +): HelperArguments> { + return isChoreography(choreographyOrOptions) + ? { choreography: choreographyOrOptions, options: options ?? {} } + : { options: choreographyOrOptions ?? {} }; +} + +function navigation( + target: OriginView | undefined, + placement: OriginPlacement, + history: OriginHistoryMode, +): OriginNavigationIntent { + return { target, placement, history }; +} + +/** Back navigation resolves its target at execution time, so it has none. */ +function targetlessNavigation( + placement: OriginPlacement, + history: OriginHistoryMode, +): OriginNavigationIntent { + return { placement, history }; +} + +/** Pair a navigation intent with choreography, or return it unchanged. */ +function materialize( + intent: OriginNavigationIntent, + choreography: OriginChoreography | undefined, +): OriginNavigationIntent | OriginAction { + if (!choreography) return intent; + const { placement, history } = intent; + return "target" in intent + ? { target: intent.target, choreography, placement, history } + : { choreography, placement, history }; +} + +/** + * Create the scene mutation invoked by a click, gesture, hardware command, or + * any other application event. `above` and `under` affect stacking only; they + * do not imply a universal navigation direction. + * + * @param target - View recipe to create. + * @param choreography - Visual routine controlling the operation. + * @param options - Stacking and history behavior. + * @returns An action that can be passed to `begin()`, `perform()`, or returned + * from a gesture action factory. + */ +export function originAction( + target: OriginView, + choreography: OriginChoreography, + options: OriginActionOptions = {}, +): OriginAction { + return materialize( + navigation(target, options.placement ?? "above", options.history ?? "push"), + choreography, + ) as OriginAction; +} + +/** + * Create a retained-history push intent or complete action. + * + * Committing the action parks its origin instance and leaves it mounted until + * a later committed {@link back} action pops the new entry. Omit choreography + * when declaring `.navigate()` inside a gesture builder; provide choreography + * when passing the result directly to `begin()` or `perform()`. + * + * @param target - View recipe created if the push commits. + * @param options - Stacking options for an animation-free navigation intent. + * @returns An animation-free intent for use with a gesture builder. + */ +export function forward( + target: OriginView, + options?: OriginNavigationActionOptions, +): OriginNavigationIntent; +export function forward( + target: OriginView, + choreography: OriginChoreography, + options?: OriginNavigationActionOptions, +): OriginAction; +export function forward( + target: OriginView, + choreographyOrOptions?: OriginChoreography | OriginNavigationActionOptions, + maybeOptions?: OriginNavigationActionOptions, +): OriginAction | OriginNavigationIntent { + const { choreography, options } = helperArguments( + choreographyOrOptions, + maybeOptions, + ); + return materialize( + navigation(target, options.placement ?? "above", "push"), + choreography, + ); +} + +/** + * Create a retained-history replacement intent or complete action. + * + * The target inherits the origin's previous mounted entry instead of retaining + * the origin itself. A commit unmounts the origin and activates the target, so + * a later {@link back} skips the replaced entry. A cancellation removes the + * proposed target and restores the origin unchanged. + * + * Omit choreography when declaring `.navigate()` inside a gesture builder; + * provide choreography when passing the result directly to `begin()` or + * `perform()`. + * + * @param target - View recipe created for the replacement. + * @param options - Stacking options for an animation-free navigation intent. + * @returns An animation-free replacement intent for use with a gesture builder. + */ +export function replace( + target: OriginView, + options?: OriginNavigationActionOptions, +): OriginNavigationIntent; +export function replace( + target: OriginView, + choreography: OriginChoreography, + options?: OriginNavigationActionOptions, +): OriginAction; +export function replace( + target: OriginView, + choreographyOrOptions?: OriginChoreography | OriginNavigationActionOptions, + maybeOptions?: OriginNavigationActionOptions, +): OriginAction | OriginNavigationIntent { + const { choreography, options } = helperArguments( + choreographyOrOptions, + maybeOptions, + ); + return materialize( + navigation(target, options.placement ?? "above", "replace"), + choreography, + ); +} + +/** + * Create a retained-history back intent or complete action. + * + * The action intentionally has no target recipe. At execution time the scene + * resolves the origin's mounted `previousNodeKey`, reveals that exact instance, + * and removes the current instance only if the operation commits. Omit + * choreography inside gesture `.navigate()`; provide it for a programmatic + * action. + * + * @param options - Stacking options for an animation-free navigation intent. + * @returns An animation-free back intent for use with a gesture builder. + */ +export function back( + options?: OriginNavigationActionOptions, +): OriginNavigationIntent; +export function back( + choreography: OriginChoreography, + options?: OriginNavigationActionOptions, +): OriginAction; +export function back( + choreographyOrOptions?: OriginChoreography | OriginNavigationActionOptions, + maybeOptions?: OriginNavigationActionOptions, +): OriginAction | OriginNavigationIntent { + const { choreography, options } = helperArguments( + choreographyOrOptions, + maybeOptions, + ); + return materialize( + targetlessNavigation(options.placement ?? "under", "back"), + choreography, + ); +} + +/** + * Create an intent or action whose target is stacked above its origin. + * + * This helper controls stacking, not movement direction. The supplied + * choreography may move either component however the application chooses. + * Omitting choreography produces an intent for gesture `.navigate()`. + * + * @param target - View recipe to create. + * @param choreography - Visual routine controlling the operation. + * @param options - Optional history behavior. + */ +export function above( + target: OriginView, + options?: OriginPlacementActionOptions, +): OriginNavigationIntent; +export function above( + target: OriginView, + choreography: OriginChoreography, + options?: OriginPlacementActionOptions, +): OriginAction; +export function above( + target: OriginView, + choreographyOrOptions?: OriginChoreography | OriginPlacementActionOptions, + maybeOptions?: OriginPlacementActionOptions, +): OriginAction | OriginNavigationIntent { + const { choreography, options } = helperArguments( + choreographyOrOptions, + maybeOptions, + ); + return materialize( + navigation(target, "above", options.history ?? "push"), + choreography, + ); +} + +/** + * Create an intent or action whose target is stacked underneath its origin. + * + * Commonly used for custom reveal effects, but it has no implicit history + * meaning. Prefer {@link back} for retained-history navigation. Omitting + * choreography produces an intent for gesture `.navigate()`. + * + * @param target - View recipe to create underneath the origin. + * @param choreography - Visual routine controlling the operation. + * @param options - Optional history behavior. + */ +export function under( + target: OriginView, + options?: OriginPlacementActionOptions, +): OriginNavigationIntent; +export function under( + target: OriginView, + choreography: OriginChoreography, + options?: OriginPlacementActionOptions, +): OriginAction; +export function under( + target: OriginView, + choreographyOrOptions?: OriginChoreography | OriginPlacementActionOptions, + maybeOptions?: OriginPlacementActionOptions, +): OriginAction | OriginNavigationIntent { + const { choreography, options } = helperArguments( + choreographyOrOptions, + maybeOptions, + ); + return materialize( + navigation(target, "under", options.history ?? "push"), + choreography, + ); +} diff --git a/packages/core-v2/src/actions/types.ts b/packages/core-v2/src/actions/types.ts new file mode 100644 index 0000000..0c6f7b7 --- /dev/null +++ b/packages/core-v2/src/actions/types.ts @@ -0,0 +1,106 @@ +import type { OriginChoreography } from "../choreography/types"; +import type { OriginView } from "../view/types"; + +/** + * Controls how an operation participates in retained instance history. + * + * - `push`: create a target whose previous entry is the mounted origin. + * - `replace`: create a target that inherits the origin's previous entry, then + * remove the origin on commit. + * - `back`: reuse the mounted previous entry and pop the origin on commit. + */ +export type OriginHistoryMode = "push" | "replace" | "back"; + +/** + * The target's stacking relationship to its origin while an operation exists. + */ +export type OriginPlacement = "above" | "under"; + +/** + * A scene mutation without visual choreography. + * + * Gesture builders keep navigation intent separate from animation so the same + * destination can be paired with different component-local interactions. + * Calling `.animate()` materializes this intent as an {@link OriginAction}. + */ +export interface OriginNavigationIntent { + /** + * Recipe for a newly created push or replacement target. Back navigation + * omits this because the scene resolves the retained previous instance. + */ + readonly target?: OriginView; + /** Target stacking relationship while the gesture operation is visible. */ + readonly placement?: OriginPlacement; + /** Retained-history mutation performed if the gesture commits. */ + readonly history: OriginHistoryMode; +} + +/** A complete request to create and animate a target view from an origin. */ +export interface OriginAction { + /** + * Recipe for a newly created push or replacement target. A back action omits + * this because the scene resolves its already-mounted previous entry. + */ + readonly target?: OriginView; + /** Visual relationship applied to the source, target, and shared frame. */ + readonly choreography: OriginChoreography; + /** + * Target stacking relationship during the operation. + * + * @defaultValue `"above"` + */ + readonly placement?: OriginPlacement; + /** + * History mutation applied to the target recipe. + * + * @defaultValue `"push"` + */ + readonly history?: OriginHistoryMode; +} + +/** + * Values accepted from gesture action factories. + * + * Returning `null` or `undefined` declines the recognized gesture. A promise + * allows lazy target resolution; stale results are discarded after release or + * cancellation. + */ +export type MaybeOriginAction = + OriginAction | null | undefined | Promise; + +/** Options accepted by {@link originAction}. */ +export interface OriginActionOptions { + /** + * Target stacking relationship during the operation. + * + * @defaultValue `"above"` + */ + placement?: OriginPlacement; + /** + * History mutation assigned to the created target node. + * + * @defaultValue `"push"` + */ + history?: OriginHistoryMode; +} + +/** History options shared by the {@link above} and {@link under} helpers. */ +export interface OriginPlacementActionOptions { + /** + * History mutation assigned to the created target node. + * + * @defaultValue `"push"` + */ + history?: OriginHistoryMode; +} + +/** Stacking options accepted by retained-history navigation helpers. */ +export interface OriginNavigationActionOptions { + /** + * Target stacking relationship during the operation. + * + * @defaultValue `"above"` for {@link forward} and {@link replace}, `"under"` + * for {@link back} + */ + placement?: OriginPlacement; +} diff --git a/packages/core-v2/src/choreography/compose.ts b/packages/core-v2/src/choreography/compose.ts new file mode 100644 index 0000000..6df33b4 --- /dev/null +++ b/packages/core-v2/src/choreography/compose.ts @@ -0,0 +1,80 @@ +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 }; +} diff --git a/packages/core-v2/src/choreography/define.ts b/packages/core-v2/src/choreography/define.ts new file mode 100644 index 0000000..2d8c014 --- /dev/null +++ b/packages/core-v2/src/choreography/define.ts @@ -0,0 +1,27 @@ +import type { OriginChoreography } from "./types"; + +/** + * Preserve type inference while declaring a custom choreography. + * + * The helper performs no runtime transformation. It gives custom routines a + * named, documented construction point and validates their shape in TypeScript. + * + * @param choreography - Side-effect-free visual effect calculator and optional + * release thresholds. + * @returns The same choreography object. + */ +export function defineOriginChoreography( + choreography: OriginChoreography, +): OriginChoreography { + return choreography; +} + +/** Distinguish a choreography from a plain options object. */ +export function isChoreography(value: unknown): value is OriginChoreography { + return ( + typeof value === "object" && + value !== null && + "effects" in value && + typeof value.effects === "function" + ); +} diff --git a/packages/core-v2/src/choreography/presets.ts b/packages/core-v2/src/choreography/presets.ts new file mode 100644 index 0000000..0cab8b4 --- /dev/null +++ b/packages/core-v2/src/choreography/presets.ts @@ -0,0 +1,55 @@ +import { defineOriginChoreography } from "./define"; + +const percent = (value: number) => `${(value * 100).toFixed(4)}%`; + +/** + * Native-style forward motion: the target enters above the source while the + * source recedes slightly. These presets are examples; applications can + * replace them with arbitrary `defineOriginChoreography()` callbacks. + */ +export const slideLeft = defineOriginChoreography({ + name: "slide-left", + commitThreshold: 0.36, + commitVelocity: 0.6, + effects: ({ progress }) => ({ + source: { + transform: `translate3d(${percent(progress * -0.24)}, 0, 0) scale(${1 - progress * 0.025})`, + opacity: 1 - progress * 0.16, + }, + target: { + transform: `translate3d(${percent(1 - progress)}, 0, 0)`, + }, + }), +}); + +/** + * Back motion reveals the target underneath the source. This is merely a + * visual routine; the `back()` action selects retained-history behavior. + */ +export const slideRight = defineOriginChoreography({ + name: "slide-right", + commitThreshold: 0.36, + commitVelocity: 0.6, + effects: ({ progress }) => ({ + source: { + transform: `translate3d(${percent(progress)}, 0, 0)`, + }, + target: { + transform: `translate3d(${percent(-0.24 + progress * 0.24)}, 0, 0) scale(${0.975 + progress * 0.025})`, + opacity: 0.84 + progress * 0.16, + }, + }), +}); + +/** + * Cross-fade preset that fades the source out while fading the target in. + * + * The default release thresholds from the scene are used. + */ +export const fade = defineOriginChoreography({ + name: "fade", + effects: ({ progress }) => ({ + source: { opacity: 1 - progress }, + target: { opacity: progress }, + }), +}); diff --git a/packages/core-v2/src/choreography/types.ts b/packages/core-v2/src/choreography/types.ts new file mode 100644 index 0000000..30be007 --- /dev/null +++ b/packages/core-v2/src/choreography/types.ts @@ -0,0 +1,133 @@ +import type { CSSProperties } from "vue"; +import type { OriginRect } from "../shared/geometry"; + +/** + * The lifecycle phase of an operation edge. + * + * - `preparing`: the target has been added and Vue is mounting it. + * - `interactive`: progress may be controlled by a gesture or application. + * - `settling`: the commit/cancel decision is fixed and the spring is running. + * - `finished`: the graph rewrite or cancellation cleanup has completed. + */ +export type OriginOperationPhase = + "preparing" | "interactive" | "settling" | "finished"; + +/** + * The outcome selected for an operation. + * + * Intent remains `undecided` until `finish()` is called. It becomes final + * before the settling animation completes, allowing the retained target to + * originate its own operation immediately. + */ +export type OriginOperationIntent = "undecided" | "commit" | "cancel"; + +/** + * One composable contribution to a scene node's final visual style. + * + * Transforms are concatenated in origin-to-descendant order and opacity values + * are multiplied. This lets X→Y and Y→Z affect Y simultaneously without either + * routine replacing the other routine's CSS transform. + */ +export interface OriginEffect { + /** + * A CSS transform contribution. Transform strings from inherited and local + * origin frames are concatenated rather than replacing one another. + */ + transform?: string; + /** + * An opacity contribution between `0` and `1`. Contributions from multiple + * frames are multiplied and the final value is clamped. + */ + opacity?: number; + /** + * Relative stacking contribution. `above()` defaults the target to +1 and + * `under()` defaults it to -1. + */ + layer?: number; + /** + * Escape hatch for non-geometric effects such as filter, clipPath, or + * borderRadius. Later/local effects override inherited properties. + */ + style?: CSSProperties; +} + +/** + * Independent visual contributions calculated for one operation edge. + * + * Any omitted contribution is treated as an identity effect. + */ +export interface OriginEffectSet { + /** + * Applied to both source and target. This is useful for moving an entire + * origin-relative coordinate frame. + */ + frame?: OriginEffect; + /** Applied to the component that initiated this operation. */ + source?: OriginEffect; + /** Applied to the component created by this operation and its descendants. */ + target?: OriginEffect; +} + +/** Values supplied whenever a choreography calculates its visual effects. */ +export interface OriginChoreographyContext { + /** Normalized operation progress, clamped to the inclusive range `0..1`. */ + progress: number; + /** + * Normalized progress units per second. Positive velocity moves toward + * commit; negative velocity moves back toward cancellation. + */ + velocity: number; + /** Current lifecycle phase of the operation. */ + phase: OriginOperationPhase; + /** Commit/cancel outcome, if release has already selected one. */ + intent: OriginOperationIntent; + /** Origin host bounds captured immediately before the target mounts. */ + originRect?: OriginRect; + /** Target host bounds measured after Vue mounts the target. */ + targetRect?: OriginRect; + /** Scene-container bounds, or the browser viewport when no container exists. */ + viewport: OriginRect; +} + +/** + * A choreography describes visual relationships only. Component creation, + * history, and cleanup are performed by the scene operation that uses it. + */ +export interface OriginChoreography { + /** Optional diagnostic name surfaced by scene inspectors and devtools. */ + readonly name?: string; + /** + * Calculate source, target, and shared-frame contributions for the current + * operation state. This function should be deterministic and side-effect + * free because it can run many times per animation frame. + */ + effects(context: OriginChoreographyContext): OriginEffectSet; + /** + * Gesture progress required to retain the target after release. + * + * @defaultValue `0.36` + */ + readonly commitThreshold?: number; + /** + * Reserved: normalized positive release velocity intended to commit a + * deliberate flick. + * + * The scene currently commits any release whose velocity is at least `0.3`, + * regardless of this value. See `FLICK_COMMIT_VELOCITY` in the scene module. + */ + readonly commitVelocity?: number; + /** + * Keep this choreography's progress-`1` effects connected after a committed + * push instead of collapsing the edge and hiding its source. + * + * This is intended for partial presentations such as drawers and inspectors + * where part of the retained source remains visible beside the target. The + * source remains mounted, visible, and inert. A back operation temporarily + * suspends the resting edge; cancelling back restores it. + * + * Only retained-history `push` actions support connected resting effects. + * + * @defaultValue `false` + */ + readonly persistAtRest?: boolean; +} diff --git a/packages/core-v2/src/components/OriginGesture.vue b/packages/core-v2/src/components/OriginGesture.vue index 9f5ce3d..e716ba6 100644 --- a/packages/core-v2/src/components/OriginGesture.vue +++ b/packages/core-v2/src/components/OriginGesture.vue @@ -1,7 +1,6 @@