pixi-effects 0.8.0 → 0.10.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 +22 -0
- package/README.md +6 -6
- package/ai/SKILL.md +8 -6
- package/ai/reference/cheatsheet.md +17 -5
- package/ai/reference/pitfalls.md +2 -2
- package/ai/reference/recipes.md +123 -2
- package/ai/template.html +4 -3
- package/ai/tools/check.mjs +8 -0
- package/dist/{Base-CSIulyIq.d.cts → Base-CwUxTfja.d.cts} +1 -1
- package/dist/{Base-DVgwyIUj.d.ts → Base-CyvSLoCP.d.ts} +1 -1
- package/dist/Composition-EKVTUYCI.cjs +14 -0
- package/dist/{Composition-CM62XJKT.cjs.map → Composition-EKVTUYCI.cjs.map} +1 -1
- package/dist/Composition-NJCOMGD7.js +5 -0
- package/dist/{Composition-RJDSQYTV.js.map → Composition-NJCOMGD7.js.map} +1 -1
- package/dist/Controller.cjs +34 -0
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +12 -2
- package/dist/Controller.d.ts +12 -2
- package/dist/Controller.js +34 -0
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-CQOhUmko.d.cts → Movie-puD9oZPg.d.ts} +26 -1
- package/dist/{Movie-HF3pSGjQ.d.ts → Movie-w3GliuP3.d.cts} +26 -1
- package/dist/{chunk-PDBDDGG5.cjs → chunk-4WJUSDJJ.cjs} +103 -30
- package/dist/chunk-4WJUSDJJ.cjs.map +1 -0
- package/dist/{chunk-FNKSXBKB.js → chunk-JAMNDBXW.js} +193 -14
- package/dist/chunk-JAMNDBXW.js.map +1 -0
- package/dist/{chunk-AGMNSJJH.cjs → chunk-S3ZI4MKS.cjs} +196 -12
- package/dist/chunk-S3ZI4MKS.cjs.map +1 -0
- package/dist/{chunk-O4AJYUPK.js → chunk-WDGAVOMS.js} +85 -12
- package/dist/chunk-WDGAVOMS.js.map +1 -0
- package/dist/index.cjs +369 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +231 -5
- package/dist/index.d.ts +231 -5
- package/dist/index.js +342 -15
- package/dist/index.js.map +1 -1
- package/dist/three.cjs +5 -5
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +1 -1
- package/dist/{types-oJijbn-K.d.ts → types-BXu28nJj.d.cts} +26 -2
- package/dist/{types-oJijbn-K.d.cts → types-BXu28nJj.d.ts} +26 -2
- package/docs/api.md +17 -0
- package/docs/dsl.md +108 -2
- package/llms-full.txt +275 -17
- package/package.json +4 -1
- package/dist/Composition-CM62XJKT.cjs +0 -14
- package/dist/Composition-RJDSQYTV.js +0 -5
- package/dist/chunk-AGMNSJJH.cjs.map +0 -1
- package/dist/chunk-FNKSXBKB.js.map +0 -1
- package/dist/chunk-O4AJYUPK.js.map +0 -1
- package/dist/chunk-PDBDDGG5.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
**Added**
|
|
6
|
+
|
|
7
|
+
- **Seeded randomness.** Expressions gain `rand(seed)` (0…1) and `noise(x, seed)` (smooth, −1…1), plus `lerp`, `clamp`, `smoothstep`, `mod`, `step` (the way to write an "if") and `PI`. For plain JS, `random(seed)` returns a repeatable stream. The same seed always gives the same picture, in playback, seeking and export.
|
|
8
|
+
- **Null layers and `parent`.** `{ type: 'null', name: 'rig' }` draws nothing and only moves; layers with `parent: 'rig'` are drawn inside it and follow its position, rotation, scale and alpha (orbits, clock hands, a whole group fading). Nulls chain; a child's `x` / `y` are measured from the null's origin. A wrong parent (unknown name, not a null, a cycle, a `threeD` layer) warns and the layer is drawn without one.
|
|
9
|
+
- **`stagger()`**: delays for a wave of items, GSAP style (`each` or `amount`, `from: 'start' | 'end' | 'center' | 'edges' | 'random' | index`, `grid: [cols, rows]`, `ease`, seeded `random`). `stagger(8, { each: 0.08 })` returns the delays; `stagger(layers, …)` returns the layers with `at` pushed back.
|
|
10
|
+
- A gallery piece for the path and text tools (`shape-shift`, 35 pieces now: morphing outlines, a comet on a loop, a title that arrives letter by letter), with the demo in the guide.
|
|
11
|
+
- A guide page on these four (`Motion: groups, waves and randomness`, with tested recipes) and a gallery piece that uses all of them (`orbit-rig`, 34 pieces now).
|
|
12
|
+
- **Click the picture to play / pause** in the `Controller` (a tap on a phone too), like a `<video>`; the pointer cursor shows it. A tap that only closes the export popover does not also toggle. `new Controller(movie, { canvas, clickToPlay: false })` turns it off.
|
|
13
|
+
- **`animateText()`**: per-character, per-word or per-line text animation in one call (`...animateText('Hello', style, { x, y, align, at, duration, in: 'rise', out: 'fade', stagger: { each: 0.05, from: 'center' }, idle: { y: 4 } })`). One text layer per piece laid out like the whole text, entering and leaving in a wave; presets `rise drop fade pop zoom slide spin` or your own `from` / `to`; an optional seeded idle drift; a clear error when `duration` is too short for the wave.
|
|
14
|
+
- **`followPath()`**: a layer travelling along an SVG path as keyframes (`keyframes: followPath({ d, duration, ease, orient: true, frameRate: 30 })`), by arc length so the speed is even, one sample per frame, with optional heading (`orient` / `rotate`) that never jumps by a turn, and `from` / `to` fractions.
|
|
15
|
+
- **Path morphing.** A `shape: 'path'` with `morphTo` (another SVG path) and an animatable `morph` (0 → 1) turns into the other outline: closed outlines are lined up so they do not twist, sub-paths are paired in order, fill, stroke and a trim follow, and the ends are drawn exactly as the paths.
|
|
16
|
+
- **`wiggle()`**: a seeded shake or drift as keyframes (`keyframes: [...wiggle({ duration: 6, props: { x: { around: 'GW/2', amp: 6 } } })]`); every property moves on its own and ends back at its resting value.
|
|
17
|
+
|
|
18
|
+
## 0.9.0
|
|
19
|
+
|
|
20
|
+
**Added**
|
|
21
|
+
|
|
22
|
+
- **Poster time.** `movie.init({ poster: 9.5 })` names the moment that stands for the movie (a negative value counts back from the end). Once the movie is ready the canvas shows that frame while the playhead stays at 0, and play still starts from 0, like `<video poster>` but derived from the same data, with no separate image file. `movie.poster`, `movie.posterFrame` and `movie.posterImage()` (a `snapshot()` at the poster time that leaves the movie as it was); `pixi-effects-check` writes `poster.jpg`. The gallery uses it: every piece declares `poster` once (the `?poster` handling copied into all 33 pieces and `posterFrame` in their meta are gone), `node scripts/make-posters.mjs` takes the posters and writes `posters/manifest.json` (with a hash of each page, so a poster that is out of date fails a test).
|
|
23
|
+
- A guide for people, as a standalone site on GitHub Pages (`/guide/`, next to the gallery and the live demos): your first video, how it works, guides for text, shapes, images and video, audio, filters, transitions, 2.5D, exporting, the player and reviewing, a cookbook of tested recipes with live demos, a page on working with an AI, and a FAQ. It puts the AI-development tools up front: `check`, the contact sheet, the timeline and viewer, `inspect`, `inspectAudio` and `render`. Pages are markdown in `site/guide/`, built to HTML by `scripts/build-guide.mjs` (`npm run guide` previews it in `guide-preview/`); the recipes in them are built and linted by the tests, and the first-video page is a real file checked in Chrome.
|
|
24
|
+
|
|
3
25
|
## 0.8.0
|
|
4
26
|
|
|
5
27
|
**Added**
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# pixi-effects
|
|
2
2
|
|
|
3
|
-
> **Status**: experimental — current release `0.
|
|
3
|
+
> **Status**: experimental — current release `0.10.0`. The API may still change between minor versions (see [CHANGELOG](./CHANGELOG.md): `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
|
-
**[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** ·
|
|
5
|
+
**[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 35 portfolio pieces written as plain data by AI models · **[Live demos →](https://yjmtmtk.github.io/pixi-effects/)** · 12 numbered examples + an in-browser playground.
|
|
6
6
|
|
|
7
7
|
Declarative composition and video rendering for the web. After Effects-style timelines on top of [PixiJS v8](https://pixijs.com/) and [GSAP](https://gsap.com/), with strict TypeScript types. Render to MP4 / WebM / MOV via [mediabunny](https://mediabunny.dev/).
|
|
8
8
|
|
|
@@ -40,8 +40,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
40
40
|
"gsap": "https://esm.sh/gsap@3.12.5",
|
|
41
41
|
"gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
|
|
42
42
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
43
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
44
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
43
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.10.0/dist/index.js",
|
|
44
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.10.0/dist/Controller.js"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
</script>
|
|
@@ -52,7 +52,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
52
52
|
</script>
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.
|
|
55
|
+
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.10.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.
|
|
56
56
|
|
|
57
57
|
> **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
|
|
58
58
|
>
|
|
@@ -68,7 +68,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
|
|
|
68
68
|
"imports": {
|
|
69
69
|
"...": "(everything from the importmap above)",
|
|
70
70
|
"three": "https://esm.sh/three@0.178.0",
|
|
71
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
71
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.10.0/dist/three.js"
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
</script>
|
package/ai/SKILL.md
CHANGED
|
@@ -10,16 +10,17 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
10
10
|
## Workflow
|
|
11
11
|
|
|
12
12
|
1. **Start from `template.html`** (copy it; 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
|
-
2. **
|
|
14
|
-
3. **
|
|
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
|
+
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
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.
|
|
16
|
-
|
|
17
|
+
5. **Look at it** — you cannot judge motion from code. The library has the tools built in:
|
|
17
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.
|
|
18
19
|
- `await movie.inspect(frame)` → every layer's canvas bounds and visibility, plus `issues` (text off the canvas, cut off by an edge, empty, or overlapping other text). Run it at several frames; fix every issue it names.
|
|
19
20
|
- `await movie.snapshot(frame, { as: 'dataURL' })` → one frame, canvas only (no player bar).
|
|
20
21
|
- `movie.inspectAudio()` → **hear with numbers**: for every sound its `start`, `sound.length`, `sound.peakDb`, `sound.loudestAt` (line this up with the picture) and `sound.brightnessHz` (< 500 dark, > 8000 airy), plus `issues` (limited mix, inaudible or cut-off sounds). Check it whenever the movie has audio; `render()` encodes this same mix.
|
|
21
22
|
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.
|
|
22
|
-
|
|
23
|
+
6. **Iterate** on what you saw (clipped text, collisions, wrong timing). Then export: `const blob = await movie.render({ format: 'mp4' })` (about real time; `movie.on('progress', …)` reports 0–100).
|
|
23
24
|
|
|
24
25
|
## The rules that cause most failures
|
|
25
26
|
|
|
@@ -30,7 +31,8 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
30
31
|
- **Sound effects need no files:** `{ type: 'audio', sfx: 'swoosh', at: 2 }` — presets `click pop swoosh swipe hit riser chime beep coin glitch typewriter`; knobs `pitch`, `brightness`, `seed`. The layer lasts as long as the sound (`duration` stretches it); a `riser` is loudest at its end (`at = hit − duration`), a `swoosh` 0.16 s in. A music file shorter than its layer goes silent — `loop: true`.
|
|
31
32
|
- **Masks live in the parent's coordinates and don't follow the masked layer.** **`filterArea` is in the layer's own coordinates.**
|
|
32
33
|
- **2.5D needs `threeD: true`** on each layer plus a `{ type: 'camera' }` layer; camera props go in `initial`/keyframes; keep every `z` below the camera distance (≈ 989 at 720p, fov 40); under a dolly zoom only `z = 0` stays fixed.
|
|
33
|
-
- **
|
|
34
|
+
- **Group / parent layers = `{ type: 'null', name: 'rig' }`** (draws nothing, only moves) + `parent: 'rig'` on the layers that should follow it: their `x`/`y` are then measured from the null's origin, and the null's position / rotation / scale / alpha carry them (orbits, clock hands, grouped fades). Nulls chain. Only `null` layers can be parents; not for `threeD` layers.
|
|
35
|
+
- **Built in:** `animateText()` (per-letter / per-word / per-line entrances and exits in one call: `...animateText('Title', style, { x, y, align: 'center', at: 0.5, duration: 5, in: 'rise', out: 'fade', stagger: { each: 0.05 } })`), `splitText()` / `measureText()` (per-letter / per-word layers, exact positions, by hand), `shape: 'arc'` (progress rings, pie / donut slices: animate `endAngle`), `fillGradient` (linear/radial, alpha stops → vignettes), `{value}` counters, `repeat`/`yoyo`, `orbit()`, seeded randomness (`rand(seed)` / `noise(x, seed)` in expressions, `random(seed)` in JS, never `Math.random()`) `wiggle()` (a seeded shake as keyframes) `followPath()` (a layer along an SVG path: `keyframes: followPath({ d, duration, orient: true })`), path morphing (`{ shape: 'path', d, morphTo, initial/keyframes: morph 0 → 1 }`) and `stagger()` (delays for a wave of layers: `stagger(letters, { each: 0.05, from: 'center' })`, or `stagger(n, …)` for numbers; GSAP-style `each` / `amount` / `from` / `grid` / `ease`). **Not in the DSL (hand-roll, recipes exist or loops are easy):** stroke draw-on (trim path), text that changes over time (other than `{value}`), other per-frame curves, particle emitters.
|
|
34
36
|
- **Blend modes:** `blendMode: 'add' | 'screen' | 'multiply'` on any layer (glows, light leaks).
|
|
35
37
|
- **One `Movie` per page.** A sound effect needs no `duration` (it lasts as long as its clip). Seeking is safe: counters, colours and growing shapes are right after any jump.
|
|
36
38
|
|
|
@@ -43,7 +45,7 @@ Centred text must be positioned by `anchorX/anchorY: 0.5`; estimate text width a
|
|
|
43
45
|
- `reference/cheatsheet.md` — every layer type, prop, default and rule on one page.
|
|
44
46
|
- `reference/recipes.md` — tested building blocks: slam type, lower-third, marquee, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions + music, looping, particles, gradients/vignette, three.js metal.
|
|
45
47
|
- `reference/pitfalls.md` — the full list of real mistakes, with status.
|
|
46
|
-
- `tools/check.mjs` — the one-command review (see Workflow step
|
|
48
|
+
- `tools/check.mjs` — the one-command review (see Workflow step 5).
|
|
47
49
|
- `template.html` — the starting file. (It loads the library from a CDN; when testing against a local build, point the three `pixi-effects*` importmap entries at `../../dist/…`.)
|
|
48
50
|
- `tools/save-image.py` — save a `dataURL` result (contact sheet / snapshot) to a PNG.
|
|
49
51
|
- Full reference: `docs/dsl.md` and `docs/api.md` in the repository.
|
|
@@ -9,7 +9,7 @@ A video is **data**: a tree of plain objects. Everything below is a JS object yo
|
|
|
9
9
|
| Canvas | pixels, origin top-left, `+x` right, `+y` down. Typical: 1280×720 @ 30 fps |
|
|
10
10
|
| Angles | **degrees**: `rotation`, `skew*`, `rotationX`, `rotationY` |
|
|
11
11
|
| Depth (`z`) | `+z` toward the viewer (bigger, nearer) |
|
|
12
|
-
| Numbers | any number may be an **expression string**: `'GW/2 - w/2'`, `'min(W,H)*0.4'` (build-time only; no per-frame time variable) |
|
|
12
|
+
| Numbers | any number may be an **expression string**: `'GW/2 - w/2'`, `'min(W,H)*0.4'` (build-time only; no per-frame time variable). Functions: `min max abs floor ceil round sqrt pow sin cos tan lerp clamp smoothstep mod step`, `PI`, and **seeded** `rand(seed)` (0…1) / `noise(x, seed)` (−1…1): give each layer its own seed, never `Math.random()` |
|
|
13
13
|
| Colours | `'#rrggbb'` strings or numbers; text colour is `style.fill`, shape colour is `fillColor`/`strokeColor`, image colour is `tint` |
|
|
14
14
|
| Z-order | **array order**, later = on top. A layer is hidden (not removed) outside `[at, at+duration)` and keeps its last values |
|
|
15
15
|
| Time of a sequence's `at` | seconds from the start of its **parent composition** (default 0) |
|
|
@@ -67,7 +67,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
|
|
|
67
67
|
| arc | `radius innerRadius startAngle endAngle` — degrees, **0° = 3 o'clock, clockwise**; centred on `x, y`; stroke only = an open arc line (`strokeCap: 'round'`), with a fill = a sector, with `innerRadius` = a donut slice; a sweep ≥ 360° = a full circle. A progress ring: `startAngle: -90` and animate `endAngle` from −90 to 270 |
|
|
68
68
|
| line | `from: [x,y] to: [x,y]` — plain canvas coordinates (omit `x,y`; if you give them they place the line's midpoint). Stroke in `initial`: `strokeColor`, `strokeWidth` |
|
|
69
69
|
| polygon | `points: [[x,y],…] open` — canvas coordinates like `line` |
|
|
70
|
-
| path | `d` (SVG path data) — canvas coordinates like `line` |
|
|
70
|
+
| path | `d` (SVG path data) — canvas coordinates like `line`. **Morph:** `morphTo: 'M…Z'` + animate `morph` 0 → 1 (points half way between, closed outlines lined up so they do not twist; `morphPoints` 8–2048) |
|
|
71
71
|
|
|
72
72
|
**Draw-on:** every shape but `arc` has `trimStart` / `trimEnd` (0–1 of the outline, default 0 / 1, animatable): `trimEnd: 0` + keyframe `to: { trimEnd: 1 }` draws a line / border / path on; `trimStart` 0 → 1 after it wipes it off; both = a travelling dash. The fill is NOT trimmed. Rect outline starts top-left, circle / ellipse at 12 o'clock, both clockwise. Use `strokeCap: 'round'`.
|
|
73
73
|
|
|
@@ -75,6 +75,8 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
|
|
|
75
75
|
|
|
76
76
|
**composition** — `width height duration sequences transitions`. With `threeD: true` it is a **card**: its children are drawn into one texture (content outside its `width × height` is clipped: size it for a soft shadow too; a plain 2D composition does not clip, but is skipped when its whole rectangle is off screen) and move / rotate in depth together. Children use the composition's local coordinates and times. Position/rotate/scale it as a unit; to rotate/scale about its centre set `pivotX/pivotY` to the centre and `x/y` to where that point should sit.
|
|
77
77
|
|
|
78
|
+
**null + `parent`** — `{ type: 'null', name: 'rig', initial: { x, y }, keyframes: [...] }` draws nothing; layers with `parent: 'rig'` are drawn inside it and follow its `x y rotation scale skew alpha pivotX pivotY`. Their `x / y` are measured **from the null's origin** (null at (640,360), child `x: 200` → drawn at 840), so rotating the null makes the child circle it. Nulls chain (`parent` on a null). Children are drawn at the null's place in the stack, together. Only `null` layers can be parents; not for `threeD` layers; a mask follows its layer.
|
|
79
|
+
|
|
78
80
|
**camera** (2.5D) — props in `initial`/keyframes: `x y z lookAtX lookAtY lookAtZ fov` (defaults: centred, looking at the z = 0 plane, `fov` 40, `z` auto = `(H/2)/tan(fov/2)` ≈ 989 at 720p). Never on the camera object itself. An orbit is `orbit()`; by hand: `x = cx + R·sin θ`, `z = R·cos θ`, `R = (H/2)/tan(fov/2)`, `lookAt` = the centre at `z = 0`.
|
|
79
81
|
|
|
80
82
|
**three** (optional entry `pixi-effects/three`) — `three({ type:'three', width, height, setup(ctx) { …; return { objects: { knot } } }, update?, dispose? })`; drive with `'three.knot.rotation.y'` keyframes. Call `registerThree()` before `init`.
|
|
@@ -83,6 +85,10 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
|
|
|
83
85
|
|
|
84
86
|
Add `threeD: true` to any visual layer, then use `z`, `rotationX`, `rotationY` (centre rotation with `anchorX/Y: 0.5` or `pivotX/Y`). Add a `{ type: 'camera' }` layer for a view. A `threeD` layer at `z: 0` with the default camera looks identical to a 2D one. Consecutive `threeD` layers are drawn farthest-first (equal `z` keeps array order); non-`threeD` layers ignore the camera and keep array order. Layers at/behind the camera plane are hidden. Not supported on `threeD` layers: masks, transitions, `filterArea`.
|
|
85
87
|
|
|
88
|
+
## Poster
|
|
89
|
+
|
|
90
|
+
`movie.init({ …, poster: 9.5 })` names the moment (seconds; negative = from the end) that stands for the movie: the canvas shows it before play (the playhead stays at 0, play starts from 0), `movie.poster` / `movie.posterFrame` read it, `await movie.posterImage({ as: 'dataURL', type: 'image/jpeg', scale: 0.5 })` is the picture. Pick the most striking moment, not frame 0 (which is often black).
|
|
91
|
+
|
|
86
92
|
## Player
|
|
87
93
|
|
|
88
94
|
`new Controller(movie, { canvas, theme: { accent: '#ff4d6d' } })` (import from `pixi-effects/controller`; theme keys `accent foreground track barBackground trackHeight font`, or CSS `--mc-accent` …; `controller.setTheme(…)`). Your own player: `movie.play() pause() gotoFrame(f) volume muted toggleMute()`, state `currentFrame totalFrames frameRate duration isPlaying`, events `ready frame play pause ended seeking seeked volumechange error progress` (`<video>` names). `examples/15-custom-player.html` is a complete one.
|
|
@@ -104,21 +110,27 @@ transitions: [{ kind: 'crossfade' | 'wipe' | 'iris' | 'slide' | 'dip' | 'zoom' |
|
|
|
104
110
|
## Presets
|
|
105
111
|
|
|
106
112
|
```js
|
|
107
|
-
import { kenBurns, withFade } from 'pixi-effects';
|
|
113
|
+
import { kenBurns, withFade, wiggle, stagger, followPath, random } from 'pixi-effects';
|
|
108
114
|
kenBurns({ asset, name, at, duration, motion: 'still'|'scale'|'rotation'|'position',
|
|
109
115
|
fit: 'cover'|'contain', ease, /* scale */ origin, zoom, direction, /* rotation */ angle, /* position */ from, to })
|
|
110
116
|
withFade(spec, { in: 0.5, out: 0.5 }) // alpha fade; `out` needs spec.duration
|
|
111
117
|
orbit({ duration: 6, degrees: 40 /* , radius, center, start, fov, ease, at */ }) // a camera layer that circles a point
|
|
118
|
+
// a seeded shake as keyframes (spread into a layer's keyframes); each prop moves on its own; ends back at `around`
|
|
119
|
+
keyframes: [...wiggle({ duration: 6, freq: 4, seed: 3, props: { x: { around: 'GW/2', amp: 6 }, rotation: { around: 0, amp: 1.5 } } })]
|
|
120
|
+
stagger(8, { each: 0.08 /* or amount: total s */, from: 'start'|'end'|'center'|'edges'|'random'|index, grid: [cols, rows], ease, seed }) // → delays [0, 0.08, …]: `at: 1 + d[i]`
|
|
121
|
+
stagger(layers, { each: 0.05, from: 'center' }) // → the same layers with `at` pushed back (originals untouched)
|
|
122
|
+
keyframes: followPath({ d: 'M 120 560 C 360 120 920 120 1160 560', duration: 4, ease, orient: true /* faces the way it goes; rotate: -90 for an up-pointing image */, from: 0, to: 1, frameRate: 30 /* = the movie's */ }) // x / y (and rotation) along an SVG path; give the layer its own `at`
|
|
123
|
+
const r = random(7); r(); r(); // a repeatable stream in 0…1 for JS loops (never Math.random())
|
|
112
124
|
```
|
|
113
125
|
`kenBurns` covers the canvas, so source images should be at least canvas-sized (1920×1080 for 1280×720).
|
|
114
126
|
|
|
115
127
|
```js
|
|
116
|
-
import { measureText, splitText } from 'pixi-effects'; // text layout helpers (measured with the renderer's own fonts)
|
|
128
|
+
import { measureText, splitText, animateText } from 'pixi-effects'; // text layout helpers (measured with the renderer's own fonts)
|
|
117
129
|
measureText('Hello', style) // → { width, height, lines }
|
|
118
130
|
splitText('Hello world', style, { by: 'chars' | 'words' | 'lines', x, y, align: 'left' | 'center' | 'right' })
|
|
119
131
|
// → [{ text, x, y, width, index, line }] — one text layer per piece, same style, anchor top-left
|
|
120
132
|
```
|
|
121
|
-
Per-letter / per-word animation
|
|
133
|
+
Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, by: 'chars'|'words'|'lines', at, duration /* required: until the last piece is gone */, in: 'rise'|'drop'|'fade'|'pop'|'zoom'|'slide'|'spin'|{ from, to, duration, ease }|false, out: <same, runs in the same wave>, stagger: { each, from, … }, idle: { y: 4, rotation: 1 }, styleFor: p => ({ fill }) })` → text layers `<name>-0…` (anchor centre; `x`/`y` in `from` are offsets) to spread into `sequences` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
|
|
122
134
|
|
|
123
135
|
## Assets
|
|
124
136
|
|
package/ai/reference/pitfalls.md
CHANGED
|
@@ -32,8 +32,8 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
32
32
|
17. **Gradients / vignettes**: use shape `fillGradient` (linear or radial, stops may have alpha). Earlier sessions hand-rolled canvas images or dozens of stacked bands. *(fixed)*
|
|
33
33
|
18. **Counters**: `text: '{value}'` + animate `value` (with `format`). Other text content cannot change over time. Earlier sessions generated one text layer per number (40+ per bar). *(fixed)*
|
|
34
34
|
19. **Loops**: `repeat` (finite) / `yoyo` / `repeatDelay` on any keyframe; infinite repeats are rejected with a warning. *(fixed)*
|
|
35
|
-
20. **No per-frame expression time.** Expressions are evaluated once at build. A camera orbit is `orbit()`; any other curve (waves, spirals) must be sampled into short linear keyframes (~0.1 s) in a JS loop. *(partly fixed)*
|
|
36
|
-
21. **Per-letter / per-word text is `splitText()` (one layer per piece, positions measured for you);
|
|
35
|
+
20. **No per-frame expression time.** Expressions are evaluated once at build. A camera orbit is `orbit()`; a shake or drift is `wiggle()`, travel along a path is `followPath()`, and any other curve (waves, spirals) must be sampled into short linear keyframes (~0.1 s) in a JS loop. **Randomness must be seeded** (`rand(seed)` / `noise(x, seed)` in expressions, `random(seed)` in JS): `Math.random()` makes playback and the exported file disagree. *(partly fixed)*
|
|
36
|
+
21. **Per-letter / per-word text is `animateText()` (one call: a layer per piece, a wave in and out) or `splitText()` by hand (one layer per piece, positions measured for you); a group / parent is a `{ type: 'null' }` layer + `parent: 'name'`, no particle emitter, no caption-linked-to-slide.** One layer per letter; many layers; seeded random loops (`random(seed)`). *(hand-roll)*
|
|
37
37
|
22. **Colour tweens look muddy in RGB** (red→green passes through brown). Use `colorSpace: 'oklab'` or `'oklch'` on the layer. In `oklch`, a nearly grey end (chroma < ~0.08, e.g. `#9a9ab4`) borrows the other end's hue, so grey → yellow no longer flashes through pink. *(docs / fixed)*
|
|
38
38
|
23. **Generated images**: `canvas.toDataURL()` as an asset `src` works. Make them ≥ canvas size or `kenBurns` zooms look soft. *(docs)*
|
|
39
39
|
|
package/ai/reference/recipes.md
CHANGED
|
@@ -431,6 +431,82 @@ return sequences;
|
|
|
431
431
|
|
|
432
432
|
---
|
|
433
433
|
|
|
434
|
+
## A wave of delays (`stagger`)
|
|
435
|
+
|
|
436
|
+
`stagger()` gives the delays for a wave of items, GSAP style: `each` (gap between neighbours) or `amount` (total), `from` (`'start'` `'end'` `'center'` `'edges'` `'random'` or an index), `grid: [cols, rows]`, `ease`, `seed`. Give it a count and it returns numbers; give it layers and it returns them with `at` pushed back.
|
|
437
|
+
|
|
438
|
+
```js
|
|
439
|
+
// @recipe stagger
|
|
440
|
+
const style = { fontFamily: 'Arial, sans-serif', fontSize: 96, fontWeight: 'bold', fill: '#ffffff' };
|
|
441
|
+
// letters rising from the middle outwards: the same layers, each starting a little later
|
|
442
|
+
const letters = stagger(
|
|
443
|
+
splitText('RIPPLE', style, { x: 640, y: 120, align: 'center' }).map(p => ({
|
|
444
|
+
type: 'text', name: 'letter-' + p.index, text: p.text, style, at: 0.2, duration: 4,
|
|
445
|
+
initial: { x: p.x, y: p.y, alpha: 0 },
|
|
446
|
+
keyframes: [{ at: 0, from: { y: p.y + 40, alpha: 0 }, to: { y: p.y, alpha: 1 }, duration: 0.4, ease: 'back.out(2)' }],
|
|
447
|
+
})),
|
|
448
|
+
{ each: 0.07, from: 'center' });
|
|
449
|
+
// a 6 x 3 grid of tiles that pop in as a ripple from the middle (numbers: use them in `at`)
|
|
450
|
+
const COLS = 6, ROWS = 3, delays = stagger(COLS * ROWS, { each: 0.12, grid: [COLS, ROWS], from: 'center', ease: 'sine.out' });
|
|
451
|
+
const tiles = delays.map((d, i) => ({
|
|
452
|
+
type: 'shape', shape: 'rect', width: 120, height: 120, cornerRadius: 18, name: 'tile-' + i, at: 1 + d, duration: 3 - d,
|
|
453
|
+
initial: { x: 190 + (i % COLS) * 180, y: 330 + Math.floor(i / COLS) * 150, fillColor: '#4cc9f0', scale: 0 },
|
|
454
|
+
keyframes: [{ at: 0, to: { scale: 1 }, duration: 0.4, ease: 'back.out(2)' }],
|
|
455
|
+
}));
|
|
456
|
+
return [...letters, ...tiles];
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Per-letter text in one call (`animateText`)
|
|
462
|
+
|
|
463
|
+
`animateText(text, style, options)` returns one text layer per character / word / line, laid out like the whole text, each arriving (and leaving) in a wave. `duration` is required: seconds from `at` until the last piece is gone (it throws if the wave does not fit). Letters turn and scale about their own centre.
|
|
464
|
+
|
|
465
|
+
```js
|
|
466
|
+
// @recipe animate-text
|
|
467
|
+
const style = { fontFamily: 'Arial, sans-serif', fontSize: 110, fontWeight: 'bold', fill: '#ffffff' };
|
|
468
|
+
return [
|
|
469
|
+
...animateText('Kinetic type', style, { x: 640, y: 230, align: 'center', at: 0.3, duration: 5,
|
|
470
|
+
in: 'rise', out: 'fade', stagger: { each: 0.05, from: 'center' }, idle: { y: 5, rotation: 1 },
|
|
471
|
+
styleFor: p => (p.index === 0 ? { fill: '#ffd166' } : {}) }),
|
|
472
|
+
...animateText('one word at a time', { fontFamily: 'Arial, sans-serif', fontSize: 44, fill: '#8fa0bf' },
|
|
473
|
+
{ by: 'words', x: 640, y: 420, align: 'center', at: 1.6, duration: 3.7, in: 'pop', stagger: { each: 0.25 } }),
|
|
474
|
+
];
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Presets for `in` / `out`: `rise` (default) `drop` `fade` `pop` `zoom` `slide` `spin`; or `{ from: { x: -30, scale: 0.5, rotation: 20 }, duration: 0.8, ease: 'power3.out' }` (`x` / `y` in `from` are offsets from the resting place, the rest are absolute values). `idle` drifts each piece between the entrance and the exit.
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Travel along a path, and morph one shape into another
|
|
482
|
+
|
|
483
|
+
`followPath()` gives a layer's `x` / `y` (and `rotation` with `orient`) along an SVG path at an even speed. `morphTo` + `morph` turn one path outline into another.
|
|
484
|
+
|
|
485
|
+
```js
|
|
486
|
+
// @recipe follow-path
|
|
487
|
+
const ROUTE = 'M 120 560 C 360 120 920 120 1160 560';
|
|
488
|
+
return [
|
|
489
|
+
{ type: 'shape', shape: 'path', d: ROUTE, duration: 5, initial: { strokeColor: '#ffffff', strokeWidth: 3, strokeAlpha: 0.25 } }, // the route, faint
|
|
490
|
+
{ type: 'shape', shape: 'polygon', points: [[-18, -12], [18, 0], [-18, 12]], at: 0.5, duration: 4.5, initial: { fillColor: '#ffd166' },
|
|
491
|
+
keyframes: followPath({ d: ROUTE, duration: 4, ease: 'power2.inOut', orient: true, frameRate: 30 }) }, // a pointer that faces the way it goes
|
|
492
|
+
];
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
```js
|
|
496
|
+
// @recipe morph
|
|
497
|
+
const BLOB = 'M 640 160 C 780 160 860 260 860 360 C 860 470 770 560 640 560 C 510 560 420 470 420 360 C 420 250 500 160 640 160 Z';
|
|
498
|
+
const STAR = 'M 640 130 L 700 300 L 880 300 L 735 410 L 790 590 L 640 480 L 490 590 L 545 410 L 400 300 L 580 300 Z';
|
|
499
|
+
return [{
|
|
500
|
+
type: 'shape', shape: 'path', d: BLOB, morphTo: STAR, duration: 4,
|
|
501
|
+
initial: { fillColor: '#ffd166', strokeColor: '#ffffff', strokeWidth: 6, strokeJoin: 'round' },
|
|
502
|
+
keyframes: [{ at: 0.5, to: { morph: 1 }, duration: 1.5, ease: 'power2.inOut', repeat: 1, yoyo: true }], // there and back
|
|
503
|
+
}];
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Both outlines are in canvas coordinates. Closed outlines are lined up so they do not twist.
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
434
510
|
## Looping motion
|
|
435
511
|
|
|
436
512
|
`repeat` (a finite count) and `yoyo` go on any keyframe. Total time = `duration × (repeat + 1)`.
|
|
@@ -450,8 +526,7 @@ return [{
|
|
|
450
526
|
|
|
451
527
|
```js
|
|
452
528
|
// @recipe particles
|
|
453
|
-
|
|
454
|
-
const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
|
|
529
|
+
const rnd = random(7); // seeded: the same stream every run (never Math.random())
|
|
455
530
|
return Array.from({ length: 40 }, () => {
|
|
456
531
|
const x = rnd() * 1280, y = 100 + rnd() * 620;
|
|
457
532
|
return {
|
|
@@ -464,6 +539,52 @@ return Array.from({ length: 40 }, () => {
|
|
|
464
539
|
|
|
465
540
|
---
|
|
466
541
|
|
|
542
|
+
## Group and orbit with a null layer (`parent`)
|
|
543
|
+
|
|
544
|
+
A `null` layer draws nothing and only moves; layers with `parent: 'name'` are drawn inside it. Their `x` / `y` are measured from the null's origin, so turning the null makes them circle it. Nulls chain (a moon on a null that is itself on the planet's null).
|
|
545
|
+
|
|
546
|
+
```js
|
|
547
|
+
// @recipe null-orbit
|
|
548
|
+
return [
|
|
549
|
+
{ type: 'shape', shape: 'circle', radius: 50, duration: 8, initial: { x: 640, y: 360, fillColor: '#ffd166' } }, // the sun
|
|
550
|
+
{ type: 'null', name: 'earth-orbit', initial: { x: 640, y: 360 }, // pivot at the sun
|
|
551
|
+
keyframes: [{ at: 0, to: { rotation: 360 }, duration: 8, ease: 'none' }] },
|
|
552
|
+
{ type: 'shape', shape: 'circle', radius: 20, parent: 'earth-orbit', initial: { x: 240, y: 0, fillColor: '#4cc9f0' } }, // 240 px from the sun
|
|
553
|
+
{ type: 'null', name: 'moon-orbit', parent: 'earth-orbit', initial: { x: 240, y: 0 }, // pivot at the earth
|
|
554
|
+
keyframes: [{ at: 0, to: { rotation: 1440 }, duration: 8, ease: 'none' }] },
|
|
555
|
+
{ type: 'shape', shape: 'circle', radius: 7, parent: 'moon-orbit', initial: { x: 55, y: 0, fillColor: '#e8eefc' } }, // 55 px from the earth
|
|
556
|
+
];
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## A hand-held shake and a flickering light (`wiggle`, seeded)
|
|
562
|
+
|
|
563
|
+
`wiggle()` returns keyframes (spread it into `keyframes`): each property drifts between seeded random targets and ends back at `around`. Use `freq` ~3 for a slow float, 8+ for a shake, `ease: 'none'` for a jittery flicker. Per-layer variety: give each its own `seed`.
|
|
564
|
+
|
|
565
|
+
```js
|
|
566
|
+
// @recipe wiggle
|
|
567
|
+
return [
|
|
568
|
+
// a title that floats and tilts a little, like a hand-held camera
|
|
569
|
+
{ type: 'text', text: 'NIGHT SHIFT', duration: 6,
|
|
570
|
+
style: { fontSize: 120, fill: '#ffffff', fontWeight: '800' },
|
|
571
|
+
initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
|
572
|
+
keyframes: wiggle({ duration: 6, freq: 3, seed: 1, props: { x: { around: 'GW/2', amp: 8 }, y: { around: 'GH/2', amp: 6 }, rotation: { around: 0, amp: 1.2 } } }) },
|
|
573
|
+
// a neon sign whose light stutters (fast, straight-line steps)
|
|
574
|
+
{ type: 'shape', shape: 'rect', width: 520, height: 8, duration: 6,
|
|
575
|
+
initial: { x: 'GW/2', y: 'GH/2 + 90', fillColor: '#ff4d6d' },
|
|
576
|
+
keyframes: wiggle({ duration: 6, freq: 12, seed: 5, ease: 'none', props: { alpha: { around: 0.8, amp: 0.2 } } }) },
|
|
577
|
+
// dust: every speck has its own seed, so its drift is its own and never changes between runs
|
|
578
|
+
...Array.from({ length: 24 }, (_, i) => ({
|
|
579
|
+
type: 'shape', shape: 'circle', radius: 2 + rand(i) * 3, duration: 6,
|
|
580
|
+
initial: { x: 40 + rand(i + 100) * 1200, y: 40 + rand(i + 200) * 640, fillColor: '#ffffff', fillAlpha: 0.25 + rand(i + 300) * 0.4 },
|
|
581
|
+
keyframes: [...wiggle({ duration: 6, freq: 0.6, seed: i, props: { y: { around: 40 + rand(i + 200) * 640, amp: 30 } } })],
|
|
582
|
+
})),
|
|
583
|
+
];
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
467
588
|
## Gradients and vignettes
|
|
468
589
|
|
|
469
590
|
`fillGradient` fills a shape with a linear or radial gradient (positions are 0–1 of the shape's own bounds; colours can have alpha). A radial gradient from transparent to dark, on a full-screen rect placed last, is a vignette.
|
package/ai/template.html
CHANGED
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"gsap": "https://esm.sh/gsap@3.12.5",
|
|
37
37
|
"gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
|
|
38
38
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
39
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
40
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
41
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
39
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.10.0/dist/index.js",
|
|
40
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.10.0/dist/Controller.js",
|
|
41
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.10.0/dist/three.js",
|
|
42
42
|
"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
|
|
43
43
|
"three": "https://esm.sh/three@0.178.0"
|
|
44
44
|
}
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
await movie.init({
|
|
74
74
|
canvas: document.getElementById('stage'),
|
|
75
75
|
width: W, height: H, duration: DURATION, frameRate: FPS,
|
|
76
|
+
poster: DURATION * 0.75, // the moment that stands for the video before it plays (pick your most striking one)
|
|
76
77
|
background: '#0a0a0f',
|
|
77
78
|
// assets: [{ name: 'photo', src: './photo.jpg' }, { name: 'bgm', src: './bgm.mp3' }],
|
|
78
79
|
composition: { sequences },
|
package/ai/tools/check.mjs
CHANGED
|
@@ -279,6 +279,14 @@ export async function runCheck(opts, log = console.log) {
|
|
|
279
279
|
fs.writeFileSync(sheetFile, Buffer.from(sheet.slice(sheet.indexOf(',') + 1), 'base64'));
|
|
280
280
|
report.files.sheet = shown(sheetFile);
|
|
281
281
|
|
|
282
|
+
// the poster: the picture that stands for the movie (its poster time, or the first frame)
|
|
283
|
+
const poster = await cdp.eval(`movie.posterImage({ as: 'dataURL', type: 'image/jpeg', scale: 0.5 })`).catch(() => null);
|
|
284
|
+
if (poster) {
|
|
285
|
+
const posterFile = path.join(outDir, 'poster.jpg');
|
|
286
|
+
fs.writeFileSync(posterFile, Buffer.from(poster.slice(poster.indexOf(',') + 1), 'base64'));
|
|
287
|
+
report.files.poster = shown(posterFile);
|
|
288
|
+
}
|
|
289
|
+
|
|
282
290
|
// the timeline: every layer as a bar on a time axis (for a human to open in a browser)
|
|
283
291
|
const timeline = await cdp.eval(`movie.timelineChart({ title: ${JSON.stringify(name)} })`).catch(() => null);
|
|
284
292
|
if (timeline) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape, B as AudioDescriptor } from './types-
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape, B as AudioDescriptor } from './types-BXu28nJj.cjs';
|
|
3
3
|
import { gsap } from 'gsap';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape, B as AudioDescriptor } from './types-
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape, B as AudioDescriptor } from './types-BXu28nJj.js';
|
|
3
3
|
import { gsap } from 'gsap';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunk4WJUSDJJ_cjs = require('./chunk-4WJUSDJJ.cjs');
|
|
4
|
+
require('./chunk-S3ZI4MKS.cjs');
|
|
5
|
+
require('./chunk-UQBGW5DO.cjs');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Object.defineProperty(exports, "CompositionSequence", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: function () { return chunk4WJUSDJJ_cjs.CompositionSequence; }
|
|
12
|
+
});
|
|
13
|
+
//# sourceMappingURL=Composition-EKVTUYCI.cjs.map
|
|
14
|
+
//# sourceMappingURL=Composition-EKVTUYCI.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-EKVTUYCI.cjs"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-NJCOMGD7.js"}
|
package/dist/Controller.cjs
CHANGED
|
@@ -334,6 +334,11 @@ var Controller = class _Controller {
|
|
|
334
334
|
volumeContainerEl = null;
|
|
335
335
|
fullscreenBtn = null;
|
|
336
336
|
isVolumeScrubbing = false;
|
|
337
|
+
canvasClickHandler = null;
|
|
338
|
+
canvasPointerDownHandler = null;
|
|
339
|
+
canvasCursor = "";
|
|
340
|
+
/** Set when a pointerdown on the canvas closed the export popover: the click that follows must not play / pause. */
|
|
341
|
+
swallowCanvasClick = false;
|
|
337
342
|
fullscreenChangeHandler = null;
|
|
338
343
|
wheelHandler = null;
|
|
339
344
|
/** Ends a drag the pointerup of which never arrives (see bindScrubbing). */
|
|
@@ -347,6 +352,7 @@ var Controller = class _Controller {
|
|
|
347
352
|
canvas: options.canvas,
|
|
348
353
|
showExportButton: options.showExportButton ?? true,
|
|
349
354
|
enableKeyboardShortcuts: options.enableKeyboardShortcuts ?? true,
|
|
355
|
+
clickToPlay: options.clickToPlay ?? true,
|
|
350
356
|
className: options.className ?? "movie-controller"
|
|
351
357
|
};
|
|
352
358
|
installStyles();
|
|
@@ -366,6 +372,7 @@ var Controller = class _Controller {
|
|
|
366
372
|
this.root.setAttribute("data-state", "visible");
|
|
367
373
|
this.bindMovieEvents();
|
|
368
374
|
this.bindPlayButton();
|
|
375
|
+
if (this.options.clickToPlay) this.bindCanvasClick();
|
|
369
376
|
this.bindScrubbing();
|
|
370
377
|
this.bindMuteButton();
|
|
371
378
|
this.bindVolumeSlider();
|
|
@@ -556,6 +563,25 @@ var Controller = class _Controller {
|
|
|
556
563
|
}
|
|
557
564
|
});
|
|
558
565
|
}
|
|
566
|
+
/** Clicking the picture plays / pauses (a tap on a phone too). */
|
|
567
|
+
bindCanvasClick() {
|
|
568
|
+
const canvas = this.options.canvas;
|
|
569
|
+
this.canvasCursor = canvas.style.cursor;
|
|
570
|
+
canvas.style.cursor = "pointer";
|
|
571
|
+
this.canvasPointerDownHandler = () => {
|
|
572
|
+
this.swallowCanvasClick = false;
|
|
573
|
+
};
|
|
574
|
+
this.canvasClickHandler = () => {
|
|
575
|
+
if (this.swallowCanvasClick) {
|
|
576
|
+
this.swallowCanvasClick = false;
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (this.movie.isPlaying) this.movie.pause();
|
|
580
|
+
else this.movie.play();
|
|
581
|
+
};
|
|
582
|
+
canvas.addEventListener("pointerdown", this.canvasPointerDownHandler);
|
|
583
|
+
canvas.addEventListener("click", this.canvasClickHandler);
|
|
584
|
+
}
|
|
559
585
|
refreshPlayIcon() {
|
|
560
586
|
this.playBtn.innerHTML = this.movie.isPlaying ? ICONS.pause : ICONS.play;
|
|
561
587
|
this.playBtn.setAttribute("aria-label", this.movie.isPlaying ? "Pause" : "Play");
|
|
@@ -778,6 +804,7 @@ var Controller = class _Controller {
|
|
|
778
804
|
if (!target) return;
|
|
779
805
|
if (this.settingsPopoverEl?.contains(target)) return;
|
|
780
806
|
if (this.exportBtn?.contains(target)) return;
|
|
807
|
+
if (this.options.canvas.contains(target)) this.swallowCanvasClick = true;
|
|
781
808
|
this.toggleSettings(false);
|
|
782
809
|
};
|
|
783
810
|
document.addEventListener("pointerdown", this.settingsOutsideHandler);
|
|
@@ -1003,6 +1030,13 @@ var Controller = class _Controller {
|
|
|
1003
1030
|
this.resizeObserver.disconnect();
|
|
1004
1031
|
this.resizeObserver = null;
|
|
1005
1032
|
}
|
|
1033
|
+
if (this.canvasClickHandler) {
|
|
1034
|
+
this.options.canvas.removeEventListener("click", this.canvasClickHandler);
|
|
1035
|
+
this.options.canvas.removeEventListener("pointerdown", this.canvasPointerDownHandler);
|
|
1036
|
+
this.options.canvas.style.cursor = this.canvasCursor;
|
|
1037
|
+
this.canvasClickHandler = null;
|
|
1038
|
+
this.canvasPointerDownHandler = null;
|
|
1039
|
+
}
|
|
1006
1040
|
if (this.fitObserver) {
|
|
1007
1041
|
this.fitObserver.disconnect();
|
|
1008
1042
|
this.fitObserver = null;
|