pixi-effects 0.35.0 → 0.37.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 +35 -0
- package/README.md +5 -5
- package/ai/chat-template.html +3 -3
- package/ai/tools/check.mjs +38 -9
- package/ai/tools/lookup.mjs +15 -2
- package/dist/{Base-pNq6sVGl.d.cts → Base-BlA6SeqJ.d.cts} +1 -1
- package/dist/{Base-CXsMdmJB.d.ts → Base-YqM0Ljvp.d.ts} +1 -1
- package/dist/Composition-5KZETM4J.js +7 -0
- package/dist/{Composition-NDXKQHB4.js.map → Composition-5KZETM4J.js.map} +1 -1
- package/dist/Composition-MHDI7UNZ.cjs +16 -0
- package/dist/{Composition-OLH7RC5Z.cjs.map → Composition-MHDI7UNZ.cjs.map} +1 -1
- package/dist/Controller.d.cts +2 -2
- package/dist/Controller.d.ts +2 -2
- package/dist/{Movie-VYtUN4TS.d.ts → Movie-CJN_gvLB.d.cts} +26 -2
- package/dist/{Movie-Db4QnMIH.d.cts → Movie-CfHaGmUP.d.ts} +26 -2
- package/dist/{Spans-QHHWC2PB.js → Spans-FSSO2HGX.js} +6 -6
- package/dist/{Spans-QHHWC2PB.js.map → Spans-FSSO2HGX.js.map} +1 -1
- package/dist/{Spans-QRMN2EZO.cjs → Spans-WVJH77BW.cjs} +11 -11
- package/dist/{Spans-QRMN2EZO.cjs.map → Spans-WVJH77BW.cjs.map} +1 -1
- package/dist/{blendModes-WIUCYIMC.cjs → blendModes-FD3THLNC.cjs} +5 -5
- package/dist/{blendModes-WIUCYIMC.cjs.map → blendModes-FD3THLNC.cjs.map} +1 -1
- package/dist/{blendModes-PP5NPBXY.js → blendModes-FMYO6SQN.js} +4 -4
- package/dist/{blendModes-PP5NPBXY.js.map → blendModes-FMYO6SQN.js.map} +1 -1
- package/dist/{chunk-ITDYFBB6.js → chunk-32MMNDDN.js} +7 -6
- package/dist/chunk-32MMNDDN.js.map +1 -0
- package/dist/{chunk-GL2AQFLQ.cjs → chunk-3SAS3ZE5.cjs} +7 -6
- package/dist/chunk-3SAS3ZE5.cjs.map +1 -0
- package/dist/{chunk-7TTCPHAV.cjs → chunk-E26M4NTL.cjs} +235 -49
- package/dist/chunk-E26M4NTL.cjs.map +1 -0
- package/dist/{chunk-XV4L72FW.js → chunk-IPQNT65V.js} +6 -6
- package/dist/chunk-IPQNT65V.js.map +1 -0
- package/dist/{chunk-IMQJVRMT.cjs → chunk-JN7JHNZR.cjs} +5 -5
- package/dist/{chunk-IMQJVRMT.cjs.map → chunk-JN7JHNZR.cjs.map} +1 -1
- package/dist/{chunk-2GNK6DPV.js → chunk-KNDEKWQO.js} +203 -18
- package/dist/chunk-KNDEKWQO.js.map +1 -0
- package/dist/{chunk-5IJENZOG.cjs → chunk-MJMOPWMK.cjs} +92 -92
- package/dist/chunk-MJMOPWMK.cjs.map +1 -0
- package/dist/{chunk-LEECUCTG.cjs → chunk-QHIOOK5S.cjs} +436 -29
- package/dist/chunk-QHIOOK5S.cjs.map +1 -0
- package/dist/{chunk-Y253JQOH.js → chunk-SBR75G73.js} +416 -11
- package/dist/chunk-SBR75G73.js.map +1 -0
- package/dist/{chunk-RTJD3ZSW.js → chunk-VNP7QCJE.js} +4 -4
- package/dist/{chunk-RTJD3ZSW.js.map → chunk-VNP7QCJE.js.map} +1 -1
- package/dist/index.cjs +41 -33
- package/dist/index.d.cts +47 -7
- package/dist/index.d.ts +47 -7
- package/dist/index.js +5 -5
- package/dist/loader.css +1 -1
- package/dist/present.cjs +5 -5
- package/dist/present.d.cts +2 -2
- package/dist/present.d.ts +2 -2
- package/dist/present.js +5 -5
- package/dist/three.cjs +6 -6
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +2 -2
- package/dist/{types-CavnUvd5.d.cts → types-DvnXXm33.d.cts} +67 -3
- package/dist/{types-CavnUvd5.d.ts → types-DvnXXm33.d.ts} +67 -3
- package/docs/api.md +5 -4
- package/docs/dsl.md +42 -5
- package/llms-full.txt +305 -22
- package/package.json +1 -1
- package/skills/pixi-effects/SKILL.md +5 -4
- package/skills/pixi-effects/reference/cheatsheet.md +9 -6
- package/skills/pixi-effects/reference/pitfalls.md +7 -0
- package/skills/pixi-effects/reference/recipes.md +237 -3
- package/skills/pixi-effects/template.html +6 -6
- package/dist/Composition-NDXKQHB4.js +0 -7
- package/dist/Composition-OLH7RC5Z.cjs +0 -16
- package/dist/chunk-2GNK6DPV.js.map +0 -1
- package/dist/chunk-5IJENZOG.cjs.map +0 -1
- package/dist/chunk-7TTCPHAV.cjs.map +0 -1
- package/dist/chunk-GL2AQFLQ.cjs.map +0 -1
- package/dist/chunk-ITDYFBB6.js.map +0 -1
- package/dist/chunk-LEECUCTG.cjs.map +0 -1
- package/dist/chunk-XV4L72FW.js.map +0 -1
- package/dist/chunk-Y253JQOH.js.map +0 -1
|
@@ -3,11 +3,11 @@ name: pixi-effects
|
|
|
3
3
|
description: Use when asked to make, animate, preview or export a video, motion graphics, an animated title, a lower third, kinetic type, a promo, a slideshow with transitions, an animated chart or data story, a social clip, a 2.5D / depth scene, or music and sound for a video, written as code in JavaScript or HTML. pixi-effects is a JS library where a video is a plain-object composition (text, shape, image, video, audio and file-free sfx / music, camera, light and shader layers with keyframes) that plays in the browser, is checked by a tool the agent can run, and exports MP4 / WebM / MOV with no server. Not for a project that already uses another video tool (Remotion, Motion Canvas, ffmpeg); follow that tool instead.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
> Written for pixi-effects 0.
|
|
6
|
+
> Written for pixi-effects 0.37.0. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
|
|
7
7
|
|
|
8
8
|
# pixi-effects — write a video as data
|
|
9
9
|
|
|
10
|
-
You describe a video as a tree of plain objects; the library plays it in a canvas and exports it (MP4/WebM/MOV). You never write per-frame code. **Read `reference/quickstart.md` first (2 KB): the commands, a working title card, and what goes wrong most.** For anything it does not answer, ask the docs: `npx pixi-effects help <word>` prints the sections about a word (mask, threeD, keyframes, music …); `npx pixi-effects recipe` lists the tested recipes (a title, a lower third, a chart, kinetic type, a 2.5D scene …) and `npx pixi-effects recipe <id>` prints one with its code. `reference/cheatsheet.md` (35 KB) and `reference/pitfalls.md` (41 KB, numbered) are for looking things up when a warning or the picture surprises you: search them for the word, do not read them end to end. A simple title needs the quick start and one recipe, nothing more.
|
|
10
|
+
You describe a video as a tree of plain objects; the library plays it in a canvas and exports it (MP4/WebM/MOV). You never write per-frame code (except inside a `canvas` layer, which you add only for a picture nothing else draws). **Read `reference/quickstart.md` first (2 KB): the commands, a working title card, and what goes wrong most.** For anything it does not answer, ask the docs: `npx pixi-effects help <word>` prints the sections about a word (mask, threeD, keyframes, music …); `npx pixi-effects recipe` lists the tested recipes (a title, a lower third, a chart, kinetic type, a 2.5D scene …) and `npx pixi-effects recipe <id>` prints one with its code. `reference/cheatsheet.md` (35 KB) and `reference/pitfalls.md` (41 KB, numbered) are for looking things up when a warning or the picture surprises you: search them for the word, do not read them end to end. A simple title needs the quick start and one recipe, nothing more.
|
|
11
11
|
|
|
12
12
|
## Workflow
|
|
13
13
|
|
|
@@ -18,10 +18,10 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
18
18
|
**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.
|
|
19
19
|
5. **Look at it** — you cannot judge motion from code. The library has the tools built in:
|
|
20
20
|
- `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.
|
|
21
|
-
- `await movie.inspect(frame)` → every layer's canvas bounds and visibility, plus `issues` (text off the canvas, cut off by an edge, empty,
|
|
21
|
+
- `await movie.inspect(frame)` → every layer's canvas bounds and visibility, plus `issues` (text off the canvas, cut off by an edge, empty, overlapping other text, or hidden / struck through by a picture: an opaque shape or image drawn OVER part of it, or a thin line drawn over its body; a shadow, a glow, a ring or a backdrop under it is not reported). Run it at several frames; fix every issue it names.
|
|
22
22
|
- `await movie.snapshot(frame, { as: 'dataURL' })` → one frame, canvas only (no player bar).
|
|
23
23
|
- `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.
|
|
24
|
-
- **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.
|
|
24
|
+
- **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`; with `--out DIR` they land in `DIR/frames/` and `DIR/at.png`, there is no `<name>/` inside DIR); add `--crop X,Y,W,H` (canvas pixels) to get each picture as just that part, enlarged up to 4x, so a 40 px figure, a fine line or a texture can be judged (the contact sheet tiles are too small for that): 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.
|
|
25
25
|
- **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 inspectStops(movie)` (from `pixi-effects/present`) 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 stopImages(movie, { which: 'pages' })` / `exportPDF(movie)` give the pages themselves.
|
|
26
26
|
From a browser tool the result is a `data:` URL string: `agent-browser eval "movie.contactSheet({ count: 6, as: 'dataURL' })" | python3 scripts/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.
|
|
27
27
|
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.
|
|
@@ -47,6 +47,7 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
47
47
|
- **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.
|
|
48
48
|
- **Time:** `speed` (slow motion, backward) and `time` (a curve of the layer's own seconds: freeze, rewind, ramp) on video, audio-file and composition layers; put what you want to rewind in a composition. Children of such a composition are in ITS seconds. `ease: 'cubic-bezier(.4, 0, .2, 1)'` is the CSS curve. See the recipe `rewind-and-replay` and pitfalls 69–73.
|
|
49
49
|
- **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; on a camera put it on `offsetX` / `offsetY` so it rides on the move) `cameraPath()` (a camera flight through `[x, y, z]` points as keyframes; layers it passes: `hideBehindCamera: true`) depth of field (camera `focus` + `aperture`: `{ type: 'camera', initial: { focus: 'title' } }` blurs the other threeD layers by depth; a rack focus is a keyframe on `focus`) `music` (a tune with no file: `{ type: 'audio', music: { bpm, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4' }, { inst: 'bass', notes: 'c2:2 g1:2' }], drums: { from: 8, kick: 'x...x...' } } }`: notes and chords as text, counted in beats; instruments `keys pluck pad bass sub lead bell musicbox`; `musicEnvelope(music)` feeds `react()`), `audioEnvelope()` / `bpmEnvelope()` + `react()` (visuals that follow music: `const env = await audioEnvelope('music.mp3', { frameRate })` before `movie.init`, then `keyframes: react(env, { duration, props: { scale: { base: 1, amount: 0.4, band: 'bass', release: 0.2 } } })`; `beats: true` pulses on the beats; `bpmEnvelope(120, …)` when there is no file), `deck()` + `Presenter` from `pixi-effects/present` (a talk as one movie: pages, steps, stops that pause playback, arrow keys / click / swipe to move on: `...deck({ transition: { kind: 'slide', duration: 0.6 }, pages: [{ name, duration, stops, sequences }] })`, `new Presenter(movie, { canvas })` from `pixi-effects/presenter`; G = page overview, P = presenter view with notes and a timer; `pixi-effects render talk.html -o talk.pdf` = a PDF of the pages), `particles()` (bursts, snow, confetti, sparks as seeded layers: `...particles({ count: 90, at: 2, life: [1, 1.8], area: { x: 640, y: 330 }, speed: [120, 420], gravity: 240, fade: { out: 0.6 } })`), `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 with a JS loop):** other per-frame curves (waves, spirals: sample them into short linear keyframes). (Draw-on strokes `trimEnd`, text that changes over time `set: { text }` / `visibleChars`, shake `wiggle()` and particles are built in.)
|
|
50
|
+
- **Draw it yourself (`canvas` layer):** when nothing in the library draws what you need (an afterimage, rings, a brush stroke, a pixel sprite, a chart), `{ type: 'canvas', width, height, draw: (ctx, t, info) => { … } }`: your function of the time draws into the layer's own cleared 2D canvas and it is a normal layer. Pure function of `t` and `info.params` (`info.random()` is seeded; no `Math.random` / `Date.now`); a simulation is computed in `setup` and read by `info.frame`. `info.time` is the movie's clock (several layers share it; never add `at` by hand), `info.canvas(w, h)` makes sprites and masks in `setup`, data shared by several canvas layers lives in module scope, and `import { ease, noise, noise2, random, samplePath } from 'pixi-effects'` gives the library's easing, seeded noise and a path walked by length. `check` prints a `canvas` line (deterministic, slowest draw in ms) and does NOT see text drawn in a canvas. `help canvas`, recipes `canvas-trail`, `canvas-life`, `canvas-text-mask`, `canvas-text-on-path`, `canvas-bars`.
|
|
50
51
|
- **Shader layer and warp:** `{ type: 'shader', fragment: 'void mainImage(out vec4 fragColor, in vec2 fragCoord) { … }', uniforms: { speed: 1, tint: '#ff8040' } }` draws a Shadertoy-style formula (GLSL ES 3.00; `iTime` `iResolution` `iFrame` are declared; keyframes move `'uniforms.speed'`; opaque by default; it cannot read the layers behind it; a compile error comes back in your own line numbers). `filters: [{ type: 'warp', kind: 'wave' | 'haze', strength: 6 }]` bends a layer's picture like water or heat.
|
|
51
52
|
- **Light, shadow, fog (2.5D):** a `{ type: 'light', kind: 'ambient', initial: { intensity: 0.2 } }` plus a `spot` / `point` / `parallel` light (`castsShadows: true` on the light AND on the `threeD` layers that cast) shades every `threeD` layer by angle and distance; `lit: false` opts a layer out; camera `fogNear` / `fogFar` / `fogColor` fade what is far. What no light reaches is black (add the ambient light); layers are drawn in distance order (no depth buffer). `check --at` prints how lit each layer is.
|
|
52
53
|
- **Blend modes:** `blendMode` takes 18 CSS names (`add` `screen` `multiply` `overlay` `soft-light` `hard-light` `color-dodge` `color-burn` `darken` `lighten` `difference` `exclusion` `hue` `saturation` `color` `luminosity` `linear-burn`, hyphenated) on any layer: glows and light leaks (`add`, `color-dodge`), colour casts and contrast (`soft-light`, `overlay`), vignettes (`multiply`). It blends with everything below the layer; on a composition each child blends (as one picture: `threeD: true` or a filter); 10+ of the new modes on screen at once warn; it works with every mask.
|
|
@@ -10,9 +10,10 @@ A video is **data**: a tree of plain objects. Everything below is a JS object yo
|
|
|
10
10
|
| Angles | **degrees**: `rotation`, `skew*`, `rotationX`, `rotationY` |
|
|
11
11
|
| Depth (`z`) | `+z` toward the viewer (bigger, nearer) |
|
|
12
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()`. **Relative values** work in a keyframe's `to` / `from` / `set` (not in `initial`, a style, or the start of a `from`+`to`): `'+=36'` / `'-=36'` are measured from where the layer stands, and the right side is an expression (`'-=GW*0.1'`). A slide-in is `from: { x: '-=36' }`, a nudge `to: { y: '+=40' }`; keyframes one after another stack; with `repeat` the same move replays from the same start (it does not stack). Expressions work in layer `x y` and the other numeric props, **not** in `line.from / to`, `polygon.points` or `path.d`: compute those in JS (`const W = 1920`) |
|
|
13
|
-
| Colours | `'#rrggbb'` strings or numbers; text colour is `style.fill
|
|
13
|
+
| Colours | `'#rrggbb'` strings or numbers; text colour is `style.fill` (or `initial: { fill }`; a `fill` keyframe animates it), 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) |
|
|
16
|
+
| `from` before its `at` | a keyframe's `from` holds **from the start of the layer**, not from its `at` (so a title waits invisible with `from: { alpha: 0 }`), and it replaces `initial`: `initial: { alpha: 0 }` with `{ at: 2, from: { alpha: 1 }, … }` shows the layer at alpha 1 from 0 s. To appear only at 2 s give the layer `at: 2`, or start from the same value in both |
|
|
16
17
|
| Time of a keyframe's `at` | seconds from the start of **its own layer**; **negative = back from the layer's end** |
|
|
17
18
|
| Time of a transition's `at` | seconds from the start of the **parent composition**; negative = back from its end |
|
|
18
19
|
| `duration` | defaults to the parent composition's duration |
|
|
@@ -50,9 +51,9 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
|
|
|
50
51
|
|
|
51
52
|
## Layer types
|
|
52
53
|
|
|
53
|
-
**text** — `text`, `style` (any PixiJS TextStyle field: `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding`; expressions OK for numbers), `colorSpace`. Default anchor is **top-left**; use `anchorX/anchorY: 0.5` to centre on `x,y`. A text layer has one animatable number, `value`, printed where the text contains `{value}` (`text: '{value} users'`, `initial: { value: 0 }`, keyframe `to: { value: 2480 }`; `format: { decimals, grouping, pad /* zero-pad the whole part: pad 2 → 05 */ }`) — that is how counters work. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change). **Colour inside one line:** `spans: [{ text: 'type', fill: '#7fa07f' }, ': ', { text: "'a'", fill: '#ffd166', fontWeight: '700' }, ',\n next line']` instead of `text` (strings use the layer's `style`; a span changes only fill / fontWeight / fontStyle / fontVariant / letterSpacing / dropShadow, never a size, font or stroke (those are the layer's `style`); `\n` starts a line; `style.align: 'left'` for code; `visibleChars` types over all the spans; it becomes a group of text layers `<name>.0…`).
|
|
54
|
+
**text** — `text`, `resolution` (1–8: draw at N× the pixels so text inside a parent scaled up N× stays sharp), `style` (any PixiJS TextStyle field: `fontSize fontFamily fontWeight fill letterSpacing lineHeight align wordWrap wordWrapWidth stroke dropShadow padding`; expressions OK for numbers), `colorSpace`. Default anchor is **top-left**; use `anchorX/anchorY: 0.5` to centre on `x,y`. A text layer has one animatable number, `value`, printed where the text contains `{value}` (`text: '{value} users'`, `initial: { value: 0 }`, keyframe `to: { value: 2480 }`; `format: { decimals, grouping, pad /* zero-pad the whole part: pad 2 → 05 */, time: 'hh:mm:ss'|'h:mm:ss'|'hh:mm'|'mm:ss'|'m:ss' /* the value in SECONDS as a clock */ }`) — that is how counters work. **Changing text:** a keyframe `set: { text: 'Two' }` swaps the string at that time (undone on seek back; `to` / `from` cannot tween a string); `visibleChars` (number, first N characters; starts at 0 once animated) is a typewriter: `to: { visibleChars: 30 }, duration: 2.4, ease: 'none'` — use `anchorX: 0` + `style.align: 'left'` so it grows rightward. Multi-line text is centre-aligned by default (`style.align: 'left'` to change). **Colour inside one line:** `spans: [{ text: 'type', fill: '#7fa07f' }, ': ', { text: "'a'", fill: '#ffd166', fontWeight: '700' }, ',\n next line']` instead of `text` (strings use the layer's `style`; a span changes only fill / fontWeight / fontStyle / fontVariant / letterSpacing / dropShadow, never a size, font or stroke (those are the layer's `style`); `\n` starts a line; `style.align: 'left'` for code; `visibleChars` types over all the spans; it becomes a group of text layers `<name>.0…`).
|
|
54
55
|
|
|
55
|
-
**image** — `asset`, `tint`, `colorSpace: 'rgb'|'oklab'|'oklch'
|
|
56
|
+
**image** — `asset`, `tint`, `colorSpace: 'rgb'|'oklab'|'oklch'`, `scaleMode: 'nearest'` (hard square pixels when scaled up: pixel art; default `'linear'`, smooth; it belongs to the asset). Default anchor top-left; natural size = `w`,`h`.
|
|
56
57
|
|
|
57
58
|
**video** — `asset`, `loop`, `audio`, `volume`, **`speed`**; `initial: { scale: 'cover' }`. `speed: 0.5` slow motion, `speed: -1` backward (it starts at |speed| × `duration`, so a 3 s file with no `duration` plays whole, backward); with no `duration` the layer lasts the file's length ÷ |speed|. For anything that is not one straight speed — a freeze, a ramp, a loop — animate **`time`** (seconds in the file) like any property: `keyframes: [{ at: 0, from: { time: 0 }, to: { time: 2 }, duration: 1 }, { at: 1, to: { time: 2 }, duration: 0.5 }, { at: 1.5, to: { time: 6 }, duration: 3, ease: 'power2.inOut' }]` (play 2 s, hold, then accelerate). `repeat` loops it, `yoyo` ping-pongs. `speed` itself is not animatable (it warns): animate `time`. The sound follows (pitch too, like a tape).
|
|
58
59
|
|
|
@@ -70,14 +71,16 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
|
|
|
70
71
|
| 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 |
|
|
71
72
|
| 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` |
|
|
72
73
|
| polygon | `points: [[x,y],…] open` — canvas coordinates like `line` |
|
|
73
|
-
| path | `d` (SVG path data) — canvas coordinates like `line
|
|
74
|
+
| path | `d` (SVG path data) — canvas coordinates like `line`; leave `x` / `y` out and it sits where `d` says; give them (even `y: 0`) and they place its MIDDLE, not an offset (measured: `y: 0` put a square's centre on the top edge). **Holes:** several sub-paths are each filled alone (a donut written as two circles is a disc) unless `fillRule: 'evenodd'`: the biggest is the shape, the others are holes. **Morph:** `morphTo: 'M…Z'` + animate `morph` 0 → 1 (points half way between, closed outlines lined up so they do not twist; `morphPoints` 8–2048; two outlines with the same number of corners are paired corner to corner, so a quad stays a sharp quad) |
|
|
74
75
|
|
|
75
|
-
**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. A `path` with several sub-paths walks them in order as one length; `trimEach: true` trims each on its own (all draw on together). Use `strokeCap: 'round'`. **Dashes:** there is no dash option; write the outline as alternating sub-paths (`d: 'M 0 0 L 10 0 M 20 0 L 30 0 …'`, or sample the curve) and `trimEnd` 0 → 1 walks them in order, so the dashes draw on one after another (as in `
|
|
76
|
+
**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. A `path` with several sub-paths walks them in order as one length; `trimEach: true` trims each on its own (all draw on together). Use `strokeCap: 'round'`. **Dashes:** there is no dash option; write the outline as alternating sub-paths (`d: 'M 0 0 L 10 0 M 20 0 L 30 0 …'`, or sample the curve) and `trimEnd` 0 → 1 walks them in order, so the dashes draw on one after another (as in `route-atlas`).
|
|
76
77
|
|
|
77
78
|
Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha strokeWidth`; `colorSpace: 'oklab'|'oklch'` for clean colour tweens. rect/circle/ellipse are **centred on `x,y` by default** (`anchorX/anchorY` default 0.5): for a bar growing from its base use `anchorY: 1` (or `anchorX: 0` for left-to-right). **`fillGradient`** (top level or `initial`; instead of `fillColor`): `{ type?: 'linear'|'radial', stops: [[0, '#000'], [1, 'rgba(0,0,0,.6)']], angle? /* linear, deg; DEFAULT 90 = top→bottom, 0 = left→right: a wide gradient slid sideways as a matte needs angle: 0 */, center?, innerRadius?, radius? /* radial, 0–1 of the SHAPE's own bounds, not the canvas: a glow on a 1500 px circle is placed with fractions of that circle */ }` — stops may have alpha, so a radial transparent→dark is a vignette. **Animate it** with a partial gradient in a keyframe (`to: { fillGradient: { angle: 360, stops: [...] } }`: keys you leave out stay; the same number of stops; `type` cannot change; a start gradient is required; use `colorSpace: 'oklch'` for a vivid fade). A text layer takes `fillGradient` too (LINEAR only: angle and stops; type / center / radius are ignored with a warning; letters re-rasterised per change). `from` alone runs from that gradient to the layer's own. The names `'fillGradient.angle'` / `gradientAngle` are not properties: write `fillGradient: { angle }`. Stroke ends and corners: `strokeCap: 'butt'|'round'|'square'`, `strokeJoin: 'miter'|'round'|'bevel'` (top level or in `initial`; not animated — `round` keeps sharp corners like an M from poking out).
|
|
78
79
|
|
|
79
80
|
**composition** — `width height duration sequences transitions`, **`speed`** (the composition's own time: its content plays backward, in slow motion, … — `time` keyframes give a freeze, a rewind, a ramp; a composition's sound and sfx follow; its own x / alpha keyframes stay in the outer time; **children are placed in the composition's local seconds**; the content is as long as the larger of `duration` and the farthest `time` it reaches (`speed: 2` → twice `duration`; `time` keyframes → their largest number), and children default to that length. A child whose life ends exactly where a held `time` stands is hidden during the hold (a life is [at, at + duration)): give it a `duration` that reaches past. `time` keyframes and ordinary ones (`x`, `alpha`, a `wiggle`) may share one `keyframes` list. A sound inside goes silent while the clock stands still). 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.
|
|
80
81
|
|
|
82
|
+
**canvas** — `{ type: 'canvas', width, height, draw: (ctx, t, info) => { … }, setup?, params?, seed?, resolution?, smoothing? }`: a picture drawn by YOUR function of the time into the layer's own cleared canvas (`t` = the layer's seconds; `info` = `{ frame, duration, progress, width, height, resolution, frameRate, params, state, random, start, time, canvas }`: `time` is the MOVIE's seconds (`start + t`; share it between layers instead of adding `at` by hand), `canvas(w, h)` an offscreen canvas for sprites and masks made in `setup`). It is a normal layer (initial, keyframes, mask, filters, blendMode …); `params` are numbers keyframes animate as `'params.x'`; no `Math.random` / `Date.now` (`info.random()` is seeded); a simulation is computed in `setup` and read by `info.frame`; it cannot see what is below it. Data several canvas layers share lives in module scope; `import { ease, noise, noise2, random, rand, samplePath } from 'pixi-effects'` for easing by keyframe name, seeded noise and a path walked by length (`samplePath(d).at(u)` / `.trim(a, b)`). `check` draws it twice per moment (one draw thrown away first) and reports a picture that differs, with how much; it prints a `canvas` line when all pass and does NOT see text drawn in a canvas.
|
|
83
|
+
|
|
81
84
|
**shader** — `{ type: 'shader', fragment: 'void mainImage(out vec4 fragColor, in vec2 fragCoord) { … }', uniforms: { speed: 1, tint: '#ff8040', pos: [0.5, 0.5] }, width?, height?, resolution?: 0.5, transparent?: false }` — a Shadertoy fragment shader drawn into a layer (GLSL ES 3.00): `iResolution` `iTime` (the layer's own seconds) `iFrame` and your uniforms are declared for you; keyframes move uniforms as `'uniforms.speed'` and components as `'uniforms.tint.0'`; opaque unless `transparent: true`; it cannot read what is behind it (no `iChannel` / `iMouse` / `iDate`); no `while` or non-constant `for`; a compile error is reported in your own line numbers and the layer is a checkerboard.
|
|
82
85
|
|
|
83
86
|
**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.
|
|
@@ -155,7 +158,7 @@ measureText('Hello', style) // → { width, height,
|
|
|
155
158
|
splitText('Hello world', style, { by: 'chars' | 'words' | 'lines', x, y, align: 'left' | 'center' | 'right' })
|
|
156
159
|
// → [{ text, x, y, width, index, line }] — one text layer per piece, same style, anchor top-left
|
|
157
160
|
```
|
|
158
|
-
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, in the same wave (leave `out` out and the pieces stay until the layer ends); by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead>, 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`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
|
|
161
|
+
Per-letter / per-word / per-line animation in one call: `...animateText(text, style, { x, y, align, vertical: true /* tategaki: down a column, new line = new column to the left, x = RIGHT edge of the first column; 、。 ー 「」 placed and turned for you */, 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, in the same wave (leave `out` out and the pieces stay until the layer ends); by default BACKWARDS FROM THE LAYER'S END (at + duration, so to be gone at time T set duration = T − at); `out: { preset: 'fade', at: 6 }` starts the exit at composition second 6 instead>, 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`. **`x` / `y` are pixel NUMBERS (an expression like `'GW/2'` throws: compute `W / 2`), and `y` is the TOP of the line box**: the visible text is centred `lineHeight / 2` below it, so an underline goes at `y + lineHeight`, not at `y` (recipe `animate-text`). By hand: `splitText(...)` + one layer per piece (recipe `split-text`). A web font must be loaded before measuring.
|
|
159
162
|
|
|
160
163
|
## Assets
|
|
161
164
|
|
|
@@ -139,3 +139,10 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
139
139
|
109. **No font is bundled, and each script needs a font that has its characters.** The machine's own fonts are used; characters a font lacks are drawn with whatever the machine falls back to, so the same page can look different on another machine. Load a web font that covers the script (Noto Sans JP, Noto Sans KR for Hangul, Noto Sans SC) with `await document.fonts.load('40px "Noto Sans KR"')` before `movie.init`, and say it in `style.fontFamily` (pitfall 103). `check` lists a layer none of whose fonts is available, but a family that nothing installs or loads is silently replaced by the next one in the list (or the generic name) without a word: the first name in `fontFamily` is not proof it was used.
|
|
140
140
|
110. **Right-to-left text: three traps.** `splitText` / `animateText` order words left to right, so Arabic or Hebrew comes out reversed (split by words yourself, from the right edge, with `measureText` of the logical prefix); a mark below the font box can be clipped (the dots of an Arabic ي) unless the layer has `style.padding`; `check` sees neither. `bulgePinch.center` is 0–1 of the layer, not pixels (pitfalls elsewhere say canvas pixels for the other centres: `twist`, `zoomBlur`, `shockwave`). `npx pixi-effects help arabic` and `help bulgePinch` print the sections.
|
|
141
141
|
111. **Only some named filters are tested on both backends.** `glow`, `outline`, `dropShadow`, `bevel`, `bloom`, `pixelate`, `crt`, `rgbSplit`, `dot`, `radialBlur` and `glitch` look the same on WebGPU (the default) and WebGL; `multiColorReplace` draws everything black on WebGPU, `kawaseBlur` is far weaker and `advancedBloom` sharper there: use `colorReplace`, `blur`, `bloom`. A filter follows the edges of the layer's own picture: `glow` on a full-screen opaque layer shows nothing (put it on the shapes and the text).
|
|
142
|
+
112. **Text inside a parent that is scaled up comes out soft.** Text is drawn once at its own size and then stretched, so `{ type: 'null', initial: { scale: 3 } }` with a text child of `fontSize: 30` is blurry (shapes stay sharp). Give the text `resolution: 3` (it is drawn at 3 times the pixels, for a parent that grows to 3x; 1 to 8, the cost is the memory), or write it at the size it is shown (`fontSize: 90`, parent scale 1), or let the parent only scale DOWN (a pull-back from big to normal is sharp the whole way). `check --at … --crop` shows the difference.
|
|
143
|
+
113. **Two limits on sound that `help sfx` / `help music` did not say.** An `sfx` lasts at most 10 s (`MAX_LENGTH` in the synth): for a longer rain or room tone, chain two audio layers that overlap by a moment, or use `loop` on a short one. A `music` layer plays at most 16 tracks, and a warning says that only the first 16 play: put the rest in a second audio layer.
|
|
144
|
+
114. **A keyframe's `from` is on screen from the start of the layer, and it beats `initial`.** `initial: { alpha: 0 }` plus `{ at: 2, from: { alpha: 1 }, to: { alpha: 0.5 }, duration: 1 }` shows the layer at full alpha from second 0 (measured in Chrome: the red square was there at 0.5 s). That is how a title waits invisible with `from: { alpha: 0 }` until its `at`, but it surprises when `from` is the visible end. Give the layer its own `at: 2`, or use `set` at the moment it should appear. No warning: the same pattern (`initial` = the resting value, `from` = where it starts) is what the recipes do everywhere.
|
|
145
|
+
115. **A `canvas` layer is a function of the time, nothing more.** The canvas is cleared before every call and nothing survives into the next, so `x += 1` per call, "draw over the last picture" and `Math.random()` all break: a seek or an export would not match playback (`check` draws each canvas layer twice at a few moments and reports a layer whose pictures differ). Compute from `t` (a trail is the positions of the past), use `info.random()` (seeded) and `params` that keyframes animate, and compute a whole simulation in `setup` (an array by frame) and read `info.frame`. A cache of a pure function is fine (same input, same output); state that depends on which frames were drawn before is not. A canvas layer cannot see or paint on what is below it: use `filters`, a matte or a composition for that. Pixel art needs `smoothing: false` (hard pixels when scaled up) and a small `width` / `height` with a big `initial.scale`.
|
|
146
|
+
116. **Several canvas layers that need the same thing: module scope and `info.time`, not `setup` and a hand-added `at`.** The state a layer's `setup` returns is private to that layer. A plant, a city or a palette that two layers draw is computed once before `movie.init` (from `random(seed)` and plain functions) and read by both; the shared clock is `info.time` (the movie's seconds), not `t + 3` with the layer's `at` copied in, which goes wrong without a word when someone moves the layer.
|
|
147
|
+
117. **A sprite or mask made in `setup` with `document.createElement('canvas')` can be drawn differently the first time.** A GPU canvas used as the source of `drawImage`, under `destination-in` after many anti-aliased strokes, drew up to 128 of 255 differently over 40 000 pixels of a 1280×720 layer the first time and the same every time after. `info.canvas(w, h)` makes a canvas that does not do this; `check` throws away one draw of each moment before it compares, so it does not blame the layer, but an export that draws a frame for the first time is still safer with `info.canvas`. A mask is also easier to get right as: the words first, then `globalCompositeOperation = 'source-atop'` and the texture (recipe `canvas-text-mask`); two `fillText` under `destination-in` intersect.
|
|
148
|
+
118. **`check` does not see text drawn in a canvas.** The layout test (cut-off, off-canvas, overlap) reads the text layers; a label or a caption you draw with `fillText` can run off the edge or collide with another without a word, and "no cut-off text" only speaks for the text layers. Look at the sheet and at `--at … --crop`, and keep a margin.
|
|
@@ -145,6 +145,229 @@ return [
|
|
|
145
145
|
|
|
146
146
|
---
|
|
147
147
|
|
|
148
|
+
## Push in on a whole flat scene (the 2D "camera"): a null that scales, and text drawn big enough
|
|
149
|
+
|
|
150
|
+
There is no second camera for flat scenes: the camera of the 2.5D layers is for `threeD` layers. For a push-in or pull-back of an ordinary 2D scene, put everything in a `null` layer that sits at the middle of the canvas and scale THAT (children use `parent: 'world'`, and their x / y are measured from the null's origin, so the middle of the canvas is 0, 0). Text is drawn once at its own size and then stretched, so give every text `resolution: R` where R is the biggest scale the group reaches (R 1–8); otherwise it goes soft.
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
// @recipe scene-zoom
|
|
154
|
+
const R = 2; // the group grows to 2x: its text is drawn at 2x pixels
|
|
155
|
+
const inWorld = (spec) => ({ parent: 'world', ...spec });
|
|
156
|
+
return [
|
|
157
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0f1424' } },
|
|
158
|
+
{ type: 'null', name: 'world', initial: { x: 'GW/2', y: 'GH/2' },
|
|
159
|
+
keyframes: [{ at: 0.5, from: { scale: 1 }, to: { scale: R }, duration: 3.5, ease: 'power2.inOut' }] },
|
|
160
|
+
inWorld({ type: 'shape', shape: 'circle', radius: 120, initial: { x: 0, y: 0, fillColor: '#3b5bdb' } }),
|
|
161
|
+
inWorld({ type: 'shape', shape: 'circle', radius: 40, initial: { x: 260, y: -120, fillColor: '#ffd166' } }),
|
|
162
|
+
inWorld({ type: 'text', text: 'Zoom in', resolution: R, style: { fontSize: 48, fontWeight: 'bold', fill: '#ffffff' }, initial: { x: 0, y: 0, anchorX: 0.5, anchorY: 0.5 } }),
|
|
163
|
+
inWorld({ type: 'text', text: 'sharp at 2x', resolution: R, style: { fontSize: 22, fill: '#c9d3f5' }, initial: { x: 0, y: 150, anchorX: 0.5, anchorY: 0.5 } }),
|
|
164
|
+
];
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Parallax: layers that scroll past at their own speed (a window of a train, a side-scroller)
|
|
170
|
+
|
|
171
|
+
Each layer is a long strip of repeated shapes that moves left at a constant speed, `speed × duration` pixels in all, so it must be that long plus the width of the screen. The speed is the depth: the near strip fast, the far one slow. A strip that is longer than it needs to be is only more layers.
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
// @recipe parallax-strips
|
|
175
|
+
const D = 8, W = 1280;
|
|
176
|
+
// one strip: `count` shapes `gap` px apart, scrolling left at `speed` px/s from x = 0 for D seconds
|
|
177
|
+
const strip = (name, speed, gap, y, size, color) => {
|
|
178
|
+
const count = Math.ceil((speed * D + W) / gap) + 1;
|
|
179
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
180
|
+
type: 'shape', shape: 'rect', name: `${name}-${i}`, width: size, height: size * 2,
|
|
181
|
+
initial: { x: i * gap, y, fillColor: color },
|
|
182
|
+
keyframes: [{ at: 0, to: { x: i * gap - speed * D }, duration: D, ease: 'none' }],
|
|
183
|
+
}));
|
|
184
|
+
};
|
|
185
|
+
return [
|
|
186
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#1b2a4a' } },
|
|
187
|
+
...strip('far', 30, 240, 420, 60, '#26365c'), // distant hills: 30 px/s
|
|
188
|
+
...strip('mid', 120, 200, 500, 40, '#314a7a'), // 120 px/s
|
|
189
|
+
...strip('near', 480, 360, 600, 24, '#0b1220'), // poles: 480 px/s
|
|
190
|
+
];
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
To land something exactly where you want at a time `t` (a moon under a bridge at 6 s), compute the strip's x at `t` the same way: `i * gap - speed * t`.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Draw it yourself: the `canvas` layer (an afterimage, rings that grow, a pixel sprite, a simulation)
|
|
198
|
+
|
|
199
|
+
When no layer draws what you need, write the drawing as a **function of the time**: `draw(ctx, t, info)` paints into the layer's own cleared 2D canvas and the layer is a normal layer (`initial`, `keyframes`, `filters`, `mask`, export). Rules: the same `t` and `info.params` give the same pixels (no `Math.random` or `Date.now`: `info.random()` is seeded), nothing survives from the last frame, and a simulation is computed whole in `setup`. `help canvas` has the contract.
|
|
200
|
+
|
|
201
|
+
An **afterimage** is drawn from the positions of the past, not from the last picture:
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
// @recipe canvas-trail
|
|
205
|
+
const at = (t) => ({ x: 640 + 420 * Math.sin(t * 1.7), y: 360 + 200 * Math.sin(t * 2.9) }); // where the dot is at time t
|
|
206
|
+
return [
|
|
207
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#101420' } },
|
|
208
|
+
{ type: 'canvas', name: 'trail', duration: 6, params: { tail: 0.5 },
|
|
209
|
+
draw: (ctx, t, info) => {
|
|
210
|
+
const N = 14;
|
|
211
|
+
for (let i = N; i >= 0; i--) { // the oldest first, the head last
|
|
212
|
+
const p = at(Math.max(0, t - i * info.params.tail / N));
|
|
213
|
+
ctx.globalAlpha = (1 - i / (N + 1)) * 0.85;
|
|
214
|
+
ctx.fillStyle = '#ff4f1a';
|
|
215
|
+
ctx.beginPath(); ctx.arc(p.x, p.y, 34 - i * 1.6, 0, Math.PI * 2); ctx.fill();
|
|
216
|
+
}
|
|
217
|
+
} },
|
|
218
|
+
];
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Rings that grow**, wobbling a little with a seeded `random()`, and `params` that keyframes animate (the `progress` of the layer is `info.progress`):
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
// @recipe canvas-rings
|
|
225
|
+
return [
|
|
226
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#efe9dc' } },
|
|
227
|
+
{ type: 'canvas', name: 'rings', width: 720, height: 720, duration: 8, seed: 3, params: { years: 0 },
|
|
228
|
+
initial: { x: 640, y: 360, anchorX: 0.5, anchorY: 0.5 },
|
|
229
|
+
keyframes: [{ at: 0, to: { 'params.years': 40 }, duration: 8, ease: 'power1.inOut' }],
|
|
230
|
+
draw: (ctx, t, info) => {
|
|
231
|
+
const cx = info.width / 2, cy = info.height / 2, n = Math.floor(info.params.years);
|
|
232
|
+
const wob = Array.from({ length: n + 1 }, () => [info.random() * 6, info.random() * Math.PI * 2]); // the same wobble at every frame
|
|
233
|
+
for (let k = n; k >= 0; k--) {
|
|
234
|
+
const r = 8 + k * 8.5 + (k === n ? (info.params.years - n) * 8.5 : 0);
|
|
235
|
+
ctx.beginPath();
|
|
236
|
+
for (let a = 0; a <= 64; a++) { const th = (a / 64) * Math.PI * 2, rr = r + wob[k][0] * Math.sin(th * 3 + wob[k][1]); ctx.lineTo(cx + rr * Math.cos(th), cy + rr * Math.sin(th)); }
|
|
237
|
+
ctx.closePath();
|
|
238
|
+
ctx.fillStyle = k % 2 ? '#c9a36a' : '#a87c45'; ctx.fill();
|
|
239
|
+
}
|
|
240
|
+
} },
|
|
241
|
+
];
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
A **pixel sprite**: drawn small with `smoothing: false`, shown big (the layer is scaled up and the pixels stay hard):
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
// @recipe canvas-pixel-sprite
|
|
248
|
+
const ROWS = ['..1111..', '.111111.', '11.11.11', '11111111', '.1.11.1.', '..1111..', '.1....1.', '1......1'];
|
|
249
|
+
return [
|
|
250
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#1b1330' } },
|
|
251
|
+
{ type: 'canvas', name: 'sprite', width: 8, height: 8, smoothing: false, duration: 8,
|
|
252
|
+
initial: { x: 640, y: 360, anchorX: 0.5, anchorY: 0.5, scale: 30 },
|
|
253
|
+
keyframes: [{ at: 0, from: { y: 300 }, to: { y: 420 }, duration: 8, ease: 'steps(16)' }], // moves in whole steps
|
|
254
|
+
draw: (ctx, t) => {
|
|
255
|
+
const blink = Math.floor(t * 2) % 2; // two frames of animation
|
|
256
|
+
ROWS.forEach((row, y) => [...row].forEach((c, x) => { if (c === '1') { ctx.fillStyle = (y === 2 && blink) ? '#1b1330' : '#ffd166'; ctx.fillRect(x, y, 1, 1); } }));
|
|
257
|
+
} },
|
|
258
|
+
];
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
A **simulation** (a game of life) is computed WHOLE in `setup`, one array per generation, and `draw` only reads the generation for the time (`info.frame`, or here `t`): a pure function of the time, so a seek or an export agrees with playback:
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
// @recipe canvas-life
|
|
265
|
+
const W = 48, H = 27, GENERATIONS = 90;
|
|
266
|
+
const step = (g) => g.map((row, y) => row.map((v, x) => {
|
|
267
|
+
let n = 0; for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) if (dx || dy) n += g[(y + dy + H) % H][(x + dx + W) % W];
|
|
268
|
+
return n === 3 || (v && n === 2) ? 1 : 0;
|
|
269
|
+
}));
|
|
270
|
+
return [
|
|
271
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0b0f14' } },
|
|
272
|
+
{ type: 'canvas', name: 'life', width: 960, height: 540, duration: 6, seed: 11, initial: { x: 640, y: 360, anchorX: 0.5, anchorY: 0.5 },
|
|
273
|
+
setup: (info) => {
|
|
274
|
+
let g = Array.from({ length: H }, () => Array.from({ length: W }, () => (info.random() < 0.3 ? 1 : 0)));
|
|
275
|
+
const all = [g]; for (let i = 1; i < GENERATIONS; i++) all.push((g = step(g)));
|
|
276
|
+
return all;
|
|
277
|
+
},
|
|
278
|
+
draw: (ctx, t, info) => {
|
|
279
|
+
const g = info.state[Math.min(info.state.length - 1, Math.floor(t * 15))]; // 15 generations a second
|
|
280
|
+
ctx.fillStyle = '#7bd88f';
|
|
281
|
+
g.forEach((row, y) => row.forEach((v, x) => { if (v) ctx.fillRect(x * 20 + 2, y * 20 + 2, 16, 16); }));
|
|
282
|
+
} },
|
|
283
|
+
];
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Type, curves and charts in a canvas layer (type through a moving texture, text along a curve, bars that swap places)
|
|
289
|
+
|
|
290
|
+
The `canvas` layer (see the section above and `help canvas`) is also where text effects the text layer cannot do and live charts are drawn. `info.canvas(w, h)` makes sprites and masks in `setup`, `info.time` is the movie's clock, and `ease`, `noise`, `noise2`, `random`, `samplePath` are importable from `pixi-effects`. `check` does not see text drawn in a canvas: look at the sheet and at `--crop` enlargements.
|
|
291
|
+
|
|
292
|
+
**Type as a window onto something that moves.** Draw the words once into a canvas from `info.canvas` (in `setup`), draw them back first, then paint with `source-atop`: the paint shows only where the words are (two `fillText` under `destination-in` would intersect instead):
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
// @recipe canvas-text-mask
|
|
296
|
+
return [
|
|
297
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0d0a14' } },
|
|
298
|
+
{ type: 'canvas', name: 'window', width: 1100, height: 320, duration: 6, initial: { x: 640, y: 360, anchorX: 0.5, anchorY: 0.5 },
|
|
299
|
+
setup: (info) => { // the words, drawn once into a canvas of their own
|
|
300
|
+
const word = info.canvas(1100, 320), c = word.getContext('2d');
|
|
301
|
+
c.fillStyle = '#fff'; c.font = '900 280px "Arial Black", "Helvetica Neue", sans-serif';
|
|
302
|
+
c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText('LIGHT', 550, 160);
|
|
303
|
+
return { word };
|
|
304
|
+
},
|
|
305
|
+
draw: (ctx, t, info) => {
|
|
306
|
+
ctx.drawImage(info.state.word, 0, 0); // the words first: they are the window …
|
|
307
|
+
ctx.globalCompositeOperation = 'source-atop'; // … and what is drawn next appears only where they are
|
|
308
|
+
ctx.fillStyle = '#2a1030'; ctx.fillRect(0, 0, 1100, 320);
|
|
309
|
+
for (let k = 0; k < 14; k++) { // bands of colour sliding through
|
|
310
|
+
ctx.fillStyle = k % 2 ? '#ff4f1a' : '#ffb347';
|
|
311
|
+
ctx.fillRect(((k * 90 + t * 160) % 1260) - 80, 0, 38, 320);
|
|
312
|
+
}
|
|
313
|
+
} },
|
|
314
|
+
];
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Text along a curve, and a line that draws on.** `samplePath(d)` walks SVG path data by length: `at(u)` is the point and the heading a fraction `u` along, `trim(a, b)` the part between two fractions. `ease('power2.out')` is the same easing a keyframe takes. Build the sampler once, outside `draw`:
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
// @recipe canvas-text-on-path
|
|
321
|
+
const route = samplePath('M 140 560 C 340 120 940 120 1140 560');
|
|
322
|
+
const WORD = 'ALONG THE CURVE', EASE = ease('power2.out');
|
|
323
|
+
return [
|
|
324
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#101420' } },
|
|
325
|
+
{ type: 'canvas', name: 'along', duration: 6,
|
|
326
|
+
draw: (ctx, t) => {
|
|
327
|
+
const run = EASE(Math.min(1, t / 3)); // 0 → 1: the line draws on, then the letters settle
|
|
328
|
+
ctx.strokeStyle = '#ff4f1a'; ctx.lineWidth = 3; ctx.beginPath();
|
|
329
|
+
for (const piece of route.trim(0, run)) { ctx.moveTo(piece[0], piece[1]); for (let i = 2; i < piece.length; i += 2) ctx.lineTo(piece[i], piece[i + 1]); }
|
|
330
|
+
ctx.stroke();
|
|
331
|
+
ctx.font = '700 54px Georgia, serif'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#f4efe6';
|
|
332
|
+
let d = 0; // distance along the route, in px
|
|
333
|
+
for (const ch of WORD) {
|
|
334
|
+
const w = ctx.measureText(ch).width;
|
|
335
|
+
const u = (d + w / 2 + 90) / route.length - (1 - run) * 0.5; // where this letter's middle is, a fraction along
|
|
336
|
+
d += w + 4;
|
|
337
|
+
if (u < 0 || u > 1) continue;
|
|
338
|
+
const p = route.at(u);
|
|
339
|
+
ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.angle); ctx.globalAlpha = run; ctx.fillText(ch, -w / 2, -30); ctx.restore();
|
|
340
|
+
}
|
|
341
|
+
} },
|
|
342
|
+
];
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Bars that swap places smoothly.** A bar's row is its rank averaged over the last third of a second, computed from `t` alone (the values at those earlier moments are a function of time too): when two bars change places they glide past each other, passing through one another for the length of the swap as in any bar race, nothing jumps, and a seek is exact:
|
|
346
|
+
|
|
347
|
+
```js
|
|
348
|
+
// @recipe canvas-bars
|
|
349
|
+
const NAMES = ['Oslo', 'Lima', 'Kyiv', 'Pune', 'Graz'], FROM = [40, 55, 30, 20, 45], TO = [60, 35, 70, 50, 25];
|
|
350
|
+
const BAR = 90, GAP = 28, EASE = ease('power2.inOut');
|
|
351
|
+
const valuesAt = (time) => FROM.map((a, i) => a + (TO[i] - a) * EASE(Math.min(1, Math.max(0, time) / 5)));
|
|
352
|
+
return [
|
|
353
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#101420' } },
|
|
354
|
+
{ type: 'canvas', name: 'race', width: 1000, height: 600, duration: 6, initial: { x: 140, y: 60 },
|
|
355
|
+
draw: (ctx, t) => {
|
|
356
|
+
const now = valuesAt(t), before = Array.from({ length: 8 }, (_, k) => valuesAt(t - k * 0.04)); // the last third of a second
|
|
357
|
+
ctx.font = '700 34px system-ui, sans-serif'; ctx.textBaseline = 'middle';
|
|
358
|
+
now.forEach((value, i) => {
|
|
359
|
+
const rank = before.reduce((sum, v) => sum + v.filter(other => other > v[i]).length, 0) / before.length; // 0 = top
|
|
360
|
+
const y = rank * (BAR + GAP);
|
|
361
|
+
ctx.fillStyle = i === 2 ? '#ff4f1a' : '#4c5a78'; ctx.fillRect(160, y, value * 10, BAR);
|
|
362
|
+
ctx.fillStyle = '#f4efe6'; ctx.textAlign = 'right'; ctx.fillText(NAMES[i], 140, y + BAR / 2);
|
|
363
|
+
ctx.textAlign = 'left'; ctx.fillText(String(Math.round(value)), 160 + value * 10 + 14, y + BAR / 2);
|
|
364
|
+
});
|
|
365
|
+
} },
|
|
366
|
+
];
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
148
371
|
## A ticker that scrolls out completely (`w` = the text's own width)
|
|
149
372
|
|
|
150
373
|
`'-w'` is the exact x at which the text has left the screen on the left (`w` is measured after the style is applied).
|
|
@@ -477,18 +700,18 @@ return sequences;
|
|
|
477
700
|
```js
|
|
478
701
|
// @recipe progress-ring
|
|
479
702
|
const CX = 640, CY = 360, R = 120, TO = 0.82; // 82 % filled
|
|
480
|
-
const
|
|
703
|
+
const EASE = 'power3.out', AT = 0.3, DUR = 2;
|
|
481
704
|
return [
|
|
482
705
|
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0f1424' } },
|
|
483
706
|
{ type: 'shape', shape: 'arc', name: 'track', radius: R, startAngle: 0, endAngle: 360,
|
|
484
707
|
initial: { x: CX, y: CY, strokeColor: '#27304d', strokeWidth: 24 } },
|
|
485
708
|
{ type: 'shape', shape: 'arc', name: 'progress', radius: R, startAngle: -90, endAngle: -90, strokeCap: 'round',
|
|
486
709
|
initial: { x: CX, y: CY, strokeColor: '#ffd166', strokeWidth: 24 },
|
|
487
|
-
keyframes: [{ at: AT, to: { endAngle: -90 + 360 * TO }, duration: DUR, ease }] },
|
|
710
|
+
keyframes: [{ at: AT, to: { endAngle: -90 + 360 * TO }, duration: DUR, ease: EASE }] },
|
|
488
711
|
{ type: 'text', name: 'percent', text: '{value}%', format: { decimals: 0 },
|
|
489
712
|
style: { fontSize: 72, fontWeight: 'bold', fill: '#ffffff' },
|
|
490
713
|
initial: { x: CX, y: CY, anchorX: 0.5, anchorY: 0.5, value: 0 },
|
|
491
|
-
keyframes: [{ at: AT, to: { value: TO * 100 }, duration: DUR, ease }] },
|
|
714
|
+
keyframes: [{ at: AT, to: { value: TO * 100 }, duration: DUR, ease: EASE }] },
|
|
492
715
|
];
|
|
493
716
|
```
|
|
494
717
|
|
|
@@ -596,6 +819,17 @@ return [
|
|
|
596
819
|
];
|
|
597
820
|
```
|
|
598
821
|
|
|
822
|
+
**Vertical Japanese (tategaki)** is one option: `vertical: true`. The characters go down a column and a new line is a new column to the LEFT, so `x` is the RIGHT edge of the first column. The commas sit at the upper right of their cell, and the long vowel mark and the brackets are turned a quarter, as in print.
|
|
823
|
+
|
|
824
|
+
```js
|
|
825
|
+
// @recipe vertical-text
|
|
826
|
+
const style = { fontFamily: 'Hiragino Mincho ProN, Yu Mincho, serif', fontSize: 56, fill: '#1c1a17' };
|
|
827
|
+
return [
|
|
828
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#efe9dc' } },
|
|
829
|
+
...animateText('雨のあと、\nコーヒーは「ゆっくり」', style, { x: 900, y: 70, vertical: true, at: 0.3, duration: 5, in: 'fade', stagger: { each: 0.04 } }),
|
|
830
|
+
];
|
|
831
|
+
```
|
|
832
|
+
|
|
599
833
|
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.
|
|
600
834
|
|
|
601
835
|
---
|
|
@@ -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.37.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 */
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
"imports": {
|
|
38
38
|
"pixi.js": "https://esm.sh/pixi.js@8.22.0?bundle-deps",
|
|
39
39
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
40
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
41
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
42
|
-
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
43
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
40
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.37.0/dist/index.js",
|
|
41
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.37.0/dist/Controller.js",
|
|
42
|
+
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.37.0/dist/present.js",
|
|
43
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.37.0/dist/three.js",
|
|
44
44
|
"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
|
|
45
45
|
"three": "https://esm.sh/three@0.178.0"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
</script>
|
|
49
|
-
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
49
|
+
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.37.0/dist/index.js" /> <!-- after the import map: the library downloads while the page is parsed -->
|
|
50
50
|
|
|
51
51
|
<script type="module">
|
|
52
52
|
import { Movie, kenBurns, withFade } from 'pixi-effects';
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { CompositionSequence } from './chunk-XV4L72FW.js';
|
|
2
|
-
import './chunk-Y253JQOH.js';
|
|
3
|
-
import './chunk-RTJD3ZSW.js';
|
|
4
|
-
import './chunk-ITDYFBB6.js';
|
|
5
|
-
import './chunk-BXOCEZ4U.js';
|
|
6
|
-
//# sourceMappingURL=Composition-NDXKQHB4.js.map
|
|
7
|
-
//# sourceMappingURL=Composition-NDXKQHB4.js.map
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var chunk5IJENZOG_cjs = require('./chunk-5IJENZOG.cjs');
|
|
4
|
-
require('./chunk-LEECUCTG.cjs');
|
|
5
|
-
require('./chunk-IMQJVRMT.cjs');
|
|
6
|
-
require('./chunk-GL2AQFLQ.cjs');
|
|
7
|
-
require('./chunk-TWZNOUM7.cjs');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Object.defineProperty(exports, "CompositionSequence", {
|
|
12
|
-
enumerable: true,
|
|
13
|
-
get: function () { return chunk5IJENZOG_cjs.CompositionSequence; }
|
|
14
|
-
});
|
|
15
|
-
//# sourceMappingURL=Composition-OLH7RC5Z.cjs.map
|
|
16
|
-
//# sourceMappingURL=Composition-OLH7RC5Z.cjs.map
|