pixi-effects 0.17.0 → 0.18.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/CHANGELOG.md +26 -0
- package/README.md +6 -6
- package/ai/CHAT.md +3 -0
- package/ai/SKILL.md +2 -2
- package/ai/chat-template.html +8 -4
- package/ai/reference/cheatsheet.md +2 -0
- package/ai/template.html +5 -5
- package/ai/tools/check.mjs +25 -109
- package/ai/tools/render.mjs +1 -0
- package/dist/Composition-6JMXWNVV.cjs +14 -0
- package/dist/{Composition-LMVEASHF.cjs.map → Composition-6JMXWNVV.cjs.map} +1 -1
- package/dist/Composition-MLBHYYFF.js +5 -0
- package/dist/{Composition-HYAOYPAX.js.map → Composition-MLBHYYFF.js.map} +1 -1
- package/dist/Controller.d.cts +1 -1
- package/dist/Controller.d.ts +1 -1
- package/dist/{Movie-DHJRrUfI.d.ts → Movie-DlHQ9C02.d.ts} +46 -2
- package/dist/{Movie-CoxF2Z7t.d.cts → Movie-K2YuVAdg.d.cts} +46 -2
- package/dist/Presenter.d.cts +1 -1
- package/dist/Presenter.d.ts +1 -1
- package/dist/{chunk-LBJRXU7N.cjs → chunk-ANO5R2E6.cjs} +14 -4
- package/dist/chunk-ANO5R2E6.cjs.map +1 -0
- package/dist/{chunk-ETAIVOHP.cjs → chunk-JW67MDHQ.cjs} +26 -26
- package/dist/{chunk-ETAIVOHP.cjs.map → chunk-JW67MDHQ.cjs.map} +1 -1
- package/dist/{chunk-ORTYBA42.js → chunk-SG3Y66FN.js} +14 -4
- package/dist/chunk-SG3Y66FN.js.map +1 -0
- package/dist/{chunk-Q3IVBYT7.js → chunk-UXDR3ZDU.js} +3 -3
- package/dist/{chunk-Q3IVBYT7.js.map → chunk-UXDR3ZDU.js.map} +1 -1
- package/dist/index.cjs +142 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +119 -8
- package/dist/index.js.map +1 -1
- package/dist/loader.css +1 -1
- package/dist/three.cjs +5 -5
- package/dist/three.js +1 -1
- package/llms-full.txt +4 -2
- package/package.json +1 -1
- package/dist/Composition-HYAOYPAX.js +0 -5
- package/dist/Composition-LMVEASHF.cjs +0 -14
- package/dist/chunk-LBJRXU7N.cjs.map +0 -1
- package/dist/chunk-ORTYBA42.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
**Fixed**
|
|
6
|
+
|
|
7
|
+
- `pixi-effects-render --range 5:2` is a usage error (exit code 2, with what to write), not a failed render (exit code 1).
|
|
8
|
+
- `pixi-effects-check`: a mistake in `--at` or `--onion` is reported before any of the slow work (no sheet or report is written); a `--at` label such as `a/b@end` can no longer make a folder or an illegal file name.
|
|
9
|
+
- A mistyped `ease` that is empty (`ease: ''`) or a family name (`ease: 'Power2'`, which GSAP answers with an object, not an ease) now warns with what to write; before, `Power2` passed silently.
|
|
10
|
+
- `movie.inspectFonts()` and `check`: a web font that failed to load fails the check only when a text layer uses it; an unused broken `@font-face` (`failedUnused`) is listed for review. An offline CI page that links web fonts it never uses no longer fails.
|
|
11
|
+
- After a range export with motion blur the stage is left on the last frame that was exported, not on the end of the movie.
|
|
12
|
+
- A layer the timeline folds into a family row (`pop-1` … `pop-12` → `pop-# ×12`) can be named in `render({ range })`, `--scene` and `--at name@end` (`movie.timelineData()` rows carry `partNames`).
|
|
13
|
+
- The docs' motion-blur claim is now a measurement: 4 s of `hanabi-night` (a piece that asks for motion blur) took 14.0 s and 8.2 s as a draft, about 7 s of each being the browser's start-up.
|
|
14
|
+
|
|
15
|
+
**Checked, nothing to change:** the true peak reads 0.00 dB for a faded 0 dBFS sine from 1 to 18 kHz and +0.06 dB at 20 kHz (a sine switched on abruptly reads up to 0.4 dB higher at 20 kHz: that is real ringing at the switch-on); a test pins it.
|
|
16
|
+
|
|
17
|
+
**Added**
|
|
18
|
+
|
|
19
|
+
- **The Playground is rebuilt** ([guide](https://yjmtmtk.github.io/pixi-effects/guide/playground.html)). The editor holds the same block an AI edits in a chat (`ai/chat-template.html` between `EDIT FROM HERE` and `EDIT UNTIL HERE`), so what an AI wrote can be pasted in and what you wrote can be saved as the chat template's page. The movie runs in an `iframe` with no origin (`sandbox="allow-scripts allow-downloads"`: it cannot reach the page or the site's stored data; proved on a real browser) and the page talks to it through eight fixed commands. A problems panel shows what `movie.review()` finds (click to jump to the frame). **Share link** packs the code into the address (`#code=`, deflate + base64url, no server; a link is read, never run, until you press Run; one that unpacks to more than 200 KB is refused). **Save HTML** downloads one page that runs on its own (examples that use the site's own files load them from the site); **Copy for AI** copies the code with a sentence for an AI. Light and dark. The twelve examples are rewritten in the edit-block form and each runs with no warning. Twenty runs in a row leave one frame and raise no WebGL-context warning (a calibration warning raised in the frame proves the test can see it); a run takes 0.2 to 0.3 s for a title and 0.4 to 0.6 s for a piece with a video file (headless Chrome, libraries cached).
|
|
20
|
+
- **Ten WebMCP tools** on the Playground (`get_docs`, `list_examples`, `load_example`, `get_code`, `set_code`, `run`, `check`, `look`, `onion`, `render_draft`), registered with `document.modelContext` where the browser has it, so an AI agent in the browser writes, runs, reads the warnings and looks at the picture without a person pasting a red box back. They run one at a time (Chrome runs calls in parallel), a movie that never starts (an endless loop) or never answers is given up on after 70 s / 120 s so the queue cannot stall, content the page's own code can write (warnings, the code) is marked `untrustedContentHint`, and a mistake in an argument is an error result that says what is wrong. Checked on Chrome 154 with `--enable-features=WebMCP`: results are `{ content: [...] }` (text and image), `executeTool` takes the arguments as a JSON string, a tool that throws reaches the agent with no message, so tools return `isError` results instead.
|
|
21
|
+
- **`movie.review({ at, strict })` and `movie.resolveAt(list)`**: what `pixi-effects-check` looks at, in the library (frames inspected, problems, things to look at, fonts, the sound, the moments of `at`), so the `check` command, the Playground and the WebMCP tools share one judgement. Running it leaves the movie as it was (playhead and poster). The report of `check` is the same before and after the move: no difference in `inspect`, fonts, `at`, problems and audio on six gallery pieces.
|
|
22
|
+
- The chat template accepts an optional `const INIT = { assets: [...], composition: { transitions: [...] } }` in its edit block, merged into `movie.init`.
|
|
23
|
+
|
|
24
|
+
**Changed**
|
|
25
|
+
|
|
26
|
+
- `pixi-effects-check` needs a page that loads pixi-effects **0.18 or newer** (it calls `movie.review()`); an older page says so instead of being checked.
|
|
27
|
+
- The old `examples/_presets/` is gone (the presets live in `examples/playground/presets/`, in the edit-block form).
|
|
28
|
+
|
|
3
29
|
## 0.17.0
|
|
4
30
|
|
|
5
31
|
**Fixed**
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pixi-effects
|
|
2
2
|
|
|
3
|
-
> **Status**: experimental — current release `0.
|
|
3
|
+
> **Status**: experimental — current release `0.18.0`. The API may still change between minor versions (see [CHANGELOG](./CHANGELOG.md): `0.18.0` adds a rebuilt Playground (an editor on the chat template's edit block, a sandboxed run, a problems panel, share links, save as HTML), ten WebMCP tools for an AI agent in the browser, `movie.review()` and `movie.resolveAt()` (and `check` needs a page with 0.18 or newer), `0.17.0` adds part-of-the-movie and draft exports (`range`, `scale`, `draft`), a stronger `check` (`--at`, `--onion`, fonts, loudness and a waveform), `movie.onionSkin()`, a warning for a mistyped `ease`, two cookbook recipes, and fixes text masks and inverted masks, `0.16.3` fixes the export progress bar disappearing while the mouse is still and updates the landing page's speed figures, `0.16.2` adds a landing page, `AGENTS.md` and a one-sentence start for AI, camera flights (`cameraPath`, shake offsets), `trimEach`, the showpiece `ma` and a fix for 3D cards after a seek, `0.16.1` adds `musicBuffer()` and a music lab page with eight tunes, `0.16.0` adds music written as text (`{ type: 'audio', music }`, `musicEnvelope()`), `0.15.0` adds a way in for an AI in a browser chat (`ai/CHAT.md` and `ai/chat-template.html`), `0.14.0` adds `movie.inspectStops()` (and a check review) for stops that land mid-animation, `0.13.1` fixes decks starting past their first stop in the guide and gallery and draws the stop marks as dots, `0.13.0` makes the player bar play a deck stop by stop (`pauseAtStops`), marks the stops more clearly, adds PDF to the download panel and a `pdf` flag on stops, `0.12.0` adds presentations (`stops`, `Presenter`, `deck()`, page overview, presenter view, PDF export) and audio-reactive visuals (`audioEnvelope()`, `react()`), `0.11.0` adds particles, motion blur, a shared loader and a much faster build for pieces with many layers, `0.10.0` adds seeded randomness, `wiggle` and `stagger`, null layers with `parent`, `animateText`, `followPath` and path morphing, and click-to-play in the player, `0.9.0` adds a poster time (`movie.init({ poster })`) and a guide site, `0.8.0` adds events and a theme for building your own player, `0.7.0` adds filters by name, the `pixi-effects-render` and `pixi-effects-view` commands and the timeline chart, `0.6.0` adds draw-on strokes (`trimEnd`) and text that changes over time, `0.5.0` adds synthesised sound effects (`sfx`, no files) and the `check` tool, `0.4.0` makes a mask share its layer's lifetime, `0.3.0` made keyframe `at` relative to the layer).
|
|
4
4
|
|
|
5
5
|
**[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 39 portfolio pieces written as plain data by AI models · **[Examples →](https://yjmtmtk.github.io/pixi-effects/examples/)** · 15 numbered examples, a music lab + an in-browser playground.
|
|
6
6
|
|
|
@@ -51,8 +51,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
51
51
|
"gsap": "https://esm.sh/gsap@3.12.5",
|
|
52
52
|
"gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
|
|
53
53
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
54
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
55
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
54
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.18.0/dist/index.js",
|
|
55
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.18.0/dist/Controller.js"
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
</script>
|
|
@@ -63,7 +63,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
63
63
|
</script>
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.
|
|
66
|
+
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.18.0/dist/index.js`) rather than a CDN-rebundled build such as `esm.sh/pixi-effects` or jsDelivr's `+esm`: the entries (`pixi-effects`, `…/controller`, `…/three`) share internal chunks, which only works when each file is served untouched.
|
|
67
67
|
|
|
68
68
|
> **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
|
|
69
69
|
>
|
|
@@ -79,7 +79,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
|
|
|
79
79
|
"imports": {
|
|
80
80
|
"...": "(everything from the importmap above)",
|
|
81
81
|
"three": "https://esm.sh/three@0.178.0",
|
|
82
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
82
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.18.0/dist/three.js"
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
</script>
|
|
@@ -204,7 +204,7 @@ They ship in the npm package (`node_modules/pixi-effects/ai/`).
|
|
|
204
204
|
- `15-custom-player.html` — a complete player of your own in about 40 lines (play, seek, volume, replay) using the movie's events
|
|
205
205
|
- `14-draw-on.html` — draw-on strokes (`trimStart` / `trimEnd`) and text that changes over time (`visibleChars`, `set: { text }`)
|
|
206
206
|
- `music-lab.html` — eight tunes written as text (no audio files): press play, read each score; `musicBuffer()` plays music with no movie
|
|
207
|
-
- `playground.html` — in-browser editor
|
|
207
|
+
- `playground.html` — in-browser editor on the chat template's edit block: twelve examples, a problems panel from `movie.review()`, share links, save as HTML, and WebMCP tools for an AI agent
|
|
208
208
|
|
|
209
209
|
## Browser support
|
|
210
210
|
|
package/ai/CHAT.md
CHANGED
|
@@ -9,6 +9,8 @@ You write ONE HTML file. The person opens it (or your chat previews it), watches
|
|
|
9
9
|
3. Give the person the whole file. Tell them: open it, press play (sound starts after the first click), use the download button (bottom right of the picture) for the MP4, and **if a red box appears, copy its text back to you**.
|
|
10
10
|
4. When they paste warnings, fix exactly what each one names. The messages say what to change ("did you mean …").
|
|
11
11
|
|
|
12
|
+
If you are an AI agent in a browser that offers WebMCP tools (`document.modelContext`), you do not need to hand over a file: open `https://yjmtmtk.github.io/pixi-effects/examples/playground.html` and use its tools. `get_docs` (read `cheatsheet` first) → `set_code` (the same edit block as below) → read the warnings it returns → `check` → `look` (a picture: look at it) → fix → `set_code` again. Nobody has to paste a red box back. Tools: `get_docs list_examples load_example get_code set_code run check look onion render_draft`.
|
|
13
|
+
|
|
12
14
|
## The idea
|
|
13
15
|
|
|
14
16
|
A video is **data**: a list of layers in `sequences`. Each layer has `type`, a start `at` and a `duration` (seconds), `initial` values and `keyframes`. Repeated structure is a JS loop or function that returns layers. There is no per-frame code and no `Math.random()`.
|
|
@@ -59,6 +61,7 @@ That is a complete, working video (the block between the two markers). Change th
|
|
|
59
61
|
- **Sound with no files:** `{ type: 'audio', sfx: 'swoosh', at: 2 }`. Presets: `click pop swoosh swipe hit riser chime beep coin glitch typewriter`; optional `pitch`, `brightness`. A `riser` is loudest at its **end**: start it `duration` seconds before the hit. This page cannot load music or images from your own files; use shapes, text, sfx and `music` (below).
|
|
60
62
|
- **Repeat with code, never by copying:** `Array.from({ length: 12 }, (_, i) => ({ type: 'shape', shape: 'circle', radius: 20, at: i * 0.15, duration: DURATION - i * 0.15, initial: { x: 120 + i * 90, y: 360, fillColor: '#ffd166' }, keyframes: [{ at: 0, from: { scale: 0 }, to: { scale: 1 }, duration: 0.4, ease: 'back.out(2)' }] }))` returns twelve layers: spread them into `sequences` with `...`.
|
|
61
63
|
- **Helpers already imported** in the template: `kenBurns` (slow zoom for an image layer), `withFade`, `wiggle` (seeded shake as keyframes), `stagger`, `animateText` (per letter / word entrances), `followPath`, `particles` (seeded confetti / snow / sparks as layers), `deck` (slides). Each returns keyframes or layers: `keyframes: wiggle(...)`, `...particles(...)`. The full signatures are in `https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/reference/cheatsheet.md` (22 KB; read it only if you need a helper).
|
|
64
|
+
- **Advanced:** `const INIT = { assets: [...], composition: { transitions: [...] } }` in the edit block adds `movie.init` options (the page merges it); most videos need nothing here.
|
|
62
65
|
- **Not available on this page:** named filters beyond `blur`, `noise`, `alpha`, `colorMatrix` (the other 38 need the `pixi-filters` package), three.js layers, your own image / video / music files. Say so rather than guessing.
|
|
63
66
|
|
|
64
67
|
## Music with no file
|
package/ai/SKILL.md
CHANGED
|
@@ -12,7 +12,7 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
12
12
|
1. **Start from `template.html`** (copy it; it also has the shared loader (`<div class="pe-loader" data-label="LOADING">` + `pixi-effects/loader.css`: HTML and CSS only, animating just `transform` / `opacity`, so it keeps moving while the page builds the video; `movie.init()` fades it out). Keep it; a loader of your own is fine (`pe-loader--custom`) as long as it only animates `transform` / `opacity`, never with JavaScript or `requestAnimationFrame`; it loads the library from a CDN, collects every console warning into `window.__logs`, and exposes `window.movie`). One HTML file, plain JS. Default canvas 1280×720 @ 30 fps.
|
|
13
13
|
2. **Give the movie a poster time.** In `movie.init` add `poster: <seconds>`: the most striking moment (a title fully on screen, not the first frame, which is often black). The canvas shows that picture before play (the playhead stays at 0 and play starts from 0), and the check writes it as `poster.jpg`.
|
|
14
14
|
3. **Write the composition as data.** Anything repeated (letters, bars, particles, scenes) is a JS function or loop that returns specs — never copy-paste objects. Derive coordinates and times from constants/data, not magic numbers.
|
|
15
|
-
4. **Show the human:** `node ai/tools/view.mjs my-video.html` opens their browser on the page with a scrubbable, zoomable timeline under it (click / drag to seek, + / − or Ctrl/⌘ + wheel to zoom) (`npx pixi-effects-view …` in an installed package); it keeps running until Ctrl-C. **Deliver the file:** `node ai/tools/render.mjs my-video.html -o my-video.mp4` (`npx pixi-effects-render …` in an installed package) renders the page headless to mp4 / webm / mov / mkv and exits 0 only when the file is written; run it AFTER the check below passes. **Run the check — one command does steps 3, 4 and the export:** `node ai/tools/check.mjs my-video.html` (Node ≥ 22 and Chrome installed; no npm install; in an installed package it is `npx pixi-effects-check my-video.html`). It starts its own private Chrome (nothing is shared with other sessions), waits for `window.__ready`, and writes `timeline.html` (every layer as a bar on a time axis: ◆ keyframes, shaded transitions; hand it to the human to open) and prints: console warnings, `movie.inspect` over the WHOLE timeline (grouped), `movie.inspectAudio()`, then exports an mp4 and decodes it again (video size and length, audio length, loudness per second, silence). It writes `check-out/<name>/sheet.png` — **open that picture and look at it** — plus the mp4 and `report.json`. Exit 0 = nothing to fix; exit 1 = a list of problems; fix them and run it again. Text that overlaps text is listed under `review` (not a failure: ghost layers, glow copies and per-letter boxes are often intentional; look at the sheet; `--strict` makes them fail). The manual steps below are for looking at one frame closely.
|
|
15
|
+
4. **Show the human:** `node ai/tools/view.mjs my-video.html` opens their browser on the page with a scrubbable, zoomable timeline under it (click / drag to seek, + / − or Ctrl/⌘ + wheel to zoom) (`npx pixi-effects-view …` in an installed package); it keeps running until Ctrl-C. **Deliver the file:** `node ai/tools/render.mjs my-video.html -o my-video.mp4` (`npx pixi-effects-render …` in an installed package) renders the page headless to mp4 / webm / mov / mkv and exits 0 only when the file is written; run it AFTER the check below passes. **Run the check — one command does steps 3, 4 and the export:** `node ai/tools/check.mjs my-video.html` (Node ≥ 22 and Chrome installed; no npm install; in an installed package it is `npx pixi-effects-check my-video.html`). It starts its own private Chrome (nothing is shared with other sessions), waits for `window.__ready`, and writes `timeline.html` (every layer as a bar on a time axis: ◆ keyframes, shaded transitions; hand it to the human to open) and prints: console warnings, `movie.inspect` over the WHOLE timeline (grouped), `movie.inspectAudio()`, then exports an mp4 and decodes it again (video size and length, audio length, loudness per second, silence). It writes `check-out/<name>/sheet.png` — **open that picture and look at it** — plus the mp4 and `report.json`. Exit 0 = nothing to fix; exit 1 = a list of problems; fix them and run it again. Text that overlaps text is listed under `review` (not a failure: ghost layers, glow copies and per-letter boxes are often intentional; look at the sheet; `--strict` makes them fail). The manual steps below are for looking at one frame closely. The same judgement is in the page: `await movie.review({ at: 'title@end' })` returns `{ frames, problems, review, fonts, audio, at }` (what `check` prints, as data) and `movie.resolveAt('3.5, title@end')` turns moments into frames.
|
|
16
16
|
**Or by hand: run it and read the logs first.** `window.__ready` must be `true` and `window.__logs` should be `[]`. A pixi-effects warning is an instruction: fix what it names.
|
|
17
17
|
5. **Look at it** — you cannot judge motion from code. The library has the tools built in:
|
|
18
18
|
- `await movie.contactSheet({ count: 6, as: 'dataURL' })` → ONE image of several labelled frames (pass `frames: [...]` / `times: [...]` to choose; include the middle of every transition and the last second). Save it and view it.
|
|
@@ -22,7 +22,7 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
22
22
|
- **A moment, a movement, the sound (`pixi-effects-check` options):** `--at 3.5,title@end,50%,f120` writes those pictures to `check-out/<name>/frames/*.png` (and `at.png`): the last frame of a layer is `name@end`, the middle `name@mid`. `--onion 1:3` writes `onion.png`, eight frames of seconds 1–3 overlaid (the later the stronger): a trail that shows a movement's path and easing (even steps = constant speed, bunched = slow). `report.audio` now has `loudness` (`integratedLufs`, `truePeakDb`, `clippedSamples`, `silences`), `scenes` (per named top-level composition), `cues` (when each sound starts) and `notes` (advice: quiet or loud for web video, little headroom, silent stretches; web video is typically −14 to −16 LUFS); `waveform.png` draws the mix with the scene edges and the cues. A web font that failed to load fails the check; a text layer none of whose fonts is installed is listed for review.
|
|
23
23
|
- **A deck** (`composition.stops`, `deck()`): `await movie.contactSheet({ frames: movie.stops.map(s => s.frame), as: 'dataURL' })` is one picture of every stop (the check writes it as `stops.png`); what the audience reads while a page waits is that picture, so check every one. `await movie.inspectStops()` lists the stops where the picture is still changing (a stop landing before its animation ends); the check prints it as a review list: move the stop later or flag a settled one `pdf: true`. `await movie.stopImages({ which: 'pages' })` / `movie.exportPDF()` give the pages themselves.
|
|
24
24
|
From a browser tool the result is a `data:` URL string: `agent-browser eval "movie.contactSheet({ count: 6, as: 'dataURL' })" | python3 ai/tools/save-image.py /absolute/sheet.png`, then open the PNG. Sheet tiles are small (~480 px): use `snapshot` for detail. `inspect` output is long on busy scenes: pass `{ layers: 'none' }` and read `issues` first. Give layers a `name` so inspect paths are readable. A moving ticker is not flagged as cut off.
|
|
25
|
-
6. **Iterate** on what you saw (clipped text, collisions, wrong timing). Fast motion looks choppy: add `motionBlur: true` to `movie.init` (render, snapshot and contact sheet then show a film-style blur; renders take ~8× longer). Then export: `const blob = await movie.render({ format: 'mp4' })` (about real time; `movie.on('progress', …)` reports 0–100). **While you iterate, do not export the whole movie:** `check --no-export --at <the moment you changed>` takes seconds (3 to 10 s on the gallery pieces: most of it is loading the page and measuring the sound); to see motion, `render({ range: [from, to] })` / `pixi-effects-render --range 10:15` / `--scene name` exports only that part (picture and sound); `draft: true` / `--draft` is half size, low quality and no motion blur: a much smaller file (
|
|
25
|
+
6. **Iterate** on what you saw (clipped text, collisions, wrong timing). Fast motion looks choppy: add `motionBlur: true` to `movie.init` (render, snapshot and contact sheet then show a film-style blur; renders take ~8× longer). Then export: `const blob = await movie.render({ format: 'mp4' })` (about real time; `movie.on('progress', …)` reports 0–100). **While you iterate, do not export the whole movie:** `check --no-export --at <the moment you changed>` takes seconds (3 to 10 s on the gallery pieces: most of it is loading the page and measuring the sound); to see motion, `render({ range: [from, to] })` / `pixi-effects-render --range 10:15` / `--scene name` exports only that part (picture and sound); `draft: true` / `--draft` is half size, low quality and no motion blur: a much smaller file (a piece that asks for motion blur renders much faster as a draft: 4 s of `hanabi-night` took 14.0 s, and 8.2 s as a draft, about 7 s of either being the browser's start-up), but the drawing itself is not faster. Export the whole movie once, at the end.
|
|
26
26
|
|
|
27
27
|
## The rules that cause most failures
|
|
28
28
|
|
package/ai/chat-template.html
CHANGED
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
"gsap": "https://cdn.jsdelivr.net/npm/gsap@3.12.5/+esm",
|
|
62
62
|
"gsap/PixiPlugin": "https://cdn.jsdelivr.net/npm/gsap@3.12.5/PixiPlugin/+esm",
|
|
63
63
|
"mediabunny": "https://cdn.jsdelivr.net/npm/mediabunny@1.61.3/+esm",
|
|
64
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
65
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
66
|
-
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
64
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.18.0/dist/index.js",
|
|
65
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.18.0/dist/Controller.js",
|
|
66
|
+
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.18.0/dist/Presenter.js"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
</script>
|
|
@@ -95,13 +95,17 @@
|
|
|
95
95
|
window.movie = movie;
|
|
96
96
|
new Controller(movie, { canvas: document.getElementById('stage') });
|
|
97
97
|
|
|
98
|
+
// Optional, for advanced pages: const INIT = { assets: [...], composition: { transitions: [...] }, motionBlur: true } adds movie.init options.
|
|
99
|
+
const EXTRA = typeof INIT === 'object' && INIT ? INIT : {};
|
|
100
|
+
|
|
98
101
|
try {
|
|
99
102
|
await movie.init({
|
|
100
103
|
canvas: document.getElementById('stage'),
|
|
101
104
|
width: W, height: H, duration: DURATION, frameRate: FPS,
|
|
102
105
|
poster: POSTER,
|
|
103
106
|
background: BACKGROUND,
|
|
104
|
-
|
|
107
|
+
...EXTRA,
|
|
108
|
+
composition: { sequences, ...(EXTRA.composition || {}) },
|
|
105
109
|
});
|
|
106
110
|
window.__ready = true;
|
|
107
111
|
} catch (e) {
|
|
@@ -170,6 +170,8 @@ await movie.contactSheet({ count: 6, as: 'dataURL' }) // ONE image of several
|
|
|
170
170
|
await movie.snapshot(60, { as: 'dataURL' }) // one frame, canvas only (no player bar) — use it for detail; sheet tiles are small
|
|
171
171
|
await movie.inspect(60, { layers: 'none' }) // issues only (default lists the visible layers); text off-canvas / cut off / empty / overlapping text. Name your layers so paths are readable
|
|
172
172
|
await movie.onionSkin({ from: 1, to: 3, count: 8, as: 'dataURL' }) // frames overlaid into ONE picture of a movement (later = stronger): path and easing at a glance
|
|
173
|
+
movie.review({ at: 'title@end' }) // what `pixi-effects-check` looks at, as data: { frames, problems (text cut off, off-canvas, no size), review (text overlaps: look at them), fonts, audio, at }; leaves the movie as it was. `strict: true` makes every issue a problem
|
|
174
|
+
movie.resolveAt('3.5, 50%, f120, title@end') // moments → [{ label, frame }]; a mistake says what could not be read
|
|
173
175
|
movie.inspectFonts() // { missing: [{ layer, family }] (no font of the list is installed), failed: [web fonts whose file did not load] }
|
|
174
176
|
movie.inspectAudio() // the soundtrack as numbers; also loudness { integratedLufs, truePeakDb, clippedSamples, silences }, scenes, cues, notes (advice: -14 to -16 LUFS is typical for web video) — — you cannot listen: sources[i] = { layer, start, end, peakDb, sound: { length, peakDb, loudestAt, brightnessHz } }, windows (rms / peak / brightness per 0.1 s), issues (limited mix, inaudible / cut-off sounds)
|
|
175
177
|
new Controller(movie, { canvas }) // optional player bar (overlays the canvas bottom ~60px; not in the export)
|
package/ai/template.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>pixi-effects video</title>
|
|
7
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.18.0/dist/loader.css" /> <!-- the loader: HTML + CSS only, so it is on screen at once and keeps moving while the page builds the video -->
|
|
8
8
|
<style>
|
|
9
9
|
body { background: #0d1220; margin: 0; padding: 16px; }
|
|
10
10
|
.stage { position: relative; width: min(960px, 100%); } /* the player and the loader both sit inside this box */
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"gsap": "https://esm.sh/gsap@3.12.5",
|
|
40
40
|
"gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
|
|
41
41
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
42
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
43
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
44
|
-
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
45
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
42
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.18.0/dist/index.js",
|
|
43
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.18.0/dist/Controller.js",
|
|
44
|
+
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.18.0/dist/Presenter.js",
|
|
45
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.18.0/dist/three.js",
|
|
46
46
|
"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
|
|
47
47
|
"three": "https://esm.sh/three@0.178.0"
|
|
48
48
|
}
|
package/ai/tools/check.mjs
CHANGED
|
@@ -67,81 +67,6 @@ export function parseArgs(argv) {
|
|
|
67
67
|
return o;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
* The frames movie.inspect looks at: the first and last frame, every scene's first and last frame (where a title is cut off or a
|
|
72
|
-
* wipe leaves an overlap), and one every `step` seconds. At most `cap`: past that the even grid is widened, the scene edges stay.
|
|
73
|
-
*/
|
|
74
|
-
export function sampleFrames({ totalFrames, frameRate, scenes = [], step = 0.25, cap = 240 }) {
|
|
75
|
-
const last = totalFrames;
|
|
76
|
-
const clamp = f => Math.max(0, Math.min(last, Math.round(f)));
|
|
77
|
-
const edges = new Set([0, last]);
|
|
78
|
-
for (const s of scenes) { edges.add(clamp(s.start * frameRate)); edges.add(clamp(s.end * frameRate - 1)); }
|
|
79
|
-
const room = Math.max(0, cap - edges.size);
|
|
80
|
-
const seconds = totalFrames / frameRate;
|
|
81
|
-
const wanted = Math.floor(seconds / step) + 1;
|
|
82
|
-
const stride = wanted <= room ? step : seconds / Math.max(1, room);
|
|
83
|
-
const frames = new Set(edges);
|
|
84
|
-
for (let t = stride; frames.size < cap && t < seconds; t += stride) frames.add(clamp(t * frameRate));
|
|
85
|
-
return [...frames].sort((a, b) => a - b);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const AT_HELP = "use seconds (3.5), a percentage (50%), a frame (f120) or a layer's start / mid / end (name@end)";
|
|
89
|
-
|
|
90
|
-
/** The movie's scenes: top-level compositions with a name you gave (not `text#3`) of a second or more. Same rule as `namedScenes` in src/core/scenes.ts (a test keeps them equal). */
|
|
91
|
-
export function scenesOf(rows) {
|
|
92
|
-
return rows.filter(r => r.depth === 0 && r.type === 'composition' && !/[#×]/.test(r.name) && r.end - r.start >= 1).map(r => ({ name: r.name, start: r.start, end: r.end }));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** `--at` list → frames, each with a label for its file name. */
|
|
96
|
-
export function resolveAtList(list, ctx) {
|
|
97
|
-
const { frameRate: fps, totalFrames, duration, rows } = ctx;
|
|
98
|
-
const out = [], used = new Set();
|
|
99
|
-
for (const raw of String(list).split(',').map(s => s.trim()).filter(Boolean)) {
|
|
100
|
-
let frame, label, m;
|
|
101
|
-
if ((m = raw.match(/^(.+)@(start|mid|end)$/))) {
|
|
102
|
-
const row = rows.find(r => r.name === m[1]);
|
|
103
|
-
if (!row) throw new Error(`--at "${raw}": no layer named "${m[1]}" (names: ${rows.map(r => r.name).join(', ')})`);
|
|
104
|
-
const t = m[2] === 'start' ? row.start : m[2] === 'mid' ? (row.start + row.end) / 2 : row.end - 1 / fps;
|
|
105
|
-
frame = Math.round(t * fps); label = `${m[1]}-${m[2]}`;
|
|
106
|
-
} else if ((m = raw.match(/^(\d+(?:\.\d+)?)%$/))) { frame = Math.round(Number(m[1]) / 100 * totalFrames); label = `${m[1]}pct`; }
|
|
107
|
-
else if ((m = raw.match(/^f(\d+)$/i))) { frame = Number(m[1]); label = `f${m[1]}`; }
|
|
108
|
-
else if (/^\d+(\.\d+)?$/.test(raw)) { frame = Math.round(Number(raw) * fps); label = `${Number(raw).toFixed(2)}s`; }
|
|
109
|
-
else throw new Error(`--at: cannot read "${raw}": ${AT_HELP}`);
|
|
110
|
-
if (frame > totalFrames || frame < 0) throw new Error(`--at "${raw}" is past the end: the movie is ${duration} s (${totalFrames} frames)`);
|
|
111
|
-
let unique = label, n = 2;
|
|
112
|
-
while (used.has(unique)) unique = `${label}-${n++}`;
|
|
113
|
-
used.add(unique);
|
|
114
|
-
out.push({ label: unique, frame });
|
|
115
|
-
}
|
|
116
|
-
return out;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** [{ frame, issues[] }] → [{ message, count, firstFrame, lastFrame }]: issues that differ only by numbers are one. */
|
|
120
|
-
export function groupIssues(perFrame) {
|
|
121
|
-
const groups = new Map();
|
|
122
|
-
for (const { frame, issues } of perFrame) {
|
|
123
|
-
for (const message of issues) {
|
|
124
|
-
const key = message.replace(/-?\d+(\.\d+)?/g, '#');
|
|
125
|
-
const g = groups.get(key);
|
|
126
|
-
if (g) { g.count++; g.lastFrame = frame; }
|
|
127
|
-
else groups.set(key, { message, count: 1, firstFrame: frame, lastFrame: frame });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return [...groups.values()];
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Layout issues → { problems, review }. Cut off by an edge, outside the canvas and "no size" are exact: problems. Text that
|
|
135
|
-
* overlaps text is judged on layout boxes, not ink, so intentional designs trip it (ghost layers, a glow copy under a title,
|
|
136
|
-
* per-letter boxes, a wipe between two scenes: 7 of 30 gallery pieces): those are `review` items to look at on the contact
|
|
137
|
-
* sheet. `strict` makes every issue a problem.
|
|
138
|
-
*/
|
|
139
|
-
export function splitIssues(groups, strict = false) {
|
|
140
|
-
if (strict) return { problems: groups, review: [] };
|
|
141
|
-
const overlap = g => /\boverlap\b/.test(g.message);
|
|
142
|
-
return { problems: groups.filter(g => !overlap(g)), review: groups.filter(overlap) };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
70
|
const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.mjs': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.mp4': 'video/mp4', '.webm': 'video/webm', '.woff2': 'font/woff2', '.woff': 'font/woff', '.ttf': 'font/ttf', '.wasm': 'application/wasm', '.map': 'application/json', '.md': 'text/plain; charset=utf-8', '.txt': 'text/plain; charset=utf-8' };
|
|
146
71
|
|
|
147
72
|
/** A plain static server for `root` (no redirects, so `?query` survives). Resolves { server, port }. */
|
|
@@ -154,7 +79,7 @@ export function serve(root, onRequest = null, port = 0) {
|
|
|
154
79
|
if (!file.startsWith(root)) { res.writeHead(403).end(); return; }
|
|
155
80
|
if (fs.existsSync(file) && fs.statSync(file).isDirectory()) file = path.join(file, 'index.html');
|
|
156
81
|
if (!fs.existsSync(file)) { res.writeHead(404).end('not found'); return; }
|
|
157
|
-
res.writeHead(200, { 'content-type': MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream', 'cache-control': 'no-store' });
|
|
82
|
+
res.writeHead(200, { 'content-type': MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream', 'cache-control': 'no-store', 'access-control-allow-origin': '*' }); // like GitHub Pages: a page with no origin (a sandboxed iframe) may load these files
|
|
158
83
|
fs.createReadStream(file).pipe(res);
|
|
159
84
|
} catch { res.writeHead(500).end(); }
|
|
160
85
|
});
|
|
@@ -197,10 +122,11 @@ export class Cdp {
|
|
|
197
122
|
}
|
|
198
123
|
}
|
|
199
124
|
|
|
200
|
-
|
|
125
|
+
/** Start a private headless Chrome. `extraArgs`: more command-line flags (the tests use it for `--enable-features=WebMCP`). */
|
|
126
|
+
export async function launchChrome(chrome, userDataDir, extraArgs = []) {
|
|
201
127
|
const proc = spawn(chrome, [
|
|
202
128
|
'--headless=new', '--remote-debugging-port=0', `--user-data-dir=${userDataDir}`, '--no-first-run', '--no-default-browser-check',
|
|
203
|
-
'--autoplay-policy=no-user-gesture-required', '--mute-audio', '--ignore-gpu-blocklist', '--enable-unsafe-swiftshader', '--window-size=1400,900', 'about:blank',
|
|
129
|
+
'--autoplay-policy=no-user-gesture-required', '--mute-audio', '--ignore-gpu-blocklist', '--enable-unsafe-swiftshader', '--window-size=1400,900', ...extraArgs, 'about:blank',
|
|
204
130
|
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
205
131
|
const endpoint = await new Promise((resolve, reject) => {
|
|
206
132
|
let buf = '';
|
|
@@ -221,18 +147,7 @@ export async function launchChrome(chrome, userDataDir) {
|
|
|
221
147
|
const INFO = `(() => { const m = window.movie; if (!m) return { hasMovie: false, logs: window.__logs || [] };
|
|
222
148
|
const c = document.querySelector('canvas');
|
|
223
149
|
return { hasMovie: true, logs: window.__logs || [], totalFrames: m.totalFrames, frameRate: m.frameRate, duration: m.duration,
|
|
224
|
-
width: m.width ?? (c && c.width), height: m.height ?? (c && c.height), hasAudio: !!m.audioBuffer,
|
|
225
|
-
rows: (() => { try { return m.timelineData().rows.map(r => ({ name: r.name, type: r.type, start: r.start, end: r.end, depth: r.depth })); } catch { return []; } })() }; })()`;
|
|
226
|
-
|
|
227
|
-
const sweepScript = list => `(async () => {
|
|
228
|
-
const frames = new Set(${JSON.stringify(list)});
|
|
229
|
-
const perFrame = [];
|
|
230
|
-
for (const f of [...frames].sort((a, b) => a - b)) {
|
|
231
|
-
const r = await movie.inspect(f, { layers: 'none' });
|
|
232
|
-
if (r.issues.length) perFrame.push({ frame: f, issues: r.issues });
|
|
233
|
-
}
|
|
234
|
-
return { checked: frames.size, perFrame };
|
|
235
|
-
})()`;
|
|
150
|
+
width: m.width ?? (c && c.width), height: m.height ?? (c && c.height), hasAudio: !!m.audioBuffer, hasReview: typeof m.review === 'function' }; })()`;
|
|
236
151
|
|
|
237
152
|
const exportScript = (format, draft = false) => `(() => {
|
|
238
153
|
window.__x = { state: 'running' };
|
|
@@ -363,23 +278,25 @@ export async function runCheck(opts, log = console.log) {
|
|
|
363
278
|
Object.assign(report, { width: info.width, height: info.height, frameRate: info.frameRate, totalFrames: info.totalFrames, duration: info.duration, hasAudio: info.hasAudio });
|
|
364
279
|
if (report.logs.length) report.problems.push(`${report.logs.length} console warning(s) / error(s) — each says what to change`);
|
|
365
280
|
|
|
366
|
-
// layout
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
report.fonts = fonts;
|
|
377
|
-
if (fonts.failed.length) report.problems.push(`${fonts.failed.length} web font(s) failed to load: ${fonts.failed.join(', ')} (the layers that use them are drawn in a fallback)`);
|
|
378
|
-
for (const m of fonts.missing) {
|
|
379
|
-
(opts.strict ? report.inspect.issues : report.inspect.review).push({ message: `layer "${m.layer}": none of the fonts "${m.family}" is available here (it is drawn in a fallback font)`, count: 1, firstFrame: 0, lastFrame: 0 });
|
|
380
|
-
if (opts.strict) report.problems.push(`layer "${m.layer}": none of its fonts is available (${m.family})`);
|
|
381
|
-
}
|
|
281
|
+
// the library reviews the movie (layout over the whole timeline, fonts, sound): the same call the Playground and the WebMCP tools make
|
|
282
|
+
if (!info.hasReview) throw new Error("this page's pixi-effects is older than the check tool: use 0.18 or newer (the version in the page's import map)");
|
|
283
|
+
|
|
284
|
+
// a mistake in --at or --onion is a usage error: say so now, not after the slow work
|
|
285
|
+
let atList = null;
|
|
286
|
+
if (opts.at) {
|
|
287
|
+
// the page's error is returned as a value: thrown, it would arrive with the page's stack trace
|
|
288
|
+
const r = JSON.parse(await cdp.eval(`(() => { try { return JSON.stringify({ at: movie.resolveAt(${JSON.stringify(opts.at)}) }); } catch (e) { return JSON.stringify({ error: String(e && e.message || e) }); } })()`));
|
|
289
|
+
if (r.error) throw new Error(r.error.replace(/^pixi-effects: /, ''));
|
|
290
|
+
atList = r.at;
|
|
382
291
|
}
|
|
292
|
+
if (opts.onion && opts.onion[1] > info.duration + 1e-9) throw new Error(`--onion ${opts.onion[0]}:${opts.onion[1]} goes past the end: the movie is ${info.duration} s`);
|
|
293
|
+
|
|
294
|
+
const rv = await cdp.eval(`movie.review(${JSON.stringify({ strict: !!opts.strict })})`);
|
|
295
|
+
report.inspect = { checkedFrames: rv.frames, issues: rv.problems, review: rv.review };
|
|
296
|
+
if (rv.problems.length) report.problems.push(`${rv.problems.length} kind(s) of layout issue from movie.inspect`);
|
|
297
|
+
// fonts: a web font whose file did not load is a problem (the library lists a layer with no available font for review)
|
|
298
|
+
report.fonts = rv.fonts;
|
|
299
|
+
if (rv.fonts.failed.length) report.problems.push(`${rv.fonts.failed.length} web font(s) failed to load: ${rv.fonts.failed.join(', ')} (the layers that use them are drawn in a fallback)`);
|
|
383
300
|
left();
|
|
384
301
|
|
|
385
302
|
// contact sheet
|
|
@@ -390,7 +307,7 @@ export async function runCheck(opts, log = console.log) {
|
|
|
390
307
|
|
|
391
308
|
// --at: the pictures at moments you name (seconds, 50%, f120, title@end), one file each and one sheet
|
|
392
309
|
if (opts.at) {
|
|
393
|
-
const wanted =
|
|
310
|
+
const wanted = atList;
|
|
394
311
|
const framesDir = path.join(outDir, 'frames');
|
|
395
312
|
fs.mkdirSync(framesDir, { recursive: true });
|
|
396
313
|
for (const w of wanted) {
|
|
@@ -408,7 +325,6 @@ export async function runCheck(opts, log = console.log) {
|
|
|
408
325
|
// --onion A:B: one picture of the movement between A and B seconds (8 frames overlaid, the later the stronger)
|
|
409
326
|
if (opts.onion) {
|
|
410
327
|
const [from, to] = opts.onion;
|
|
411
|
-
if (to > info.duration + 1e-9) throw new Error(`--onion ${from}:${to} goes past the end: the movie is ${info.duration} s`);
|
|
412
328
|
const onion = await cdp.eval(`movie.onionSkin({ from: ${from}, to: ${to}, count: 8, as: 'dataURL' })`);
|
|
413
329
|
const onionFile = path.join(outDir, 'onion.png');
|
|
414
330
|
fs.writeFileSync(onionFile, Buffer.from(onion.slice(onion.indexOf(',') + 1), 'base64'));
|
|
@@ -451,7 +367,7 @@ export async function runCheck(opts, log = console.log) {
|
|
|
451
367
|
}
|
|
452
368
|
|
|
453
369
|
// audio
|
|
454
|
-
report.audio = info.hasAudio ?
|
|
370
|
+
report.audio = info.hasAudio ? rv.audio : null;
|
|
455
371
|
if (report.audio) {
|
|
456
372
|
const wave = await cdp.eval(waveformScript(report.audio, info.duration)).catch(() => null);
|
|
457
373
|
if (wave) {
|
package/ai/tools/render.mjs
CHANGED
|
@@ -67,6 +67,7 @@ export function parseRenderArgs(argv) {
|
|
|
67
67
|
const m = need(i++, a).match(/^(\d+(?:\.\d+)?)?:(\d+(?:\.\d+)?)?$/);
|
|
68
68
|
if (!m || (m[1] === undefined && m[2] === undefined)) throw new Error(`--range must look like 2:5 (seconds; 2: runs to the end, :5 starts at the beginning), got "${argv[i]}"`);
|
|
69
69
|
o.range = [m[1] === undefined ? null : Number(m[1]), m[2] === undefined ? null : Number(m[2])];
|
|
70
|
+
if (o.range[0] !== null && o.range[1] !== null && !(o.range[0] < o.range[1])) throw new Error(`--range ${argv[i]}: it must start before it ends (2:5 is seconds 2 to 5)`);
|
|
70
71
|
} else if (a === '--scene') o.scene = need(i++, a);
|
|
71
72
|
else if (a === '--scale') {
|
|
72
73
|
o.scale = Number(need(i++, a));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkJW67MDHQ_cjs = require('./chunk-JW67MDHQ.cjs');
|
|
4
|
+
require('./chunk-ANO5R2E6.cjs');
|
|
5
|
+
require('./chunk-UQBGW5DO.cjs');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Object.defineProperty(exports, "CompositionSequence", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: function () { return chunkJW67MDHQ_cjs.CompositionSequence; }
|
|
12
|
+
});
|
|
13
|
+
//# sourceMappingURL=Composition-6JMXWNVV.cjs.map
|
|
14
|
+
//# sourceMappingURL=Composition-6JMXWNVV.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-6JMXWNVV.cjs"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-MLBHYYFF.js"}
|
package/dist/Controller.d.cts
CHANGED
package/dist/Controller.d.ts
CHANGED
|
@@ -86,8 +86,40 @@ interface FontReport {
|
|
|
86
86
|
layer: string;
|
|
87
87
|
family: string;
|
|
88
88
|
}>;
|
|
89
|
-
/** Web fonts (`@font-face`) whose file could not be loaded. */
|
|
89
|
+
/** Web fonts (`@font-face`) whose file could not be loaded and that a text layer uses. */
|
|
90
90
|
failed: string[];
|
|
91
|
+
/** Web fonts whose file could not be loaded but no text layer uses (an unused face: worth a look, not a fault). */
|
|
92
|
+
failedUnused: string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One kind of issue, with how many frames it was seen in. Issues that differ only by numbers are one kind. */
|
|
96
|
+
interface IssueGroup {
|
|
97
|
+
message: string;
|
|
98
|
+
count: number;
|
|
99
|
+
firstFrame: number;
|
|
100
|
+
lastFrame: number;
|
|
101
|
+
}
|
|
102
|
+
interface ReviewOptions {
|
|
103
|
+
/** Moments to resolve and report: `3.5`, `50%`, `f120`, `title@end` (comma-separated). */
|
|
104
|
+
at?: string;
|
|
105
|
+
/** Every layout issue is a problem (by default text overlaps are only listed for review). */
|
|
106
|
+
strict?: boolean;
|
|
107
|
+
}
|
|
108
|
+
interface ReviewReport {
|
|
109
|
+
/** How many frames were inspected. */
|
|
110
|
+
frames: number;
|
|
111
|
+
/** Text cut off by an edge, outside the canvas, or with no size (with `strict`: every issue). */
|
|
112
|
+
problems: IssueGroup[];
|
|
113
|
+
/** Text overlaps and fonts that are not available: often intentional, look at them on the contact sheet. */
|
|
114
|
+
review: IssueGroup[];
|
|
115
|
+
fonts: FontReport;
|
|
116
|
+
/** The sound as numbers (`movie.inspectAudio()`), or null for a movie with no audio. */
|
|
117
|
+
audio: AudioReport | null;
|
|
118
|
+
/** The moments of `at`, as frames with a label that is safe as a file name. */
|
|
119
|
+
at: Array<{
|
|
120
|
+
label: string;
|
|
121
|
+
frame: number;
|
|
122
|
+
}>;
|
|
91
123
|
}
|
|
92
124
|
|
|
93
125
|
interface Rect {
|
|
@@ -149,6 +181,8 @@ interface TimelineRow {
|
|
|
149
181
|
start: number;
|
|
150
182
|
end: number;
|
|
151
183
|
}>;
|
|
184
|
+
/** With `parts`: the name of each layer the row stands for, in the same order (so `render({ range: 'pop-3' })` can find it). */
|
|
185
|
+
partNames?: string[];
|
|
152
186
|
/** A short text for the tooltip (a text layer's words, an asset name). */
|
|
153
187
|
detail?: string;
|
|
154
188
|
}
|
|
@@ -522,6 +556,16 @@ declare class Movie {
|
|
|
522
556
|
* The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
|
|
523
557
|
* (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
|
|
524
558
|
*/
|
|
559
|
+
/** The pictures to look at for moments such as `3.5`, `50%`, `f120`, `title@end` (comma-separated). A mistake says what could not be read. */
|
|
560
|
+
resolveAt(list: string): Array<{
|
|
561
|
+
label: string;
|
|
562
|
+
frame: number;
|
|
563
|
+
}>;
|
|
564
|
+
/**
|
|
565
|
+
* One call that looks at everything `pixi-effects-check` looks at inside the page: layout issues over the whole timeline (every
|
|
566
|
+
* scene's first and last frame and a frame every 0.25 s), fonts and the sound. See `ReviewReport`. Page warnings are in `window.__logs`. The movie is left as it was.
|
|
567
|
+
*/
|
|
568
|
+
review(opts?: ReviewOptions): Promise<ReviewReport>;
|
|
525
569
|
/**
|
|
526
570
|
* The fonts of the text layers: `missing` lists layers none of whose `fontFamily` entries is available in this browser (they are
|
|
527
571
|
* drawn in a fallback font), `failed` lists web fonts (`@font-face`) whose file could not be loaded.
|
|
@@ -589,4 +633,4 @@ declare class Movie {
|
|
|
589
633
|
destroy(): Promise<void>;
|
|
590
634
|
}
|
|
591
635
|
|
|
592
|
-
export { type AudioInspectOptions as A, type ContactSheetOptions as C, type FontReport as F, type InspectOptions as I, type LayerInfo as L, type MotionBlurOptions as M, type OnionSkinOptions as O, type ProgressEvent as P, type RenderOptions as R, type SnapshotOptions as S, type TimelineData as T, type VolumeEvent as V, type AudioReport as a, type AudioSourceReport as b, type FrameEvent as c, type InspectReport as d, type
|
|
636
|
+
export { type AudioInspectOptions as A, type ContactSheetOptions as C, type FontReport as F, type InspectOptions as I, type LayerInfo as L, type MotionBlurOptions as M, type OnionSkinOptions as O, type ProgressEvent as P, type RenderOptions as R, type SnapshotOptions as S, type TimelineData as T, type VolumeEvent as V, type AudioReport as a, type AudioSourceReport as b, type FrameEvent as c, type InspectReport as d, type IssueGroup as e, type LoaderOption as f, type MotionBlurSpec as g, Movie as h, type MovieErrorEvent as i, type MovieOptions as j, type ReviewOptions as k, type ReviewReport as l, type SoundMeasure as m, type StopEvent as n, type StopImage as o, type StopImagesOptions as p, type TimelineHtmlOptions as q, type TimelineRow as r, type TimelineSvgOptions as s, type TimelineTransition as t, dismissLoader as u, failLoader as v, resolveLoader as w };
|