xevol 0.16.0 → 0.17.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/dist/index.js +291 -265
- package/package.json +3 -1
- package/skills/xevol-film/SKILL.md +170 -0
- package/starters/blank/assets/fonts/TestSans.woff2 +1 -0
- package/starters/blank/assets/plate.txt +1 -0
- package/starters/blank/film.html +8 -0
- package/starters/blank/notes.txt +1 -0
- package/starters/blank/out/audio.wav +1 -0
- package/starters/blank/render.json +26 -0
- package/starters/blank/scripts/finish.py +1 -0
- package/starters/earshot-ad/README.md +25 -0
- package/starters/earshot-ad/assets/fonts/OFL-Onest.txt +93 -0
- package/starters/earshot-ad/assets/fonts/onest-latin.woff2 +0 -0
- package/starters/earshot-ad/film.html +193 -0
- package/starters/earshot-ad/render.json +5 -0
- package/starters/earshot-ad/scripts/finish.py +178 -0
- package/starters/earshot-ad/scripts/mix.py +208 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xevol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Xevol is a tool to consume, remix, make, publish, and offer systems, products, and workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
],
|
|
47
47
|
"files": [
|
|
48
48
|
"dist",
|
|
49
|
+
"starters",
|
|
50
|
+
"skills",
|
|
49
51
|
"README.md",
|
|
50
52
|
"package.json"
|
|
51
53
|
],
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xevol-film
|
|
3
|
+
description: Use when making or editing a code-drawn motion film (film.html, render.json and the film's own scripts) and rendering it with the xevol CLI, from the first animation pass through review of the finished render.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# xevol-film
|
|
7
|
+
|
|
8
|
+
A film is code: one HTML page draws every frame, the renderer captures it in the cloud, and the film's own scripts mix, finish and check it. You write the film on your machine; Xevol renders it.
|
|
9
|
+
|
|
10
|
+
## 1. The film contract
|
|
11
|
+
|
|
12
|
+
Folder layout:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
my-film/
|
|
16
|
+
film.html the page that draws frame t
|
|
17
|
+
render.json what may be packed, the size, the length, the steps
|
|
18
|
+
assets/ fonts, plates, footage, the mark
|
|
19
|
+
scripts/mix.py makes the WAV from the page's cues
|
|
20
|
+
scripts/finish.py trims, encodes and writes the masters under out/
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The page contract. The image must be a function of `t` alone:
|
|
24
|
+
|
|
25
|
+
- `window.seek(t)` draws the film at `t` seconds; it may return a promise.
|
|
26
|
+
- `window.DUR` is the duration in seconds.
|
|
27
|
+
- `window.ready` is an optional promise that resolves once fonts and assets are loaded. Capture starts after it.
|
|
28
|
+
- No wall clock (`Date.now`, `performance.now`), no unseeded randomness, no state carried from the previous seek, no network. Any request outside the film folder fails the run.
|
|
29
|
+
- `window.CUTS` (hard-cut times) keeps motion-blur subframes from straddling a cut. `window.CUES`, or `window.cues()`, feeds the mixer.
|
|
30
|
+
- `window.STILLS` gives still times; eight even times otherwise.
|
|
31
|
+
|
|
32
|
+
`render.json`:
|
|
33
|
+
|
|
34
|
+
| Key | Meaning |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `width`, `height` | even, 16 to 3840 |
|
|
37
|
+
| `fps` | one of 24, 25, 30, 50, 60 |
|
|
38
|
+
| `sub` | averaged subframes per frame, 1 to 8; 4 matches the accepted masters |
|
|
39
|
+
| `shutter` | degrees in (0, 360]; 180 spreads subframes over half a frame |
|
|
40
|
+
| `duration` or `frames` | REQUIRED. No browser runs before the upload, so the film declares its own length |
|
|
41
|
+
| `query` | page query string (`lang`, `format`, …), reachable in the page as `location.search` |
|
|
42
|
+
| `out` | the picture folder, `out/` or below it |
|
|
43
|
+
| `bundle` | globs of the files the page needs. Every glob must match; hidden files are never packed |
|
|
44
|
+
| `audio` | the WAV the container muxes under the picture |
|
|
45
|
+
| `mix` | `{cues, run, out}`: extra arguments for `render.mjs --cues` (or omit `cues`), the film's `.py` with its arguments, and the WAV it writes |
|
|
46
|
+
| `finish` | one command or up to four `["scripts/finish.py", "arg"]`, run after the picture |
|
|
47
|
+
| `formats` | named overrides (`width`, `height`, `out`, `query`, `stills`), chosen with `--format` |
|
|
48
|
+
|
|
49
|
+
- `{k}` placeholders in `out`, `bundle`, `audio`, `finish` and `mix` are filled from `query` and `--set k=v`, so one `render.json` serves several pages.
|
|
50
|
+
- With `mix`, the upload does not carry the WAV, the mix's inputs travel through `bundle`, and `audio` must equal `mix.out`.
|
|
51
|
+
- Size, fps and frame count travel with the bundle and the runtime re-checks them: even sizes 16 to 3840, 24/25/30/50/60 fps, ≤ 9000 frames.
|
|
52
|
+
- Without `finish` the master is the assembly encode (x264 slow CRF 15, yuv420p, BT.709-tagged, AAC 256k) with the film's sound.
|
|
53
|
+
|
|
54
|
+
## 2. Commands
|
|
55
|
+
|
|
56
|
+
Start a film from one of our own starters, or take an existing film's source:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
xevol film new --list # the starters that ship with the CLI: blank, earshot-ad
|
|
60
|
+
xevol film new <dir> [--from <starter>] # copy a starter into a new folder (default: blank)
|
|
61
|
+
xevol film pull <film> <dir> [--brand <brand>] # take an existing film's source
|
|
62
|
+
xevol film skill [--install [dir]] # print this guide; --install writes it to <dir>/xevol-film/SKILL.md (~/.claude/skills by default)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`film new` refuses a folder that exists and is not empty. `blank` is a small synthetic film with the whole
|
|
66
|
+
contract in it; `earshot-ad` is our own MotionPls 9:16 ad whose sound `scripts/mix.py` renders from earshot's
|
|
67
|
+
bank, and it is the worked example of `mix`.
|
|
68
|
+
|
|
69
|
+
Render a folder (paid, billed to the Workspace's credits):
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
xevol film render <folder> --brand <brand> [--format NAME] [--set k=v] [--draft] [--wait]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- The CLI packs exactly what `render.json` lists, uploads it as `<sha256>.tgz` to the Brand's source folder for the film, and starts one render. `--name` overrides the name taken from the folder. `--set k=v` repeats. It exits non-zero when a render ends FAILED.
|
|
76
|
+
- `--wait` follows the render and prints one line per check. Without it, keep the render id.
|
|
77
|
+
- `--draft` renders a review cut: 30 fps and one subframe (about 1/16 of the captures), no `finish`, no checks, no motion blur. Judge timing, copy and layout from it, not motion quality.
|
|
78
|
+
- A final runs the film's `finish` and the checks, and each final is billed in full (about $0.08 to $0.15 for 15 seconds, measured 2026-10-21). Render one only for something a person will watch.
|
|
79
|
+
|
|
80
|
+
Read and fetch renders:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
xevol film get <id> [--download <dir>] # state, error, output paths, one line per check
|
|
84
|
+
xevol film list [--brand <brand>] # the Workspace's films; with --brand, each one's latest render
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`see_video` does not work on film renders yet, because a code render makes no frame sheet. Download a draft with `xevol film get <id> --download out/` and look at its frames yourself.
|
|
88
|
+
|
|
89
|
+
Voice: `make_voice` (MCP) takes the script (at most 1,000 characters), `voiceId` (a named voice, default "brian", or any ElevenLabs id), `model` (`eleven_multilingual_v2` or `eleven_v3`), `settings` (stability, similarityBoost, style, speakerBoost, speed 0.7 to 1.2) and `seed`, which keeps a read repeatable. It returns the Files path, the length in seconds and when each word is spoken. Give each voice-over a new `operationId`; about 24 credits per 1,000 characters, nothing charged when it fails.
|
|
90
|
+
|
|
91
|
+
Never render or encode on a production server. On the ex44 host there is no browser capture, no `finish.py`, no ffmpeg master and no local Docker render, because it starves the services it runs. Reading and editing source, `mix.py`, `ffprobe`, and packing and submitting cloud jobs are allowed. Do not start a background job that outlives your session.
|
|
92
|
+
|
|
93
|
+
## 3. Craft norms that change a film
|
|
94
|
+
|
|
95
|
+
The numbers are the check; the principles decide whether the film reads.
|
|
96
|
+
|
|
97
|
+
Formats in use (measured on accepted masters):
|
|
98
|
+
|
|
99
|
+
- Portrait social: 1080×1920, 60 fps, 720 frames / 12.000 s.
|
|
100
|
+
- Landscape film: 1920×1080, 30 fps, 600 frames / 20 s or 1419 frames / 47.3 s.
|
|
101
|
+
- Declare the format and let the audit check it; it treats a mismatch as an error and never guesses.
|
|
102
|
+
|
|
103
|
+
Text (enforced by `text-audit`):
|
|
104
|
+
|
|
105
|
+
- Smallest readable type at a 1080 short side: 18 px. Readable also means effective alpha ≥ 0.5.
|
|
106
|
+
- Fastest readable speed: 20 px/frame at 60 fps.
|
|
107
|
+
- Minimum readable time per line: max(0.5 s, 0.3 s × words). House target per line: max(1.0 s, 0.7 s + 0.133 s × words).
|
|
108
|
+
- At most 8 words visible on one frame. A fragment is 2 to 3 words, at most 4 new; a thought of 5 or more words becomes a chain with picture between its fragments.
|
|
109
|
+
- Size: readable UI text at least 2.5 % FH (27 px at 1080); statements 5.5 to 11 % FH. Contrast at least 4.5:1 for anything that must be read.
|
|
110
|
+
- Copy rate about 15 to 25 words per 30 s for product-led films; copy on at most about 65 % of frames, with text-free stretches.
|
|
111
|
+
- Speed while read about 2 px/f. Entry from a partial state (about 40 % opacity), rise about 25 px, readable within 8 f, settled in 12 to 24 f. Exit with the cut.
|
|
112
|
+
- A hold drifts 0.3 to 1 px/f or creeps 1 to 3 %; it does not freeze.
|
|
113
|
+
- Measure the plate where the type sits. Keep type off baked-in marks and the primary action.
|
|
114
|
+
- A script without spaces takes `--script cjk`, which counts characters at two to one word: 6.6 characters a second at the floor.
|
|
115
|
+
|
|
116
|
+
Sound (enforced by `master-audit`):
|
|
117
|
+
|
|
118
|
+
- Loudness band: −19 to −14 LUFS. House levels: −18 LUFS for effects-only and calm social films, −14 LUFS for launch films with music, −16 LUFS for a sting or a corporate cut.
|
|
119
|
+
- True peak: ≤ −1.5 dBTP. Master to −1.8 dBTP: a WAV mastered to −1.5 dBTP arrived at −1.4 dBTP after AAC.
|
|
120
|
+
- Audio must cover picture: gap ≤ 20 ms. Loop seam: last sample to first ≤ 0.5 RMS.
|
|
121
|
+
- Never loudnorm a loop: it dropped the last 83 ms of audio and left a click. Apply one linear gain to the measured target.
|
|
122
|
+
- One soft sound per fragment, 0 to 80 ms after it lands, near-silence while it is read, the big hit only for the one reveal. Every hit has a visible cause; tune pitched sounds to the key.
|
|
123
|
+
|
|
124
|
+
Motion and picture (enforced by `master-audit`, the rest judged by eye):
|
|
125
|
+
|
|
126
|
+
- H.264, yuv420p, BT.709 tagged; audio 48 kHz, 2 channels, AAC.
|
|
127
|
+
- Dead frame: mean luma < 8/255; on a near-black brand ground use `--dead-luma 3`. Isolated flash: brightness swing > 15/255 up and back within 2 frames.
|
|
128
|
+
- A still longer than 0.5 s (30 frames at 60 fps) must be declared in `qa-declared.json`; an undeclared hold fails. Repeated opening frames: ≥ 3 identical at frame 0.
|
|
129
|
+
- First visible change by 0.1 s; frame 0 is composed and already moving. Premise felt by 1 s, readable by about 2 s.
|
|
130
|
+
- Visible change about every 1 s calm, 0.7 s dense. A shot carrying copy lasts at least 0.45 s. Low-energy stretch under 4 s.
|
|
131
|
+
- End card assembles over 1.0 to 1.3 s, then holds 0.6 to 1.3 s with a creep of at most 6 %.
|
|
132
|
+
- No overshoot by default; spend at most two, of 1 to 7 %, never on copy. Entrances decay, exits accelerate, and one settle constant serves all type (about 20 to 24 f).
|
|
133
|
+
- Every shot is a U in speed: it enters at 20 to 45 px/f, cruises at 1.5 to 8 px/f while the eye reads, and accelerates over its last 10 to 30 f into the cut.
|
|
134
|
+
- About half of beat changes are hard cuts; no scene crossfades (only 1 to 2 f blends on a motion peak). No locked-off shot over 1.5 s, no move above about 80 px/f.
|
|
135
|
+
- Holds stay alive: drift 0.3 to 2.5 px/f or creep about 4 % per second.
|
|
136
|
+
- Read every key frame as a paused still, and check every claim in the footage at full size.
|
|
137
|
+
|
|
138
|
+
FILM-CRAFT §12, shot language for AI video prompts (read it before asking a model for a plate):
|
|
139
|
+
|
|
140
|
+
- Prompt shape: `Camera: <move>. Movement: <what the camera does and what stays fixed>. Speed: <one pace>. Framing: <where the subject sits>. End: <the last frame as a picture>.`
|
|
141
|
+
- One move per shot; build a combination from cuts. Say what stays fixed as well as what changes, and name the mechanics, not only the move.
|
|
142
|
+
- Only two moves were tested (dolly zoom and arc, Seedance 2.5 at 480p, one take each); everything else is untested wording.
|
|
143
|
+
- A "keep the light and colours" line is not a lock. Compare the first and last frames and check the camera, not the picture.
|
|
144
|
+
- Record what happened in the film's README before promising a client an exact move.
|
|
145
|
+
|
|
146
|
+
## 4. Fonts
|
|
147
|
+
|
|
148
|
+
- Ship the font file inside the film under `assets/fonts/` and commit its licence next to it. Never hotlink a catalogue URL from a composition.
|
|
149
|
+
- Use only faces cleared for commercial use with no credit requirement, such as SIL Open Font License faces: Inter, Geist, Instrument Sans, Instrument Serif, Newsreader, Archivo, Fraunces, Manrope, Rubik, Overpass, Libre Franklin, JetBrains Mono, IBM Plex, DM Mono.
|
|
150
|
+
- Do not ship Clash Display, Clash Grotesk or Satoshi (their licence obliges you to credit ITF), or any family with empty or odd licence metadata: Nacelle, Josafronde, Paragon, Barlowfold. If the licence is ambiguous, the answer is no.
|
|
151
|
+
- Prefer `.woff2`; a 900 KB variable TTF bloats every bundle. Declare the weight range for a variable file (`{weight: '100 900'}`), not a single weight, or Chromium ignores intermediate values.
|
|
152
|
+
- Load with `@font-face` or `loadFont({family, url, weight, style})`, and resolve `window.ready` only after `document.fonts.ready`, so no frame is captured in a fallback face.
|
|
153
|
+
- Cyrillic: Inter, Geist, Manrope, Rubik, Overpass, Libre Franklin and JetBrains Mono carry it; Space Grotesk, Barlow, Public Sans and Work Sans do not. A missing glyph draws as tofu, not an error, so check with `fc-query` before committing an RU cut.
|
|
154
|
+
|
|
155
|
+
## 5. Checks
|
|
156
|
+
|
|
157
|
+
Every final render runs three model-free checks in the container that made the picture and returns them as `checks` in the render's report. A draft runs none.
|
|
158
|
+
|
|
159
|
+
- `text-audit` reads the film's page: drawn text too fast, too brief, too small, too many words on one frame, holds under the floor. The numbers are in section 3.
|
|
160
|
+
- `glyph-audit` reads `lang/<code>.json` against the fonts in `assets/fonts/` when the film has `lang/`: a missing glyph, a tofu, a string the face cannot set.
|
|
161
|
+
- `master-audit` reads each MP4 the `finish` wrote: format, decode, dead frames, flashes, undeclared holds, loudness, true peak, tail, loop seam. The film's `qa-declared.json` is passed as `--declare`, so a declared hold is a note and an undeclared one fails.
|
|
162
|
+
|
|
163
|
+
How to read them: `xevol film get <id>` and `render --wait` print one line per check, naming the tool, the target's file name, and pass or fail with the tool's exit code. Each entry in the report is `{tool, target, ok, exit, result|error}`, where `result` is the tool's own JSON report and `error` the tail of its stderr when it wrote none. A check never fails the render, so read `result` for the numbers before you call the render good. A passing run is not an approval: a person still watches the film.
|
|
164
|
+
|
|
165
|
+
## 6. Facts and claims
|
|
166
|
+
|
|
167
|
+
- Use only the company's own copy, marks and figures. Never invent a customer, a number, a result or a quote.
|
|
168
|
+
- Check every claim against the footage at full size before it goes on screen.
|
|
169
|
+
- A drawn interface does not explain a product; the client's own footage and claims do.
|
|
170
|
+
- Keep the source of every version anyone sees: copy `film.html` and `scripts/mix.py` to `versions/<name>/` before rewriting a film that is live.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
synthetic font placeholder
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
synthetic plate
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
not declared in render.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
RIFFsynthetic-wav
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"width": 1080,
|
|
3
|
+
"height": 1920,
|
|
4
|
+
"fps": 30,
|
|
5
|
+
"sub": 2,
|
|
6
|
+
"shutter": 180,
|
|
7
|
+
"query": { "shape": "9x16" },
|
|
8
|
+
"out": "out/9x16",
|
|
9
|
+
"bundle": ["film.html", "assets/**"],
|
|
10
|
+
"audio": "out/audio.wav",
|
|
11
|
+
"finish": ["scripts/finish.py", "9x16"],
|
|
12
|
+
"duration": 2,
|
|
13
|
+
"formats": {
|
|
14
|
+
"tall": {
|
|
15
|
+
"width": 1920,
|
|
16
|
+
"height": 1080,
|
|
17
|
+
"out": "out/16x9",
|
|
18
|
+
"query": { "shape": "16x9" },
|
|
19
|
+
"finish": ["scripts/finish.py", "16x9"]
|
|
20
|
+
},
|
|
21
|
+
"localized": {
|
|
22
|
+
"out": "out/{lang}",
|
|
23
|
+
"finish": ["scripts/finish.py", "9x16", "{lang}.json"]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
print("synthetic finish")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# earshot-ad — a MotionPls starter
|
|
2
|
+
|
|
3
|
+
A 12-second, mute-first, 9:16 ad for our own brand, MotionPls. The page (`film.html`) draws every frame from `t`;
|
|
4
|
+
the sound is fully generated by [earshot](https://github.com/Leonxlnx/earshot)'s sound bank, driven by the film's own
|
|
5
|
+
cue list — no recorded audio, no music track, no downloaded assets.
|
|
6
|
+
|
|
7
|
+
- `film.html` — the picture: `window.seek(t)`, `window.DUR`, `window.CUES`, the Onest face from `assets/fonts/`.
|
|
8
|
+
- `render.json` — 1080×1920, 60 fps, 4 subframes, 12 s, and a `mix` that makes the WAV in the cloud.
|
|
9
|
+
- `scripts/mix.py` — reads `out/cues.json` (the `mix.cues` step `render.mjs --cues --out out` writes it) and renders
|
|
10
|
+
`out/audio.wav`: a kalimba arpeggio plus one bank sound per picture cue, one linear gain to −18 LUFS.
|
|
11
|
+
- `scripts/finish.py` — trims and encodes the masters under `out/`.
|
|
12
|
+
- `assets/fonts/onest-latin.woff2` with its licence (`OFL-Onest.txt`): Onest is our own face, SIL Open Font License.
|
|
13
|
+
|
|
14
|
+
Render it through Xevol (a draft first; a final is billed):
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
xevol film render . --brand MotionPls --draft --wait
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`mix.py` needs earshot importable (`python` with `earshot` installed); the container provides it. To mix locally:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
node ../shared/render/render.mjs . --cues # out/cues.json
|
|
24
|
+
python scripts/mix.py # out/audio.wav, out/stems/, out/sound.json
|
|
25
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2021 The Onest Project Authors (https://github.com/simpals/onest)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>MotionPls — every cut can land</title>
|
|
6
|
+
<style>
|
|
7
|
+
@font-face { font-family: "Onest"; src: url("assets/fonts/onest-latin.woff2") format("woff2"); font-weight: 100 900; }
|
|
8
|
+
html, body { margin: 0; background: #000; }
|
|
9
|
+
canvas { display: block; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<canvas id="frame"></canvas>
|
|
14
|
+
<script>
|
|
15
|
+
// "Every cut can land." — a 12 s mute-first 9:16 ad for MotionPls, 24 beats at 120 BPM.
|
|
16
|
+
// PICTURE: every frame is a pure function of t (seek). No images, no video, no external assets but the font.
|
|
17
|
+
// SOUND: the whole soundtrack is rendered from earshot's sound bank by scripts/mix.py — no recorded audio,
|
|
18
|
+
// no Mixkit, no music track. Every cue below is a bank sound placed where the picture lands on it; mix.py reads
|
|
19
|
+
// window.CUES, renders each id with its own knobs (apex for whooshes and risers, pitch for the bell), and writes
|
|
20
|
+
// the measured onset of every placed cue to out/sound.json.
|
|
21
|
+
// 0-2 paper: "your ad is silent." Deliberately dry; nothing lands.
|
|
22
|
+
// 2-4 the word drops, "So nothing lands." types in on three ticks.
|
|
23
|
+
// 4-6 ink floods, the score starts, two hits land on the ruler.
|
|
24
|
+
// 6-8 four bars grow on the beat: whoosh, hit, tick, riser; the riser runs into the drop.
|
|
25
|
+
// 8-10 orange flood: "From $299." lands hard.
|
|
26
|
+
// 10-12 the mark, the chime, "Book a call", the tail.
|
|
27
|
+
const W = 1080, H = 1920, CX = W / 2, FPS = 60, DUR = 12;
|
|
28
|
+
const PAPER = '#f3f1ec', INK = '#0b0b0c', GREY = '#8b8b90', ORANGE = '#ff4d00', LIME = '#d6ff3b';
|
|
29
|
+
const BEAT = 0.5; // 120 BPM
|
|
30
|
+
|
|
31
|
+
// Every sound cue the picture places. mix.py maps the kind to a bank id; the time is the picture's own.
|
|
32
|
+
const CUES = [
|
|
33
|
+
{t: 2.00, kind: 'impact'}, // the word hits the floor
|
|
34
|
+
{t: 2.35, kind: 'tick'}, {t: 2.60, kind: 'tick'}, {t: 2.85, kind: 'tick'}, // one per word
|
|
35
|
+
{t: 4.00, kind: 'whoosh'}, // the ink floods in
|
|
36
|
+
{t: 4.85, kind: 'hit'}, {t: 5.35, kind: 'hit'}, // "sound", "on the cut"
|
|
37
|
+
{t: 6.00, kind: 'tick'}, {t: 6.50, kind: 'tick'}, {t: 7.00, kind: 'tick'}, {t: 7.50, kind: 'tick'},
|
|
38
|
+
{t: 6.80, kind: 'riser'}, // runs into the drop at 8.0
|
|
39
|
+
{t: 8.00, kind: 'impact'}, // "From $299."
|
|
40
|
+
{t: 8.90, kind: 'tick'},
|
|
41
|
+
{t: 10.00, kind: 'chime'}, {t: 10.55, kind: 'pop'}, {t: 11.10, kind: 'tick'},
|
|
42
|
+
];
|
|
43
|
+
window.CUES = CUES; window.DUR = DUR;
|
|
44
|
+
|
|
45
|
+
const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
|
|
46
|
+
const lerp = (a, b, k) => a + (b - a) * k;
|
|
47
|
+
const eOut = k => 1 - Math.pow(1 - clamp(k), 3);
|
|
48
|
+
const eIn = k => Math.pow(clamp(k), 3);
|
|
49
|
+
const eInOut = k => { k = clamp(k); return k < 0.5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2; };
|
|
50
|
+
const eBack = k => { k = clamp(k); const c = 1.7; return 1 + (c + 1) * Math.pow(k - 1, 3) + c * Math.pow(k - 1, 2); };
|
|
51
|
+
const at = (t, a, d = 0.34) => clamp((t - a) / d); // progress of an element appearing at `a`
|
|
52
|
+
|
|
53
|
+
const main = document.getElementById('frame');
|
|
54
|
+
main.width = W; main.height = H;
|
|
55
|
+
const M = main.getContext('2d');
|
|
56
|
+
|
|
57
|
+
function font(size, weight = 800) {
|
|
58
|
+
M.font = `${weight} ${size}px Onest, system-ui, sans-serif`;
|
|
59
|
+
M.letterSpacing = size >= 110 ? `${(-0.045 * size).toFixed(1)}px` : `${(-0.012 * size).toFixed(1)}px`;
|
|
60
|
+
}
|
|
61
|
+
function word(s, x, y, size, color, {k = 1, weight = 800, align = 'center', dy = 0, ls = null} = {}) {
|
|
62
|
+
if (k <= 0 || !s) return;
|
|
63
|
+
M.save();
|
|
64
|
+
M.globalAlpha *= k;
|
|
65
|
+
font(size, weight);
|
|
66
|
+
if (ls !== null) M.letterSpacing = `${ls}px`;
|
|
67
|
+
M.fillStyle = color; M.textAlign = align; M.textBaseline = 'middle';
|
|
68
|
+
M.fillText(s, x, y + dy);
|
|
69
|
+
M.restore();
|
|
70
|
+
}
|
|
71
|
+
function rr(x, y, w, h, r) { M.beginPath(); M.roundRect(x, y, w, h, r); }
|
|
72
|
+
function disc(x, y, r, c) { if (r > 0) { M.fillStyle = c; M.beginPath(); M.arc(x, y, r, 0, 7); M.fill(); } }
|
|
73
|
+
function panel(yTop, h, c) { M.fillStyle = c; M.fillRect(0, yTop, W, h); }
|
|
74
|
+
// a centred line built from words that appear at different times (`parts`: [{s, color, at}])
|
|
75
|
+
function wordsLine(t0, parts, cx, y, size, weight = 800) {
|
|
76
|
+
const gap = size * 0.26;
|
|
77
|
+
M.save(); font(size, weight);
|
|
78
|
+
const ws = parts.map(p => M.measureText(p.s).width);
|
|
79
|
+
const total = ws.reduce((a, b) => a + b, 0) + gap * (parts.length - 1);
|
|
80
|
+
M.restore();
|
|
81
|
+
let x = cx - total / 2;
|
|
82
|
+
parts.forEach((p, i) => {
|
|
83
|
+
const k = eOut(at(t0, p.at, 0.26));
|
|
84
|
+
if (k > 0) word(p.s, x, y + 34 * (1 - k), size, p.color, {weight, k, align: 'left'});
|
|
85
|
+
x += ws[i] + gap;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function draw(t) {
|
|
90
|
+
const jolt = t >= 2 ? 11 * Math.sin((t - 2) * 95) * Math.exp(-(t - 2) * 17) : 0;
|
|
91
|
+
|
|
92
|
+
// ------------------------------------------------------------------ A/B: paper, the dry ad (0 - 4 s)
|
|
93
|
+
panel(0, H, PAPER);
|
|
94
|
+
M.save(); M.translate(0, jolt);
|
|
95
|
+
word('MOTIONPLS', 96, 424, 26, GREY, {weight: 700, align: 'left', ls: 9, k: at(t, 0.05, 0.3)});
|
|
96
|
+
// "your ad" leaves as the next idea arrives, so the two lines never sit on each other
|
|
97
|
+
const leave = eOut(at(t, 2.0, 0.3));
|
|
98
|
+
word('your ad', CX, 848 - 54 * leave, 168, INK, {k: eOut(at(t, 0.15)) * (1 - leave)});
|
|
99
|
+
const drop = eIn(at(t, 2.0, 0.36));
|
|
100
|
+
word('is silent.', CX, 1040 + 780 * drop, 168, ORANGE, {k: 1 - drop});
|
|
101
|
+
const ul = eOut(at(t, 1.35, 0.42)) * (1 - clamp((t - 2.0) / 0.2));
|
|
102
|
+
if (ul > 0) { M.globalAlpha = ul; M.fillStyle = INK; M.fillRect(CX - 250 * ul, 1146, 500 * ul, 5); M.globalAlpha = 1; }
|
|
103
|
+
// "So nothing" on one line, "lands." under it, a tick per word
|
|
104
|
+
wordsLine(t, [{s: 'So', color: INK, at: 2.35}, {s: 'nothing', color: INK, at: 2.60}], CX, 902, 128);
|
|
105
|
+
word('lands.', CX + 168, 1052, 128, ORANGE, {k: eOut(at(t, 2.85, 0.26)), dy: 34 * (1 - eOut(at(t, 2.85, 0.26)))});
|
|
106
|
+
word('sound is half the ad.', CX, 1210, 34, GREY, {weight: 600, k: at(t, 3.3, 0.3)});
|
|
107
|
+
M.restore();
|
|
108
|
+
|
|
109
|
+
// ------------------------------------------------------------------ C/D: ink, the score (4 - 8 s)
|
|
110
|
+
const inkK = eInOut(at(t, 4.0, 0.3));
|
|
111
|
+
if (inkK > 0) {
|
|
112
|
+
panel(H - H * inkK, H * inkK, INK);
|
|
113
|
+
const settle = eOut(at(t, 6.0, 0.45));
|
|
114
|
+
const y = lerp(812, 540, settle), size = lerp(150, 88, settle), gap = lerp(160, 96, settle);
|
|
115
|
+
const dim = t > 6 ? 0.42 : 1;
|
|
116
|
+
word('we put', CX, y, size, PAPER, {k: dim * eOut(at(t, 4.35, 0.3)), dy: 40 * (1 - eOut(at(t, 4.35, 0.3)))});
|
|
117
|
+
word('sound', CX, y + gap, size, LIME, {k: dim * eOut(at(t, 4.85, 0.3)), dy: 40 * (1 - eOut(at(t, 4.85, 0.3)))});
|
|
118
|
+
word('on the cut.', CX, y + 2 * gap, size, PAPER, {k: dim * eOut(at(t, 5.35, 0.3)), dy: 40 * (1 - eOut(at(t, 5.35, 0.3)))});
|
|
119
|
+
|
|
120
|
+
// the ruler: 4.0 - 6.0 s across the frame, a lime tick where the picture lands
|
|
121
|
+
const L = 96, R = W - 96, base = 1280;
|
|
122
|
+
const rk = eOut(at(t, 4.4, 0.9));
|
|
123
|
+
if (rk > 0.02) {
|
|
124
|
+
M.strokeStyle = '#3a3a40'; M.lineWidth = 2;
|
|
125
|
+
M.beginPath(); M.moveTo(L, base); M.lineTo(lerp(L, R, rk), base); M.stroke();
|
|
126
|
+
for (const tc of [4.85, 5.35, 6.0]) {
|
|
127
|
+
const k = eOut(at(t, tc, 0.2));
|
|
128
|
+
if (k <= 0) continue;
|
|
129
|
+
const x = lerp(L, R, (tc - 4.0) / 2.0);
|
|
130
|
+
M.strokeStyle = LIME; M.lineWidth = 4;
|
|
131
|
+
M.beginPath(); M.moveTo(x, base - 30 * k); M.lineTo(x, base + 30 * k); M.stroke();
|
|
132
|
+
}
|
|
133
|
+
// four bars grow on the beat, one per cue
|
|
134
|
+
const bars = [[6.0, 280, 'whoosh'], [6.5, 352, 'hit'], [7.0, 208, 'tick'], [7.5, 312, 'riser']];
|
|
135
|
+
bars.forEach(([tc, h, label], i) => {
|
|
136
|
+
const k = eOut(at(t, tc, 0.16));
|
|
137
|
+
if (k <= 0) return;
|
|
138
|
+
const x = 252 + i * 192, bw = 58;
|
|
139
|
+
const hh = h * k * (1 + 0.05 * Math.sin((t - tc) * 7.5 + i));
|
|
140
|
+
M.fillStyle = LIME; rr(x - bw / 2, base - hh, bw, hh, 10); M.fill();
|
|
141
|
+
word(label, x, base + 62, 24, GREY, {weight: 600, k});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ------------------------------------------------------------------ E: orange flood (8 - 10 s)
|
|
147
|
+
const orK = eInOut(at(t, 8.0, 0.3));
|
|
148
|
+
if (orK > 0) {
|
|
149
|
+
panel(H - H * orK, H * orK, ORANGE);
|
|
150
|
+
const land = eOut(at(t, 8.06, 0.26));
|
|
151
|
+
const s = lerp(1.12, 1, land);
|
|
152
|
+
M.save(); M.translate(CX, 872); M.scale(s, s); M.translate(-CX, -872);
|
|
153
|
+
word('From $299.', CX, 872, 158, INK, {k: land});
|
|
154
|
+
M.restore();
|
|
155
|
+
word('one 30-second promo', CX, 1004, 44, INK, {weight: 600, k: at(t, 8.9, 0.3)});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ------------------------------------------------------------------ F: ink, the mark (10 - 12 s)
|
|
159
|
+
const fk = eInOut(at(t, 10.0, 0.28));
|
|
160
|
+
if (fk > 0) {
|
|
161
|
+
panel(0, H * fk, INK);
|
|
162
|
+
const k = at(t, 10.05, 0.4);
|
|
163
|
+
if (k > 0) {
|
|
164
|
+
const y = 792, ang = (t - 10) * 1.15, spin = (t - 10) * 2.3;
|
|
165
|
+
M.save(); M.globalAlpha = k;
|
|
166
|
+
M.strokeStyle = PAPER; M.lineWidth = 6;
|
|
167
|
+
M.beginPath(); M.arc(CX, y, 92, 0, 7); M.stroke();
|
|
168
|
+
M.save(); M.translate(CX, y); M.rotate(ang); M.strokeStyle = '#4a4a52'; M.lineWidth = 3;
|
|
169
|
+
M.beginPath(); M.ellipse(0, 0, 196, 68, 0, 0, 7); M.stroke(); M.restore();
|
|
170
|
+
disc(CX + 196 * Math.cos(spin), y + 68 * Math.sin(spin), 16, LIME);
|
|
171
|
+
M.restore();
|
|
172
|
+
word('motionpls', CX, 972, 66, PAPER, {weight: 700, ls: 1, k: at(t, 10.3, 0.35)});
|
|
173
|
+
const pk = eBack(at(t, 10.55, 0.34));
|
|
174
|
+
if (pk > 0) {
|
|
175
|
+
const pw = 392, ph = 98, py = 1096 + 34 * (1 - pk);
|
|
176
|
+
M.save(); M.globalAlpha = clamp(pk); M.fillStyle = LIME;
|
|
177
|
+
rr(CX - pw / 2, py, pw, ph, 49); M.fill(); M.restore();
|
|
178
|
+
word('Book a call', CX, py + ph / 2 + 2, 40, INK, {weight: 700, k: clamp(pk)});
|
|
179
|
+
}
|
|
180
|
+
word('motionpls.com', CX, 1268, 30, GREY, {weight: 600, k: at(t, 11.1, 0.3)});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
window.seek = t => { draw(Math.max(0, Math.min(DUR, t))); };
|
|
185
|
+
window.ready = (async () => {
|
|
186
|
+
await document.fonts.load('900 100px Onest');
|
|
187
|
+
await document.fonts.load('700 100px Onest');
|
|
188
|
+
await document.fonts.load('600 100px Onest');
|
|
189
|
+
draw(0);
|
|
190
|
+
})();
|
|
191
|
+
</script>
|
|
192
|
+
</body>
|
|
193
|
+
</html>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{"width": 1080, "height": 1920, "fps": 60, "sub": 4,
|
|
2
|
+
"duration": 12,
|
|
3
|
+
"stills": [0.9, 2.6, 3.6, 4.6, 5.6, 6.9, 8.4, 9.6, 10.8, 11.8],
|
|
4
|
+
"bundle": ["film.html", "assets/**"], "audio": "out/audio.wav",
|
|
5
|
+
"mix": {"cues": ["--out", "out"], "run": ["scripts/mix.py"], "out": "out/audio.wav"}}
|