greenlight-motion 0.3.4 → 0.5.0
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/NOTICE +4 -0
- package/README.md +43 -10
- package/THIRD_PARTY_LICENSES.md +1 -1
- package/cli/index.mjs +118 -13
- package/cli/plugin3d.mjs +202 -0
- package/dist/assets/rigWorker-CGpWyn_f.js +1 -0
- package/dist/bundle-inputs.json +59 -1
- package/dist/greenlight-motion-script.mjs +17146 -355
- package/dist/greenlight-motion.js +1104 -763
- package/dist/greenlight-motion.mjs +79577 -73213
- package/dist/script-inputs.json +8 -1
- package/package.json +9 -1
- package/schema/capabilities.json +1 -1
- package/skill/SKILL.md +22 -11
- package/skill/references/scene-script.md +88 -2
- package/skill/references/video-3d.md +323 -101
- package/skill/references/video-backgrounds.md +89 -41
- package/skill/references/video-camera.md +159 -83
- package/skill/references/video-design.md +39 -7
- package/skill/references/video-effects.md +969 -641
- package/skill/references/video-grid.md +24 -24
- package/skill/references/video-keyframes.md +151 -52
- package/skill/references/video-particles.md +42 -29
- package/skill/references/video-paths.md +70 -65
- package/skill/references/video-projects.md +175 -98
- package/skill/references/video-shapes.md +95 -31
- package/skill/references/video-text-kinetic.md +78 -27
- package/skill/references/video-text-presets.md +738 -17
- package/skill/references/video-text.md +209 -81
- package/skill/references/video-transitions.md +72 -17
- package/skill/references/video-world3d.md +1548 -0
- package/skill/references/world3d-schema.json +15118 -0
|
@@ -10,19 +10,19 @@
|
|
|
10
10
|
|
|
11
11
|
## Contents
|
|
12
12
|
|
|
13
|
-
- [Text caption layers](
|
|
14
|
-
- [How a text layer is structured](
|
|
15
|
-
- [Text item fields (defaults
|
|
16
|
-
- [3D Text — real extruded letters (`text3d`)](
|
|
17
|
-
- [Dynamic text placeholders (counter, timer, word cycle, scramble, variables, markers, timecode)](
|
|
18
|
-
- [Advanced Animations — effects over a moving range of characters, words or lines](
|
|
19
|
-
- [Worked example — add a bottom caption to an existing project](
|
|
13
|
+
- [Text caption layers](#text-caption-layers)
|
|
14
|
+
- [How a text layer is structured](#how-a-text-layer-is-structured)
|
|
15
|
+
- [Text item fields (defaults the app fills in)](#text-item-fields-defaults-the-app-fills-in): [caption bar look](#caption-bar-look), [caption-bar reveal](#caption-bar-reveal), [Grow Shape](#grow-shape), [shadow](#shadow), [case, decoration and clip mask](#case-decoration-and-clip-mask), [fill style and outline](#fill-style-and-outline), [voice-synced captions](#voice-synced-captions), [Text Randomizer fields](#text-randomizer-fields)
|
|
16
|
+
- [3D Text — real extruded letters (`text3d`)](#3d-text--real-extruded-letters-text3d)
|
|
17
|
+
- [Dynamic text placeholders (counter, timer, word cycle, scramble, variables, markers, timecode)](#dynamic-text-placeholders)
|
|
18
|
+
- [Advanced Animations — effects over a moving range of characters, words or lines](#advanced-animations--effects-over-a-moving-range-of-characters-words-or-lines)
|
|
19
|
+
- [Worked example — add a bottom caption to an existing project](#worked-example--add-a-bottom-caption-to-an-existing-project)
|
|
20
20
|
|
|
21
21
|
## Text caption layers
|
|
22
22
|
|
|
23
23
|
Use this to add **animated text captions** — e.g. a subtitle that sits at the bottom of a video and reveals word-by-word (or letter-by-letter) with a fade/rise/blur. The motion is **Advanced Animations** (`textAnimators`, its own section below). There is no `/from-clips`-style helper for text; build the text layer by hand into the project's `data` and `PUT` it back.
|
|
24
24
|
|
|
25
|
-
>
|
|
25
|
+
> Render captions with **`POST /api/moodboards/{board_id}/video-projects/{project_id}/render-onserver`** (the full headless render bakes text, animations and voice in). The fast `POST …/render` composites only single-channel `video`/`image` timelines and drops text layers. See rendering.md (GreenLight Dash only).
|
|
26
26
|
|
|
27
27
|
### How a text layer is structured
|
|
28
28
|
|
|
@@ -36,9 +36,9 @@ A text layer is two things added to the editor `data`:
|
|
|
36
36
|
|
|
37
37
|
**Channel order = z-order.** Channels composite in array order; the **last** channel in `channels` renders on top. So the video channel comes first, the caption's text channel comes **after** it.
|
|
38
38
|
|
|
39
|
-
**Timing.** Give the caption channel `"unsnapped": true` and set the item's `freeStart` (timeline seconds where the caption appears). The on-screen duration is `trimEnd - trimStart` (the text clip's natural length
|
|
39
|
+
**Timing.** Give the caption channel `"unsnapped": true` and set the item's `freeStart` (timeline seconds where the caption appears). The on-screen duration is `trimEnd - trimStart` (the text clip's `duration` is its natural length: set it at least as long as the caption shows, with `trimEnd` ≤ it). Example: `freeStart: 1, trimStart: 0, trimEnd: 3` → caption visible from 1s to 4s. An animator's **in** plays at the start of that window, its **out** at the end.
|
|
40
40
|
|
|
41
|
-
### Text item fields (defaults
|
|
41
|
+
### Text item fields (defaults the app fills in)
|
|
42
42
|
|
|
43
43
|
Any field you omit is filled with the default below, so a caption item only needs `id`, `clipId`, the text/style fields you want to change, the position, the timing, and the animation block.
|
|
44
44
|
|
|
@@ -47,7 +47,7 @@ Any field you omit is filled with the default below, so a caption item only need
|
|
|
47
47
|
| `textContent` | `"Type your text"` | The caption string. `\n` makes hard line breaks; long lines auto-wrap at 86% of frame width. |
|
|
48
48
|
| `textColor` | `"#ffffff"` | Any CSS color. |
|
|
49
49
|
| `textFontSizeUnit` | `"px"` | `"px"` uses `textFontSize`; `"pct"` uses `textFontSizePct`. |
|
|
50
|
-
| `textFontSize` | `96` | Composition pixels (8–1000), used only in `"px"` mode. 96 is title-size at 1080p; captions are commonly 52–72px. A render at another size **with the project's aspect ratio** (720p / 4K of a 1080p project) scales it with the rest of the frame. Only a render at a **different** aspect ratio, which reflows the composition to the new frame, keeps the absolute px (see Output size in
|
|
50
|
+
| `textFontSize` | `96` | Composition pixels (8–1000), used only in `"px"` mode. 96 is title-size at 1080p; captions are commonly 52–72px. A render at another size **with the project's aspect ratio** (720p / 4K of a 1080p project) scales it with the rest of the frame. Only a render at a **different** aspect ratio, which reflows the composition to the new frame, keeps the absolute px (see Output size in rendering.md (GreenLight Dash only)). |
|
|
51
51
|
| `textFontSizePct` | `8.9` | Font size as % of the **composition height** (0.5–100), used only in `"pct"` mode: `5.9` equals about 64px at 1080p. Prefer it for API-authored projects that may be rendered at a different aspect ratio: it follows the reflowed frame there, where px does not. Same-aspect renders scale px and pct alike. |
|
|
52
52
|
| `textFontFamily` | `"'Helvetica Neue', Helvetica, Arial, sans-serif"` | CSS font stack. |
|
|
53
53
|
| `textFontWeight` | `400` | 100–900. Captions read better at 700–800. |
|
|
@@ -56,32 +56,110 @@ Any field you omit is filled with the default below, so a caption item only need
|
|
|
56
56
|
| `textLetterSpacing` | `0` | px (−20–80). |
|
|
57
57
|
| `textAlign` | `"center"` | `left` \| `center` \| `right`. |
|
|
58
58
|
| `textWidthUnit` | `"pct"` | `"pct"` uses `textWidthPct`; `"px"` uses `textWidthPx`. |
|
|
59
|
-
| `textWidthPct` | `86` | Text box width as % of the **composition width** (5–100). The box is the wrap width and the left/right-align margin.
|
|
59
|
+
| `textWidthPct` | `86` | Text box width as % of the **composition width** (5–100). The box is the wrap width and the left/right-align margin. |
|
|
60
60
|
| `textWidthPx` | `null` | Text box width in composition pixels (40–20000), used only in `"px"` mode. |
|
|
61
61
|
| `textNoWrap` | `false` | `true` = white-space nowrap: never auto-wrap, only explicit `\n` line breaks start a new line. The box width is ignored. Overflowing text is NOT clipped — the layer's texture grows to the content (up to 4× the composition width), so a zoom-out or a camera/pan move can travel across a long single line. |
|
|
62
62
|
| `textWrapAnywhere` | `false` | `true` = a line may break between any two characters (mid-word) to fit the box. Ignored with `textNoWrap`. |
|
|
63
63
|
| `panX` | `0` | **Horizontal position**, % of the frame WIDTH from center (±50 = the frame edge; no limit, beyond that is off-frame). + = right. |
|
|
64
64
|
| `panY` | `0` | **Vertical position**, % of the frame HEIGHT from center (±50 = the frame edge; no limit). **+ = down**, − = up. Bottom caption ≈ **`panY: 35`** (≈ `38` sits lower). |
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
#### Caption bar look
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
A caption-style bar behind each text line. All lengths are composition px.
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
| Field | Default | Notes |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `textBgEnabled` | `false` | Draws the bar's fill. |
|
|
73
|
+
| `textBgColor` | `"rgba(0,0,0,0.6)"` | Any CSS colour. |
|
|
74
|
+
| `textBgPadding` | `16` | px (0–200): the spacing around the text on both axes. |
|
|
75
|
+
| `textBgPaddingX` / `textBgPaddingY` | `null` | px (0–200): the horizontal and vertical spacing on their own. `null` or omitted falls back to `textBgPadding`. |
|
|
76
|
+
| `textBgRadius` | `null` | px (0–200): the corner radius of fill and border. `null` or omitted = automatic: the padding, capped at 40 % of the font size and 40 px. |
|
|
77
|
+
| `textBorderEnabled` | `false` | Strokes a border on the bar. It is set apart from the fill, so a bar can be outline-only (`textBorderEnabled` without `textBgEnabled`). The stroke sits inside the bar's bounds, so its width never grows the bar. |
|
|
78
|
+
| `textBorderColor` | `"#ffffff"` | Any CSS colour, rgba allowed. |
|
|
79
|
+
| `textBorderWidth` | `2` | px (0.5–40). |
|
|
80
|
+
| `textBorderStyle` | `"solid"` | `solid` \| `dashed` \| `dotted`. |
|
|
81
|
+
|
|
82
|
+
#### Caption-bar reveal
|
|
83
|
+
|
|
84
|
+
Animate the bar (fill or border-only) in and out, so it does not pop in fully drawn while the words are still
|
|
85
|
+
animating. In plays over the item's first seconds, Out over its last.
|
|
86
|
+
|
|
87
|
+
| Field | Default | Notes |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| `textBgInType` / `textBgOutType` | `"none"` | `none` \| `fade` \| `mask-right` \| `mask-left` \| `mask-down` \| `mask-up` \| `center-h` \| `center-v`. A mask name is the direction the motion travels, for In and Out alike: In `mask-right` sweeps the bar on left→right and Out `mask-right` sweeps it away left→right, so the same value both ways reads as one continuous pass. `center-*` grow or shrink each bar about its middle. |
|
|
90
|
+
| `textBgInStyle` / `textBgOutStyle` | `"wipe"` | `wipe`: the finished bar is clipped by a rectangle, so the leading edge is flat. `slide`: the fill slides behind a mask fixed at the bar's final shape (`mask-*` only). `grow` ([Grow Shape](#grow-shape)): the bar's own rounded shape scales from the anchored edge and keeps its corner radius; pick it for pill-shaped bars. `center-*` take `wipe` or `grow`. |
|
|
91
|
+
| `textBgInDuration` / `textBgOutDuration` | `0.5` / `0.4` | s (0.05–4). |
|
|
92
|
+
| `textBgInDelay` | `0` | s (0–4): keeps the bar hidden this long before the In starts. Use it when the bar should land after the words have begun animating. There is no Out delay. |
|
|
93
|
+
| `textBgInEasing` / `textBgOutEasing` | `"Ease Out"` / `"Ease In"` | An easing display value, the same values as `transitionInEasing` (`"Expo Out"`, `"Back Out"`…). |
|
|
94
|
+
|
|
95
|
+
#### Grow Shape
|
|
96
|
+
|
|
97
|
+
With style `grow` the bar extends from a **seed** at the anchored edge, so the fill really starts at about zero
|
|
98
|
+
width. Nothing scales about the moving box centre, so the shape never drifts. Out mirrors In: the bar retracts to the
|
|
99
|
+
seed, then the seed goes. These fields are ignored unless the slot's style is `grow`.
|
|
100
|
+
|
|
101
|
+
| Field | Default | Notes |
|
|
102
|
+
| --- | --- | --- |
|
|
103
|
+
| `textBgInGrowStart` / `textBgOutGrowStart` | `0` | px (0–600): the seed's size. At 0 the seed is the pill's diameter (a circle) when the bar has a radius, otherwise a hairline caret. |
|
|
104
|
+
| `textBgInGrowAppear` / `textBgOutGrowAppear` | `"none"` | How the seed appears. `none`: it is there from the first frame and extends. `fade`: it fades in while extending. `scale-in`: it zooms from a dot at the start point (to a caret line, or to a circle with a radius) and only then extends, so the In takes the appear duration plus the duration. `scale-down`: it settles from 150 % about the start point while fading in. |
|
|
105
|
+
| `textBgInGrowAppearDuration` / `textBgOutGrowAppearDuration` | `0.4` | s (0.05–4). |
|
|
106
|
+
| `textBgInGrowAppearEasing` / `textBgOutGrowAppearEasing` | `"Ease Out"` / `"Ease In"` | Easing display values. |
|
|
107
|
+
|
|
108
|
+
#### Shadow
|
|
109
|
+
|
|
110
|
+
Set these only when the user asks for a text shadow: a shadow under type reads as a stock default
|
|
111
|
+
([video-design.md](video-design.md#hierarchy)).
|
|
112
|
+
|
|
113
|
+
| Field | Default | Notes |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| `textShadowX` / `textShadowY` | `0` | px (−200–200): the offset. |
|
|
116
|
+
| `textShadowBlur` | `0` | px (0–200). The shadow is off while the offset and the blur are all 0. |
|
|
117
|
+
| `textShadowColor` | `"rgba(0,0,0,0.75)"` | Any CSS colour. |
|
|
118
|
+
| `textShadowSpread` | `0` | px (0–50): thickens the shadow before the blur. |
|
|
119
|
+
|
|
120
|
+
#### Case, decoration and clip mask
|
|
121
|
+
|
|
122
|
+
| Field | Default | Notes |
|
|
123
|
+
| --- | --- | --- |
|
|
124
|
+
| `textLetterCase` | `"none"` | `none` \| `uppercase` \| `lowercase` \| `capitalize`. Re-cases the drawn glyphs without touching `textContent`, so a caption can shout while the stored text does not. |
|
|
125
|
+
| `textDecoration` | `"none"` | `none` \| `underline` \| `line-through` \| `overline`. |
|
|
126
|
+
| `textClipMask` | `false` | `true` turns the layer into a clipping mask, so footage below plays inside the letters. See the **Clipping masks** paragraph in [video-shapes.md](video-shapes.md#shape-layers-mediatype-shape). |
|
|
127
|
+
|
|
128
|
+
#### Fill style and outline
|
|
129
|
+
|
|
130
|
+
`textFillMode` is `"fill"` (default, solid glyphs), `"outline"` (hollow glyphs — the glyph boundary only) or `"both"`. `textOutlineWidth` is the stroke width in px on the text canvas (0–60, default 2; outline modes never go below a 0.5px hairline). `textOutlineColor` is optional — **omit it and the outline uses `textColor`**, so switching mode never silently changes the layer's colour; set it for a contrasting rim in `"both"` mode. Gradients work on the outline exactly as they do on the fill.
|
|
71
131
|
|
|
72
|
-
|
|
132
|
+
Use `textFillMode: "outline"` when putting **Running Glow** on type ([effects.md](video-effects.md#running-glow-entry)): the glow draws on the glyph boundary, and a filled glyph swamps it, whereas a hollow glyph is nothing but boundary. A very thin outline gives the effect little to grab — 3px+ at title sizes, or raise `glowRadius`.
|
|
73
133
|
|
|
74
|
-
|
|
75
|
-
| --- | --- |
|
|
76
|
-
| Caption bar look | `textBgEnabled` + `textBgColor` (fill), `textBgPadding` or per-axis `textBgPaddingX` / `textBgPaddingY` (px), `textBorderEnabled` + `textBorderColor` + `textBorderWidth` (px) + `textBorderStyle` (`solid`\|`dashed`\|`dotted`), `textBgRadius` (px, omit for auto) — full semantics under **Text extras** above |
|
|
77
|
-
| Caption-bar reveal (needs `textBgEnabled` or `textBorderEnabled`) | `textBgInType` / `textBgOutType` (`none`\|`fade`\|`mask-right`\|`mask-left`\|`mask-down`\|`mask-up`\|`center-h`\|`center-v`), `textBgInStyle` / `textBgOutStyle` (`wipe`\|`slide`\|`grow` — grow keeps the corner radius while the bar scales), `textBgInGrowStart` / `textBgOutGrowStart` (px, grow only — start size), `textBgInGrowAppear` / `textBgOutGrowAppear` (`none`\|`fade`\|`scale-in`\|`scale-down`, grow only) + `…GrowAppearDuration` (s) + `…GrowAppearEasing`, `textBgInDelay` (s, 0–4, default 0 — wait before the In starts), `textBgInDuration` / `textBgOutDuration` (s, 0.05–4, defaults 0.5/0.4), `textBgInEasing` / `textBgOutEasing` (easing **display values** like `"Expo Out"`) — animates the background bar in/out so it doesn't pop in while the words are still animating. Full semantics under **Text extras** above |
|
|
78
|
-
| Text Randomizer (static per-unit styling, seeded) | `textRandomEnabled` (bool), `textRandomScope` (`words`\|`lines`\|`symbols` — which units get their own style), `textRandomDist` (`uniform`\|`gaussian`\|`nth`\|`wave` — how the variation is distributed across units; `nth` accents every `textRandomNth`-th unit (2–12), `wave` rolls a sine across the text), `textRandomSeed` (0–99999 — same seed = same look; change to reshuffle), `textRandomSizeEnabled` + `textRandomSizeMin`/`textRandomSizeMax` (% of the base font size, 20–300, defaults 80/145), `textRandomColorEnabled` + `textRandomColors` (array of hex/rgba, any count, default `["#33efab"]`) + `textRandomCoverage` (0–100 % — the share of units that get a colour; the rest keep `textColor`), `textRandomWeightEnabled` + `textRandomWeights` (array from 100–900, e.g. `[300, 700]` — each unit draws one), `textRandomItalicEnabled` + `textRandomItalicMode` (`mix`\|`all`) + `textRandomItalicShare` (0–100 % — `mix` only) |
|
|
134
|
+
#### Voice-synced captions
|
|
79
135
|
|
|
80
|
-
|
|
136
|
+
Caption items produced by the AI Captions, per-take Captions and AI Shorts flows carry `textWordTimings` — an array with **exactly one `{ "startMs": int, "endMs": int }` entry per whitespace word of `textContent`**, in item-source-local milliseconds (0 = the item's untrimmed start). An animator with `range.drive: "voice"` gives every word (or letter, or line) its turn when it is spoken — see **Drive by Time / Voice** under Advanced Animations. When `textWordTimings` is missing or its length no longer matches the word count of `textContent` (the caption text was edited), a voice animator runs as `time`; the timings stay stored, so restoring the text restores the sync. Attach your own `textWordTimings` (item-local ms, exactly one entry per whitespace word) to any caption item to get the same behaviour.
|
|
81
137
|
|
|
82
|
-
**Voice link (`textVoiceSource`).** A text layer can name the audio / video / voice layer whose speech times its words: `"textVoiceSource": { "itemId": "<that timeline item's id>", "clipId": "<its clip id>" }` (the app also stores `text`, `at`, `srcAt`, `matched`, `total` — what the last sync saw).
|
|
138
|
+
**Voice link (`textVoiceSource`).** A text layer can name the audio / video / voice layer whose speech times its words: `"textVoiceSource": { "itemId": "<that timeline item's id>", "clipId": "<its clip id>" }` (the app also stores `text`, `at`, `srcAt`, `matched`, `total` — what the last sync saw). A voice sync in the editor derives `textWordTimings` from it: the layer's words are matched against the source's transcript (`item.aiData.transcribe.words`, else `clip.transcript.words`, seconds) where both layers sit on the timeline (trim and speed curve included), loosely enough that a corrected spelling, an inserted or a dropped word still lands; unmatched words are placed between their matched neighbours. A sync runs again when asked, after the text is edited or either layer moves, for one layer or for every layer on the same voice; it can also move a layer whose words are spoken elsewhere onto them, and give every other text layer of the same track the same voice. Captions made by AI Captions and Transcribe are linked when created. When you write a caption yourself, either compute `textWordTimings` directly, or set `textVoiceSource` (`itemId` + `clipId`) and ask the user to run the voice sync in the editor — the link alone does not re-time anything until a sync runs.
|
|
83
139
|
|
|
84
|
-
|
|
140
|
+
#### Text Randomizer fields
|
|
141
|
+
|
|
142
|
+
The `textRandom*` fields give each word/letter/line its own size, colour, font weight and italic — a deterministic, seeded "ransom note / editorial mix" look. It works on static text and composes with Advanced Animations: units are styled first, the animators then run on the styled result. The distribution value drives size, weight **and** colour eligibility together (the top `textRandomCoverage` % of units get a colour), so with `dist: "nth"` the accented units are the same across all properties.
|
|
143
|
+
|
|
144
|
+
| Field | Default | Notes |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
| `textRandomEnabled` | `false` | Turns the Randomizer on. Size, colour and weight then vary unless their own `…Enabled` is set `false`. |
|
|
147
|
+
| `textRandomScope` | `"words"` | `words` \| `lines` \| `symbols`: which units get their own style. |
|
|
148
|
+
| `textRandomDist` | `"uniform"` | `uniform` \| `gaussian` \| `nth` \| `wave`: how the variation is spread across units. `nth` accents every `textRandomNth`-th unit; `wave` rolls a sine across the text. |
|
|
149
|
+
| `textRandomNth` | `3` | 2–12, `nth` only. |
|
|
150
|
+
| `textRandomSeed` | `7` | 0–99999: the same seed gives the same look; change it to reshuffle. |
|
|
151
|
+
| `textRandomSizeEnabled` | `true` | Varies the size between `textRandomSizeMin` and `textRandomSizeMax`. |
|
|
152
|
+
| `textRandomSizeMin` / `textRandomSizeMax` | `80` / `145` | % of the base font size (20–300). |
|
|
153
|
+
| `textRandomColorEnabled` | `true` | Gives a share of the units a colour from `textRandomColors`. |
|
|
154
|
+
| `textRandomColors` | `["#33efab"]` | Up to 12 hex or rgba strings. |
|
|
155
|
+
| `textRandomCoverage` | `35` | 0–100 %: the share of units that get a colour; the rest keep `textColor`. |
|
|
156
|
+
| `textRandomWeightEnabled` | `true` | Each unit draws a weight from `textRandomWeights`. |
|
|
157
|
+
| `textRandomWeights` | `[300, 700]` | Up to 9 weights from 100–900. |
|
|
158
|
+
| `textRandomItalicEnabled` | `false` | Sets units in italic. |
|
|
159
|
+
| `textRandomItalicMode` | `"mix"` | `mix` (a share of the units) \| `all`. |
|
|
160
|
+
| `textRandomItalicShare` | `30` | 0–100 %, `mix` only. |
|
|
161
|
+
|
|
162
|
+
Ready-to-paste example (three-colour ransom-note letters, the look of [Classic Ransom Note](video-text-presets.md#classic-ransom-note)):
|
|
85
163
|
|
|
86
164
|
```json
|
|
87
165
|
{
|
|
@@ -112,19 +190,56 @@ layer transform. **Rotate 3D in a Text Animator turns the letters for real**, sh
|
|
|
112
190
|
"palette": ["#e8392f", "#2f6fe8", "#f5c518", "#2fb04a"], // optional: colours cycled per letter (toy look)
|
|
113
191
|
"material": "plastic",// plastic | matte | metal | glass
|
|
114
192
|
"light": "studio", // studio | soft | dramatic — only without a 3D Rig in the project
|
|
115
|
-
"envIntensity": 1
|
|
193
|
+
"envIntensity": 1, // reflections strength (0–3), only without a Rig
|
|
194
|
+
"castShadow": true, // the letters' shadow on the set, the models and each other (false: none, Draft and Final)
|
|
195
|
+
"receiveShadow": true,// the set's and the models' shadows on the letters (false: none in Draft; Final traces real light)
|
|
196
|
+
"sceneMaterial": "neon" // optional, in a 3D Scene only: wear this material of the Scene (its id), an animated one included
|
|
116
197
|
}
|
|
117
198
|
```
|
|
118
199
|
|
|
119
|
-
- **Lighting.**
|
|
120
|
-
|
|
121
|
-
|
|
200
|
+
- **Lighting.** Without a Rig, `light` picks a studio set and `envIntensity` the reflections. With
|
|
201
|
+
`renderer: "classic"` the Rig's lights and environment light the letters, and they cast and receive the Rig's
|
|
202
|
+
shadows. In a 3D Scene (any other Rig, on WebGPU) the letters stand in the Scene's world itself, as a model layer
|
|
203
|
+
does: they cast and take shadows between each other, the models, the scene objects and the floor, show in
|
|
204
|
+
reflections, and are path traced in Final; 2D layers and the Camera layer's depth of field meet them by their real
|
|
205
|
+
depth ([video-world3d.md](video-world3d.md#3d-text-in-the-world)).
|
|
122
206
|
- **Fonts.** Each letter is traced from the font the layer really draws with, so Google, uploaded and system fonts
|
|
123
207
|
and any weight work. The face is the 2D glyph's exact outline; the bevel swells the sides outward.
|
|
124
208
|
- **Not in 3D:** the caption bar, outline stroke, text shadow and glow, blur and decorations are 2D paint and are not
|
|
125
209
|
drawn on 3D letters. A Text Animator's opacity and colour apply; its blur, glow, shadow and stroke do not.
|
|
126
210
|
- **Cost:** a glyph's geometry is built once per font, character, depth and bevel and shared; each frame poses the
|
|
127
|
-
letters and renders one 3D pass
|
|
211
|
+
letters and renders one 3D pass (in a 3D Scene, no pass of their own: the world draws them, and moving or
|
|
212
|
+
recolouring a letter rebuilds nothing). `depth`, `bevel` and `bevelSegments` are not keyframable (they rebuild
|
|
213
|
+
geometry).
|
|
214
|
+
|
|
215
|
+
### Flat text in the 3D Scene (`textInWorld`)
|
|
216
|
+
|
|
217
|
+
A text layer can stand in a 3D Scene's world as flat letters instead of lying on top of the frame: models in front hide them, Position
|
|
218
|
+
`z` is real depth, Rotate 3D a real turn, and the Scene's camera and depth of field film them ([video-world3d.md](video-world3d.md#flat-text-in-the-world)).
|
|
219
|
+
Everything else stays the text layer's own, and with no world Scene on the frame the field does nothing.
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
"textInWorld": { "enabled": true } // exact 2D colours, no shadows (the default)
|
|
223
|
+
"textInWorld": { "enabled": true, "lit": true, "castShadow": true } // lit by the Scene, casting and taking shadows
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
| Field | Default | Notes |
|
|
227
|
+
| --- | --- | --- |
|
|
228
|
+
| `enabled` | `false` | The letters stand in the Scene. Stored only when set; omit the whole block for ordinary text. |
|
|
229
|
+
| `lit` | `false` | Each letter takes the Scene's light and look (a matte surface) instead of keeping its exact colours. |
|
|
230
|
+
| `castShadow` | `false` | The letters' shadow falls on the world, lit or not. |
|
|
231
|
+
| `receiveShadow` | `true` with `lit` | `false` keeps the world's shadows off lit letters. Unlit letters take none. |
|
|
232
|
+
|
|
233
|
+
- Under the Scene's own camera (`scene.camera`) the layer's plane (`panZ` 0) stands on the plane the camera looks at (its `target`), whatever the
|
|
234
|
+
composition's size; `panZ` moves the letters in depth by the Scene's units ([video-world3d.md](video-world3d.md#flat-text-in-the-world)).
|
|
235
|
+
- A text layer's `shadowPlate` (Cast shadow, Receive shadow, Depth occlusion: the classic Rig's flat plate) does nothing over a world Scene; these
|
|
236
|
+
`textInWorld` fields are its way into the Scene's light and shadows.
|
|
237
|
+
|
|
238
|
+
- One flat card per unit of the Text Animators (`basedOn`: a letter, a word or a line); letters when there are none.
|
|
239
|
+
- Not drawn in the Scene: the caption bar and its border, and the animators' glow, box and caret. The layer's In and Out transitions do
|
|
240
|
+
not play there; bring the letters in and out with Text Animators.
|
|
241
|
+
- A layer with effects, colour filters, blur or Grow Fill, a clip mask (`textClipMask`), one in a sequence or a camera-locked channel
|
|
242
|
+
stays flat. `text3d.enabled` wins.
|
|
128
243
|
|
|
129
244
|
### Dynamic text placeholders
|
|
130
245
|
|
|
@@ -134,14 +249,14 @@ layer transform. **Rotate 3D in a Text Animator turns the letters for real**, sh
|
|
|
134
249
|
|---|---|
|
|
135
250
|
| `{{{COUNTER:0-100}}}` | A number animating between two values. Format follows the typed range: `1,000-25,000` separators · `0.0-99.9` decimals (max 4) · `00-59` zero-padded · `100-0` counts down · `-50..50` negatives (`..`, `to`, `–` also separate) · `250` alone = 0 → 250. |
|
|
136
251
|
| `{{{TIMER:05:00-00:00}}}` | A clock: `ss`, `mm:ss` or `h:mm:ss`, optional `.s` decimals. One value alone (`05:00`) counts down to zero. Runs in real time by default; same styles as COUNTER. |
|
|
137
|
-
| `{{{CYCLE:fast\|simple\|yours}}}` | Words take turns: each holds `hold` s, then rolls/fades/blurs to the next
|
|
252
|
+
| `{{{CYCLE:fast\|simple\|yours}}}` | Words or phrases take turns: each holds `hold` s, then rolls / fades / blurs / **morphs** to the next. Morph: the letters both share travel from their place in one to their place in the next; the others leave and arrive. |
|
|
138
253
|
| `{{{SCRAMBLE:Access granted}}}` | Random characters decoding into the text one by one; keeps its final width the whole time. |
|
|
139
254
|
| `{{{VAR:product}}}` | A PROJECT variable — `data.textVariables: [{ "name": "product", "value": "GreenLight" }]` at the top level of the project data (names unique ignoring case). Expands to plain text, so it wraps and animates like typed words. |
|
|
140
255
|
| `{{{MARKER}}}` / `{{{MARKER:Intro}}}` | The name of the last timeline marker (`data.markers`) the playhead passed; the argument is shown before the first one (and for an unnamed marker). Plain text, like VAR. |
|
|
141
256
|
| `{{{TIMECODE}}}` | The timeline time `hh:mm:ss:ff` in equal digit cells. Arguments: `clip` (time since the layer started) and/or a format `hh:mm:ss`, `mm:ss`, `mm:ss:ff`, `ss:ff`, `ss` — e.g. `{{{TIMECODE:clip mm:ss}}}`. |
|
|
142
257
|
| `{{{FRAME}}}` / `{{{FRAME:clip}}}` | The frame number (project, or from the layer's start). |
|
|
143
258
|
|
|
144
|
-
**Each placeholder's own settings go INSIDE its token**, after `;` as `key=value` pairs — write only what differs from the default (the
|
|
259
|
+
**Each placeholder's own settings go INSIDE its token**, after `;` as `key=value` pairs — write only what differs from the default (the editor writes exactly this, and removes an option set back to its default). Settings therefore travel with the text:
|
|
145
260
|
|
|
146
261
|
```
|
|
147
262
|
Orders {{{COUNTER:0-2,480; style=roll; duration=2.5; easing=expo_out}}} in {{{TIMER:05:00; duration=10}}}
|
|
@@ -152,26 +267,48 @@ Orders {{{COUNTER:0-2,480; style=roll; duration=2.5; easing=expo_out}}} in {{{TI
|
|
|
152
267
|
|---|---|
|
|
153
268
|
| COUNTER | `style` odometer\|roll\|count (odometer) · `start` s (0) · `duration` s (1.8) · `easing` a TransitionEasing key in lowercase, e.g. `cubic_out` (cubic_out) · `width` fixed\|fit (fixed) · `digits` tabular\|natural, Count only (tabular) · `turns` 0–10 (3) · `lands` left\|right\|together (left) · `cascade` 0–90 % (45) · `direction` auto\|up\|down (auto) · `blur` 0–100 % (35) · `kf` a track id |
|
|
154
269
|
| TIMER | the same keys; defaults are a real clock: `style` count, `duration` = the timer's own span (05:00 → 300 s), `easing` linear |
|
|
155
|
-
| CYCLE | `style` roll\|fade\|blur (roll) · `start` (0) · `hold` s per
|
|
270
|
+
| CYCLE | `style` roll\|fade\|blur\|morph (roll) · `start` (0) · `hold` s per state (1.2) · `duration` transition s (0.45; morph = each letter's travel, 0.8) · `easing` (cubic_out; morph expo_in_out) · `width` fit\|fixed (fit) · `direction` up\|down: roll, and a morph's rise/arc (up) · `loop` on\|off (on) · morph only: `match` order\|any (order) · `case` ignore\|keep (ignore) · `stagger` s between letters 0–2 (0.03) · `arc` 0–1 (0) · `leave` mask\|fade\|blur\|scale (fade) · `order` left\|center\|all (left) |
|
|
156
271
|
| SCRAMBLE | `start` (0) · `duration` (1.2) · `chars` symbols\|letters\|digits\|binary\|blocks (symbols) · `order` ltr\|rtl\|random (ltr) · `flicker` glyph changes/s (18) |
|
|
157
272
|
| TIMECODE / FRAME | no options — `clip` and the format are the token body (`{{{TIMECODE:clip mm:ss}}}`) |
|
|
158
273
|
|
|
159
274
|
- Styles: `odometer` — each digit column rolls on its own from start digit to end digit, `turns` extra spins on the rightmost column, columns landing in a cascade; `roll` — a mechanical odometer (the last digit spins fastest, carries turn the next column; a timer's seconds-tens wheel has six digits); `count` — the digits just change.
|
|
160
275
|
- Timing is seconds on the item's **source** clock (`trimStart` + clip time). `width: fixed` reserves room for the widest value (nothing around it moves); `fit` closes the gap so neighbouring words slide.
|
|
161
276
|
- Keyframed progress: `kf=<id>` makes the placeholder follow `propertyKeyframes["ph:<id>"]` (values `"0"`–`"100"`, % of the way from start to end) instead of start/duration/easing — e.g. `{{{COUNTER:0-100; kf=1}}}` with a `ph:1` track. Each placeholder can have its own.
|
|
162
|
-
- Unknown or malformed options are ignored (the token still renders). The layer fields `textCounter*` / `textCycle*` / `textScramble*` and the layer-wide `textCounterProgress` track are
|
|
277
|
+
- Unknown or malformed options are ignored (the token still renders). The layer fields `textCounter*` / `textCycle*` / `textScramble*` and the layer-wide `textCounterProgress` track are read for tokens without options; write token options instead.
|
|
278
|
+
|
|
279
|
+
**Text Morph** (`style=morph`): one text turns into the next. `match=order` pairs letters that keep their order; `any` lets any letter go anywhere (anagrams: `Dash|Hands; match=any; order=center; arc=0.7; leave=scale`). Pairs that differ only in case (`i`↔`I`) crossfade on the way unless `case=keep`. Letters leave in the direction (`up`) and arrive from the other side. `leave`: `mask` slides them out of a line-high mask; `fade` rises 0.4 em and fades; `blur` blurs out; `scale` shrinks away. Each step lasts `duration + stagger × (letters involved − 1)`, then the next state holds `hold` s. A state at rest looks exactly like the typed text; letters travel around the line's alignment (left / centre / right edge).
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
{{{CYCLE:Give it a studio|GIVIT Studio; style=morph; loop=off}}}
|
|
283
|
+
Any {{{CYCLE:model|bot|agent; style=morph; leave=mask}}}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Line states** (`textLineStates`): every line of `textContent` is one state of a cycle, no token needed. The first line shows, then turns into the next, and so on. The field takes the CYCLE options as JSON (`easing` = a TransitionEasing key in UPPERCASE, `loop` a boolean). Only `style` is required; omit defaults; `null` / absent = off. Blank lines are skipped. A placeholder inside a line shows its resting value.
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{ "textContent": "Connect\nGenerate\nTalk\nPost",
|
|
290
|
+
"textLineStates": { "style": "morph", "hold": 0.9, "leave": "mask", "stagger": 0.026 } }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{ "textContent": "Fast\nSimple\nYours", "textLineStates": { "style": "roll", "hold": 1, "loop": false } }
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The whole cycle is one unit of the text: Text Animators, the randomizer and text on path treat it as one character. 3D Text films the letters; there `mask` and `blur` become fades. A state never wraps onto a second line.
|
|
163
298
|
|
|
164
299
|
### Advanced Animations — effects over a moving range of characters, words or lines
|
|
165
300
|
|
|
166
301
|
**Start from a preset recipe** before building animators from scratch: the app's presets are ready JSON, tuned in the app.
|
|
167
|
-
- Energetic text (a title, hook, promo or social line, anything punchy, bouncy or kinetic): [video-text-kinetic.md](video-text-kinetic.md) —
|
|
168
|
-
- Everything else: [video-text-presets.md](video-text-presets.md) —
|
|
302
|
+
- Energetic text (a title, hook, promo or social line, anything punchy, bouncy or kinetic): [video-text-kinetic.md](video-text-kinetic.md) — 22 in-and-out animations (Swing Pop and its family).
|
|
303
|
+
- Everything else: [video-text-presets.md](video-text-presets.md) — 54 recipes plus the app's 29 Classic ones. Sweep Reveal (typing under a colour band), Type on Shape (text typed onto a pill, card or bar that grows with it), Depth (words and letters moving in real depth under one camera: Rack Focus, Stand Up, Strata, Spotlight, Ripple), Emphasis (one highlight pass), Loop (neon, rainbow, float, flicker), Characters (decode, binary, glitch), Voice (caption karaoke on `textWordTimings`) and Classic (calm word or letter reveals, highlights, caption bars and randomizer looks, e.g. Fade Up, Rise (Mask), Typewriter, Lower Third).
|
|
169
304
|
|
|
170
|
-
**Pace:**
|
|
305
|
+
**Pace:** entrances, exits and passes take ≥ 0.8 s by default; recipes keep their tuned timing. The rule, its formula and exceptions: [video-design.md](video-design.md#text-animation-pace).
|
|
306
|
+
|
|
307
|
+
`GET /api/video-editor/capabilities` does not list Text Animator effects or presets: this page and the two preset pages are their schema.
|
|
171
308
|
|
|
172
309
|
The After Effects text-animator model on a text layer (stored as `textAnimators`). `item.textAnimators` is a list of animators; each one **selects** units of the text with a range (plus an optional wiggle) and applies its **effects** to them. Every effect value is what a unit gets at 100 % selection; the unit's selection amount (−1…1) scales it. What moves the selection is the range's `drive`: `range` (default) = Start / End / Offset, animated with `txa:` keyframe tracks or with `flow` for an endless keyframe-free travel; `time` = every unit takes its turn in seconds (a stagger, with In / Out and a highlight clock); `voice` = every unit takes its turn when it is spoken (captions). Renders identically in preview, `/render-onserver` and stills.
|
|
173
310
|
|
|
174
|
-
`textAnimators` is the only text animation. Never write `textSplit*`, `splitIn*`, `splitOut*`, `textHighlight*` or `textCaret*` fields, or `textRevealProgress` / `textHighlightColor` tracks: the app ignores them. A stored project that
|
|
311
|
+
`textAnimators` is the only text animation. Never write `textSplit*`, `splitIn*`, `splitOut*`, `textHighlight*` or `textCaret*` fields, or `textRevealProgress` / `textHighlightColor` tracks: the app ignores them. A stored project that carries them is converted into `textAnimators` when the app loads it, so a `GET` of a project the app has not loaded may show them.
|
|
175
312
|
|
|
176
313
|
```jsonc
|
|
177
314
|
"textAnimators": [{
|
|
@@ -189,6 +326,8 @@ The After Effects text-animator model on a text layer (stored as `textAnimators`
|
|
|
189
326
|
}],
|
|
190
327
|
"textAnimatorPivotX": 0, // −100…100 % of the unit's box from its centre (100 = right edge)
|
|
191
328
|
"textAnimatorPivotY": 0 // −100…100 % (100 = bottom)
|
|
329
|
+
"textAnimatorCamera": "layer", // who films Position z and Rotate 3D: "unit" (default, omit) | "layer" | "project"
|
|
330
|
+
"textAnimatorPerspective": 1300 // "layer" only: the camera's distance in px; omit / 0 = the composition camera's (2166 px at 1080p)
|
|
192
331
|
```
|
|
193
332
|
|
|
194
333
|
Rules the sanitizer enforces (anything else is dropped or clamped on load): `aid`/`pid` match `[A-Za-z0-9_-]{1,24}` and are unique; at most 8 animators and 12 effects per animator; unknown effect `type`s are dropped; every field takes its default when missing. Omit `textAnimators` (or leave it empty) for a layer without animators — the three layer fields are written only alongside a non-empty list. All px values are composition px.
|
|
@@ -209,8 +348,8 @@ Rules the sanitizer enforces (anything else is dropped or clamped on load): `aid
|
|
|
209
348
|
| `easeHigh`, `easeLow` | −100…100 % (0) | Non-square shapes: bend the profile near 1 / near 0 (+ lingers, − snaps). |
|
|
210
349
|
| `amount` | −100…100 % (100) | Scales the selection; negative pushes every effect the other way. |
|
|
211
350
|
| `randomize`, `seed` | bool (false), 0–9999 (1) | Shuffles which unit sits where in the span (deterministic per seed). |
|
|
212
|
-
| `flow` | −400…400 (0) | Offset added per second (%/s or units/s) — continuous travel without keyframes. |
|
|
213
|
-
| `loop` | bool (false) | The span wraps around the ends (pair with `flow` for an endless pass). Under `time`: the cascade repeats. |
|
|
351
|
+
| `flow` | −400…400 (0) | Offset added per second (%/s or units/s) — continuous travel without keyframes. Without `loop`, a span starting at a negative offset flows in and stops at 0; start at 0 to travel off the text. |
|
|
352
|
+
| `loop` | bool (false) | The span wraps around the ends (pair with `flow` for an endless pass). Under `time`: the cascade repeats (with `in`/`active`/`stay`, not `out`/`inOut`, nor while voice-synced). |
|
|
214
353
|
|
|
215
354
|
**Drive by Time / Voice.** Every unit gets its own clock instead of a moving span; no keyframes, and the seconds are anchored to the layer's start (In) and end (Out), so trimming a layer never stretches the motion.
|
|
216
355
|
|
|
@@ -221,7 +360,7 @@ Rules the sanitizer enforces (anything else is dropped or clamped on load): `aid
|
|
|
221
360
|
| `delay` | −60…600 s (0) | In: after the layer starts. Out: before it ends. Voice: after the word is spoken (negative = before). |
|
|
222
361
|
| `stagger` | 0…10 s (0.06) | Time: between one unit's turn and the next. |
|
|
223
362
|
| `duration` | 0…20 s (0.6) | How long each unit takes (0 = instant). For `active`/`stay` it is the fade-in. |
|
|
224
|
-
| `curve` | the app's easing names (`Ease Out`) | Each unit's own easing: `Linear`, `Ease In/Out`, `Smooth`, `Smooth Glide`, `Glide Easing Stop`, `Sine/Expo/Back In/Out/Smooth`, `Cubic In/Out`, `Power`, `Punch`, `Apple Out`, `Bounce Out`, `Elastic`, `Overshoot`, `Smooth Overshoot`, `Impulse`, `Swing`, `Anticipate`, `Natural`, `Slow Down`, `AE In (arrive)`, `AE Smooth`, `AE Out (leave)` (
|
|
363
|
+
| `curve` | the app's easing names (`Ease Out`) | Each unit's own easing: `Linear`, `Ease In/Out`, `Smooth`, `Smooth Glide`, `Glide Easing Stop`, `Sine/Expo/Back In/Out/Smooth`, `Cubic In/Out`, `Power`, `Punch`, `Apple Out`, `Bounce Out`, `Elastic`, `Overshoot`, `Smooth Overshoot`, `Impulse`, `Swing`, `Anticipate`, `Natural`, `Slow Down`, `AE In (arrive)`, `AE Smooth`, `AE Out (leave)` (an alias such as `Power2 Smooth` loads as the name it maps to). Back, Bounce, Elastic and the Overshoots land each unit past its rest and settle (transforms follow the overshoot, colours clamp). `Anticipate` is an exit: the unit winds back the other way (to −0.38 of the move at 58 %), then snaps through in the last frames. `Custom` plays `bezier`. |
|
|
225
364
|
| `bezier` | `[x1, y1, x2, y2]` or null (null) | The `Custom` curve's handles, the same shape as a keyframe's `bezier` (x 0…1, y −3…4; past 0…1 = overshoot). Write it together with `"curve": "Custom"`: a `Custom` without it plays `Ease Out`, and any named `curve` drops it. |
|
|
226
365
|
| `hold` | 0…600 s (0) | `active`: how long a unit stays lit; 0 = until the next unit's turn (voice: until the next word starts). |
|
|
227
366
|
| `fadeOut` | 0…20 s (0) | `active`: fade back down after `hold`. |
|
|
@@ -236,10 +375,10 @@ Rules the sanitizer enforces (anything else is dropped or clamped on load): `aid
|
|
|
236
375
|
|
|
237
376
|
| `type` | Fields (default) | How the selection applies |
|
|
238
377
|
|---|---|---|
|
|
239
|
-
| `position` | `x` 0, `y` −80 px | adds value × amount |
|
|
378
|
+
| `position` | `x` 0, `y` −80, `z` 0 px | adds value × amount. `z` = depth, + toward the viewer. Under a shared camera (`textAnimatorCamera` `layer` / `project`) the unit really moves in depth: nearer units grow, all converge to one vanishing point, nearer covers farther, a unit at or past the camera is not drawn. With each unit's own camera (default) `z` scales the unit in place by D / (D − z), D = its Rotate 3D `perspective`, else `textAnimatorPerspective`, else the composition camera's distance |
|
|
240
379
|
| `scale` | `scale` 160, `x` 100, `y` 100 % | 100 % → value, multiplies |
|
|
241
380
|
| `rotation` | `angle` 45° | adds |
|
|
242
|
-
| `rotate3d` | `x` 90, `y` 0°, `perspective` 800 px | adds; real perspective
|
|
381
|
+
| `rotate3d` | `x` 90, `y` 0°, `perspective` 800 px | adds; real perspective. Each unit's own camera (default): a camera `perspective` px in front of the unit (0 = flat); every unit turns like its own card. Under a shared camera `perspective` is ignored and every turn shares that camera's vanishing point. +X tips the top away, +Y the right side |
|
|
243
382
|
| `skew` | `skew` 25°, `axis` 0° | adds |
|
|
244
383
|
| `tracking` | `amount` 40 px | adds space around the unit; the row reflows and re-aligns |
|
|
245
384
|
| `opacity` | `opacity` 0 % | 100 % → value, multiplies |
|
|
@@ -255,64 +394,53 @@ Rules the sanitizer enforces (anything else is dropped or clamped on load): `aid
|
|
|
255
394
|
| `scramble` | `charset` `upper`\|`lower`\|`digits`\|`alnum`\|`symbols`\|`binary`\|`blocks`, `rate` 18/s, `seed` 1 | a unit shows random glyphs with probability = its selection |
|
|
256
395
|
| `charOffset` | `offset` 6 | shifts letters/digits by `round(offset × amount)` through the alphabet |
|
|
257
396
|
|
|
258
|
-
|
|
397
|
+
`textAnimatorCamera` / `textAnimatorPerspective` are written only while the camera is shared; `"unit"` and a Perspective of 0 are dropped.
|
|
398
|
+
|
|
399
|
+
**Depth and one camera.**
|
|
400
|
+
- `layer` puts one camera in front of the text plate's centre; it rides the layer, like CSS perspective inside a 3D-transformed element.
|
|
401
|
+
- `project` films the letters with the project's Camera layer. A dolly, pan, tilt or the layer's own `rotateX`/`rotateY` gives true parallax between letters at different `z`; with no camera move it equals `layer`.
|
|
402
|
+
- 3D Text (`text3d`) letters, and flat letters in a 3D Scene (`textInWorld`), move in real depth with `z`; the camera field doesn't apply there.
|
|
403
|
+
- Track: `txa:<aid>:<pid>.z`.
|
|
404
|
+
|
|
405
|
+
```json
|
|
406
|
+
{ "textAnimators": [{ "aid": "a1", "name": "Fly in", "enabled": true,
|
|
407
|
+
"range": { "drive": "time", "animate": "in", "basedOn": "charsNoSpace", "stagger": 0.06, "duration": 1.4, "curve": "Expo Out" },
|
|
408
|
+
"effects": [{ "pid": "p1", "type": "position", "x": 0, "y": 0, "z": -3200 }, { "pid": "p2", "type": "opacity", "opacity": 0 }] }],
|
|
409
|
+
"textAnimatorCamera": "project" }
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Colours must be 6-digit `#rrggbb`; `#fff`, `rgba()` or names silently become the effect's default colour. On a text layer used as a clip mask (`textClipMask`), `glow`, `shadow` and `box` are skipped (they would become coverage). `box` is not drawn on text on a path.
|
|
259
413
|
|
|
260
414
|
**Keyframes.** `propertyKeyframes["txa:<aid>:<chan>"]`, the usual `{ position, value, easing }` keys, value in the field's own units:
|
|
261
415
|
- range: `start`, `end`, `offset`, `amount`, `smooth`, `easeHigh`, `easeLow` (under `time`/`voice` only `amount`)
|
|
262
416
|
- wiggle: `wig.max`, `wig.min`, `wig.corr`
|
|
263
|
-
- effect: `<pid>.<field>` for any numeric effect field except `scramble`
|
|
417
|
+
- effect: `<pid>.<field>` for any numeric effect field except `scramble` `rate`/`seed` and `caret` `width`/`blink`/`hideAfter` (e.g. `txa:a1:p2.y`)
|
|
264
418
|
|
|
265
419
|
Rates (`flow`, wiggle `rate`) and seeds are not keyframable. A track whose animator or effect no longer exists is dropped on load.
|
|
266
420
|
|
|
267
|
-
|
|
421
|
+
**Speed curves.** A text layer's own animation plays on its **speed-mapped clock**, like a video's frames: with `speedPoints` (or a `speed` track) on the layer, the animators, the times they run on (`time`/`voice` delays and durations, Flow, Wiggle, the caret) and the caption-bar reveal follow the curve. So a `txa:` key's `position` is a % of the clip's **source** length (`trimEnd − trimStart`, its 1× length), not of its timeline length; on a flat curve the two are the same. Its other keyframe tracks (position, scale, opacity, colours) stay on the timeline. The editor draws and edits `txa:` keys where they play.
|
|
268
422
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
423
|
+
Worked examples of the mechanisms (spread onto a text item; durations assume a 4 s layer). For a finished look, take the
|
|
424
|
+
app's recipe instead: a band sweeping the letters once is [Colour Sweep](video-text-presets.md#colour-sweep), typing under a
|
|
425
|
+
colour band is [Ember Type](video-text-presets.md#ember-type) and the other Sweep Reveal recipes, an endless neon pass is
|
|
426
|
+
[Neon Flow](video-text-presets.md#neon-flow), a box under each spoken word is [Spoken Box](video-text-presets.md#spoken-box),
|
|
427
|
+
letters typed at speaking pace are [Typed as Spoken](video-text-presets.md#typed-as-spoken), and energetic in-and-out
|
|
428
|
+
titles are in [video-text-kinetic.md](video-text-kinetic.md).
|
|
274
429
|
|
|
430
|
+
```jsonc
|
|
275
431
|
// Cascade in: letters drop and spin into place left to right (rampUp = everything after the span is still hidden).
|
|
276
432
|
"textAnimators": [{ "aid": "a1", "name": "Cascade", "range": { "end": 30, "shape": "rampUp" },
|
|
277
433
|
"effects": [ { "pid": "p1", "type": "position", "x": 0, "y": -140 }, { "pid": "p2", "type": "rotation", "angle": -35 },
|
|
278
434
|
{ "pid": "p3", "type": "opacity", "opacity": 0 } ] }],
|
|
279
435
|
"propertyKeyframes": { "txa:a1:offset": [ { "position": 0, "value": "-30", "easing": "Smooth" }, { "position": 85, "value": "100", "easing": "Smooth" } ] }
|
|
280
436
|
|
|
281
|
-
//
|
|
282
|
-
// into place, and a second animator's band (start 20, end 80, smooth) runs across in a warm colour.
|
|
283
|
-
// Offset −80 parks the band before the text and +80 past it, so the text starts and ends in its own colour.
|
|
284
|
-
"textAnimators": [
|
|
285
|
-
{ "aid": "a1", "name": "Fade down", "range": { "end": 8, "shape": "rampUp" },
|
|
286
|
-
"effects": [ { "pid": "p1", "type": "position", "x": 0, "y": -30 }, { "pid": "p2", "type": "opacity", "opacity": 0 } ] },
|
|
287
|
-
{ "aid": "a2", "name": "Ember band", "range": { "start": 20, "end": 80, "shape": "smooth" },
|
|
288
|
-
"effects": [ { "pid": "p3", "type": "fill", "color": "#e8892b" } ] }],
|
|
289
|
-
"propertyKeyframes": {
|
|
290
|
-
"txa:a1:offset": [ { "position": 0, "value": "-8", "easing": "Linear" }, { "position": 45, "value": "100", "easing": "Linear" } ],
|
|
291
|
-
"txa:a2:offset": [ { "position": 7.5, "value": "-80", "easing": "Sine Smooth" }, { "position": 75, "value": "80", "easing": "Sine Smooth" } ] }
|
|
292
|
-
|
|
293
|
-
// Endless neon pass: a glowing band loops across dim text with no keyframes.
|
|
294
|
-
"textColor": "#56646f",
|
|
295
|
-
"textAnimators": [{ "aid": "a1", "name": "Neon", "range": { "end": 26, "shape": "triangle", "flow": 36, "loop": true },
|
|
296
|
-
"effects": [ { "pid": "p1", "type": "fill", "color": "#f2ffff" }, { "pid": "p2", "type": "glow", "color": "#36f1ff", "size": 36, "strength": 180 } ] }]
|
|
297
|
-
|
|
298
|
-
// Fade Up (Time): words rise in 0.06 s apart, and leave upward at the end.
|
|
437
|
+
// Fade Up, pace-adjusted (Time; slower than the app's Classic Fade Up): words rise in 0.06 s apart, and leave upward at the end.
|
|
299
438
|
"textAnimators": [
|
|
300
439
|
{ "aid": "a1", "name": "In", "range": { "drive": "time", "animate": "in", "basedOn": "words", "stagger": 0.06, "duration": 0.8, "curve": "Smooth" },
|
|
301
440
|
"effects": [ { "pid": "p1", "type": "position", "x": 0, "y": 50 }, { "pid": "p2", "type": "opacity", "opacity": 0 } ] },
|
|
302
441
|
{ "aid": "a2", "name": "Out", "range": { "drive": "time", "animate": "out", "basedOn": "words", "stagger": 0.06, "duration": 0.7, "curve": "Smooth" },
|
|
303
442
|
"effects": [ { "pid": "p3", "type": "position", "x": 0, "y": -50 }, { "pid": "p4", "type": "opacity", "opacity": 0 } ] }]
|
|
304
443
|
|
|
305
|
-
// Energetic in and out (Swing Pop and 20 more, each two Time animators): video-text-kinetic.md.
|
|
306
|
-
// Every other app preset (typing reveals, text on shapes, highlights, loops, decoders, voice): video-text-presets.md.
|
|
307
|
-
|
|
308
|
-
// Caption karaoke on the voice (needs textWordTimings): a box sits under each word while it is spoken.
|
|
309
|
-
"textAnimators": [{ "aid": "a1", "name": "Spoken box", "range": { "drive": "voice", "animate": "active", "basedOn": "words", "duration": 0.08, "curve": "Ease Out", "fadeOut": 0.08 },
|
|
310
|
-
"effects": [ { "pid": "p1", "type": "box", "color": "#33efab", "padX": 12, "padY": 4, "radius": 14, "grow": false }, { "pid": "p2", "type": "fill", "color": "#0b0d0c" } ] }]
|
|
311
|
-
|
|
312
|
-
// Letters typed at speaking pace behind a caret (captions).
|
|
313
|
-
"textAnimators": [{ "aid": "a1", "name": "Type", "range": { "drive": "voice", "animate": "in", "basedOn": "chars", "duration": 0.04, "curve": "Linear", "spread": true },
|
|
314
|
-
"effects": [ { "pid": "p1", "type": "opacity", "opacity": 0 }, { "pid": "p2", "type": "caret", "style": "bar" } ] }]
|
|
315
|
-
|
|
316
444
|
// Typewriter with a caret (Time): letters appear 0.05 s apart; the caret blinks once typing ends and hides 1 s later.
|
|
317
445
|
"textAnimators": [{ "aid": "a1", "name": "Type", "range": { "drive": "time", "animate": "in", "basedOn": "charsNoSpace", "stagger": 0.05, "duration": 0, "curve": "Linear" },
|
|
318
446
|
"effects": [ { "pid": "p1", "type": "opacity", "opacity": 0 }, { "pid": "p2", "type": "caret", "style": "bar", "hide": true, "hideAfter": 1 } ] }]
|
|
@@ -374,8 +502,8 @@ PUT /api/moodboards/{board_id}/video-projects/{project_id}
|
|
|
374
502
|
}
|
|
375
503
|
```
|
|
376
504
|
|
|
377
|
-
|
|
505
|
+
The app fills every omitted field (transitions, filters, opacity, etc.) with defaults, so the caption item above is complete. For multiple captions, reuse the channel only when their visible windows do **not** overlap. Any lines that must appear together (for example eyebrow + headline + sub-caption) need separate channels, one live item per channel.
|
|
378
506
|
|
|
379
|
-
3. To produce the finished file with the caption burned in, `POST /api/moodboards/{board_id}/video-projects/{project_id}/render-onserver`
|
|
507
|
+
3. To produce the finished file with the caption burned in, `POST /api/moodboards/{board_id}/video-projects/{project_id}/render-onserver` (the full render; see the note at the top). Optionally `.../activate` first so it also opens as the board's active project.
|
|
380
508
|
|
|
381
509
|
* * *
|