@onmark/cli 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,177 +1,141 @@
1
- # Onmark
1
+ <h1 align="center">Onmark</h1>
2
2
 
3
- Onmark is a screenplay-first video compiler and rendering engine for people and
4
- agents.
3
+ <p align="center"><strong>Write the film. Own every frame.</strong></p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@onmark/cli"><img src="https://img.shields.io/npm/v/%40onmark%2Fcli?color=111214" alt="npm version"></a>
7
+ <a href="https://github.com/varo-yang/onmark/actions/workflows/ci.yml"><img src="https://github.com/varo-yang/onmark/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
8
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-f04d32" alt="MIT license"></a>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://onmark-cdn-1313593665.cos.ap-guangzhou.myqcloud.com/onmark-hero-compiled-draft-v2.mp4">
13
+ <img src="docs/onmark-hero.gif" alt="Onmark screenplay compiling into a rendered composition" width="720">
14
+ </a>
15
+ </p>
16
+
17
+ Onmark is a screenplay-first video compiler for people and agents. Write one
18
+ HTML document with semantic film elements, ordinary CSS, and optional seekable
19
+ motion. Onmark compiles the authored intent into an exact timeline, renders it
20
+ through Chromium, mixes media with FFmpeg, and writes an MP4.
5
21
 
6
22
  ```text
7
- screenplay → deterministic Timeline IR → browser frames + audio plan → MP4
23
+ screenplay → Timeline IR → Render Units → browser frames + audio → MP4
8
24
  ```
9
25
 
10
- Delivery gates one through seven and distributed incremental reuse are
11
- complete. No later delivery gate is currently active. Gate one renders and
12
- independently verifies one real screenplay through the compiler, browser
13
- protocol, Chromium, and FFmpeg. Gate two partitions one media-bearing two-shot
14
- film into two local Render Units and
15
- proves that their assembled decoded video and audio match the whole-film
16
- baseline. Gate three sends those
17
- same portable units to two concurrent arm64 Lambda workers, verifies their
18
- immutable S3 frame artifacts against a remote whole-film capture by canonical
19
- raw-RGBA pixels, and assembles the partitions through the shared H.264/AAC
20
- path. Gate four carries authored music, shot-local effects, voice-over, and
21
- imported SRT/WebVTT/ASS captions through the same local whole-film and
22
- partitioned paths, with both canonical raw-RGBA frames and decoded audio
23
- checked for equivalence.
24
-
25
- Gate five admits exact-frame effects and a closed presentation temporal
26
- capability after bounded WAAPI, GSAP, and Three.js experiments. Its exit
27
- conformance proves that the effect-bearing presentation produces identical
28
- canonical pixels as one whole-film unit or two independent units. Unknown
29
- presentation code remains sequential. Deployment work remains frozen.
30
-
31
- Gate six carries content-addressed local image, SVG, and font resources through
32
- bounded browser readiness that names the resource still pending. It also gives
33
- film, scene, shot, and content facts Rust-owned node identity and ownership.
34
- Authored IDs retain semantic identity across unit projections. Its Linux exit
35
- conformance proves equal pixels across cold browsers and whole-film versus
36
- partitioned execution without adding a general props channel, new screenplay
37
- spelling, or a second timing system.
38
-
39
- Gate seven admits transparent browser presentation over persistent native media
40
- decode and composition for an explicitly separable visual capability. Its
41
- locked experiment passes at 4.18× the Chromium-media baseline speed and 79.73%
42
- of its peak RSS. The production executor preserves a bounded stream, exact
43
- whole-film/partition equivalence, declared color conformance, and the existing
44
- Chromium-media path for presentations without the capability. The real-process
45
- exit fixture passes; this is neither new screenplay syntax nor a hidden
46
- fallback.
47
-
48
- The executor now also carries an independent, proved browser-frame behavior.
49
- For placement-bounded foregrounds it captures only the first frame and solved
50
- placement changes, then shares the immutable PNG between intervening output
51
- frames. The real-process layered control reduces browser evaluation from 75
52
- authored frames to one while retaining exact canonical raw-RGBA output;
53
- browser-owned video and unknown authored effects remain per-frame.
54
-
55
- The completed foundation includes the pure compiler and versioned wire types in
56
- `onmark-core`; bounded ffprobe and strict SubRip/WebVTT/ASS normalization in
57
- `onmark-media`; deterministic video and overlay presentation in
58
- `@onmark/runtime`; authored-HTML bindings in `@onmark/authoring`; immutable
59
- presentation artifacts in `@onmark/bundler`; the typed Chromium-to-FFmpeg
60
- executor in `onmark-render`; and the `onmark-cli` composition root. A production
61
- deployment workflow and infrastructure definition remain deliberately absent;
62
- there is still no queue, lease system, scheduler, or coordinator.
63
-
64
- ## Render
65
-
66
- The native command needs one authored HTML document and writes a no-clobber
67
- `renders/<name>.mp4` by default. Onmark custom elements carry screenplay intent;
68
- ordinary HTML and inline CSS carry presentation. An optional inline
69
- `type="module" data-om-motion` script exports vendor-neutral motion
70
- assembled from adapters such as `onmark/motion/gsap`:
26
+ ## One timeline. Three visual languages.
27
+
28
+ Each film is one authored HTML document rendered by the same deterministic CLI.
29
+
30
+ <table>
31
+ <tr>
32
+ <td width="33%" align="center">
33
+ <a href="https://onmark-cdn-1313593665.cos.ap-guangzhou.myqcloud.com/onmark-luminous-story.mp4">
34
+ <img src="docs/showcase-luminous.gif" alt="Luminous Story: chromatic light and kinetic typography">
35
+ </a>
36
+ </td>
37
+ <td width="33%" align="center">
38
+ <a href="https://onmark-cdn-1313593665.cos.ap-guangzhou.myqcloud.com/onmark-exact-system.mp4">
39
+ <img src="docs/showcase-system.gif" alt="Exact System: black-and-lime deterministic interface">
40
+ </a>
41
+ </td>
42
+ <td width="33%" align="center">
43
+ <a href="https://onmark-cdn-1313593665.cos.ap-guangzhou.myqcloud.com/onmark-printed-motion.mp4">
44
+ <img src="docs/showcase-printed.gif" alt="Printed Motion: monochrome editorial choreography">
45
+ </a>
46
+ </td>
47
+ </tr>
48
+ <tr>
49
+ <td align="center"><strong>Luminous Story</strong><br><sub>Chromatic atmosphere</sub></td>
50
+ <td align="center"><strong>Exact System</strong><br><sub>Deterministic interface</sub></td>
51
+ <td align="center"><strong>Printed Motion</strong><br><sub>Editorial choreography</sub></td>
52
+ </tr>
53
+ </table>
54
+
55
+ ## Quick start
56
+
57
+ Install the desktop CLI on macOS arm64, Linux x64, or Windows x64:
71
58
 
72
59
  ```bash
73
60
  npm install --global @onmark/cli
61
+ ```
62
+
63
+ Create `film.html`:
64
+
65
+ ```html
66
+ <om-film>
67
+ <om-scene>
68
+ <om-shot duration="3s">
69
+ <om-title>Hello, motion.</om-title>
70
+ </om-shot>
71
+ </om-scene>
72
+ </om-film>
73
+ ```
74
+
75
+ Render it:
76
+
77
+ ```bash
74
78
  onmark render film.html
75
- onmark render film.html --output review.mp4
76
- onmark render film.html --fps 30000/1001 --width 1920 --height 1080
77
- onmark render film.html --subtitle captions.vtt
78
79
  ```
79
80
 
80
- `--subtitle` imports strict UTF-8 `.srt`, `.vtt`, or `.ass` files without adding
81
- external-format syntax to the screenplay. Invalid files produce diagnostics
82
- against their own path and byte spans before browser or media processes start.
83
-
84
- Presentation capabilities are not inferred from source text or observed pixel
85
- equality. The production authored-HTML adapter is admitted as random-access,
86
- browser-composited, and per-frame; unknown future components remain sequential
87
- until their own conformance proves otherwise. Immutable bundle metadata carries
88
- the admitted facts into Render Graph planning.
89
-
90
- The desktop command projects each shot into an isolated browser region and
91
- reuses verified contract-addressed frame artifacts across processes. Editing one
92
- shot leaves unrelated region artifacts intact; final encoding and audio mixing
93
- still use the same assembler as a cold render. Every completed command reports
94
- the exact reused region and frame counts plus pipeline phase timings, so
95
- incremental work is visible without inspecting the cache.
96
-
97
- The Lambda adapter applies the same artifact identity before materializing
98
- inputs or preparing Chromium. A verified S3 hit skips capture; a corrupt object
99
- is conditionally repaired through the ordinary worker path. This closes
100
- distributed incremental reuse without adding a coordinator or durable progress
101
- service.
102
-
103
- ## Showcases
104
-
105
- [`showcases/`](showcases/) contains twenty self-contained films rendered through
106
- the public CLI. They cover native media, imported captions, audio, GSAP, Canvas
107
- 2D, raw WebGL, Three.js, spatial CSS, editorial typography, and continuous
108
- brand motion without relying on a shared visual template or remote assets.
109
- Rendered outputs remain untracked; each checked-in source and exact render
110
- command stays reproducible.
111
-
112
- The desktop artifact is admitted on macOS arm64, Linux x64, and Windows x64. It
113
- exposes one `@onmark/cli` package and `onmark` command, carries the native CLI,
114
- `ffmpeg`, and `ffprobe` in a platform sidecar, and installs the pinned browser
115
- into a verified private cache. Linux uses Chrome for Testing's
116
- `chrome-headless-shell` with CDP BeginFrameControl; macOS and Windows use
117
- ordinary Chrome's portable screenshot backend. macOS selects Metal and verifies
118
- the active renderer through CDP; Linux and Windows retain `SwiftShader`. Every
119
- target renders the same admitted screenplay twice in an empty consumer before
120
- release artifacts are retained. After the initial package bootstrap, merging a
121
- `release/vX.Y.Z` pull request admits and publishes the same archives through npm
122
- Trusted Publishing, then creates the matching GitHub Release. Execution
123
- overrides remain explicit:
124
- `--graphics software` selects the canonical software control, and
125
- `--video-encoder-threads` can replace the stable four-thread local default with
126
- a bounded value from 1 through 64.
127
- Deterministic comparisons apply within the same locked browser environment,
128
- capture mode, and graphics backend.
129
-
130
- ## Worker capture
131
-
132
- Gate three introduced a narrow local worker entry point for already-composed
133
- visual work:
81
+ The command writes `renders/film.mp4` without overwriting an existing file.
82
+ Resolution, exact rational frame rates, and imported subtitles stay explicit:
134
83
 
135
84
  ```bash
136
- onmark worker capture --input worker-input --output opening.onmark-frames \
137
- --browser /path/to/chrome-headless-shell --ffmpeg /path/to/ffmpeg
85
+ onmark render film.html \
86
+ --output launch.mp4 \
87
+ --width 1920 \
88
+ --height 1080 \
89
+ --fps 30000/1001 \
90
+ --subtitle captions.vtt
138
91
  ```
139
92
 
140
- `worker-input` contains a versioned `request.json`, including the locked
141
- capture-environment identity, the `bundle/` payload files named by that
142
- request's manifest, and any frozen `assets/sha256/` bytes. The worker accepts no
143
- screenplay and does not compile source. It publishes one checksum-verified,
144
- no-clobber frame artifact; retry reuse requires both the planned unit and the
145
- declared capture environment to match. This is the same portable interchange
146
- used by the Lambda adapter; it is not a cloud coordinator or a replacement for
147
- `onmark render`.
148
-
149
- ## Lambda capture adapter
150
-
151
- `deploy/aws-lambda` wraps that same worker contract with a bounded S3 download
152
- and conditional artifact publication. Its V1 invocation and result schemas are
153
- checked in under `schemas/`; its required environment, IAM scope, limits, and
154
- intentional non-goals are documented in
155
- [its deployment README](deploy/aws-lambda/README.md). The real arm64 Lambda ZIP
156
- experiment and deterministic package command are recorded there. Infrastructure
157
- provisioning remains outside this gate; desktop npm publication is a separate
158
- release boundary.
159
-
160
- ## Repository map
161
-
162
- - `crates/` contains Rust product code.
163
- - `packages/` contains browser and Node product packages.
164
- - `conformance/` groups compiler, media, browser, CLI, wire, and admission evidence.
165
- - `evals/` contains frozen language-admission experiments and raw model output.
166
- - `schemas/` contains the five generated, versioned wire contracts.
167
- - `scripts/` contains the `xtask` entry point plus evaluation, schema,
168
- TypeScript-quality, and release tooling; release npm and media toolchains have
169
- separate process-owned subdirectories.
93
+ ## Why screenplay-first
94
+
95
+ - **Intent before coordinates.** Source describes scenes, shots, content, cues,
96
+ and relationships. Rust owns absolute frame placement and timing provenance.
97
+ - **The browser remains the canvas.** Use HTML, CSS, Canvas, WebGL, Three.js,
98
+ GSAP, media, fonts, and SVG without introducing a second layout engine.
99
+ - **One execution model.** Whole-film, partitioned, local, Lambda, and
100
+ incremental renders consume the same verified Render Units and assembler.
101
+ - **Failures stay explainable.** Authored mistakes are diagnostics with source
102
+ spans; browser, media, and infrastructure faults remain typed errors.
103
+
104
+ ## Authoring
105
+
106
+ An Onmark film is one self-contained HTML document. Custom elements carry
107
+ screenplay meaning; ordinary HTML and inline CSS carry presentation. Optional
108
+ `data-om-motion` code exports seekable animation through adapters such as
109
+ `onmark/motion/gsap`.
110
+
111
+ The compiler supports authored video, audio, voice-over, music, sound effects,
112
+ titles, calls to action, cues, and imported SRT, WebVTT, or ASS captions.
113
+ Unknown browser components remain sequential until conformance proves that they
114
+ are safe to seek or partition.
115
+
116
+ Browse [`showcases/`](showcases/) for complete films using native media, CSS 3D,
117
+ GSAP, Canvas 2D, raw WebGL, Three.js, captions, and audio.
118
+
119
+ ## Architecture
120
+
121
+ Rust owns compilation, planning, media normalization, browser control, encoding,
122
+ and artifact verification. TypeScript owns authoring bindings and browser
123
+ presentation. The compiler core has no filesystem, network, clock, Chromium,
124
+ FFmpeg, or cloud dependency.
125
+
126
+ The repository contains four Rust crates:
127
+
128
+ - `onmark-core` — model, parser, diagnostics, compiler, IR, and wire contracts;
129
+ - `onmark-media` — bounded media and subtitle normalization;
130
+ - `onmark-render` — Chromium, FFmpeg, Render Units, and verified artifacts;
131
+ - `onmark-cli` — the desktop command and composition root.
132
+
133
+ Browser packages live under [`packages/`](packages/). Rust wire types generate
134
+ the checked-in schemas and TypeScript codecs; CI rejects generated drift.
170
135
 
171
136
  ## Development
172
137
 
173
- Rust 1.97.0, Node.js 26.4.0, and pnpm 11.9.0 are pinned for reproducible
174
- development.
138
+ Rust 1.97.0, Node.js 26.4.0, and pnpm 11.9.0 are pinned.
175
139
 
176
140
  ```bash
177
141
  cargo test --workspace
@@ -183,20 +147,14 @@ pnpm lint
183
147
  pnpm typecheck
184
148
  pnpm test
185
149
  cargo xtask schema --check
186
- cargo xtask eval audio
187
- cargo xtask eval html
188
150
  ```
189
151
 
190
- ## Design documents
152
+ Design contracts:
191
153
 
192
154
  - [Architecture](docs/en/architecture.md)
193
155
  - [Language specification](docs/en/language-specification.md)
194
156
  - [Presentation contract](docs/en/presentation-contract.md)
195
157
  - [Competitive pipeline review](docs/en/competitive-pipeline-review.md)
196
- - [Rust style guide](docs/en/rust-style-guide.md)
197
- - [TypeScript style guide](docs/en/typescript-style-guide.md)
198
158
  - [中文文档](docs/zh-CN/)
199
159
 
200
- The design documents remain the project contract. Code and documentation
201
- disagreements must be resolved explicitly rather than silently choosing one
202
- side.
160
+ Onmark is available under the [MIT License](LICENSE).
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "packageName": "@onmark/cli",
4
- "version": "0.1.3",
5
- "sourceRevision": "25000b2f44d6b2463083ed977ead461b198b9f8b",
4
+ "version": "0.1.4",
5
+ "sourceRevision": "faaf3134890c979f082912e6882d99750452031d",
6
6
  "files": [
7
7
  {
8
8
  "path": "LICENSE",
@@ -11,23 +11,43 @@
11
11
  },
12
12
  {
13
13
  "path": "README.md",
14
- "bytes": 9972,
15
- "sha256": "60000f53e551fea49368f36a68d71933423f314e6dd5bbf1cec495cad84f6c60"
14
+ "bytes": 5791,
15
+ "sha256": "d2da7345c7e5a623ca47e26296994f1c4a0febf26415b698397dca44e01e0c8a"
16
16
  },
17
17
  {
18
18
  "path": "package.json",
19
19
  "bytes": 1406,
20
- "sha256": "93a60078967eb56474b3bcca33115c037432399df91908a161983a3e699d203d"
20
+ "sha256": "9527406d0676f0d8ff7bd545075a4f66f55072ac71082349400c3ce74ae65321"
21
+ },
22
+ {
23
+ "path": "packages/authoring/dist/src/frame-motion.d.ts",
24
+ "bytes": 1006,
25
+ "sha256": "119a4a0327ec0cb6baecf313ed90041b302f6207b2d963ca268a8ba348850e40"
26
+ },
27
+ {
28
+ "path": "packages/authoring/dist/src/frame-motion.js",
29
+ "bytes": 2685,
30
+ "sha256": "ca5775d727f39b1d6310d775d090d3db074a55e3e3d2e9a2ad62a9f355c1e578"
21
31
  },
22
32
  {
23
33
  "path": "packages/authoring/dist/src/index.d.ts",
24
- "bytes": 444,
25
- "sha256": "04a987076863ba1a11fabea37a8c15878abbffa12b4942c28d9b26da1e98f8f3"
34
+ "bytes": 775,
35
+ "sha256": "bdf86f3a16736963d83abf2ac8412b26b623bad45140f450666d9060688a5aae"
26
36
  },
27
37
  {
28
38
  "path": "packages/authoring/dist/src/index.js",
29
- "bytes": 266,
30
- "sha256": "bdff297aee92136a3304af030c07f1b03ee386feee9435deb22b0993ceaf19bb"
39
+ "bytes": 413,
40
+ "sha256": "2b9da5ff9f6a3cea24eaf7a835353676aee14d471ece6a45ebe2a0438a4c0d62"
41
+ },
42
+ {
43
+ "path": "packages/authoring/dist/src/interpolation.d.ts",
44
+ "bytes": 901,
45
+ "sha256": "0155b6fa4873204b20043c83ad23e6f4442eda727c68bee54fa7042a411c2770"
46
+ },
47
+ {
48
+ "path": "packages/authoring/dist/src/interpolation.js",
49
+ "bytes": 4007,
50
+ "sha256": "8cf8289e743b6885514602e336d64db2da7e3d1b2392f1c1ce8efebc32bc25d6"
31
51
  },
32
52
  {
33
53
  "path": "packages/authoring/dist/src/motion.d.ts",
@@ -59,6 +79,16 @@
59
79
  "bytes": 2544,
60
80
  "sha256": "6f1d471c95678c4b0b4493c704771a59098e5d1d0c7ef81145901f7d86ad31b1"
61
81
  },
82
+ {
83
+ "path": "packages/authoring/dist/src/spring.d.ts",
84
+ "bytes": 865,
85
+ "sha256": "97349b3da4eaaa2ebf4622d543264d47d5a7836061cc3b61b9cf4b93df7d7db3"
86
+ },
87
+ {
88
+ "path": "packages/authoring/dist/src/spring.js",
89
+ "bytes": 4259,
90
+ "sha256": "3b11f90933144ec36ab434b91e534dc74ac4503cc1633e85addd456229030274"
91
+ },
62
92
  {
63
93
  "path": "packages/authoring/dist/src/types.d.ts",
64
94
  "bytes": 133,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onmark/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Screenplay-first deterministic browser video compiler",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,8 +45,8 @@
45
45
  "yauzl": "3.4.0"
46
46
  },
47
47
  "optionalDependencies": {
48
- "@onmark/cli-darwin-arm64": "0.1.3",
49
- "@onmark/cli-linux-x64": "0.1.3",
50
- "@onmark/cli-win32-x64": "0.1.3"
48
+ "@onmark/cli-darwin-arm64": "0.1.4",
49
+ "@onmark/cli-linux-x64": "0.1.4",
50
+ "@onmark/cli-win32-x64": "0.1.4"
51
51
  }
52
52
  }
@@ -0,0 +1,17 @@
1
+ import type { PresentationExtension, PresentationExtensionContext, PresentationTargetKind } from "./motion.js";
2
+ /** One immutable sample inside a semantic element's solved interval. */
3
+ export interface FrameMotionContext {
4
+ readonly durationFrames: number;
5
+ readonly element: HTMLElement;
6
+ readonly frameRate: PresentationExtensionContext["frameRate"];
7
+ readonly localFrame: number;
8
+ readonly progress: number;
9
+ }
10
+ /** Applies browser-owned visual state for one exact local frame. */
11
+ export type FrameMotionHandler = (context: FrameMotionContext) => void;
12
+ /** Semantic handlers and optional selector handlers for exact-frame motion. */
13
+ export type FrameMotionDefinition = Readonly<Partial<Record<PresentationTargetKind, FrameMotionHandler>> & {
14
+ readonly selectors?: Readonly<Record<string, FrameMotionHandler>>;
15
+ }>;
16
+ /** Creates local browser effects driven directly by exact runtime frames. */
17
+ export declare function frameMotion(definition: FrameMotionDefinition): PresentationExtension;
@@ -0,0 +1,80 @@
1
+ // Vendor-neutral exact-frame motion over compiler-owned semantic intervals.
2
+ // Each sample is local to one target, so presentation code cannot reschedule it.
3
+ /** Creates local browser effects driven directly by exact runtime frames. */
4
+ export function frameMotion(definition) {
5
+ const rules = ownRules(definition);
6
+ return Object.freeze({
7
+ bind(context) {
8
+ return {
9
+ effects: bindEffects(rules, context),
10
+ resources: [],
11
+ };
12
+ },
13
+ });
14
+ }
15
+ function bindEffects(rules, context) {
16
+ const effects = [];
17
+ for (const target of context.targets) {
18
+ const handlers = matchingHandlers(rules, target);
19
+ if (handlers.length > 0) {
20
+ effects.push(bindEffect(target, handlers, context.frameRate));
21
+ }
22
+ }
23
+ return Object.freeze(effects);
24
+ }
25
+ function bindEffect(target, handlers, frameRate) {
26
+ const durationFrames = target.interval.end - target.interval.start;
27
+ return Object.freeze({
28
+ apply(frame) {
29
+ const localFrame = frame.index - target.interval.start;
30
+ if (localFrame < 0 || localFrame >= durationFrames) {
31
+ return;
32
+ }
33
+ const sample = Object.freeze({
34
+ durationFrames,
35
+ element: target.element,
36
+ frameRate,
37
+ localFrame,
38
+ progress: localFrame / durationFrames,
39
+ });
40
+ for (const handler of handlers) {
41
+ handler(sample);
42
+ }
43
+ },
44
+ dispose() {
45
+ // Direct frame motion retains no vendor playhead or browser resource.
46
+ },
47
+ });
48
+ }
49
+ function ownRules(definition) {
50
+ const kinds = Object.freeze({
51
+ film: definition.film,
52
+ scene: definition.scene,
53
+ shot: definition.shot,
54
+ video: definition.video,
55
+ title: definition.title,
56
+ callToAction: definition.callToAction,
57
+ caption: definition.caption,
58
+ });
59
+ const selectors = Object.freeze(Object.entries(definition.selectors ?? {}).map(ownSelector));
60
+ return Object.freeze({ kinds, selectors });
61
+ }
62
+ function ownSelector([selector, apply]) {
63
+ if (selector.trim().length === 0) {
64
+ throw new RangeError("exact-frame motion selector cannot be blank");
65
+ }
66
+ return Object.freeze({ apply, selector });
67
+ }
68
+ function matchingHandlers(rules, target) {
69
+ const handlers = [];
70
+ const kind = rules.kinds[target.kind];
71
+ if (kind !== undefined) {
72
+ handlers.push(kind);
73
+ }
74
+ for (const rule of rules.selectors) {
75
+ if (target.element.matches(rule.selector)) {
76
+ handlers.push(rule.apply);
77
+ }
78
+ }
79
+ return handlers;
80
+ }
@@ -1,3 +1,6 @@
1
+ export { frameMotion, type FrameMotionContext, type FrameMotionDefinition, type FrameMotionHandler, } from "./frame-motion.js";
2
+ export { easing, interpolate, type EasingFunction, type Extrapolation, type InterpolationOptions, } from "./interpolation.js";
3
+ export { spring, type SpringFrame, type SpringOptions } from "./spring.js";
1
4
  export { combineMotion, type PresentationExtension, type PresentationExtensionContext, type PresentationTarget, type PresentationTargetKind, } from "./motion.js";
2
5
  export { createDomPresentationBindings, type DomPresentationOptions, type VideoSource, } from "./presentation.js";
3
6
  export { createFontResource, createImageResource, type FontResource, type FontResourceOptions, type ImageResource, type ImageResourceOptions, } from "./resource.js";
@@ -1,4 +1,7 @@
1
1
  // Public facade for semantic browser authoring and optional motion adapters.
2
+ export { frameMotion, } from "./frame-motion.js";
3
+ export { easing, interpolate, } from "./interpolation.js";
4
+ export { spring } from "./spring.js";
2
5
  export { combineMotion, } from "./motion.js";
3
6
  export { createDomPresentationBindings, } from "./presentation.js";
4
7
  export { createFontResource, createImageResource, } from "./resource.js";
@@ -0,0 +1,19 @@
1
+ export type EasingFunction = (progress: number) => number;
2
+ export type Extrapolation = "clamp" | "extend";
3
+ export interface InterpolationOptions {
4
+ /** Maps normalized segment progress before output interpolation. */
5
+ readonly easing?: EasingFunction;
6
+ /** Defaults to clamp. */
7
+ readonly extrapolateLeft?: Extrapolation;
8
+ /** Defaults to clamp. */
9
+ readonly extrapolateRight?: Extrapolation;
10
+ }
11
+ /** Stateless easing functions for native exact-frame motion. */
12
+ export declare const easing: Readonly<{
13
+ inCubic(progress: number): number;
14
+ inOutCubic(progress: number): number;
15
+ linear(progress: number): number;
16
+ outCubic(progress: number): number;
17
+ }>;
18
+ /** Maps a scalar through one piecewise-linear visual domain. */
19
+ export declare function interpolate(input: number, inputRange: readonly number[], outputRange: readonly number[], options?: InterpolationOptions): number;
@@ -0,0 +1,100 @@
1
+ // Piecewise visual-value interpolation for native exact-frame motion.
2
+ // The closed edge policy prevents implicit extrapolation beyond authored ranges.
3
+ /** Stateless easing functions for native exact-frame motion. */
4
+ export const easing = Object.freeze({
5
+ inCubic(progress) {
6
+ return progress ** 3;
7
+ },
8
+ inOutCubic(progress) {
9
+ if (progress < 0.5) {
10
+ return 4 * progress ** 3;
11
+ }
12
+ return 1 - (-2 * progress + 2) ** 3 / 2;
13
+ },
14
+ linear(progress) {
15
+ return progress;
16
+ },
17
+ outCubic(progress) {
18
+ return 1 - (1 - progress) ** 3;
19
+ },
20
+ });
21
+ /** Maps a scalar through one piecewise-linear visual domain. */
22
+ export function interpolate(input, inputRange, outputRange, options = {}) {
23
+ validateInterpolation(input, inputRange, outputRange);
24
+ validateOptions(options);
25
+ const value = applyExtrapolation(input, inputRange, options);
26
+ const segment = segmentFor(value, inputRange);
27
+ const inputStart = rangeValue(inputRange, segment);
28
+ const inputEnd = rangeValue(inputRange, segment + 1);
29
+ const outputStart = rangeValue(outputRange, segment);
30
+ const outputEnd = rangeValue(outputRange, segment + 1);
31
+ const progress = (value - inputStart) / (inputEnd - inputStart);
32
+ const eased = (options.easing ?? easing.linear)(progress);
33
+ requireFinite(eased, "interpolation easing must return finite progress");
34
+ const output = outputStart + (outputEnd - outputStart) * eased;
35
+ requireFinite(output, "interpolation output must be finite");
36
+ return output;
37
+ }
38
+ function validateOptions(options) {
39
+ if (options.easing !== undefined && typeof options.easing !== "function") {
40
+ throw new RangeError("interpolation easing must be a function");
41
+ }
42
+ validateExtrapolation(options.extrapolateLeft, "left");
43
+ validateExtrapolation(options.extrapolateRight, "right");
44
+ }
45
+ function validateExtrapolation(extrapolation, edge) {
46
+ if (extrapolation !== undefined &&
47
+ extrapolation !== "clamp" &&
48
+ extrapolation !== "extend") {
49
+ throw new RangeError(`interpolation ${edge} extrapolation must be clamp or extend`);
50
+ }
51
+ }
52
+ function validateInterpolation(input, inputRange, outputRange) {
53
+ requireFinite(input, "interpolation input must be finite");
54
+ if (inputRange.length < 2) {
55
+ throw new RangeError("interpolation ranges need at least two values");
56
+ }
57
+ if (inputRange.length !== outputRange.length) {
58
+ throw new RangeError("interpolation ranges must have the same length");
59
+ }
60
+ for (let index = 0; index < inputRange.length; index += 1) {
61
+ const inputValue = rangeValue(inputRange, index);
62
+ const outputValue = rangeValue(outputRange, index);
63
+ requireFinite(inputValue, "interpolation input range must be finite");
64
+ requireFinite(outputValue, "interpolation output range must be finite");
65
+ if (index > 0 && inputValue <= rangeValue(inputRange, index - 1)) {
66
+ throw new RangeError("interpolation input range must be strictly increasing");
67
+ }
68
+ }
69
+ }
70
+ function applyExtrapolation(input, inputRange, options) {
71
+ const first = rangeValue(inputRange, 0);
72
+ const last = rangeValue(inputRange, inputRange.length - 1);
73
+ if (input < first && (options.extrapolateLeft ?? "clamp") === "clamp") {
74
+ return first;
75
+ }
76
+ if (input > last && (options.extrapolateRight ?? "clamp") === "clamp") {
77
+ return last;
78
+ }
79
+ return input;
80
+ }
81
+ function segmentFor(input, inputRange) {
82
+ for (let index = 1; index < inputRange.length; index += 1) {
83
+ if (input <= rangeValue(inputRange, index)) {
84
+ return index - 1;
85
+ }
86
+ }
87
+ return inputRange.length - 2;
88
+ }
89
+ function requireFinite(value, message) {
90
+ if (!Number.isFinite(value)) {
91
+ throw new RangeError(message);
92
+ }
93
+ }
94
+ function rangeValue(range, index) {
95
+ const value = range[index];
96
+ if (value === undefined) {
97
+ throw new RangeError("interpolation range is incomplete");
98
+ }
99
+ return value;
100
+ }
@@ -0,0 +1,19 @@
1
+ import type { PresentationExtensionContext } from "./motion.js";
2
+ export interface SpringFrame {
3
+ readonly frameRate: PresentationExtensionContext["frameRate"];
4
+ readonly localFrame: number;
5
+ }
6
+ export interface SpringOptions {
7
+ /** Damping coefficient. Defaults to 10. */
8
+ readonly damping?: number;
9
+ /** Initial progress velocity per second toward the target. Defaults to zero. */
10
+ readonly initialVelocity?: number;
11
+ /** Moving mass. Defaults to one. */
12
+ readonly mass?: number;
13
+ /** Whether progress may cross the zero-to-one visual range. */
14
+ readonly overshoot?: "allow" | "clamp";
15
+ /** Restoring-force stiffness. Defaults to 100. */
16
+ readonly stiffness?: number;
17
+ }
18
+ /** Samples a damped physical spring directly at one exact local frame. */
19
+ export declare function spring(frame: SpringFrame, options?: SpringOptions): number;
@@ -0,0 +1,95 @@
1
+ // Closed-form spring sampling over exact local frame facts.
2
+ // Direct evaluation stays independent of DOM state and prior seek order.
3
+ const DEFAULT_SPRING = Object.freeze({
4
+ damping: 10,
5
+ initialVelocity: 0,
6
+ mass: 1,
7
+ overshoot: "allow",
8
+ stiffness: 100,
9
+ });
10
+ /** Samples a damped physical spring directly at one exact local frame. */
11
+ export function spring(frame, options = {}) {
12
+ validateFrame(frame);
13
+ const parameters = springParameters(options);
14
+ const seconds = (frame.localFrame / frame.frameRate.numerator) *
15
+ frame.frameRate.denominator;
16
+ const displacement = springDisplacement(seconds, parameters);
17
+ const progress = 1 - displacement;
18
+ requireFinite(progress, "spring progress must be finite");
19
+ if (parameters.overshoot === "allow") {
20
+ return progress;
21
+ }
22
+ return Math.max(0, Math.min(progress, 1));
23
+ }
24
+ function springDisplacement(seconds, parameters) {
25
+ const frequency = Math.sqrt(parameters.stiffness / parameters.mass);
26
+ const decay = parameters.damping / (2 * parameters.mass);
27
+ const criticalTolerance = frequency * 1e-7;
28
+ if (decay < frequency - criticalTolerance) {
29
+ return underDampedDisplacement(seconds, frequency, decay, parameters);
30
+ }
31
+ if (decay > frequency + criticalTolerance) {
32
+ return overDampedDisplacement(seconds, frequency, decay, parameters);
33
+ }
34
+ // The general solutions divide by the vanishing distance between roots.
35
+ // Their critical limit is both equivalent and numerically stable here.
36
+ return criticallyDampedDisplacement(seconds, frequency, parameters);
37
+ }
38
+ function underDampedDisplacement(seconds, frequency, decay, parameters) {
39
+ const dampedFrequency = Math.sqrt(frequency ** 2 - decay ** 2);
40
+ const velocityTerm = (decay - parameters.initialVelocity) / dampedFrequency;
41
+ return (Math.exp(-decay * seconds) *
42
+ (Math.cos(dampedFrequency * seconds) +
43
+ velocityTerm * Math.sin(dampedFrequency * seconds)));
44
+ }
45
+ function criticallyDampedDisplacement(seconds, decay, parameters) {
46
+ const velocityTerm = decay - parameters.initialVelocity;
47
+ return Math.exp(-decay * seconds) * (1 + velocityTerm * seconds);
48
+ }
49
+ function overDampedDisplacement(seconds, frequency, decay, parameters) {
50
+ const root = Math.sqrt(decay ** 2 - frequency ** 2);
51
+ const slowRoot = -decay + root;
52
+ const fastRoot = -decay - root;
53
+ const slowWeight = (-parameters.initialVelocity - fastRoot) / (slowRoot - fastRoot);
54
+ const fastWeight = 1 - slowWeight;
55
+ return (slowWeight * Math.exp(slowRoot * seconds) +
56
+ fastWeight * Math.exp(fastRoot * seconds));
57
+ }
58
+ function springParameters(options) {
59
+ const parameters = {
60
+ damping: options.damping ?? DEFAULT_SPRING.damping,
61
+ initialVelocity: options.initialVelocity ?? DEFAULT_SPRING.initialVelocity,
62
+ mass: options.mass ?? DEFAULT_SPRING.mass,
63
+ overshoot: options.overshoot ?? DEFAULT_SPRING.overshoot,
64
+ stiffness: options.stiffness ?? DEFAULT_SPRING.stiffness,
65
+ };
66
+ requirePositive(parameters.damping, "spring damping");
67
+ requireFinite(parameters.initialVelocity, "spring initial velocity must be finite");
68
+ requirePositive(parameters.mass, "spring mass");
69
+ requirePositive(parameters.stiffness, "spring stiffness");
70
+ if (parameters.overshoot !== "allow" && parameters.overshoot !== "clamp") {
71
+ throw new RangeError("spring overshoot must be allow or clamp");
72
+ }
73
+ return parameters;
74
+ }
75
+ function validateFrame(frame) {
76
+ if (!Number.isSafeInteger(frame.localFrame) || frame.localFrame < 0) {
77
+ throw new RangeError("spring local frame must be a non-negative safe integer");
78
+ }
79
+ if (!Number.isSafeInteger(frame.frameRate.numerator) ||
80
+ !Number.isSafeInteger(frame.frameRate.denominator) ||
81
+ frame.frameRate.numerator <= 0 ||
82
+ frame.frameRate.denominator <= 0) {
83
+ throw new RangeError("spring frame rate must contain positive safe integers");
84
+ }
85
+ }
86
+ function requirePositive(value, label) {
87
+ if (!Number.isFinite(value) || value <= 0) {
88
+ throw new RangeError(`${label} must be positive and finite`);
89
+ }
90
+ }
91
+ function requireFinite(value, message) {
92
+ if (!Number.isFinite(value)) {
93
+ throw new RangeError(message);
94
+ }
95
+ }