pixi-effects 0.14.0 → 0.16.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 +14 -0
- package/README.md +13 -6
- package/ai/CHAT.md +103 -0
- package/ai/SKILL.md +2 -1
- package/ai/chat-template.html +113 -0
- package/ai/reference/cheatsheet.md +2 -1
- package/ai/reference/pitfalls.md +6 -0
- package/ai/reference/recipes.md +25 -0
- package/ai/template.html +5 -5
- package/dist/{Base-CQOlB2VH.d.ts → Base-DPEqyeB0.d.cts} +1 -1
- package/dist/{Base-C6ZnJ9pD.d.cts → Base-qvqNxvyt.d.ts} +1 -1
- package/dist/Composition-X2VZSVPD.js +5 -0
- package/dist/{Composition-HIISJAX2.js.map → Composition-X2VZSVPD.js.map} +1 -1
- package/dist/Composition-ZZGSMQ4E.cjs +14 -0
- package/dist/{Composition-7TEKHZNQ.cjs.map → Composition-ZZGSMQ4E.cjs.map} +1 -1
- package/dist/Controller.d.cts +2 -2
- package/dist/Controller.d.ts +2 -2
- package/dist/{Movie-BwT6XtGz.d.ts → Movie-BIGDIla0.d.ts} +1 -1
- package/dist/{Movie-qmgOU4in.d.cts → Movie-CJMfAizW.d.cts} +1 -1
- package/dist/Presenter.d.cts +2 -2
- package/dist/Presenter.d.ts +2 -2
- package/dist/{chunk-C6QHKYC5.js → chunk-7D7RAQWM.js} +29 -4
- package/dist/chunk-7D7RAQWM.js.map +1 -0
- package/dist/{chunk-3IPDNC3E.js → chunk-I2YXLHIU.js} +3 -3
- package/dist/{chunk-3IPDNC3E.js.map → chunk-I2YXLHIU.js.map} +1 -1
- package/dist/{chunk-6NWJLVCA.cjs → chunk-SPPNTV7E.cjs} +29 -4
- package/dist/chunk-SPPNTV7E.cjs.map +1 -0
- package/dist/{chunk-F7NCHS6Q.cjs → chunk-V54NBCWG.cjs} +26 -26
- package/dist/{chunk-F7NCHS6Q.cjs.map → chunk-V54NBCWG.cjs.map} +1 -1
- package/dist/index.cjs +40 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -5
- package/dist/index.d.ts +18 -5
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/loader.css +1 -1
- package/dist/music-3OF5C6X5.cjs +826 -0
- package/dist/music-3OF5C6X5.cjs.map +1 -0
- package/dist/music-4HDFULNR.js +809 -0
- package/dist/music-4HDFULNR.js.map +1 -0
- 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-CBSiUKau.d.cts → types-C8XEUoqe.d.cts} +76 -2
- package/dist/{types-CBSiUKau.d.ts → types-C8XEUoqe.d.ts} +76 -2
- package/docs/api.md +10 -0
- package/docs/dsl.md +39 -0
- package/llms-full.txt +83 -2
- package/llms.txt +3 -2
- package/package.json +1 -1
- package/dist/Composition-7TEKHZNQ.cjs +0 -14
- package/dist/Composition-HIISJAX2.js +0 -5
- package/dist/chunk-6NWJLVCA.cjs.map +0 -1
- package/dist/chunk-C6QHKYC5.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0
|
|
4
|
+
|
|
5
|
+
**Added**
|
|
6
|
+
|
|
7
|
+
- **Music written as text.** `{ type: 'audio', music: { bpm, tracks, drums } }` plays a tune with no audio file: notes (`c4:2`), chords by name (`Am7`, `Cmaj7`, `G7sus4`, `Bm7b5`, `C@4`, `Am7/e`), rests, holds and accents as strings, counted in beats, played by a small built-in synthesiser. Eight instruments (`keys` electric piano, `pluck`, `pad`, `bass`, `sub`, `lead`, `bell`, `musicbox`) and ten drums (`kick snare hat openhat clap rim tom crash shaker sleigh`) as step patterns, with `swing`, `reverb`, a `grid` for 6/8 and triplet feels, `humanize`, `transpose`, per-track `vol` points (a fade or a swell), `pan`, `tone`, `reverb` send, `attack` / `release` / `ring`, drum sections (`from` / `to`), and a tempo that changes (`bpm: [[0, 96], [28, 96], [32, 60]]`, a ritardando). Deterministic, in the exported file at the same moment, its own chunk (movies without music never load it). It lasts its notes plus a tail and ends with the movie; `loop: true` repeats it; at `volume: 1` it peaks at −6 dBFS. Mistakes warn with "did you mean" (an instrument, a drum, an option, a note it cannot read names the track and the token). The sound was approved by ear from a spike: eight tunes written by AI sessions from the notation alone (lo-fi, a heroic build, a jingle, a dark opening, three Christmas pieces).
|
|
8
|
+
- **`musicEnvelope(music, { frameRate })`**: the envelope of that music for `react()`: `level` / `bass` / `mid` / `treble` from analysing the very sound the layer plays, exact beats (the kick hits, or every beat) and the starting tempo.
|
|
9
|
+
- A gallery piece with a score and no audio file, `christmas-eve` (38 pieces), guide section "Music written as text" in Audio, a recipe, `ai/CHAT.md` section with a tested example, and DSL / API / cheatsheet / pitfalls entries.
|
|
10
|
+
|
|
11
|
+
## 0.15.0
|
|
12
|
+
|
|
13
|
+
**Added**
|
|
14
|
+
|
|
15
|
+
- **A way in for an AI in a browser chat (ChatGPT, Claude) with no shell and no files.** `ai/CHAT.md` is a short guide (the rules that prevent most failures and a worked example) and `ai/chat-template.html` a self-contained starter page that loads only from `cdn.jsdelivr.net` (so it also runs in a sandboxed preview), shows every warning in a red box with a Copy button to paste back to the chat, and has the player bar's download button for the MP4. Tested in a real Chrome under a strict content security policy: no warnings, ready, an MP4 exported, and the worked example runs. The README, `llms.txt`, SKILL and the guide's "Working with an AI" page point to it with a one-paragraph prompt to paste.
|
|
16
|
+
|
|
3
17
|
## 0.14.0
|
|
4
18
|
|
|
5
19
|
**Added**
|
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# pixi-effects
|
|
2
2
|
|
|
3
|
-
> **Status**: experimental — current release `0.
|
|
3
|
+
> **Status**: experimental — current release `0.16.0`. The API may still change between minor versions (see [CHANGELOG](./CHANGELOG.md): `0.16.0` adds music written as text (`{ type: 'audio', music }`, `musicEnvelope()`), `0.15.0` adds a way in for an AI in a browser chat (`ai/CHAT.md` and `ai/chat-template.html`), `0.14.0` adds `movie.inspectStops()` (and a check review) for stops that land mid-animation, `0.13.1` fixes decks starting past their first stop in the guide and gallery and draws the stop marks as dots, `0.13.0` makes the player bar play a deck stop by stop (`pauseAtStops`), marks the stops more clearly, adds PDF to the download panel and a `pdf` flag on stops, `0.12.0` adds presentations (`stops`, `Presenter`, `deck()`, page overview, presenter view, PDF export) and audio-reactive visuals (`audioEnvelope()`, `react()`), `0.11.0` adds particles, motion blur, a shared loader and a much faster build for pieces with many layers, `0.10.0` adds seeded randomness, `wiggle` and `stagger`, null layers with `parent`, `animateText`, `followPath` and path morphing, and click-to-play in the player, `0.9.0` adds a poster time (`movie.init({ poster })`) and a guide site, `0.8.0` adds events and a theme for building your own player, `0.7.0` adds filters by name, the `pixi-effects-render` and `pixi-effects-view` commands and the timeline chart, `0.6.0` adds draw-on strokes (`trimEnd`) and text that changes over time, `0.5.0` adds synthesised sound effects (`sfx`, no files) and the `check` tool, `0.4.0` makes a mask share its layer's lifetime, `0.3.0` made keyframe `at` relative to the layer).
|
|
4
4
|
|
|
5
|
-
**[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[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/)** · 38 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
|
+
|
|
7
|
+
**Make a video with an AI chat** (no install): paste this to ChatGPT or Claude in the browser, then open the file it gives you and use the download button in the player bar for the MP4:
|
|
8
|
+
|
|
9
|
+
> Read https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/CHAT.md and follow it. Make a 10-second 1280×720 title video for … (what you see, the colours, what you hear). Give me the whole HTML file.
|
|
10
|
+
|
|
11
|
+
(For an AI that has a shell and a browser, such as Claude Code: [`ai/SKILL.md`](./ai/SKILL.md).)
|
|
6
12
|
|
|
7
13
|
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
14
|
|
|
@@ -14,6 +20,7 @@ Declarative composition and video rendering for the web. After Effects-style tim
|
|
|
14
20
|
- **Masks, transitions, chroma key** — inline masks; chroma key; seven scene transitions (`crossfade`, `wipe`, `iris`, `slide`, `dip`, `zoom`, `dissolve`).
|
|
15
21
|
- **Presets** — `kenBurns` for stills, `withFade` for fade-in/out; `wiggle`, `stagger`, `animateText`, `followPath`, `particles` and `react` return keyframes or layers (seeded, so every render is the same).
|
|
16
22
|
- **Presentations** — `stops` in the composition pause the movie where you say; `Presenter` (`pixi-effects/presenter`) moves on with arrow keys, a click or a swipe, with a page overview (G), a presenter view with notes and a timer (P), and `deck()` to lay out pages and steps. Export a deck as PDF (`movie.exportPDF()`, `pixi-effects-render talk.html -o talk.pdf`). See the [Presenting guide](https://yjmtmtk.github.io/pixi-effects/guide/presenting.html).
|
|
23
|
+
- **Music written as text** — `{ type: 'audio', music: { bpm: 96, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4' }], drums: { kick: 'x...x...' } } }`: a tune with no audio file, played by a small built-in synthesiser (keys, pluck, pad, bass, lead, bell, music box, drums; chords by name, swing, fades, a ritardando). Deterministic, in the exported file at the same moment; an AI writes it as easily as a title.
|
|
17
24
|
- **Audio-reactive visuals** — `audioEnvelope('music.mp3')` analyses loudness, bass / mid / treble and the beats per frame; `react()` turns them into keyframes, so the picture follows the music and still renders identically.
|
|
18
25
|
- **2.5D layers & camera** — add `threeD: true`, `z`, `rotationX/Y` and a `{ type: 'camera' }` layer for parallax, flips and dolly zooms; no three.js needed.
|
|
19
26
|
- **three.js layer** *(optional)* — drop a real three.js scene in as a layer via `pixi-effects/three`; keyframes drive its objects (`three.cube.rotation.y`).
|
|
@@ -42,8 +49,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
42
49
|
"gsap": "https://esm.sh/gsap@3.12.5",
|
|
43
50
|
"gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
|
|
44
51
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
45
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
46
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
52
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.16.0/dist/index.js",
|
|
53
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.16.0/dist/Controller.js"
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
</script>
|
|
@@ -54,7 +61,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
54
61
|
</script>
|
|
55
62
|
```
|
|
56
63
|
|
|
57
|
-
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.
|
|
64
|
+
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.16.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.
|
|
58
65
|
|
|
59
66
|
> **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
|
|
60
67
|
>
|
|
@@ -70,7 +77,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
|
|
|
70
77
|
"imports": {
|
|
71
78
|
"...": "(everything from the importmap above)",
|
|
72
79
|
"three": "https://esm.sh/three@0.178.0",
|
|
73
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
80
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.16.0/dist/three.js"
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
</script>
|
package/ai/CHAT.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# pixi-effects for an AI in a chat (no shell, no files)
|
|
2
|
+
|
|
3
|
+
You write ONE HTML file. The person opens it (or your chat previews it), watches, and can download an MP4 with the button in the player bar. You cannot run commands or look at the result, so you rely on two things: the rules below, and the red box the page shows when something is wrong (ask the person to paste its text back to you).
|
|
4
|
+
|
|
5
|
+
## Do this
|
|
6
|
+
|
|
7
|
+
1. Fetch `https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/chat-template.html` and copy it whole. (Use the `raw.githubusercontent.com` address; the normal github.com page does not return the file.) If you cannot fetch it, ask the person to paste it.
|
|
8
|
+
2. Replace only what is between `EDIT FROM HERE` and `EDIT UNTIL HERE`. Keep the rest: it loads the library from one CDN, collects warnings into a red box with a Copy button, and adds the player bar.
|
|
9
|
+
3. Give the person the whole file. Tell them: open it, press play (sound starts after the first click), use the download button (bottom right of the picture) for the MP4, and **if a red box appears, copy its text back to you**.
|
|
10
|
+
4. When they paste warnings, fix exactly what each one names. The messages say what to change ("did you mean …").
|
|
11
|
+
|
|
12
|
+
## The idea
|
|
13
|
+
|
|
14
|
+
A video is **data**: a list of layers in `sequences`. Each layer has `type`, a start `at` and a `duration` (seconds), `initial` values and `keyframes`. Repeated structure is a JS loop or function that returns layers. There is no per-frame code and no `Math.random()`.
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
// @chat-example
|
|
18
|
+
const W = 1280, H = 720, FPS = 30, DURATION = 8;
|
|
19
|
+
const BACKGROUND = '#f3eee4';
|
|
20
|
+
const INK = '#2b2118', ACCENT = '#c0583a';
|
|
21
|
+
|
|
22
|
+
const sequences = [
|
|
23
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: BACKGROUND } },
|
|
24
|
+
{
|
|
25
|
+
type: 'text', text: 'Rye & Sons', name: 'title', at: 0.4, duration: DURATION - 0.4,
|
|
26
|
+
style: { fontFamily: 'Georgia, serif', fontSize: 110, fill: INK },
|
|
27
|
+
initial: { x: 'GW/2', y: 300, anchorX: 0.5, anchorY: 0.5 },
|
|
28
|
+
keyframes: [{ at: 0, from: { alpha: 0, y: 330 }, to: { alpha: 1, y: 300 }, duration: 0.9, ease: 'power3.out' }],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: 'shape', shape: 'line', from: [420, 380], to: [860, 380], name: 'rule', at: 1.2, duration: DURATION - 1.2,
|
|
32
|
+
initial: { strokeColor: ACCENT, strokeWidth: 4, trimEnd: 0 },
|
|
33
|
+
keyframes: [{ at: 0, to: { trimEnd: 1 }, duration: 1, ease: 'power2.inOut' }],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'text', text: 'Bakers since 1952', name: 'tagline', at: 2.2, duration: DURATION - 2.2,
|
|
37
|
+
style: { fontFamily: 'Georgia, serif', fontSize: 40, fill: INK, letterSpacing: 4 },
|
|
38
|
+
initial: { x: 'GW/2', y: 430, anchorX: 0.5, anchorY: 0.5 },
|
|
39
|
+
keyframes: [{ at: 0, from: { alpha: 0 }, to: { alpha: 1 }, duration: 0.8 }],
|
|
40
|
+
},
|
|
41
|
+
{ type: 'audio', sfx: 'chime', at: 2.2 },
|
|
42
|
+
];
|
|
43
|
+
const POSTER = 4; // the picture that stands for the video before play: its most striking moment
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
That is a complete, working video (the block between the two markers). Change the content, not the structure.
|
|
47
|
+
|
|
48
|
+
## Rules that prevent most failures
|
|
49
|
+
|
|
50
|
+
- **Time.** A layer's `at` is seconds from the start of the movie. A keyframe's `at` is seconds from the start of **its own layer** (negative = back from the layer's end). A layer is hidden after `at + duration`; give it a `duration` that reaches the end of its scene.
|
|
51
|
+
- **Order.** Layers are drawn in array order: later = on top. Put the background rect first.
|
|
52
|
+
- **Units.** Pixels from the top-left; angles in **degrees**; colours `'#rrggbb'`. Any number may be an expression string: `'GW/2'`, `'GH - 80'`, `'min(GW, GH) * 0.4'`.
|
|
53
|
+
- **Anchors.** `rect`, `circle`, `ellipse` are centred on `x, y`. **Text and image are top-left**: add `anchorX: 0.5, anchorY: 0.5` to centre them. A bar that grows from its base needs `anchorY: 1`.
|
|
54
|
+
- **Animate with keyframes.** `{ at, duration, ease, to: {...} }` animates to a value, `from: {...}` from one, `from` + `to` both (a delayed fade-in needs no `initial.alpha`), `set: {...}` jumps. Eases are GSAP names: `'power2.out'`, `'back.out(1.7)'`, `'none'`.
|
|
55
|
+
- **Draw a line on:** `trimEnd: 0` in `initial`, then a keyframe `to: { trimEnd: 1 }` (every shape except `arc`).
|
|
56
|
+
- **Shapes:** `shape: 'rect' | 'circle' | 'ellipse' | 'arc' | 'line' | 'polygon' | 'path'`. `rect`: `width height cornerRadius`; `circle`: `radius`; `line`: `from: [x, y], to: [x, y]` (canvas coordinates, no `x, y`); `path`: `d` (SVG path data). Style: `fillColor strokeColor strokeWidth fillAlpha`.
|
|
57
|
+
- **Text:** `style` is a PixiJS text style (`fontSize fontFamily fontWeight fill letterSpacing align wordWrap wordWrapWidth lineHeight`). Use fonts that exist everywhere (`Georgia, serif`, `Arial, sans-serif`, `ui-monospace, monospace`); this page loads no web fonts. Estimate a text's width at about 0.6 × `fontSize` per character, keep important text 5 % away from the edges, and keep the bottom 60 px free (the player bar covers it in the preview, not in the file).
|
|
58
|
+
- **Scenes.** Stack layers with `at` / `duration` over an opaque background rect. Do not remove layers.
|
|
59
|
+
- **Sound with no files:** `{ type: 'audio', sfx: 'swoosh', at: 2 }`. Presets: `click pop swoosh swipe hit riser chime beep coin glitch typewriter`; optional `pitch`, `brightness`. A `riser` is loudest at its **end**: start it `duration` seconds before the hit. This page cannot load music or images from your own files; use shapes, text, sfx and `music` (below).
|
|
60
|
+
- **Repeat with code, never by copying:** `Array.from({ length: 12 }, (_, i) => ({ type: 'shape', shape: 'circle', radius: 20, at: i * 0.15, duration: DURATION - i * 0.15, initial: { x: 120 + i * 90, y: 360, fillColor: '#ffd166' }, keyframes: [{ at: 0, from: { scale: 0 }, to: { scale: 1 }, duration: 0.4, ease: 'back.out(2)' }] }))` returns twelve layers: spread them into `sequences` with `...`.
|
|
61
|
+
- **Helpers already imported** in the template: `kenBurns` (slow zoom for an image layer), `withFade`, `wiggle` (seeded shake as keyframes), `stagger`, `animateText` (per letter / word entrances), `followPath`, `particles` (seeded confetti / snow / sparks as layers), `deck` (slides). Each returns keyframes or layers: `keyframes: wiggle(...)`, `...particles(...)`. The full signatures are in `https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/reference/cheatsheet.md` (22 KB; read it only if you need a helper).
|
|
62
|
+
- **Not available on this page:** named filters beyond `blur`, `noise`, `alpha`, `colorMatrix` (the other 38 need the `pixi-filters` package), three.js layers, your own image / video / music files. Say so rather than guessing.
|
|
63
|
+
|
|
64
|
+
## Music with no file
|
|
65
|
+
|
|
66
|
+
Write the tune as text: an audio layer with `music`. Tracks of notes and chords, optional drums, a tempo. The unit is the **beat** (a quarter note; a 4/4 bar is 4 beats; make every track the same length).
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
// @chat-music
|
|
70
|
+
const W = 1280, H = 720, FPS = 30, DURATION = 22;
|
|
71
|
+
const BACKGROUND = '#0e1a2b';
|
|
72
|
+
|
|
73
|
+
const sequences = [
|
|
74
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: BACKGROUND } },
|
|
75
|
+
{
|
|
76
|
+
type: 'text', text: 'Snowfall', name: 'title', at: 1, duration: DURATION - 1,
|
|
77
|
+
style: { fontFamily: 'Georgia, serif', fontSize: 120, fill: '#e8f1ff', letterSpacing: 6 },
|
|
78
|
+
initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
|
79
|
+
keyframes: [{ at: 0, from: { alpha: 0 }, to: { alpha: 1 }, duration: 2.5 }],
|
|
80
|
+
},
|
|
81
|
+
{ type: 'audio', volume: 0.8, music: { // a slow 6/8 lullaby in F major: 8 bars of 3 beats
|
|
82
|
+
bpm: 72, grid: 2, reverb: 0.3, seed: 5,
|
|
83
|
+
tracks: [
|
|
84
|
+
{ inst: 'musicbox', vol: 0.8, notes: 'a5:1.5 c6:1.5 | a5:1 g5:0.5 f5:1.5 | d6:1.5 a5:1.5 | bb5:1 a5:0.5 g5:1.5 | g5:1.5 d6:1.5 | e6:1 d6:0.5 c6:1.5 | a5:1 c6:0.5 f6:1.5 | f6:3' },
|
|
85
|
+
{ inst: 'pluck', vol: 0.45, pan: -0.2, step: 0.5, notes: 'f3 a3 c4 a3 c4 a3 | f3 a3 c4 a3 c4 a3 | d3 f3 a3 f3 a3 f3 | bb2 d3 f3 d3 f3 d3 | g3 bb3 d4 bb3 d4 bb3 | c3 e3 g3 e3 g3 e3 | f3 a3 c4 a3 c4 a3 | f3 a3 c4 g4 c4 a3' },
|
|
86
|
+
{ inst: 'pad', vol: [[0, 0], [6, 0.5]], attack: 1.2, notes: '[f3 a3 c4]:6 [d3 f3 a3 c4]:3 [bb2 d3 f3 a3]:3 [g2 bb2 d3 f3]:3 [c3 e3 g3 bb3]:3 [f3 a3 c4]:3 [f3 g3 a3 c4]:3' },
|
|
87
|
+
],
|
|
88
|
+
} },
|
|
89
|
+
];
|
|
90
|
+
const POSTER = 8;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Every token is a note (`c4`, `f#3`, `Bb2`) with its length in beats after a colon, a chord by name (`Am7`, `Dsus4`, `C@4`, `Am7/e`) or written out (`[c4 e4 g4]:2`), a rest `_`, or a hold `~`; `|` is ignored. Instruments: `keys pluck pad bass sub lead bell musicbox`. Drums: `drums: [{ from: 8, kick: 'x.......x.x.....', snare: '....x.......x...', hat: 'x.x.x.x.x.x.x.xo' }]` (16 steps = one 4/4 bar; `from` is a beat; `x` hit, `o` soft, `.` nothing; also `openhat clap rim tom crash shaker sleigh`). Shape it with `swing: 0.16` (a lilt), `reverb`, a track's `vol: [[0, 0], [8, 0.7]]` (a fade-in over 8 beats), `tone` (darker when lower), `transpose`, and a slowing tempo `bpm: [[0, 96], [28, 96], [32, 60]]`. Use `loop: true` on the layer to repeat a short tune under a long video. A mistake prints a warning in the red box that names the track and the token. Keep 3 to 6 tracks, vary the bars, end on the home chord.
|
|
94
|
+
|
|
95
|
+
## Check your own work before you answer
|
|
96
|
+
|
|
97
|
+
Read your file once for these: every layer has a `type`; every keyframe `at` is inside its layer's lifetime; text is anchored where you meant; layer `duration`s reach the end of their scene; colours have enough contrast; nothing important sits near the edges. A mistake in the data prints a warning in the red box; the person's paste is your test run.
|
|
98
|
+
|
|
99
|
+
## If the preview is blank
|
|
100
|
+
|
|
101
|
+
A blank preview with no red box usually means the chat's preview window blocks external scripts or does not allow `eval`. Tell the person to **save the file as `video.html` and open it in a browser** (Chrome, Edge or Firefox on a computer); that always works.
|
|
102
|
+
|
|
103
|
+
More: the full skill (`ai/SKILL.md`) and pitfalls (`ai/reference/pitfalls.md`) are for an AI with a shell and a browser; this page is the short version for a chat.
|
package/ai/SKILL.md
CHANGED
|
@@ -33,7 +33,7 @@ You describe a video as a tree of plain objects; the library plays it in a canva
|
|
|
33
33
|
- **Masks live in the parent's coordinates and don't follow the masked layer.** **`filterArea` is in the layer's own coordinates.**
|
|
34
34
|
- **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.
|
|
35
35
|
- **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.
|
|
36
|
-
- **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) `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` (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), shared masks. (Draw-on strokes `trimEnd`, text that changes over time `set: { text }` / `visibleChars`, shake `wiggle()` and particles are built in.)
|
|
36
|
+
- **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) `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` (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), shared masks. (Draw-on strokes `trimEnd`, text that changes over time `set: { text }` / `visibleChars`, shake `wiggle()` and particles are built in.)
|
|
37
37
|
- **Blend modes:** `blendMode: 'add' | 'screen' | 'multiply'` on any layer (glows, light leaks).
|
|
38
38
|
- **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.
|
|
39
39
|
|
|
@@ -43,6 +43,7 @@ Centred text must be positioned by `anchorX/anchorY: 0.5`; estimate text width a
|
|
|
43
43
|
|
|
44
44
|
## Files
|
|
45
45
|
|
|
46
|
+
- `CHAT.md` and `chat-template.html` — the short version for an AI in a chat with no shell: one self-contained page that loads from a single CDN, shows warnings in a red box, and a worked example.
|
|
46
47
|
- `reference/cheatsheet.md` — every layer type, prop, default and rule on one page.
|
|
47
48
|
- `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.
|
|
48
49
|
- `reference/pitfalls.md` — the full list of real mistakes, with status.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<!--
|
|
3
|
+
pixi-effects starter for an AI that works in a CHAT (no shell, no files, a preview window or a file the person opens).
|
|
4
|
+
- Everything loads from cdn.jsdelivr.net only (no esm.sh, no extra stylesheet), so it also runs in sandboxed previews.
|
|
5
|
+
- Edit ONLY the block between "EDIT FROM HERE" and "EDIT UNTIL HERE". Keep everything else.
|
|
6
|
+
- Problems appear in a red box under the picture, with a Copy button: ask the person to paste that text back to you.
|
|
7
|
+
- The player bar's download button (bottom right of the picture) saves the finished MP4. Sound starts after the first click.
|
|
8
|
+
- Named filters other than blur / noise / alpha / colorMatrix need pixi-filters, which this page does not load.
|
|
9
|
+
Reference: https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/CHAT.md
|
|
10
|
+
-->
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="UTF-8" />
|
|
14
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
15
|
+
<title>pixi-effects video</title>
|
|
16
|
+
<style>
|
|
17
|
+
body { background: #0d1220; margin: 0; padding: 16px; font-family: system-ui, sans-serif; color: #e8ecf4; }
|
|
18
|
+
.stage { position: relative; width: min(960px, 100%); }
|
|
19
|
+
.stage canvas { display: block; width: 100%; height: auto; border-radius: 8px; }
|
|
20
|
+
.loading { position: absolute; inset: 0; display: grid; place-items: center; letter-spacing: 0.3em; font-size: 12px; opacity: 0.6; pointer-events: none; }
|
|
21
|
+
#problems { display: none; max-width: 960px; margin-top: 12px; padding: 10px 12px; border: 1px solid #ff6b6b; border-radius: 8px; background: #2a1216; font: 12px/1.5 ui-monospace, monospace; white-space: pre-wrap; }
|
|
22
|
+
#problems button { float: right; margin-left: 8px; }
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<div class="stage">
|
|
27
|
+
<canvas id="stage" width="1280" height="720"></canvas>
|
|
28
|
+
<div class="loading" id="loading">LOADING</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div id="problems"><button id="copy" type="button">Copy</button><b>Paste this back to the AI:</b>
|
|
31
|
+
<span id="problems-text"></span></div>
|
|
32
|
+
|
|
33
|
+
<script>
|
|
34
|
+
// Every warning and error is collected in window.__logs and shown in the red box. pixi-effects warnings are INSTRUCTIONS: they say what to change.
|
|
35
|
+
window.__logs = [];
|
|
36
|
+
const show = () => {
|
|
37
|
+
const box = document.getElementById('problems');
|
|
38
|
+
box.style.display = 'block';
|
|
39
|
+
document.getElementById('problems-text').textContent = '\n' + window.__logs.join('\n');
|
|
40
|
+
};
|
|
41
|
+
const add = (line) => { window.__logs.push(line); show(); };
|
|
42
|
+
for (const k of ['warn', 'error']) {
|
|
43
|
+
const orig = console[k].bind(console);
|
|
44
|
+
console[k] = (...a) => { add(k + ': ' + a.map(x => (x && x.message) || String(x)).join(' ')); orig(...a); };
|
|
45
|
+
}
|
|
46
|
+
window.addEventListener('error', e => add('uncaught: ' + e.message));
|
|
47
|
+
window.addEventListener('unhandledrejection', e => add('unhandled: ' + (e.reason && e.reason.message || e.reason)));
|
|
48
|
+
document.getElementById('copy').onclick = () => {
|
|
49
|
+
const text = window.__logs.join('\n');
|
|
50
|
+
(navigator.clipboard ? navigator.clipboard.writeText(text) : Promise.reject()).catch(() => {
|
|
51
|
+
const r = document.createRange(); r.selectNodeContents(document.getElementById('problems-text')); getSelection().removeAllRanges(); getSelection().addRange(r);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
window.__add = add;
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<script type="importmap">
|
|
58
|
+
{
|
|
59
|
+
"imports": {
|
|
60
|
+
"pixi.js": "https://cdn.jsdelivr.net/npm/pixi.js@8.22.0/dist/pixi.min.mjs",
|
|
61
|
+
"gsap": "https://cdn.jsdelivr.net/npm/gsap@3.12.5/+esm",
|
|
62
|
+
"gsap/PixiPlugin": "https://cdn.jsdelivr.net/npm/gsap@3.12.5/PixiPlugin/+esm",
|
|
63
|
+
"mediabunny": "https://cdn.jsdelivr.net/npm/mediabunny@1.61.3/+esm",
|
|
64
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.16.0/dist/index.js",
|
|
65
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.16.0/dist/Controller.js",
|
|
66
|
+
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.16.0/dist/Presenter.js"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<script type="module">
|
|
72
|
+
import { Movie, kenBurns, withFade, wiggle, stagger, animateText, followPath, particles, deck } from 'pixi-effects';
|
|
73
|
+
import { Controller } from 'pixi-effects/controller';
|
|
74
|
+
// A talk instead of a video: import { Presenter } from 'pixi-effects/presenter'; use deck({ pages: [...] }) and new Presenter(movie, { canvas }) instead of the Controller.
|
|
75
|
+
|
|
76
|
+
// ===================== EDIT FROM HERE =====================
|
|
77
|
+
const W = 1280, H = 720, FPS = 30, DURATION = 6;
|
|
78
|
+
const BACKGROUND = '#0a0a0f';
|
|
79
|
+
|
|
80
|
+
// The video is DATA: a list of layers. Repeated structure = a JS loop or function that returns layers.
|
|
81
|
+
// Keyframe `at` counts from the START OF ITS OWN LAYER. Angles are degrees. Numbers can be expressions: 'GW/2', 'GH - 80'.
|
|
82
|
+
const sequences = [
|
|
83
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: BACKGROUND } },
|
|
84
|
+
{
|
|
85
|
+
type: 'text', text: 'Hello', at: 0.5, duration: DURATION - 0.5,
|
|
86
|
+
style: { fontSize: 120, fontWeight: 'bold', fill: '#ffffff' },
|
|
87
|
+
initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5, alpha: 0 },
|
|
88
|
+
keyframes: [{ at: 0, to: { alpha: 1 }, duration: 0.6, ease: 'power2.out' }],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
const POSTER = DURATION * 0.75; // the moment that stands for the video before it plays: pick the most striking one
|
|
92
|
+
// ===================== EDIT UNTIL HERE =====================
|
|
93
|
+
|
|
94
|
+
const movie = new Movie();
|
|
95
|
+
window.movie = movie;
|
|
96
|
+
new Controller(movie, { canvas: document.getElementById('stage') });
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await movie.init({
|
|
100
|
+
canvas: document.getElementById('stage'),
|
|
101
|
+
width: W, height: H, duration: DURATION, frameRate: FPS,
|
|
102
|
+
poster: POSTER,
|
|
103
|
+
background: BACKGROUND,
|
|
104
|
+
composition: { sequences },
|
|
105
|
+
});
|
|
106
|
+
window.__ready = true;
|
|
107
|
+
} catch (e) {
|
|
108
|
+
window.__add('init failed: ' + (e && e.message || e));
|
|
109
|
+
}
|
|
110
|
+
document.getElementById('loading').remove();
|
|
111
|
+
</script>
|
|
112
|
+
</body>
|
|
113
|
+
</html>
|
|
@@ -56,6 +56,7 @@ Keyframe kinds: `set` (jump at `at`; undone when you seek back), `to` (animate t
|
|
|
56
56
|
|
|
57
57
|
**audio** — a file: `asset`, `loop`, `volume`; `volume` (top level, or in `initial` like every other layer's starting values); fade with volume keyframes (`{ at: -2, to: { volume: 0 }, duration: 2 }` fades the last 2 s). Omit `duration` for a one-shot clip: it lasts exactly as long as the clip. With `loop: true` and no `duration` it lasts until the composition ends. An explicit `duration` longer than the clip goes silent after the clip unless `loop: true`.
|
|
58
58
|
**Or a sound effect with no file — `sfx`:** `{ type: 'audio', sfx: 'swoosh', at: 2 }` is the whole thing. Presets, default length in s: `click` 0.04 · `pop` 0.12 · `swoosh` 0.5 (loudest 0.16 s in, moves left → right) · `swipe` 0.22 · `hit` 0.7 · `riser` 1 (loudest at its END: `at = hit − duration`) · `chime` 1.4 · `beep` 0.16 · `coin` 0.4 · `glitch` 0.35 · `typewriter` 0.06. Knobs: `sfx: { preset, pitch /* semitones ±24 */, brightness /* −1 dark … 1 bright */, seed /* another take */ }`. **The layer's `duration` IS the sound's length** (omit it; set it to stretch, e.g. a riser); `volume` is its level (1 = the preset's standard level, peaking at −12 … −18 dBFS by preset). No `loop`: one layer per hit (a JS loop). Custom sound: `sfx: { voices: [{ wave: 'noise' | 'sine' | 'square' | 'saw' | 'triangle', freq: 440 | [from, to], filter: { type: 'lowpass' | 'highpass' | 'bandpass', freq: [from, to] }, envelope: 'fall' | 'bell' | 'swell' | 'hold', pan: [-1, 1] }] }`. The export (mp4 / webm / mov) encodes this same mix (lossy AAC / Opus), on time within a few ms.
|
|
59
|
+
**Or music with no file — `music`:** `{ type: 'audio', music: { bpm: 96, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4 Dm7:4 G7:4' }, { inst: 'bass', notes: 'c2:2 g1:2 …' }], drums: { kick: 'x...x...', snare: '....x...', hat: 'x.x.x.x.' } } }` — a tune written as text (notes `c4:2` in beats, chords `Am7` `C@4` `Am7/e`, `[c4 e4 g4]`, rest `_`, hold `~`, `!` accent; `|` ignored). Instruments `keys pluck pad bass sub lead bell musicbox`; drums `kick snare hat openhat clap rim tom crash shaker sleigh` as 16th-note step strings (`x` hit, `o` soft, `.` none; a list of `{ from, to, … }` for sections). Score options `bpm` (or `[[beat, bpm], …]` for a ritardando) `swing reverb humanize drumVol grid transpose tail seed`; track options `vol` (or `[[beat, level], …]` for a fade) `pan step strum legato tone reverb attack release ring transpose`. Lasts its notes + tail, ends with the movie, `loop: true` repeats; peaks −6 dBFS at `volume: 1`. `await musicEnvelope(music, { frameRate })` → an envelope for `react()`.
|
|
59
60
|
|
|
60
61
|
**shape** — `shape: 'rect' | 'circle' | 'ellipse' | 'arc' | 'line' | 'polygon' | 'path'`:
|
|
61
62
|
|
|
@@ -110,7 +111,7 @@ transitions: [{ kind: 'crossfade' | 'wipe' | 'iris' | 'slide' | 'dip' | 'zoom' |
|
|
|
110
111
|
## Presets
|
|
111
112
|
|
|
112
113
|
```js
|
|
113
|
-
import { kenBurns, withFade, wiggle, stagger, followPath, particles, deck, audioEnvelope, bpmEnvelope, react, random } from 'pixi-effects';
|
|
114
|
+
import { kenBurns, withFade, wiggle, stagger, followPath, particles, deck, audioEnvelope, bpmEnvelope, musicEnvelope, react, random } from 'pixi-effects';
|
|
114
115
|
kenBurns({ asset, name, at, duration, motion: 'still'|'scale'|'rotation'|'position',
|
|
115
116
|
fit: 'cover'|'contain', ease, /* scale */ origin, zoom, direction, /* rotation */ angle, /* position */ from, to })
|
|
116
117
|
withFade(spec, { in: 0.5, out: 0.5 }) // alpha fade; `out` needs spec.duration
|
package/ai/reference/pitfalls.md
CHANGED
|
@@ -82,3 +82,9 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
|
|
|
82
82
|
55. **Presenter and Controller both listen to click and keys.** Use the `Presenter` for a talk (or let the Controller's Present button start one); do not run both on the same canvas yourself. `movie.play()` ignores stops, but the Controller's play button / Space / click-on-picture play to the next stop and pause (`pauseAtStops: false` plays through); `movie.next()` plays to the next stop (a second press skips to it), `prev()` jumps back (nothing plays in reverse).
|
|
83
83
|
56. **Analyse audio before `init`, and give it the movie's frame rate.** `const env = await audioEnvelope('music.mp3', { frameRate })` is async; `react()` is pure and bakes the keyframes, so there is no per-frame callback. A custom `bands` list replaces the defaults (include `bass` if you want it; beats are picked there). No file yet: `bpmEnvelope(120, { duration })` is a stand-in with a kick on every beat. Offset the music with `audioOffset` when the audio layer does not start at 0.
|
|
84
84
|
57. **Review a deck by its stops, not by its timeline.** `pixi-effects-check` writes `stops.png` (one picture per stop, in order) for a page with `stops`; the picture at a stop is what the audience reads while it waits. A stop that lands a few frames before an animation ends shows it half-finished (also in the PDF): `movie.inspectStops()` / the check's review list names such stops; give an animation a little more time than its step (e.g. the next stop 0.3 s after it ends).
|
|
85
|
+
58. **`music` counts BEATS, and every track has to add up.** A token's `:n` is beats (a quarter note), a 4/4 bar is 4: a bar of `c4:1 e4:1 g4:1` is a beat short and every later bar drifts. Keep all tracks the same length (the music lasts the longest; a track of rests `'_:16'` sets a length) and check the printed length with `movie.inspectAudio()`. 6/8 is a bar of 3 beats (dotted quarter `:1.5`, eighth `:0.5`) with `grid: 2`; `grid: 3` is triplet eighths in 4/4.
|
|
86
|
+
59. **A groove that must enter late needs `from`, not a long string.** `drums: [{ kick: 'x...' }, { from: 8, snare: '....x...' }]`. Patterns repeat; a drum object is a loop of its longest string. A `crash` on the last bar: its own section `{ from: 28, crash: 'x' }`.
|
|
87
|
+
60. **Build-ups are layers and `vol` points, not louder notes.** A track's `vol` can be `[[0, 0], [8, 0.7]]` (a fade over 8 beats); the pad's `attack` can be 2–3 s; enter instruments with rests. Per-note `!` and `,` only accent and soften.
|
|
88
|
+
61. **Music is a bed: `volume` 0.5–0.8 under speech or sfx.** At `volume: 1` it peaks at −6 dBFS (an sfx peaks at −12). A layer shorter than the music cuts it with a fade; longer is silent after it unless `loop: true`. Do not also add an mp3 bed.
|
|
89
|
+
62. **Chord names are voiced close around octave 3**: use `[c3 g3 e4]` for an exact voicing or `Am7/e` for a bass note; `swing` only moves notes that start on an off-beat eighth (`.5`) or sixteenth (`.75`); the bell and music box ring for `ring` seconds whatever the written length (shorten `ring` for fast bell lines); the synth is a handy bed, not an orchestra: keep tracks to 3–6, vary the bars, and end on the tonic.
|
|
90
|
+
|
package/ai/reference/recipes.md
CHANGED
|
@@ -280,6 +280,31 @@ Source images should be at least canvas-sized (`kenBurns` zooms in): draw genera
|
|
|
280
280
|
|
|
281
281
|
---
|
|
282
282
|
|
|
283
|
+
## Music with no file (`music`)
|
|
284
|
+
|
|
285
|
+
A tune written as text: tracks of notes and chords (`c4:2`, `Am7:4`, `_` rest), drum patterns, a tempo. Count the beats (a 4/4 bar is 4); keep every track the same length; give the drums `from` so they enter after the intro; fade a track in with `vol` points. It lasts its notes plus a reverb tail and ends with the movie; `loop: true` repeats it. The mix sits at −6 dBFS peak at `volume: 1`: use 0.5–0.8 under speech or sound effects.
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
// @recipe music-lofi
|
|
289
|
+
return [
|
|
290
|
+
{ type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#1b1a2e' } },
|
|
291
|
+
{ type: 'text', text: 'evening', at: 0.5, style: { fontFamily: 'Georgia, serif', fontSize: 96, fill: '#f3eee4' }, initial: { x: 'GW/2', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
|
292
|
+
keyframes: [{ at: 0, from: { alpha: 0 }, to: { alpha: 1 }, duration: 1.2 }] },
|
|
293
|
+
{ type: 'audio', at: 0, volume: 0.8, music: {
|
|
294
|
+
bpm: 84, swing: 0.16, reverb: 0.26, seed: 7,
|
|
295
|
+
tracks: [
|
|
296
|
+
{ inst: 'keys', vol: 0.7, pan: -0.15, strum: 0.012, notes: 'Cmaj7:4 Am7:4 Dm7:4 G7:4 | Cmaj7:4 Am7:4 Dm7:2 G7:2 Cmaj7:4' },
|
|
297
|
+
{ inst: 'bass', vol: 0.85, notes: 'c2:1.5 _:0.5 g2:1 c2:1 | a1:1.5 _:0.5 e2:1 a1:1 | d2:1.5 _:0.5 a2:1 d2:1 | g1:1.5 _:0.5 d2:1 g1:1 | c2:1.5 _:0.5 g2:1 c2:1 | a1:1.5 _:0.5 e2:1 a1:1 | d2:2 f2:1 a1:1 | g1:2 b1:1 d2:1 | c2:4' },
|
|
298
|
+
{ inst: 'pluck', vol: [[0, 0], [8, 0.55]], pan: 0.25, step: 0.5, notes: '_:8 | e5:1 d5:0.5 c5:0.5 e5:2 | c5:1 a4:1 e5:2 | f5:1 e5:0.5 d5:0.5 a4:2 | d5:1.5 b4:0.5 g4:2 | e5:1 g5:1 e5:1 d5:1 | c5:1 a4:1 c5:2 | d5:1 f5:1 e5:1 d5:1 | b4:2 d5:2 | c5:4' },
|
|
299
|
+
{ inst: 'pad', vol: 0.5, notes: '[c3 g3 e4]:8 [a2 e3 c4]:8 [d3 a3 f4]:8 [g2 d3 b3]:8' },
|
|
300
|
+
],
|
|
301
|
+
drums: [{ from: 8, kick: 'x.......x.x.....', snare: '....x.......x...', hat: 'x.x.x.x.x.x.x.xo' }],
|
|
302
|
+
} },
|
|
303
|
+
];
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Other feels: a 6/8 lullaby is `bpm: 72`, `grid: 2`, bars of 3 beats (a dotted quarter `:1.5`, an eighth `:0.5`), instruments `musicbox` + `pluck` + `pad`; a build-up is layers entering with rests and `vol` points on the pad, drums from a later `from`; a ritardando is `bpm: [[0, 96], [28, 96], [32, 60]]`; a bright jingle is `lead` + `bell` + `sleigh` / `shaker` over a bouncy `bass`.
|
|
307
|
+
|
|
283
308
|
## Sound effects without files
|
|
284
309
|
|
|
285
310
|
No `assets`, no files: a sound effect is one audio layer with `sfx`. It lasts as long as the sound.
|
package/ai/template.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>pixi-effects video</title>
|
|
7
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.16.0/dist/loader.css" /> <!-- the loader: HTML + CSS only, so it is on screen at once and keeps moving while the page builds the video -->
|
|
8
8
|
<style>
|
|
9
9
|
body { background: #0d1220; margin: 0; padding: 16px; }
|
|
10
10
|
.stage { position: relative; width: min(960px, 100%); } /* the player and the loader both sit inside this box */
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"gsap": "https://esm.sh/gsap@3.12.5",
|
|
40
40
|
"gsap/PixiPlugin": "https://esm.sh/gsap@3.12.5/PixiPlugin",
|
|
41
41
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
42
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
43
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
44
|
-
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
45
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
42
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.16.0/dist/index.js",
|
|
43
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.16.0/dist/Controller.js",
|
|
44
|
+
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.16.0/dist/Presenter.js",
|
|
45
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.16.0/dist/three.js",
|
|
46
46
|
"pixi-filters": "https://esm.sh/pixi-filters@6.1.5?external=pixi.js",
|
|
47
47
|
"three": "https://esm.sh/three@0.178.0"
|
|
48
48
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape,
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape, a0 as AudioDescriptor } from './types-C8XEUoqe.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,
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape, a0 as AudioDescriptor } from './types-C8XEUoqe.js';
|
|
3
3
|
import { gsap } from 'gsap';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-X2VZSVPD.js"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkV54NBCWG_cjs = require('./chunk-V54NBCWG.cjs');
|
|
4
|
+
require('./chunk-SPPNTV7E.cjs');
|
|
5
|
+
require('./chunk-UQBGW5DO.cjs');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Object.defineProperty(exports, "CompositionSequence", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: function () { return chunkV54NBCWG_cjs.CompositionSequence; }
|
|
12
|
+
});
|
|
13
|
+
//# sourceMappingURL=Composition-ZZGSMQ4E.cjs.map
|
|
14
|
+
//# sourceMappingURL=Composition-ZZGSMQ4E.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-ZZGSMQ4E.cjs"}
|
package/dist/Controller.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { f as Movie } from './Movie-
|
|
1
|
+
import { f as Movie } from './Movie-CJMfAizW.cjs';
|
|
2
2
|
import 'pixi.js';
|
|
3
3
|
import 'gsap';
|
|
4
|
-
import './types-
|
|
4
|
+
import './types-C8XEUoqe.cjs';
|
|
5
5
|
|
|
6
6
|
/** The bar's default stylesheet. Its colours and sizes are CSS custom properties (`--mc-accent`, `--mc-fg`, `--mc-track`, `--mc-bar-bg`, `--mc-track-height`, `--mc-font`): set them in your page CSS, or with the `theme` option. */
|
|
7
7
|
declare const CONTROLLER_CSS = "\n.movie-controller-wrap { position: relative; display: inline-block; line-height: 0; }\n.movie-controller {\n position: absolute;\n pointer-events: none;\n font-family: var(--mc-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n opacity: 1;\n transition: opacity 200ms ease;\n}\n.movie-controller[data-state=\"hidden\"] { opacity: 0; }\n.movie-controller > * { pointer-events: auto; }\n\n.mc-progress {\n position: relative;\n width: 100%;\n height: 16px;\n cursor: pointer;\n display: flex;\n align-items: center;\n outline: none;\n}\n.mc-progress:focus-visible {\n outline: 2px solid var(--mc-accent, #007AFF);\n outline-offset: 2px;\n}\n.mc-progress::before {\n content: \"\"; position: absolute; left: 12px; right: 12px;\n height: var(--mc-track-height, 3px); background: var(--mc-track, rgba(255,255,255,0.25));\n transition: height 120ms ease;\n}\n.mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }\n.mc-stop-tick { position: absolute; top: 50%; width: 7px; height: 7px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5); }\n.mc-stop-page { width: 11px; height: 11px; box-shadow: 0 0 0 2.5px var(--mc-accent, #007AFF), 0 0 0 4px rgba(0,0,0,0.35); }\n.mc-progress-fill {\n position: absolute; left: 12px; top: 50%;\n height: var(--mc-track-height, 3px);\n width: calc((100% - 24px) * var(--mc-fill, 0));\n background: var(--mc-accent, #007AFF);\n transform: translateY(-50%);\n transition: height 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-fill,\n.mc-progress.mc-scrubbing .mc-progress-fill { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-progress-thumb {\n position: absolute; top: 50%;\n left: calc(12px + (100% - 24px) * var(--mc-fill, 0));\n width: 12px; height: 12px; border-radius: 50%;\n background: var(--mc-accent, #007AFF);\n transform: translate(-50%, -50%) scale(0);\n transition: transform 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-thumb,\n.mc-progress.mc-scrubbing .mc-progress-thumb { transform: translate(-50%, -50%) scale(1); }\n\n.mc-bar {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 12px 8px 12px;\n background: var(--mc-bar-bg, linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%));\n color: var(--mc-fg, #fff);\n line-height: 1;\n}\n.mc-btn {\n background: none; border: 0; padding: 0; margin: 0;\n width: 28px; height: 28px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--mc-fg, #fff); opacity: 0.85; cursor: pointer;\n transition: opacity 120ms ease;\n}\n.mc-btn:hover { opacity: 1; }\n.mc-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n.mc-time {\n font-size: 12px; font-family: Menlo, Monaco, monospace;\n color: var(--mc-fg, #fff); opacity: 0.85;\n min-width: 90px;\n}\n.mc-spacer { flex: 1; }\n\n.mc-volume { display: flex; align-items: center; }\n.mc-vol-slider {\n position: relative;\n width: 0;\n height: 28px;\n display: flex;\n align-items: center;\n cursor: pointer;\n margin-left: 0;\n outline: none;\n transition: width 200ms ease, margin-left 200ms ease;\n overflow: hidden;\n}\n.mc-volume:hover .mc-vol-slider,\n.mc-volume:focus-within .mc-vol-slider,\n.mc-vol-slider.mc-scrubbing {\n width: 70px;\n margin-left: 6px;\n}\n.mc-vol-slider:focus-visible { outline: 2px solid var(--mc-accent, #007AFF); outline-offset: 2px; }\n.mc-vol-slider::before {\n content: \"\"; position: absolute; left: 4px; right: 4px;\n height: 3px; background: var(--mc-track, rgba(255,255,255,0.25)); border-radius: 2px;\n}\n.mc-vol-fill {\n position: absolute; left: 4px; top: 50%;\n height: 3px;\n width: calc((100% - 8px) * var(--mc-volume, 1));\n background: var(--mc-fg, #fff);\n transform: translateY(-50%);\n border-radius: 2px;\n pointer-events: none;\n}\n.mc-vol-thumb {\n position: absolute; top: 50%;\n left: calc(4px + (100% - 8px) * var(--mc-volume, 1));\n width: 10px; height: 10px; border-radius: 50%;\n background: var(--mc-fg, #fff);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n[data-mc-wrap]:fullscreen {\n width: 100vw; height: 100vh;\n display: flex; align-items: center; justify-content: center;\n background: #000;\n}\n[data-mc-wrap]:fullscreen > canvas {\n width: 100% !important;\n height: 100% !important;\n max-width: none !important;\n max-height: none !important;\n object-fit: contain;\n border-radius: 0;\n}\n\n.mc-settings-popover {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 12px;\n background: rgba(20, 20, 20, 0.96);\n border-radius: 8px;\n padding: 10px 12px;\n min-width: 180px;\n color: #fff;\n font-size: 12px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.4);\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.mc-settings-popover[data-open=\"false\"] { display: none; }\n.mc-settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n.mc-settings-row > span { color: rgba(255,255,255,0.85); }\n.mc-settings-row > select {\n font: inherit;\n background: rgba(255,255,255,0.1);\n color: #fff;\n border: 1px solid rgba(255,255,255,0.18);\n border-radius: 4px;\n padding: 2px 6px;\n cursor: pointer;\n}\n.mc-export-confirm {\n margin-top: 4px;\n width: 100%;\n background: var(--mc-accent, #007AFF);\n color: #fff;\n border: 0;\n border-radius: 4px;\n padding: 6px 10px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: background 120ms ease;\n}\n.mc-export-confirm:hover { background: #0066d6; }\n.mc-export-confirm:disabled { background: rgba(255,255,255,0.18); cursor: not-allowed; }\n\n.mc-export-overlay {\n position: absolute; inset: 0;\n background: rgba(0,0,0,0.7);\n display: flex; justify-content: center; align-items: center;\n z-index: 2;\n}\n.mc-export-panel {\n background: rgba(20,20,20,0.96); color: #fff;\n border-radius: 8px; padding: 16px 20px;\n text-align: center; min-width: 200px; max-width: 80%;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n}\n.mc-export-title { font-size: 13px; margin-bottom: 10px; opacity: 0.9; }\n.mc-export-track {\n width: 100%; height: 6px;\n background: rgba(255,255,255,0.15); border-radius: 3px;\n margin-bottom: 8px; overflow: hidden;\n}\n.mc-export-fill {\n height: 100%; width: 0%;\n background: linear-gradient(90deg, var(--mc-accent, #007AFF), var(--mc-accent, #0056CC));\n transition: width 0.3s ease; border-radius: 3px;\n}\n.mc-export-text {\n color: rgba(255,255,255,0.7); font-size: 11px;\n font-family: Menlo, Monaco, monospace;\n}\n";
|
package/dist/Controller.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { f as Movie } from './Movie-
|
|
1
|
+
import { f as Movie } from './Movie-BIGDIla0.js';
|
|
2
2
|
import 'pixi.js';
|
|
3
3
|
import 'gsap';
|
|
4
|
-
import './types-
|
|
4
|
+
import './types-C8XEUoqe.js';
|
|
5
5
|
|
|
6
6
|
/** The bar's default stylesheet. Its colours and sizes are CSS custom properties (`--mc-accent`, `--mc-fg`, `--mc-track`, `--mc-bar-bg`, `--mc-track-height`, `--mc-font`): set them in your page CSS, or with the `theme` option. */
|
|
7
7
|
declare const CONTROLLER_CSS = "\n.movie-controller-wrap { position: relative; display: inline-block; line-height: 0; }\n.movie-controller {\n position: absolute;\n pointer-events: none;\n font-family: var(--mc-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n opacity: 1;\n transition: opacity 200ms ease;\n}\n.movie-controller[data-state=\"hidden\"] { opacity: 0; }\n.movie-controller > * { pointer-events: auto; }\n\n.mc-progress {\n position: relative;\n width: 100%;\n height: 16px;\n cursor: pointer;\n display: flex;\n align-items: center;\n outline: none;\n}\n.mc-progress:focus-visible {\n outline: 2px solid var(--mc-accent, #007AFF);\n outline-offset: 2px;\n}\n.mc-progress::before {\n content: \"\"; position: absolute; left: 12px; right: 12px;\n height: var(--mc-track-height, 3px); background: var(--mc-track, rgba(255,255,255,0.25));\n transition: height 120ms ease;\n}\n.mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }\n.mc-stop-tick { position: absolute; top: 50%; width: 7px; height: 7px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5); }\n.mc-stop-page { width: 11px; height: 11px; box-shadow: 0 0 0 2.5px var(--mc-accent, #007AFF), 0 0 0 4px rgba(0,0,0,0.35); }\n.mc-progress-fill {\n position: absolute; left: 12px; top: 50%;\n height: var(--mc-track-height, 3px);\n width: calc((100% - 24px) * var(--mc-fill, 0));\n background: var(--mc-accent, #007AFF);\n transform: translateY(-50%);\n transition: height 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-fill,\n.mc-progress.mc-scrubbing .mc-progress-fill { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-progress-thumb {\n position: absolute; top: 50%;\n left: calc(12px + (100% - 24px) * var(--mc-fill, 0));\n width: 12px; height: 12px; border-radius: 50%;\n background: var(--mc-accent, #007AFF);\n transform: translate(-50%, -50%) scale(0);\n transition: transform 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-thumb,\n.mc-progress.mc-scrubbing .mc-progress-thumb { transform: translate(-50%, -50%) scale(1); }\n\n.mc-bar {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 12px 8px 12px;\n background: var(--mc-bar-bg, linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%));\n color: var(--mc-fg, #fff);\n line-height: 1;\n}\n.mc-btn {\n background: none; border: 0; padding: 0; margin: 0;\n width: 28px; height: 28px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--mc-fg, #fff); opacity: 0.85; cursor: pointer;\n transition: opacity 120ms ease;\n}\n.mc-btn:hover { opacity: 1; }\n.mc-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n.mc-time {\n font-size: 12px; font-family: Menlo, Monaco, monospace;\n color: var(--mc-fg, #fff); opacity: 0.85;\n min-width: 90px;\n}\n.mc-spacer { flex: 1; }\n\n.mc-volume { display: flex; align-items: center; }\n.mc-vol-slider {\n position: relative;\n width: 0;\n height: 28px;\n display: flex;\n align-items: center;\n cursor: pointer;\n margin-left: 0;\n outline: none;\n transition: width 200ms ease, margin-left 200ms ease;\n overflow: hidden;\n}\n.mc-volume:hover .mc-vol-slider,\n.mc-volume:focus-within .mc-vol-slider,\n.mc-vol-slider.mc-scrubbing {\n width: 70px;\n margin-left: 6px;\n}\n.mc-vol-slider:focus-visible { outline: 2px solid var(--mc-accent, #007AFF); outline-offset: 2px; }\n.mc-vol-slider::before {\n content: \"\"; position: absolute; left: 4px; right: 4px;\n height: 3px; background: var(--mc-track, rgba(255,255,255,0.25)); border-radius: 2px;\n}\n.mc-vol-fill {\n position: absolute; left: 4px; top: 50%;\n height: 3px;\n width: calc((100% - 8px) * var(--mc-volume, 1));\n background: var(--mc-fg, #fff);\n transform: translateY(-50%);\n border-radius: 2px;\n pointer-events: none;\n}\n.mc-vol-thumb {\n position: absolute; top: 50%;\n left: calc(4px + (100% - 8px) * var(--mc-volume, 1));\n width: 10px; height: 10px; border-radius: 50%;\n background: var(--mc-fg, #fff);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n[data-mc-wrap]:fullscreen {\n width: 100vw; height: 100vh;\n display: flex; align-items: center; justify-content: center;\n background: #000;\n}\n[data-mc-wrap]:fullscreen > canvas {\n width: 100% !important;\n height: 100% !important;\n max-width: none !important;\n max-height: none !important;\n object-fit: contain;\n border-radius: 0;\n}\n\n.mc-settings-popover {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 12px;\n background: rgba(20, 20, 20, 0.96);\n border-radius: 8px;\n padding: 10px 12px;\n min-width: 180px;\n color: #fff;\n font-size: 12px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.4);\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.mc-settings-popover[data-open=\"false\"] { display: none; }\n.mc-settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n.mc-settings-row > span { color: rgba(255,255,255,0.85); }\n.mc-settings-row > select {\n font: inherit;\n background: rgba(255,255,255,0.1);\n color: #fff;\n border: 1px solid rgba(255,255,255,0.18);\n border-radius: 4px;\n padding: 2px 6px;\n cursor: pointer;\n}\n.mc-export-confirm {\n margin-top: 4px;\n width: 100%;\n background: var(--mc-accent, #007AFF);\n color: #fff;\n border: 0;\n border-radius: 4px;\n padding: 6px 10px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: background 120ms ease;\n}\n.mc-export-confirm:hover { background: #0066d6; }\n.mc-export-confirm:disabled { background: rgba(255,255,255,0.18); cursor: not-allowed; }\n\n.mc-export-overlay {\n position: absolute; inset: 0;\n background: rgba(0,0,0,0.7);\n display: flex; justify-content: center; align-items: center;\n z-index: 2;\n}\n.mc-export-panel {\n background: rgba(20,20,20,0.96); color: #fff;\n border-radius: 8px; padding: 16px 20px;\n text-align: center; min-width: 200px; max-width: 80%;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n}\n.mc-export-title { font-size: 13px; margin-bottom: 10px; opacity: 0.9; }\n.mc-export-track {\n width: 100%; height: 6px;\n background: rgba(255,255,255,0.15); border-radius: 3px;\n margin-bottom: 8px; overflow: hidden;\n}\n.mc-export-fill {\n height: 100%; width: 0%;\n background: linear-gradient(90deg, var(--mc-accent, #007AFF), var(--mc-accent, #0056CC));\n transition: width 0.3s ease; border-radius: 3px;\n}\n.mc-export-text {\n color: rgba(255,255,255,0.7); font-size: 11px;\n font-family: Menlo, Monaco, monospace;\n}\n";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Application } from 'pixi.js';
|
|
2
2
|
import { gsap } from 'gsap';
|
|
3
|
-
import {
|
|
3
|
+
import { U as Stop, e as AssetSpec, c as CompositionSpec } from './types-C8XEUoqe.js';
|
|
4
4
|
|
|
5
5
|
interface AudioInspectOptions {
|
|
6
6
|
/** Width of each loudness window in seconds. Default 0.1. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Application } from 'pixi.js';
|
|
2
2
|
import { gsap } from 'gsap';
|
|
3
|
-
import {
|
|
3
|
+
import { U as Stop, e as AssetSpec, c as CompositionSpec } from './types-C8XEUoqe.cjs';
|
|
4
4
|
|
|
5
5
|
interface AudioInspectOptions {
|
|
6
6
|
/** Width of each loudness window in seconds. Default 0.1. */
|
package/dist/Presenter.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { f as Movie } from './Movie-
|
|
1
|
+
import { f as Movie } from './Movie-CJMfAizW.cjs';
|
|
2
2
|
import 'pixi.js';
|
|
3
3
|
import 'gsap';
|
|
4
|
-
import './types-
|
|
4
|
+
import './types-C8XEUoqe.cjs';
|
|
5
5
|
|
|
6
6
|
interface PresenterOptions {
|
|
7
7
|
canvas: HTMLCanvasElement;
|
package/dist/Presenter.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { f as Movie } from './Movie-
|
|
1
|
+
import { f as Movie } from './Movie-BIGDIla0.js';
|
|
2
2
|
import 'pixi.js';
|
|
3
3
|
import 'gsap';
|
|
4
|
-
import './types-
|
|
4
|
+
import './types-C8XEUoqe.js';
|
|
5
5
|
|
|
6
6
|
interface PresenterOptions {
|
|
7
7
|
canvas: HTMLCanvasElement;
|