@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 +122 -164
- package/onmark-release.json +39 -9
- package/package.json +4 -4
- package/packages/authoring/dist/src/frame-motion.d.ts +17 -0
- package/packages/authoring/dist/src/frame-motion.js +80 -0
- package/packages/authoring/dist/src/index.d.ts +3 -0
- package/packages/authoring/dist/src/index.js +3 -0
- package/packages/authoring/dist/src/interpolation.d.ts +19 -0
- package/packages/authoring/dist/src/interpolation.js +100 -0
- package/packages/authoring/dist/src/spring.d.ts +19 -0
- package/packages/authoring/dist/src/spring.js +95 -0
package/README.md
CHANGED
|
@@ -1,177 +1,141 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">Onmark</h1>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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 →
|
|
23
|
+
screenplay → Timeline IR → Render Units → browser frames + audio → MP4
|
|
8
24
|
```
|
|
9
25
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
|
137
|
-
--
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
disagreements must be resolved explicitly rather than silently choosing one
|
|
202
|
-
side.
|
|
160
|
+
Onmark is available under the [MIT License](LICENSE).
|
package/onmark-release.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"packageName": "@onmark/cli",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"sourceRevision": "
|
|
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":
|
|
15
|
-
"sha256": "
|
|
14
|
+
"bytes": 5791,
|
|
15
|
+
"sha256": "d2da7345c7e5a623ca47e26296994f1c4a0febf26415b698397dca44e01e0c8a"
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"path": "package.json",
|
|
19
19
|
"bytes": 1406,
|
|
20
|
-
"sha256": "
|
|
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":
|
|
25
|
-
"sha256": "
|
|
34
|
+
"bytes": 775,
|
|
35
|
+
"sha256": "bdf86f3a16736963d83abf2ac8412b26b623bad45140f450666d9060688a5aae"
|
|
26
36
|
},
|
|
27
37
|
{
|
|
28
38
|
"path": "packages/authoring/dist/src/index.js",
|
|
29
|
-
"bytes":
|
|
30
|
-
"sha256": "
|
|
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
|
+
"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.
|
|
49
|
-
"@onmark/cli-linux-x64": "0.1.
|
|
50
|
-
"@onmark/cli-win32-x64": "0.1.
|
|
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
|
+
}
|