nanoframes 0.1.0__tar.gz
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.
- nanoframes-0.1.0/.gitignore +22 -0
- nanoframes-0.1.0/LICENSE +17 -0
- nanoframes-0.1.0/Makefile +37 -0
- nanoframes-0.1.0/PKG-INFO +54 -0
- nanoframes-0.1.0/README.md +39 -0
- nanoframes-0.1.0/docs/architecture.md +121 -0
- nanoframes-0.1.0/docs/composition.md +154 -0
- nanoframes-0.1.0/docs/text-capabilities.md +112 -0
- nanoframes-0.1.0/examples/assets/dot.png +0 -0
- nanoframes-0.1.0/examples/master-demo.nf.svg +87 -0
- nanoframes-0.1.0/examples/raster-title.nf.svg +29 -0
- nanoframes-0.1.0/examples/text-measure.nf.svg +45 -0
- nanoframes-0.1.0/examples/title-card.nf.svg +40 -0
- nanoframes-0.1.0/nanoframes/__init__.py +3 -0
- nanoframes-0.1.0/nanoframes/__main__.py +10 -0
- nanoframes-0.1.0/nanoframes/bake.py +151 -0
- nanoframes-0.1.0/nanoframes/cache.py +48 -0
- nanoframes-0.1.0/nanoframes/cli.py +423 -0
- nanoframes-0.1.0/nanoframes/curve.py +232 -0
- nanoframes-0.1.0/nanoframes/fonts/LICENSE-Sarasa-OFL.txt +113 -0
- nanoframes-0.1.0/nanoframes/fonts/SarasaMonoSC-Regular-noliga.ttf +0 -0
- nanoframes-0.1.0/nanoframes/fonts.py +298 -0
- nanoframes-0.1.0/nanoframes/lint.py +98 -0
- nanoframes-0.1.0/nanoframes/measure.py +110 -0
- nanoframes-0.1.0/nanoframes/model.py +95 -0
- nanoframes-0.1.0/nanoframes/parse.py +173 -0
- nanoframes-0.1.0/nanoframes/rastertext.py +207 -0
- nanoframes-0.1.0/nanoframes/render.py +128 -0
- nanoframes-0.1.0/nanoframes/scripts/__init__.py +70 -0
- nanoframes-0.1.0/nanoframes/scripts/verify_font.py +39 -0
- nanoframes-0.1.0/nanoframes/textflow.py +299 -0
- nanoframes-0.1.0/nanoframes/timeline.py +163 -0
- nanoframes-0.1.0/nanoframes/video.py +71 -0
- nanoframes-0.1.0/nanoframes/walkthrough.py +583 -0
- nanoframes-0.1.0/nanoframes/xmlutil.py +43 -0
- nanoframes-0.1.0/pyproject.toml +42 -0
- nanoframes-0.1.0/skills/nanoframes/SKILL.md +128 -0
- nanoframes-0.1.0/tests/snapshots/master-demo.nf_t0.1.png +0 -0
- nanoframes-0.1.0/tests/snapshots/master-demo.nf_t0.2.png +0 -0
- nanoframes-0.1.0/tests/snapshots/master-demo.nf_t1.png +0 -0
- nanoframes-0.1.0/tests/snapshots/master-demo.nf_t2.5.png +0 -0
- nanoframes-0.1.0/tests/snapshots/master-demo.nf_t2.png +0 -0
- nanoframes-0.1.0/tests/snapshots/master-demo.nf_t3.5.png +0 -0
- nanoframes-0.1.0/tests/snapshots/master-demo.nf_t4.5.png +0 -0
- nanoframes-0.1.0/tests/snapshots/text-measure.nf_t0.1.png +0 -0
- nanoframes-0.1.0/tests/snapshots/text-measure.nf_t1.png +0 -0
- nanoframes-0.1.0/tests/snapshots/text-measure.nf_t2.png +0 -0
- nanoframes-0.1.0/tests/snapshots/text-measure.nf_t3.5.png +0 -0
- nanoframes-0.1.0/tests/snapshots/title-card.nf_t0.1.png +0 -0
- nanoframes-0.1.0/tests/snapshots/title-card.nf_t1.png +0 -0
- nanoframes-0.1.0/tests/snapshots/title-card.nf_t2.png +0 -0
- nanoframes-0.1.0/tests/snapshots/title-card.nf_t3.5.png +0 -0
- nanoframes-0.1.0/tests/test_cache.py +42 -0
- nanoframes-0.1.0/tests/test_cli.py +122 -0
- nanoframes-0.1.0/tests/test_parse.py +112 -0
- nanoframes-0.1.0/tests/test_rastertext.py +272 -0
- nanoframes-0.1.0/tests/test_render.py +94 -0
- nanoframes-0.1.0/tests/test_skill.py +24 -0
- nanoframes-0.1.0/tests/test_snapshots.py +58 -0
- nanoframes-0.1.0/tests/test_textflow.py +111 -0
- nanoframes-0.1.0/tests/test_timeline.py +110 -0
- nanoframes-0.1.0/tests/test_walkthrough.py +44 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# LoopX / local goal state (private evidence — never commit)
|
|
2
|
+
.loopx/
|
|
3
|
+
.codex/
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*.egg-info/
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
|
|
14
|
+
# render outputs
|
|
15
|
+
out/
|
|
16
|
+
build/
|
|
17
|
+
*.mp4
|
|
18
|
+
.nanoframes-cache/
|
|
19
|
+
|
|
20
|
+
# macOS
|
|
21
|
+
.DS_Store
|
|
22
|
+
**/.DS_Store
|
nanoframes-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 nanoframes contributors
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
SHELL := /bin/bash
|
|
2
|
+
PY ?= python3
|
|
3
|
+
|
|
4
|
+
.PHONY: install test pytest lint build sdist upload clean
|
|
5
|
+
|
|
6
|
+
install:
|
|
7
|
+
$(PY) -m pip install -e .[dev]
|
|
8
|
+
|
|
9
|
+
test:
|
|
10
|
+
$(PY) -m pytest -q
|
|
11
|
+
|
|
12
|
+
pytest:
|
|
13
|
+
$(PY) -m pytest -q
|
|
14
|
+
|
|
15
|
+
lint:
|
|
16
|
+
ruff check nanoframes tests
|
|
17
|
+
|
|
18
|
+
# Local build: sdist + wheel into dist/ (hatchling; wheel embeds docs/skills/font).
|
|
19
|
+
build:
|
|
20
|
+
$(PY) -m pip install --upgrade build
|
|
21
|
+
$(PY) -m build .
|
|
22
|
+
|
|
23
|
+
# PyPI artifact: a fresh sdist only (mirrors ../redisk; pip builds the wheel
|
|
24
|
+
# from it on install). Dry-run check: twine check dist/*.tar.gz
|
|
25
|
+
sdist:
|
|
26
|
+
rm -rf dist
|
|
27
|
+
python3 -m pipx run build --sdist
|
|
28
|
+
|
|
29
|
+
# Publish the sdist to PyPI (needs twine + ~/.pypirc credentials).
|
|
30
|
+
# Test index first: twine upload dist/nanoframes-*.tar.gz -r testpypi
|
|
31
|
+
upload: sdist
|
|
32
|
+
twine upload dist/nanoframes-*.tar.gz -r pypi
|
|
33
|
+
|
|
34
|
+
clean:
|
|
35
|
+
rm -rf build dist *.egg-info nanoframes.egg-info
|
|
36
|
+
find . -name __pycache__ -type d -prune -exec rm -rf {} +
|
|
37
|
+
rm -rf out
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nanoframes
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SVG-first, browserless, deterministic frame rendering built on ThorVG. Offline hyperframes-style animated compositions for fast AI-agent iteration.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: pillow
|
|
10
|
+
Requires-Dist: thorvg-python
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
13
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# nanoframes
|
|
17
|
+
|
|
18
|
+
**SVG-first, browserless, deterministic frame rendering on ThorVG.**
|
|
19
|
+
|
|
20
|
+
nanoframes is an offline counterpart to [hyperframes](https://github.com/heygen-com/hyperframes): the same
|
|
21
|
+
idea — *write a frame, render a video, built for agents* — but without a browser. Compositions are
|
|
22
|
+
authored as **SVG** documents plus a small declarative animation timeline, and every frame is
|
|
23
|
+
rasterized deterministically by **ThorVG** (`thorvg-python`) to PNG, then muxed to MP4 with FFmpeg.
|
|
24
|
+
|
|
25
|
+
Because nothing depends on a browser, an AI agent can render a frame or a whole clip locally in
|
|
26
|
+
milliseconds and iterate fast: `check` → `preview` → `render`.
|
|
27
|
+
|
|
28
|
+
## Status
|
|
29
|
+
|
|
30
|
+
**v1 complete.** Full offline pipeline works end-to-end: `init` → `check` →
|
|
31
|
+
`render`/`preview` → `video`. SVG compositions + declarative keyframe timeline are
|
|
32
|
+
rendered deterministically by ThorVG; MP4 export via ffmpeg; visual snapshots guard
|
|
33
|
+
regressions. Text auto-layout (measured chips, wrap, curve, fit) + a bundled
|
|
34
|
+
**monospace CJK** font (`Sarasa Mono SC`, ligatures stripped) for solid Chinese.
|
|
35
|
+
|
|
36
|
+
## Layout
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
docs/ architecture + composition contract
|
|
40
|
+
examples/ `.nf.svg` compositions
|
|
41
|
+
skills/nanoframes/ SKILL.md — the agent production loop
|
|
42
|
+
nanoframes/ package (model, parser, timeline, bake, render, lint, cli, video)
|
|
43
|
+
tests/ unit + render + snapshot + CLI tests
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Docs
|
|
47
|
+
|
|
48
|
+
- [Architecture](docs/architecture.md) — why SVG + ThorVG, the pipeline, scope decisions.
|
|
49
|
+
- [Composition](docs/composition.md) — the `.nf.svg` contract (timing attributes + animation timeline).
|
|
50
|
+
- [Text capabilities](docs/text-capabilities.md) — measured chips/wrap/curve/fit, bundled CJK font, `text_handler` escape hatch.
|
|
51
|
+
|
|
52
|
+
`docs/` and `skills/` also ship inside the pip wheel (`nanoframes/docs`,
|
|
53
|
+
`nanoframes/skills`); running `nanoframes` with no arguments prints where the
|
|
54
|
+
docs and the agent skill live — repo checkout or installed package alike.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# nanoframes
|
|
2
|
+
|
|
3
|
+
**SVG-first, browserless, deterministic frame rendering on ThorVG.**
|
|
4
|
+
|
|
5
|
+
nanoframes is an offline counterpart to [hyperframes](https://github.com/heygen-com/hyperframes): the same
|
|
6
|
+
idea — *write a frame, render a video, built for agents* — but without a browser. Compositions are
|
|
7
|
+
authored as **SVG** documents plus a small declarative animation timeline, and every frame is
|
|
8
|
+
rasterized deterministically by **ThorVG** (`thorvg-python`) to PNG, then muxed to MP4 with FFmpeg.
|
|
9
|
+
|
|
10
|
+
Because nothing depends on a browser, an AI agent can render a frame or a whole clip locally in
|
|
11
|
+
milliseconds and iterate fast: `check` → `preview` → `render`.
|
|
12
|
+
|
|
13
|
+
## Status
|
|
14
|
+
|
|
15
|
+
**v1 complete.** Full offline pipeline works end-to-end: `init` → `check` →
|
|
16
|
+
`render`/`preview` → `video`. SVG compositions + declarative keyframe timeline are
|
|
17
|
+
rendered deterministically by ThorVG; MP4 export via ffmpeg; visual snapshots guard
|
|
18
|
+
regressions. Text auto-layout (measured chips, wrap, curve, fit) + a bundled
|
|
19
|
+
**monospace CJK** font (`Sarasa Mono SC`, ligatures stripped) for solid Chinese.
|
|
20
|
+
|
|
21
|
+
## Layout
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
docs/ architecture + composition contract
|
|
25
|
+
examples/ `.nf.svg` compositions
|
|
26
|
+
skills/nanoframes/ SKILL.md — the agent production loop
|
|
27
|
+
nanoframes/ package (model, parser, timeline, bake, render, lint, cli, video)
|
|
28
|
+
tests/ unit + render + snapshot + CLI tests
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Docs
|
|
32
|
+
|
|
33
|
+
- [Architecture](docs/architecture.md) — why SVG + ThorVG, the pipeline, scope decisions.
|
|
34
|
+
- [Composition](docs/composition.md) — the `.nf.svg` contract (timing attributes + animation timeline).
|
|
35
|
+
- [Text capabilities](docs/text-capabilities.md) — measured chips/wrap/curve/fit, bundled CJK font, `text_handler` escape hatch.
|
|
36
|
+
|
|
37
|
+
`docs/` and `skills/` also ship inside the pip wheel (`nanoframes/docs`,
|
|
38
|
+
`nanoframes/skills`); running `nanoframes` with no arguments prints where the
|
|
39
|
+
docs and the agent skill live — repo checkout or installed package alike.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# nanoframes Architecture
|
|
2
|
+
|
|
3
|
+
## North star
|
|
4
|
+
|
|
5
|
+
**Write a frame as SVG. Render frames, and a video, without a browser.** Built so an AI coding
|
|
6
|
+
agent can iterate fast and deterministically on every local change: `check` → `preview` →
|
|
7
|
+
`render` in milliseconds, no browser, no network.
|
|
8
|
+
|
|
9
|
+
## Why SVG + ThorVG (instead of HTML + Chromium)
|
|
10
|
+
|
|
11
|
+
Hyperframes uses HTML/CSS + a browser engine + GSAP, and captures frames through Chromium to a
|
|
12
|
+
seekable MP4. That is powerful but heavy and non-deterministic in some dimensions:
|
|
13
|
+
|
|
14
|
+
- The browser is a huge, opaque surface; agents can't inspect it locally without it.
|
|
15
|
+
- HTML/CSS layout is complex; matching pixel output requires capture round-trips.
|
|
16
|
+
|
|
17
|
+
nanoframes chooses the minimal deterministic subset that still covers typical motion-graphic
|
|
18
|
+
output:
|
|
19
|
+
|
|
20
|
+
- **Authoring surface = SVG**, which is (a) a first-class native output of language models, (b)
|
|
21
|
+
declarative, (c) directly rasterizable.
|
|
22
|
+
- **Rendering engine = ThorVG** (`thorvg-python`), an embedded SVG rasterizer (conforms to
|
|
23
|
+
*SVG Tiny 1.2* — see `../thorvg.wiki/SVG-Support.md`). No browser, no DOM, pure software raster
|
|
24
|
+
→ Pillow → PNG/MP4. Deterministic and trivially iterable.
|
|
25
|
+
- The `frames-pack` toolchain already demonstrated offline ThorVG→PNG rendering; we reuse that
|
|
26
|
+
pattern (thorvg-python + Pillow), and we reuse hyperframes' *timing/intent* ideas (clip
|
|
27
|
+
in/out, tracks, seek) but <b>flatten animation to a declarative timeline</b> instead of GSAP.
|
|
28
|
+
|
|
29
|
+
## Pipeline
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
composition.nf.svg (SVG scene + embedded <script> JSON animation timeline)
|
|
33
|
+
│ parse (stdlib xml) → nanoframes.Composition
|
|
34
|
+
v
|
|
35
|
+
seek(t): evaluate animation timeline → per-frame property values
|
|
36
|
+
│
|
|
37
|
+
v
|
|
38
|
+
bake(t): merge visibility + animated props into a frame-specific SVG
|
|
39
|
+
│
|
|
40
|
+
v
|
|
41
|
+
thorvg render → Pillow PNG (single frame, or batch)
|
|
42
|
+
│
|
|
43
|
+
v
|
|
44
|
+
ffmpeg → deterministic MP4 (P1)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Every stage is pure and deterministic given `(composition, t, fps)`. No web surface anywhere.
|
|
48
|
+
|
|
49
|
+
## Composition model
|
|
50
|
+
|
|
51
|
+
A composition is one `.nf.svg` file (valid SVG) carrying:
|
|
52
|
+
|
|
53
|
+
- root `<svg>` canvas metadata: `data-width`, `data-height`, `data-fps`, `data-duration`
|
|
54
|
+
(`data-composition-id` optional).
|
|
55
|
+
- per-element presence: `data-start` + `data-duration` (absolute seconds, clip in/out) and
|
|
56
|
+
optional `data-fade` (fade-in/out seconds). Elements outside their window are not rasterized.
|
|
57
|
+
- an embedded `<script type="application/nanoframes+json">` holding the **animation timeline**:
|
|
58
|
+
ordered animations, each targeting elements by CSS selector (`#id`, `.class`) with keyframe
|
|
59
|
+
lists over time.
|
|
60
|
+
|
|
61
|
+
Detailed contract: `docs/composition.md`.
|
|
62
|
+
|
|
63
|
+
## Animation timeline (declarative, seekable)
|
|
64
|
+
|
|
65
|
+
Hyperframes uses GSAP timelines loaded into the browser. We replace JS with a deterministic
|
|
66
|
+
keyframe JSON timeline evaluated by `seek(t)`:
|
|
67
|
+
|
|
68
|
+
- supported animated properties: `opacity`, `transform` (`translate`, `scale`, `rotate`),
|
|
69
|
+
`fill`, `stroke` (interpolated color).
|
|
70
|
+
- keyframes are `{ "t": <sec>, <props>: value }`; between keyframes we interpolate linearly,
|
|
71
|
+
optionally eased (`linear`, `ease-in`, `ease-out`, `ease-in-out`).
|
|
72
|
+
- clip + fade is folded into effective opacity so visibility, fade and animation compose into a
|
|
73
|
+
single `opacity` / `transform` baked value per frame.
|
|
74
|
+
|
|
75
|
+
This is deliberately *small*. Full GSAP/CSS expressiveness is out of scope; the payoff is fast,
|
|
76
|
+
deterministic, inspectable motion.
|
|
77
|
+
|
|
78
|
+
## Rendering (thorvg-python)
|
|
79
|
+
|
|
80
|
+
- `bake(t)` writes a temp standalone SVG (strips the `<script>`, applies the computed attrs).
|
|
81
|
+
- a renderer creates `thorvg_python.Engine` + `SwCanvas`, sets the target size, loads the SVG as
|
|
82
|
+
a `Picture`, `add → update → draw → sync`, and reads a Pillow image via `get_pillow()`.
|
|
83
|
+
- Energy is per-frame and isolated, so batch rendering stays deterministic and trivially
|
|
84
|
+
parallelizable later.
|
|
85
|
+
|
|
86
|
+
## Scope decisions (what we drop from hyperframes)
|
|
87
|
+
|
|
88
|
+
| Hyperframes scope | nanoframes |
|
|
89
|
+
|--------------------------------|------------|
|
|
90
|
+
| HTML/CSS + browser capture | SVG + ThorVG software raster |
|
|
91
|
+
| GSAP / Lottie / Three.js / anime | declarative keyframe timeline (v1) |
|
|
92
|
+
| inline `<video>` playback | static `<image>` only (v1); no video-in-scene |
|
|
93
|
+
| full audio mixing (buses, ducking) | optional FFmpeg audio mux passthrough (deferred P2) |
|
|
94
|
+
| hosted / Lambda / GCP rendering | local offline render only |
|
|
95
|
+
| Figma import, Remotion port | out of scope |
|
|
96
|
+
| 20 authoring workflows | a few core example templates |
|
|
97
|
+
| frame.md / design.md system | composition doc + lint (v1) |
|
|
98
|
+
| binary TreePack compressed pack | deferred optional binary cache (P2) |
|
|
99
|
+
|
|
100
|
+
**Kept:** deterministic seekable frames, clip timing, `check`/`preview`/`render` agent loop,
|
|
101
|
+
MP4 export, and an agent-facing skill.
|
|
102
|
+
|
|
103
|
+
## Non-goals
|
|
104
|
+
|
|
105
|
+
- No browser, no network, no live media.
|
|
106
|
+
- No JS runtime / no GSAP semantics.
|
|
107
|
+
- No production audio mastering; no cloud rendering.
|
|
108
|
+
|
|
109
|
+
## Milestones
|
|
110
|
+
|
|
111
|
+
- **v1 (done, 2026-09)** composition model + parser, timeline evaluator, ThorVG
|
|
112
|
+
frame renderer, CLI (`init`/`check`/`render`/`preview`/`video`/`measure`/
|
|
113
|
+
`fonts`/`walkthrough`), MP4 export + audio mux, fast re-render cache,
|
|
114
|
+
renderer-exact text story (chips/wrap/curve/fit), bundled mono CJK font,
|
|
115
|
+
`text_handler` escape hatch, example compositions + visual snapshot suite,
|
|
116
|
+
agent skill (`skills/nanoframes/SKILL.md`).
|
|
117
|
+
- **Deferred** optional binary tree-pack cache; CLI bridge for `text_handler`
|
|
118
|
+
(it is a library-API feature by design); in-scene video.
|
|
119
|
+
|
|
120
|
+
`docs/` and `skills/` ship inside the pip wheel as `nanoframes/docs` and
|
|
121
|
+
`nanoframes/skills`; `nanoframes` (no args) prints their installed locations.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# nanoframes composition contract
|
|
2
|
+
|
|
3
|
+
A composition is a single **`.nf.svg`** file: valid SVG + an embedded animation
|
|
4
|
+
timeline. Rendering is deterministic: the same `(composition, t, fps)` always
|
|
5
|
+
yields the same frame.
|
|
6
|
+
|
|
7
|
+
## Canvas (root `<svg>`)
|
|
8
|
+
|
|
9
|
+
| attribute | default | meaning |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `data-width` / `data-height` | — (required) | output canvas, pixels |
|
|
12
|
+
| `data-fps` | `30` | frames per second |
|
|
13
|
+
| `data-duration` | `1.0` | total duration, seconds |
|
|
14
|
+
| `data-composition-id` | — | name used for frame/video output |
|
|
15
|
+
|
|
16
|
+
`width`/`height` are accepted as fallbacks for `data-width`/`data-height`.
|
|
17
|
+
|
|
18
|
+
## Element presence (clip in/out)
|
|
19
|
+
|
|
20
|
+
| attribute | default | meaning |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `data-start` | `0` | absolute start time, seconds |
|
|
23
|
+
| `data-duration` | = composition duration | how long the clip is visible |
|
|
24
|
+
| `data-fade` | `0` | fade-in seconds at clip start (fade-out mirrors at clip end) |
|
|
25
|
+
|
|
26
|
+
Outside its `[start, start+duration]` window an element is `display:none` (not
|
|
27
|
+
rasterized). Fade and clip fold into the effective opacity.
|
|
28
|
+
|
|
29
|
+
## Animation timeline
|
|
30
|
+
|
|
31
|
+
Embedded as `<script type="application/nanoframes+json"><![CDATA[ … ]]></script>`.
|
|
32
|
+
The JSON has one `animations` array; each animation has a CSS `target` (selector
|
|
33
|
+
`#id`, `.class`, or tag) and ordered `keyframes`. Per keyframe: `t` (seconds),
|
|
34
|
+
any animated properties, and an optional `ease` (`linear` | `ease-in` |
|
|
35
|
+
`ease-out` | `ease-in-out`, default `linear`).
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"animations": [
|
|
40
|
+
{
|
|
41
|
+
"target": "#title",
|
|
42
|
+
"keyframes": [
|
|
43
|
+
{ "t": 0.0, "opacity": 0.0, "transform": { "translate": [0, 30] } },
|
|
44
|
+
{ "t": 0.7, "opacity": 1.0, "transform": { "translate": [0, 0] }, "ease": "ease-out" }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Between keyframes, numeric properties interpolate **linearly** (after easing);
|
|
52
|
+
colors interpolate RGB; anything non-interpolable holds the earlier keyframe.
|
|
53
|
+
|
|
54
|
+
### Supported properties
|
|
55
|
+
|
|
56
|
+
| property | value | interpolation |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `opacity` | float `0..1` | linear/eased |
|
|
59
|
+
| `transform` | `{"translate":[x,y], "scale":[sx,sy], "rotate":deg}` | elementwise |
|
|
60
|
+
| `fill` / `stroke` | `#RRGGBB` (or `#RRGGBBAA`) | RGB lerp |
|
|
61
|
+
|
|
62
|
+
Bake emits `transform="translate(x,y) rotate(d) scale(sx,sy)"` (translate ·
|
|
63
|
+
rotate · scale order).
|
|
64
|
+
|
|
65
|
+
## Media
|
|
66
|
+
|
|
67
|
+
- **Raster images**: `<image href="assets/foo.png" />` (relative paths are
|
|
68
|
+
dereferenced against the composition directory).
|
|
69
|
+
- **Fonts**: text renders with a system font loaded automatically (Arial /
|
|
70
|
+
DejaVuSans fallback). Declare `font-family`, `font-size`, `font-weight` as in SVG.
|
|
71
|
+
|
|
72
|
+
## Supported SVG surface
|
|
73
|
+
|
|
74
|
+
ThorVG rasterizes the **SVG Tiny 1.2** subset — see
|
|
75
|
+
`../thorvg.wiki/SVG-Support.md`. Shapes (`rect`, `circle`, `ellipse`, `path`,
|
|
76
|
+
`polygon`, `polyline`, `line`), `<image>`, gradients, `<text>`/`<tspan>`,
|
|
77
|
+
`<g>`, `clipPath`/`mask`, and transforms are supported. Layout/CSS keep their
|
|
78
|
+
SVG meaning but only within that subset.
|
|
79
|
+
|
|
80
|
+
### Known ThorVG behaviors
|
|
81
|
+
|
|
82
|
+
- **Keep transformed elements on-canvas.** An element whose transformed extent
|
|
83
|
+
leaves the canvas (especially rotated, partially off-screen) is clipped
|
|
84
|
+
coarsely: ThorVG paints **black** over the off-screen extent. Animate within
|
|
85
|
+
the frame (fade/scale/grow in place, translate inside bounds) rather than
|
|
86
|
+
sliding in from outside the canvas.
|
|
87
|
+
- **Text needs a loaded font.** ThorVG only rasterizes `<text>` after a font is
|
|
88
|
+
registered (`Text.font_load`, done automatically per engine). Renders are
|
|
89
|
+
therefore limited to fonts present on the host unless you supply one.
|
|
90
|
+
|
|
91
|
+
## Text auto-layout (`data-*` on `<text>`)
|
|
92
|
+
|
|
93
|
+
Measured by the same ThorVG SVG loader that rasterizes the frame, so background
|
|
94
|
+
chips / wrapped lines / curved text always line up with the rendered glyphs.
|
|
95
|
+
|
|
96
|
+
| attr | meaning | default |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `data-bg="COLOR"` | auto-sized rounded background chip behind the text | — |
|
|
99
|
+
| `data-bg-rx` | chip corner radius | `10` |
|
|
100
|
+
| `data-bg-pad-x` / `data-bg-pad-y` | ink-to-chip padding | `12` / `8` |
|
|
101
|
+
| `data-wrap="WIDTH"` | wrap text into stacked `<text>` lines fitting `WIDTH` px | — |
|
|
102
|
+
| `data-fit="WIDTH"` | auto-shrink font-size (floor `data-fit-min`) so the text fits | `data-fit-min=9` |
|
|
103
|
+
| `data-curve-d="PATH"` | place each char along a sampled SVG path `d` | — |
|
|
104
|
+
| `data-curve-circle="cx,cy,r[,startDeg]"` | place each char along a circle | — |
|
|
105
|
+
|
|
106
|
+
When auto-layout is used without a `Measurer` (e.g. a bare `bake_svg` call) it
|
|
107
|
+
is a no-op, so plain compositions bake exactly as before. See
|
|
108
|
+
`docs/text-capabilities.md` for the full audit and caveats.
|
|
109
|
+
|
|
110
|
+
## External raster text (`text_handler`)
|
|
111
|
+
|
|
112
|
+
Text that ThorVG fonts cannot cover — LaTeX math, an exotic brand face, emoji —
|
|
113
|
+
is rendered *outside* the framework by a library callback. Pass
|
|
114
|
+
`text_handler=callable` to `bake_svg` / `render_frame`; every
|
|
115
|
+
`<text data-raster="kind">` is offered to it as a `TextRequest` (content, kind,
|
|
116
|
+
style, fill, raw attrs). The handler returns **PNG bytes** and the node is
|
|
117
|
+
replaced by an `<image>` at an anchor-driven bbox, or `None` to keep the normal
|
|
118
|
+
ThorVG font path (auto-layout `data-*` still applies in that case).
|
|
119
|
+
|
|
120
|
+
| attr | meaning | default |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `data-raster="KIND"` | opt in to the handler; `KIND` is passed through for dispatch | — |
|
|
123
|
+
| `data-anchor` | which bbox corner/edge sits at `(x, y)`: `top-*` / `middle-*` / `bottom-*` × `-left` / `-center` / `-right` | `center` |
|
|
124
|
+
| `data-width` / `data-height` | target box px; PNG is uniformly contained (no distortion) | natural size |
|
|
125
|
+
| `data-yaw` | degrees clockwise, rotating the image around the anchor point | `0` |
|
|
126
|
+
|
|
127
|
+
Contract: `x`/`y` mean the anchor point (not an SVG baseline) while rasterized;
|
|
128
|
+
bytes must decode as PNG (anything else raises at bake time); a handler should
|
|
129
|
+
be a deterministic pure function of the request and is invoked once per node
|
|
130
|
+
per bake — `render_frame` bypasses `FrameCache` whenever a handler is supplied
|
|
131
|
+
(its key knows nothing about the handler); CLI rendering has no handler and
|
|
132
|
+
falls back to fonts. See `nanoframes/rastertext.py` and
|
|
133
|
+
`tests/test_rastertext.py` for the reference contract.
|
|
134
|
+
|
|
135
|
+
## Fonts
|
|
136
|
+
|
|
137
|
+
A bundled **monospace CJK** face ships with nanoframes
|
|
138
|
+
(`fonts/SarasaMonoSC-Regular-noliga.ttf`, Sarasa Mono SC, ligature feature
|
|
139
|
+
stripped). Use `font-family="Sarasa Mono SC"` for deterministic-width Chinese
|
|
140
|
+
labels (each Han char = `font-size` px wide). Query it with
|
|
141
|
+
`nanoframes fonts list`; add other faces with `nanoframes fonts add <path>`.
|
|
142
|
+
|
|
143
|
+
## CLI examples
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
nanoframes init my-video # scaffold a .nf.svg
|
|
147
|
+
nanoframes check my-video.nf.svg # lint (exit 1 on errors)
|
|
148
|
+
nanoframes measure my-video.nf.svg # report renderer-exact text widths
|
|
149
|
+
nanoframes render my-video.nf.svg --t 2.0 # single frame PNG
|
|
150
|
+
nanoframes preview my-video.nf.svg --t 2.0 # render + open
|
|
151
|
+
nanoframes render my-video.nf.svg -o frames # full batch
|
|
152
|
+
nanoframes video my-video.nf.svg -o out.mp4 # MP4 via ffmpeg
|
|
153
|
+
nanoframes video my-video.nf.svg -o out.mp4 --audio track.mp3 # + audio mux
|
|
154
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Text capabilities: audit, findings, and the nanoframes answer
|
|
2
|
+
|
|
3
|
+
This doc is the result of a full audit of our text story against the reference
|
|
4
|
+
projects — **hyperframes** and (its ancestor) **remotion** — plus the actual
|
|
5
|
+
ThorVG runtime we render with. It answers three questions with evidence and
|
|
6
|
+
specifies the features we added (and how to extend further).
|
|
7
|
+
|
|
8
|
+
## The three questions
|
|
9
|
+
|
|
10
|
+
### 1. Can we measure a text bounding box exactly? — Yes, and it must be renderer-exact
|
|
11
|
+
|
|
12
|
+
Two ways, and one of them is misleading:
|
|
13
|
+
|
|
14
|
+
| approach | exact? | notes |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| **rasterize + read ink** (what we use) | ✅ exact by construction | Reuses the *same* SVG loader, so the measured box always matches the final render. Size = tiny isolated `<text>` raster; cached by `(text, family, weight, size)`. |
|
|
17
|
+
| ThorVG `Text.get_aabb` / `get_metrics` | ❌ no | Two gotchas found empirically: (a) `set_size` is in **points** (px = size × `96/72`, so you must pass `px × 0.75`); (b) in this `thorvg-python 1.1.1` build a hand-built `Text` **reports metrics but does not rasterize**, and per-glyph widths are unreliable — a lone `i` measured `0.86 px`. The *newer* source `../thorvg-python` adds `get_glyph_metrics` + `line_count`; upgrading would give fast per-glyph advances. |
|
|
18
|
+
|
|
19
|
+
Because the background-misalignment bug you hit is exactly the "my vet habit maths the text differently than my renderer" failure, the invariant we chose is: **measure by rendering.** Same fonts, same loader, same result. Add a `Measurer` and auto-size from ink — it cannot drift.
|
|
20
|
+
|
|
21
|
+
### 2. Multiline text? — Not natively, so we build it
|
|
22
|
+
|
|
23
|
+
Empirically, in the installed ThorVG SVG loader neither route works:
|
|
24
|
+
|
|
25
|
+
- `<tspan>` with `x`/`y`/`dy` **renders all spans on the same baseline** (confirmed: two tspans at `y=100`/`dy=40` both ink a single 20 px-tall line).
|
|
26
|
+
- a literal ` ` newline is **collapsed into horizontal advance** (two "lines" render side by side, one line tall).
|
|
27
|
+
|
|
28
|
+
So multiline must be baked by us. We measure each candidate line and emit **separate `<text>` per line** (each line is its own paint, which ThorVG positions reliably). Interface: `data-wrap="<maxWidth>"`.
|
|
29
|
+
|
|
30
|
+
### 3. Text along a shape / curve? — No `<textPath>`, so we sample the curve ourselves
|
|
31
|
+
|
|
32
|
+
ThorVG's SVG loader has **no `<textPath>`** (confirmed: it loads with error code 0 but rasterizes nothing — an 8 px sliver). We implement the standard offline approach remotion exposes in `@remotion/paths` (`getPointAtLength` / `getTangentAtLength`): flatten the path, build an arc-length table, place each character at `point(s)` rotated to `tangent(s)`. Uses per-character ink advances for spacing. Mirrors remotion's `warp-path`/`paths` suite but as pure Python.
|
|
33
|
+
|
|
34
|
+
## Features we added (v1 of the text story)
|
|
35
|
+
|
|
36
|
+
All three are bake-time passes over the already-animated tree, driven by a
|
|
37
|
+
renderer-exact `Measurer` (`nanoframes/measure.py`, `curve.py`, `textflow.py`):
|
|
38
|
+
|
|
39
|
+
- **Measured background chip** — `<text data-bg="#3b5998" data-bg-pad-x="10" data-bg-pad-y="6" data-bg-rx="12">行人入侵</text>`: bake injects a rounded `<rect>` sized to the ink + padding. Padding/radius optional.
|
|
40
|
+
- **Auto-wrap paragraph** — `<text data-wrap="300">…</text>` (optionally with `data-bg` for a block chip): bake splits into stacked lines that fit the width.
|
|
41
|
+
- **Along-curve text** — `<text data-curve-d="M 90 300 C 200 190 420 190 560 300">DRIVE SAFE</text>` or `<text data-curve-circle="cx,cy,r[,startDeg]">…</text>`: bake emits rotated per-character `<text>` elements.
|
|
42
|
+
- **`nanoframes measure <comp>`** — prints each text's renderer-exact width, so an author can sanity-check widths before trusting a layout.
|
|
43
|
+
|
|
44
|
+
Consistency invariant: measurement uses the same font list, loader, and raster as the final frame. If auto-layout runs without a `Measurer` it is a no-op, so plain compositions bake exactly as before (all existing tests pass).
|
|
45
|
+
|
|
46
|
+
## External text: the `text_handler` escape hatch (v1)
|
|
47
|
+
|
|
48
|
+
Every finding above is about text **ThorVG draws with a loaded font**. When the
|
|
49
|
+
text itself cannot live there — LaTeX math, a brand face that crashes this
|
|
50
|
+
loader, emoji — the framework steps aside instead of growing a font engine:
|
|
51
|
+
|
|
52
|
+
`<text data-raster="kind">` is offered to a library-supplied `text_handler`
|
|
53
|
+
(`bake_svg` / `render_frame` parameter). The handler receives a `TextRequest`
|
|
54
|
+
(content, kind, style, the live animated fill, raw attrs) and returns **PNG
|
|
55
|
+
bytes**; bake replaces the node with an `<image>` at an anchor-driven bbox
|
|
56
|
+
(`data-anchor` = one of 9 corner/edge/center anchors, default `center`;
|
|
57
|
+
`data-width` / `data-height` contain the PNG without distortion; `data-yaw`
|
|
58
|
+
rotates it around the anchor point). `None` keeps the normal ThorVG font
|
|
59
|
+
path — chips, wrap, curve and fit all still apply there.
|
|
60
|
+
|
|
61
|
+
Consistency note: this is the deliberate exception to "measure by rendering".
|
|
62
|
+
A handled text's ink box is exact **by construction**: the PNG is literally
|
|
63
|
+
what the frame draws, and its size comes from the bytes (Pillow), never from
|
|
64
|
+
ThorVG metrics or fallback-font widths — so nothing can drift. The handler
|
|
65
|
+
must be a deterministic pure function of the request (there is no framework
|
|
66
|
+
memo; slow backends cache internally), and `render_frame` bypasses
|
|
67
|
+
`FrameCache` while a handler is attached, since the cache key knows nothing
|
|
68
|
+
about it. CLI rendering has no Python callback, so `data-raster` files fall
|
|
69
|
+
back to fonts there — this is a library-API feature.
|
|
70
|
+
|
|
71
|
+
Reference: `docs/composition.md` → External raster text,
|
|
72
|
+
`nanoframes/rastertext.py`, `tests/test_rastertext.py` (a hello → 你好 dummy
|
|
73
|
+
handler end-to-end), and the walkthrough showcase built from
|
|
74
|
+
`examples/raster-title.nf.svg`.
|
|
75
|
+
|
|
76
|
+
## What else is absorbable from hyperframes / remotion (ranked)
|
|
77
|
+
|
|
78
|
+
| idea | source | nanoframes status |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| TeX / any custom typesetting — LaTeX math, exotic faces (manim compiles TeX to glyph-path SVG so its renderer never loads fonts) | manim | ✅ v1 as the `text_handler` hook above — outside ThorVG fonts entirely (library API; a CLI bridge is deferred). |
|
|
81
|
+
| measure + auto-fit font size (`fitTextFontSize` shrink-down until it fits one line) | hyperframes `core/text`, remotion layout-utils | ⚠️ trivial to add on top of `Measurer` (binary/step search). |
|
|
82
|
+
| word-level wrapping / caption timing | remotion `@remotion/captions`, layout-utils; hyperframes caption blocks | wrap ✅ done; caption-timing needs duration→width math, straightforward. |
|
|
83
|
+
| shrinkwrap "rounded text box" | remotion `@remotion/rounded-text-box` | ✅ done as `data-bg`. |
|
|
84
|
+
| per-char / per-word highlight, kinetic captions (weight-shift, gradient-fill, clip-wipe…) | hyperframes `docs/catalog/components/caption-*` | examples to port later; core is just `tspan`-replacements + fills we already bake. |
|
|
85
|
+
| global transforms (e.g. `getBoundingRect` helpers, `@remotion/paths` point/tangent) | remotion paths | curve ✅; expose `getLength/point/tangent` as re-usable module (done in `curve.py`). |
|
|
86
|
+
| variable-font axis animation | hyperframes `registry/components/variable-font-flex` | ⚠️ needs font with variation axes + a shaper; stretch. |
|
|
87
|
+
| Google-fonts auto-download | hyperframes/remotion `fonts` | ⚠️ offline equivalent = a `fonts/` dir + exact-family registration; see font note below. |
|
|
88
|
+
|
|
89
|
+
## Findings / caveats to keep in mind
|
|
90
|
+
|
|
91
|
+
- **Font coverage decides width.** A CJK string measured under a font lacking the glyphs still "measures" (via fallback), so a chip sized for it matches the *fallback* glyphs — not a real Chinese face. Use a face that actually has the glyphs.
|
|
92
|
+
- **ThorVG's loader font-family resolution is unreliable in this build.** A loaded font is not always used (`Arial Unicode MS` loads but never matches, so Chinese falls back to a dim built-in face). Empirically only some faces render solidly, and some *crash the process at teardown* when loaded (`AppleGothic` → segfault exit 139). `.ttc` collections can't be loaded at all.
|
|
93
|
+
- **We therefore do NOT auto-register arbitrary discovered fonts in the renderer.** Instead: `nanoframes fonts list` shows CJK faces + exact family names; `nanoframes fonts add <path>` copies a font into `~/.local/share/nanoframes/fonts`. The **bundled** face below is registered explicitly (verified safe).
|
|
94
|
+
- **Bundled CJK font: Sarasa Mono SC.** `nanoframes/fonts/SarasaMonoSC-Regular-noliga.ttf` (SIL OFL) is a **monospace** CJK face (Western = Iosevka-style, Han = Source Han Sans) — every Han char is exactly `font-size` px wide, and its `calt`/`dlig` ligature features are stripped. It is loaded **first** in the candidates list, so it is ThorVG's default: CJK text (and any font-family that doesn't resolve) renders through Sarasa, solid and monospaced. Explicit `font-family="Arial"` still resolves to Arial. Verified: load-safe, resolves, solid CJK, clean exit (unlike `AppleGothic`, which crashes this ThorVG build).
|
|
95
|
+
- **等宽 for authoring.** Use `font-family="Sarasa Mono SC"` for deterministic-width Chinese labels (and `nanoframes measure` reports its exact widths).
|
|
96
|
+
- **`set_size` points-vs-px trap.** If you use the raw `Text` metric API directly, px = size × `96/72`. We avoid it for exact work.
|
|
97
|
+
- **Upgrade path.** The newer `thorvg-python` (see `../thorvg-python`) reworks text/font support and exposes `get_glyph_metrics` + `line_count`; upgrading is the enabling change for a truly bundled/等宽 CJK font.
|
|
98
|
+
|
|
99
|
+
## Contract (extended)
|
|
100
|
+
|
|
101
|
+
New optional `data-*` on `<text>`:
|
|
102
|
+
|
|
103
|
+
| attr | meaning | default |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `data-bg` | fill color of an auto-sized rounded background chip | — |
|
|
106
|
+
| `data-bg-rx` | chip corner radius | 10 |
|
|
107
|
+
| `data-bg-pad-x` / `data-bg-pad-y` | ink→chip padding | 12 / 8 |
|
|
108
|
+
| `data-wrap` | max line width (px); wraps text into stacked `<text>` lines | — |
|
|
109
|
+
| `data-curve-d` | SVG path `d` to lay characters along | — |
|
|
110
|
+
| `data-curve-circle` | `cx,cy,r[,startDeg]` circular layout | — |
|
|
111
|
+
|
|
112
|
+
Rendering a frame/video with these is unchanged: `nanoframes render|preview|video`.
|
|
Binary file
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
2
|
+
data-width="960" data-height="540" data-fps="30" data-duration="6.0"
|
|
3
|
+
data-composition-id="master-demo">
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="sky" x1="0" y1="0" x2="1" y2="1">
|
|
6
|
+
<stop offset="0" stop-color="#0a1128"/>
|
|
7
|
+
<stop offset="1" stop-color="#203a6b"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
|
|
10
|
+
<stop offset="0" stop-color="#5ef17c"/>
|
|
11
|
+
<stop offset="1" stop-color="#38bdf8"/>
|
|
12
|
+
</linearGradient>
|
|
13
|
+
</defs>
|
|
14
|
+
|
|
15
|
+
<rect id="bg" width="960" height="540" fill="url(#sky)"/>
|
|
16
|
+
|
|
17
|
+
<!-- floating image: translate + scale pulse -->
|
|
18
|
+
<image id="orb" class="float" href="assets/dot.png"
|
|
19
|
+
x="700" y="120" width="120" height="120"
|
|
20
|
+
data-start="0.2" data-duration="5.2"/>
|
|
21
|
+
<image id="orb2" class="float" href="assets/dot.png"
|
|
22
|
+
x="100" y="320" width="72" height="72" transform="scale(0.6)"
|
|
23
|
+
data-start="0.6" data-duration="4.8"/>
|
|
24
|
+
|
|
25
|
+
<!-- rotating gradient panel (translate + rotate) -->
|
|
26
|
+
<rect id="panel" x="0" y="0" width="200" height="120" rx="22" fill="url(#accent)"
|
|
27
|
+
opacity="0.9" data-start="0.0" data-duration="6.0"/>
|
|
28
|
+
|
|
29
|
+
<!-- title: fade + slide -->
|
|
30
|
+
<text id="title" x="60" y="120" font-family="Sarasa Mono SC" font-weight="700"
|
|
31
|
+
font-size="72" fill="#f4f7ff" data-start="0.0" data-duration="6.0">
|
|
32
|
+
nanoframes
|
|
33
|
+
</text>
|
|
34
|
+
|
|
35
|
+
<!-- subtitle with fade, gradient underline that grows -->
|
|
36
|
+
<rect id="rule" x="62" y="150" width="6" height="90" rx="3" fill="url(#accent)"
|
|
37
|
+
data-start="0.6" data-duration="5.0"/>
|
|
38
|
+
<text id="sub" x="90" y="205" font-family="Sarasa Mono SC" font-size="30"
|
|
39
|
+
fill="#b6c2d8" data-start="0.9" data-duration="4.8">
|
|
40
|
+
SVG renders MP4. No browser. Fast for agents.
|
|
41
|
+
</text>
|
|
42
|
+
|
|
43
|
+
<script type="application/nanoframes+json"><![CDATA[
|
|
44
|
+
{
|
|
45
|
+
"animations": [
|
|
46
|
+
{
|
|
47
|
+
"target": "#panel",
|
|
48
|
+
"keyframes": [
|
|
49
|
+
{"t": 0.0, "opacity": 0.0, "transform": {"translate": [-260, 260], "rotate": 0}},
|
|
50
|
+
{"t": 1.0, "opacity": 0.9, "transform": {"translate": [60, 380], "rotate": -14}, "ease": "ease-in-out"},
|
|
51
|
+
{"t": 4.0, "opacity": 0.9, "transform": {"translate": [700, 320], "rotate": 12}, "ease": "ease-in-out"},
|
|
52
|
+
{"t": 5.5, "opacity": 0.4, "transform": {"translate": [760, 420], "rotate": 0}}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"target": "#title",
|
|
57
|
+
"keyframes": [
|
|
58
|
+
{"t": 0.0, "opacity": 0.0, "transform": {"translate": [0, 30]}},
|
|
59
|
+
{"t": 0.7, "opacity": 1.0, "transform": {"translate": [0, 0]}, "ease": "ease-out"}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"target": ".float",
|
|
64
|
+
"keyframes": [
|
|
65
|
+
{"t": 0.2, "opacity": 0.0, "transform": {"translate": [0, 40], "scale": [0.5, 0.5]}},
|
|
66
|
+
{"t": 1.2, "opacity": 1.0, "transform": {"translate": [0, 0], "scale": [1.0, 1.0]}, "ease": "ease-out"},
|
|
67
|
+
{"t": 4.5, "opacity": 1.0, "transform": {"translate": [0, -30], "scale": [1.1, 1.1]}, "ease": "ease-in-out"}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"target": "#rule",
|
|
72
|
+
"keyframes": [
|
|
73
|
+
{"t": 0.6, "opacity": 0.0, "transform": {"scale": [1.0, 0.05]}},
|
|
74
|
+
{"t": 1.2, "opacity": 1.0, "transform": {"scale": [1.0, 1.0]}, "ease": "ease-in-out"}
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"target": "#sub",
|
|
79
|
+
"keyframes": [
|
|
80
|
+
{"t": 0.9, "opacity": 0.0},
|
|
81
|
+
{"t": 1.4, "opacity": 1.0}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]]></script>
|
|
87
|
+
</svg>
|