pixi-effects 0.30.0 → 0.30.1
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 +10 -0
- package/README.md +5 -5
- package/ai/chat-template.html +3 -3
- package/ai/tools/lookup.mjs +12 -6
- package/dist/loader.css +1 -1
- package/llms-full.txt +28 -9
- package/package.json +1 -1
- package/skills/pixi-effects/SKILL.md +1 -1
- package/skills/pixi-effects/reference/recipes.md +27 -8
- package/skills/pixi-effects/template.html +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.30.1
|
|
4
|
+
|
|
5
|
+
**Docs**
|
|
6
|
+
|
|
7
|
+
- **Found by six fresh agents (three before 0.30.0's docs, three after):** a recipe for **a plain cut with generated images and music written as text** (`slideshow-cuts`; the slideshow recipe had only transitions and a music file); the **bar chart** recipe grows its bars one at a time with `EACH = DUR + 0.3` (the highlight and the callout wait for the last bar: `PEAK_AT`); `AGENTS.md` and the skill now agree that the check writes to `check-out/<name>/` (the example passed `--out check-out`).
|
|
8
|
+
|
|
9
|
+
**Fixed**
|
|
10
|
+
|
|
11
|
+
- `pixi-effects recipe` listed only the recipes whose code block had a name, so three (`deck-in-page`, `placeholder-images`, `metallic-three`) were invisible to it; every section is listed now (a section without a name gets one made from its title), and a test requires every section of `recipes.md` to have a name.
|
|
12
|
+
|
|
3
13
|
## 0.30.0
|
|
4
14
|
|
|
5
15
|
**Added**
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pixi-effects
|
|
2
2
|
|
|
3
|
-
> **Status**: experimental — current release `0.30.
|
|
3
|
+
> **Status**: experimental — current release `0.30.1`. The API may still change between minor versions; see the [CHANGELOG](./CHANGELOG.md) for what each release changed.
|
|
4
4
|
|
|
5
5
|
**[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 52 portfolio pieces written as plain data by AI models · **[Examples →](https://yjmtmtk.github.io/pixi-effects/examples/)** · 15 numbered examples, a music lab + an in-browser playground.
|
|
6
6
|
|
|
@@ -49,8 +49,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
49
49
|
"imports": {
|
|
50
50
|
"pixi.js": "https://esm.sh/pixi.js@8.22.0?bundle-deps",
|
|
51
51
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
52
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.
|
|
53
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.
|
|
52
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/dist/index.js",
|
|
53
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/dist/Controller.js"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
</script>
|
|
@@ -61,7 +61,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
61
61
|
</script>
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.30.
|
|
64
|
+
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.30.1/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.
|
|
65
65
|
|
|
66
66
|
> **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
|
|
67
67
|
>
|
|
@@ -77,7 +77,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
|
|
|
77
77
|
"imports": {
|
|
78
78
|
"...": "(everything from the importmap above)",
|
|
79
79
|
"three": "https://esm.sh/three@0.178.0",
|
|
80
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.
|
|
80
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/dist/three.js"
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
</script>
|
package/ai/chat-template.html
CHANGED
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"imports": {
|
|
60
60
|
"pixi.js": "https://cdn.jsdelivr.net/npm/pixi.js@8.22.0/dist/pixi.min.mjs",
|
|
61
61
|
"mediabunny": "https://cdn.jsdelivr.net/npm/mediabunny@1.61.3/+esm",
|
|
62
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.
|
|
63
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.
|
|
64
|
-
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.
|
|
62
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/dist/index.js",
|
|
63
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/dist/Controller.js",
|
|
64
|
+
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/dist/present.js"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
</script>
|
package/ai/tools/lookup.mjs
CHANGED
|
@@ -53,15 +53,21 @@ export function parseRecipes(md) {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** The name `recipe` lists a section by: its first id, or a slug of its title when it has none (so no recipe hides). */
|
|
57
|
+
export function recipeId(section) {
|
|
58
|
+
return section.ids[0] ?? section.title.toLowerCase().replace(/`/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
/** { section, id } for an exact id or a unique part of one (or of a title); { many } when several match; { nearest } (maybe undefined) when none does. */
|
|
57
62
|
export function findRecipe(sections, query) {
|
|
58
63
|
const q = query.trim().toLowerCase();
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
const names = (s) => [...s.ids, recipeId(s)];
|
|
65
|
+
const exact = sections.find(s => names(s).some(id => id.toLowerCase() === q));
|
|
66
|
+
if (exact) return { section: exact, id: names(exact).find(id => id.toLowerCase() === q) };
|
|
67
|
+
const part = sections.filter(s => names(s).some(id => id.toLowerCase().includes(q)) || s.title.toLowerCase().includes(q));
|
|
62
68
|
if (part.length === 1) return { section: part[0] };
|
|
63
69
|
if (part.length > 1) return { many: part };
|
|
64
|
-
return { nearest: nearestOf(q, sections.flatMap(
|
|
70
|
+
return { nearest: nearestOf(q, sections.flatMap(names)) };
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
const readIf = (file) => { try { return fs.readFileSync(file, 'utf8'); } catch { return null; } };
|
|
@@ -164,12 +170,12 @@ export function runLookup(cmd, args, { cwd = process.cwd(), root = PACKAGE_ROOT
|
|
|
164
170
|
const query = args.join(' ').trim();
|
|
165
171
|
if (!query) {
|
|
166
172
|
const lines = ['recipes (pixi-effects recipe <id> prints one with its code):'];
|
|
167
|
-
for (const s of sections)
|
|
173
|
+
for (const s of sections) (s.ids.length ? s.ids : [recipeId(s)]).forEach((id, k) => lines.push(` ${id.padEnd(28)} ${k === 0 ? s.title : `(the same section as ${recipeId(s)})`}`));
|
|
168
174
|
return ok(lines.join('\n'));
|
|
169
175
|
}
|
|
170
176
|
const r = findRecipe(sections, query);
|
|
171
177
|
if (r.section) return ok(r.section.text);
|
|
172
|
-
if (r.many) return fail(`several recipes match "${query}": ${r.many.map(s => s
|
|
178
|
+
if (r.many) return fail(`several recipes match "${query}": ${r.many.map(s => recipeId(s)).join(', ')}. Give one id (pixi-effects recipe lists them).`);
|
|
173
179
|
return fail(`no recipe "${query}"${r.nearest ? `; did you mean "${r.nearest}"?` : ''} (pixi-effects recipe lists them)`);
|
|
174
180
|
}
|
|
175
181
|
if (cmd === 'help') {
|
package/dist/loader.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* pixi-effects loader: one line of HTML on the page, a few lines of CSS, no JavaScript of your own.
|
|
3
3
|
*
|
|
4
|
-
* <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.30.
|
|
4
|
+
* <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/dist/loader.css">
|
|
5
5
|
* <div class="stage" style="position: relative"> <!-- any box the canvas sits in, with position: relative -->
|
|
6
6
|
* <canvas id="stage" width="1280" height="720"></canvas>
|
|
7
7
|
* <div class="pe-loader" data-label="LOADING"></div> <!-- or class="pe-loader pe-loader--bar" -->
|
package/llms-full.txt
CHANGED
|
@@ -6,7 +6,7 @@ Concatenation of the skill, cheatsheet, pitfalls, recipes and the DSL / API refe
|
|
|
6
6
|
|
|
7
7
|
<!-- ===== skills/pixi-effects/SKILL.md ===== -->
|
|
8
8
|
|
|
9
|
-
> Written for pixi-effects 0.30.
|
|
9
|
+
> Written for pixi-effects 0.30.1. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
|
|
10
10
|
|
|
11
11
|
# pixi-effects — write a video as data
|
|
12
12
|
|
|
@@ -622,6 +622,8 @@ const max = Math.max(...data.map(d => d[1]));
|
|
|
622
622
|
const step = [1, 2, 2.5, 5, 10].map(m => m * 10 ** Math.floor(Math.log10(max / 4))).find(v => max / v <= 5) ?? 10;
|
|
623
623
|
const NICE = Math.ceil(max / step) * step; // axis top, so the chart survives changed data
|
|
624
624
|
const BASE = 600, CHART_H = 380, BAR_W = 90, GAP = 40, DUR = 0.9, EASE = 'power3.out';
|
|
625
|
+
const EACH = 0.12; // seconds between bars: 0.12 is a quick wave; EACH = DUR + 0.3 grows them one at a time
|
|
626
|
+
const PEAK_AT = Math.max(4, 0.4 + (data.length - 1) * EACH + DUR + 0.2); // the highlight and the callout come after the last bar has grown
|
|
625
627
|
const X0 = (1280 - (data.length * BAR_W + (data.length - 1) * GAP)) / 2, X1 = X0 + data.length * BAR_W + (data.length - 1) * GAP;
|
|
626
628
|
const sequences = [];
|
|
627
629
|
for (let v = 0; v <= NICE; v += step) { // grid lines + tick labels
|
|
@@ -632,14 +634,14 @@ for (let v = 0; v <= NICE; v += step) { // grid lines + ti
|
|
|
632
634
|
const best = data.findIndex(d => d[1] === max);
|
|
633
635
|
data.forEach(([label, value], i) => {
|
|
634
636
|
const x = X0 + i * (BAR_W + GAP) + BAR_W / 2;
|
|
635
|
-
const at = 0.4 + i *
|
|
637
|
+
const at = 0.4 + i * EACH;
|
|
636
638
|
const h = CHART_H * value / NICE;
|
|
637
639
|
sequences.push({
|
|
638
640
|
type: 'shape', shape: 'rect', width: BAR_W, height: 0, cornerRadius: 8, anchorY: 1, at, colorSpace: 'oklab',
|
|
639
641
|
initial: { x, y: BASE, fillColor: '#4f6df5' },
|
|
640
642
|
keyframes: [
|
|
641
643
|
{ at: 0, to: { height: h }, duration: DUR, ease: EASE },
|
|
642
|
-
...(i === best ? [{ at:
|
|
644
|
+
...(i === best ? [{ at: PEAK_AT - at, to: { fillColor: '#ffd166' }, duration: 0.5 }] : [{ at: PEAK_AT - at, to: { alpha: 0.45 }, duration: 0.5 }]), // highlight the maximum at t = 4 s
|
|
643
645
|
],
|
|
644
646
|
});
|
|
645
647
|
sequences.push({ // value label: counts up while riding the bar's top
|
|
@@ -653,11 +655,11 @@ data.forEach(([label, value], i) => {
|
|
|
653
655
|
// callout: a pill sized from its text (~0.58 x fontSize per glyph + padding), a pointer triangle, clamped inside the plot
|
|
654
656
|
const text = 'Peak: ' + data[best][0] + ' ' + max, FS = 26;
|
|
655
657
|
const PW = text.length * FS * 0.58 + 44, cx = Math.min(Math.max(X0 + best * (BAR_W + GAP) + BAR_W / 2, X0 + PW / 2), X1 - PW / 2), cy = BASE - CHART_H - 70;
|
|
656
|
-
sequences.push({ type: 'shape', shape: 'rect', width: PW, height: 52, cornerRadius: 26, at:
|
|
658
|
+
sequences.push({ type: 'shape', shape: 'rect', width: PW, height: 52, cornerRadius: 26, at: PEAK_AT + 0.2, initial: { x: cx, y: cy, fillColor: '#ffd166', alpha: 0 },
|
|
657
659
|
keyframes: [{ at: 0, to: { alpha: 1 }, duration: 0.3 }] });
|
|
658
|
-
sequences.push({ type: 'shape', shape: 'polygon', points: [[-10, 0], [10, 0], [0, 12]], at:
|
|
660
|
+
sequences.push({ type: 'shape', shape: 'polygon', points: [[-10, 0], [10, 0], [0, 12]], at: PEAK_AT + 0.2, initial: { x: cx, y: cy + 32, fillColor: '#ffd166', alpha: 0 },
|
|
659
661
|
keyframes: [{ at: 0, to: { alpha: 1 }, duration: 0.3 }] });
|
|
660
|
-
sequences.push({ type: 'text', text, at:
|
|
662
|
+
sequences.push({ type: 'text', text, at: PEAK_AT + 0.2, style: { fontSize: FS, fontWeight: 'bold', fill: '#1b1b2f' }, initial: { x: cx, y: cy, anchorX: 0.5, anchorY: 0.5, alpha: 0 },
|
|
661
663
|
keyframes: [{ at: 0, to: { alpha: 1 }, duration: 0.3 }] });
|
|
662
664
|
return sequences;
|
|
663
665
|
```
|
|
@@ -788,6 +790,23 @@ return { sequences, transitions, duration: total };
|
|
|
788
790
|
|
|
789
791
|
Source images should be at least canvas-sized (`kenBurns` zooms in): draw generated images at 1920×1080 for a 1280×720 movie.
|
|
790
792
|
|
|
793
|
+
**A plain cut instead of a transition, generated images, music with no file:** scenes are layers one after the other (`at` = `i × L`) with no `transitions`; the images are the assets `p1…p3` (draw them with `placeholderPhoto`, recipe `placeholder-images`). The tune is written as text: at 96 bpm a beat is 0.625 s, so 24 beats are 15 s and every track has to add up to 24 (the check's `music` read-back shows each track's length).
|
|
794
|
+
|
|
795
|
+
```js
|
|
796
|
+
// @recipe slideshow-cuts
|
|
797
|
+
const N = 3, L = 5, total = N * L;
|
|
798
|
+
const sequences = Array.from({ length: N }, (_, i) =>
|
|
799
|
+
kenBurns({ asset: 'p' + (i + 1), name: 'scene' + i, at: i * L, duration: L, motion: i % 2 ? 'position' : 'scale' }));
|
|
800
|
+
sequences.push({ type: 'audio', at: 0, volume: 0.7, duration: total, music: {
|
|
801
|
+
bpm: 96,
|
|
802
|
+
tracks: [
|
|
803
|
+
{ inst: 'pad', notes: 'Am:4 F:4 C:4 G:4 Am:4 F:4' }, // 6 chords x 4 beats = 24 beats = 15 s
|
|
804
|
+
{ inst: 'bass', notes: 'a1:4 f1:4 c2:4 g1:4 a1:4 f1:4' },
|
|
805
|
+
],
|
|
806
|
+
} });
|
|
807
|
+
return { sequences, duration: total };
|
|
808
|
+
```
|
|
809
|
+
|
|
791
810
|
---
|
|
792
811
|
|
|
793
812
|
## Music with no file (`music`)
|
|
@@ -1088,7 +1107,7 @@ return { duration: d.duration, sequences: d.composition.sequences, transitions:
|
|
|
1088
1107
|
```
|
|
1089
1108
|
|
|
1090
1109
|
```js
|
|
1091
|
-
// @docs-only
|
|
1110
|
+
// @docs-only deck-in-page
|
|
1092
1111
|
// in the page: the deck is spread into init; the Presenter (or the Controller, which then shows the stops and a Present button) plays it
|
|
1093
1112
|
import { Movie } from 'pixi-effects';
|
|
1094
1113
|
import { deck, Presenter } from 'pixi-effects/present'; // (add "pixi-effects/present" to the import map)
|
|
@@ -1457,7 +1476,7 @@ return [{ type: 'composition', name: 'film', width: 'GW', height: 'GH', sequence
|
|
|
1457
1476
|
Draw on a canvas and register `canvas.toDataURL()` as an asset (`data:` URLs work). Make the image at least canvas-sized (1920×1080 for a 1280×720 movie) so `kenBurns` zooms stay sharp.
|
|
1458
1477
|
|
|
1459
1478
|
```js
|
|
1460
|
-
// @docs-only
|
|
1479
|
+
// @docs-only placeholder-images
|
|
1461
1480
|
function placeholderPhoto(name, hueA, hueB, w = 1920, h = 1080) {
|
|
1462
1481
|
const c = Object.assign(document.createElement('canvas'), { width: w, height: h });
|
|
1463
1482
|
const g = c.getContext('2d');
|
|
@@ -1476,7 +1495,7 @@ function placeholderPhoto(name, hueA, hueB, w = 1920, h = 1080) {
|
|
|
1476
1495
|
`ctx.renderer` is a normal `WebGLRenderer`, so an environment map works; lights alone make `metalness: 1` look black. `three/addons` is not in the importmap: build the environment yourself. Size the layer to the area you want (it clips at its own rectangle) and pull the camera back (`z ≈ 5.4` for a radius-1 torus knot at fov 50).
|
|
1477
1496
|
|
|
1478
1497
|
```js
|
|
1479
|
-
// @docs-only
|
|
1498
|
+
// @docs-only metallic-three
|
|
1480
1499
|
// registerThree(); const THREE = await import('three');
|
|
1481
1500
|
three({
|
|
1482
1501
|
type: 'three', width: 'GW * 0.56', height: 'GH * 0.9', initial: { x: 'GW * 0.77', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ 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.30.
|
|
6
|
+
> Written for pixi-effects 0.30.1. 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
|
|
|
@@ -190,6 +190,8 @@ const max = Math.max(...data.map(d => d[1]));
|
|
|
190
190
|
const step = [1, 2, 2.5, 5, 10].map(m => m * 10 ** Math.floor(Math.log10(max / 4))).find(v => max / v <= 5) ?? 10;
|
|
191
191
|
const NICE = Math.ceil(max / step) * step; // axis top, so the chart survives changed data
|
|
192
192
|
const BASE = 600, CHART_H = 380, BAR_W = 90, GAP = 40, DUR = 0.9, EASE = 'power3.out';
|
|
193
|
+
const EACH = 0.12; // seconds between bars: 0.12 is a quick wave; EACH = DUR + 0.3 grows them one at a time
|
|
194
|
+
const PEAK_AT = Math.max(4, 0.4 + (data.length - 1) * EACH + DUR + 0.2); // the highlight and the callout come after the last bar has grown
|
|
193
195
|
const X0 = (1280 - (data.length * BAR_W + (data.length - 1) * GAP)) / 2, X1 = X0 + data.length * BAR_W + (data.length - 1) * GAP;
|
|
194
196
|
const sequences = [];
|
|
195
197
|
for (let v = 0; v <= NICE; v += step) { // grid lines + tick labels
|
|
@@ -200,14 +202,14 @@ for (let v = 0; v <= NICE; v += step) { // grid lines + ti
|
|
|
200
202
|
const best = data.findIndex(d => d[1] === max);
|
|
201
203
|
data.forEach(([label, value], i) => {
|
|
202
204
|
const x = X0 + i * (BAR_W + GAP) + BAR_W / 2;
|
|
203
|
-
const at = 0.4 + i *
|
|
205
|
+
const at = 0.4 + i * EACH;
|
|
204
206
|
const h = CHART_H * value / NICE;
|
|
205
207
|
sequences.push({
|
|
206
208
|
type: 'shape', shape: 'rect', width: BAR_W, height: 0, cornerRadius: 8, anchorY: 1, at, colorSpace: 'oklab',
|
|
207
209
|
initial: { x, y: BASE, fillColor: '#4f6df5' },
|
|
208
210
|
keyframes: [
|
|
209
211
|
{ at: 0, to: { height: h }, duration: DUR, ease: EASE },
|
|
210
|
-
...(i === best ? [{ at:
|
|
212
|
+
...(i === best ? [{ at: PEAK_AT - at, to: { fillColor: '#ffd166' }, duration: 0.5 }] : [{ at: PEAK_AT - at, to: { alpha: 0.45 }, duration: 0.5 }]), // highlight the maximum at t = 4 s
|
|
211
213
|
],
|
|
212
214
|
});
|
|
213
215
|
sequences.push({ // value label: counts up while riding the bar's top
|
|
@@ -221,11 +223,11 @@ data.forEach(([label, value], i) => {
|
|
|
221
223
|
// callout: a pill sized from its text (~0.58 x fontSize per glyph + padding), a pointer triangle, clamped inside the plot
|
|
222
224
|
const text = 'Peak: ' + data[best][0] + ' ' + max, FS = 26;
|
|
223
225
|
const PW = text.length * FS * 0.58 + 44, cx = Math.min(Math.max(X0 + best * (BAR_W + GAP) + BAR_W / 2, X0 + PW / 2), X1 - PW / 2), cy = BASE - CHART_H - 70;
|
|
224
|
-
sequences.push({ type: 'shape', shape: 'rect', width: PW, height: 52, cornerRadius: 26, at:
|
|
226
|
+
sequences.push({ type: 'shape', shape: 'rect', width: PW, height: 52, cornerRadius: 26, at: PEAK_AT + 0.2, initial: { x: cx, y: cy, fillColor: '#ffd166', alpha: 0 },
|
|
225
227
|
keyframes: [{ at: 0, to: { alpha: 1 }, duration: 0.3 }] });
|
|
226
|
-
sequences.push({ type: 'shape', shape: 'polygon', points: [[-10, 0], [10, 0], [0, 12]], at:
|
|
228
|
+
sequences.push({ type: 'shape', shape: 'polygon', points: [[-10, 0], [10, 0], [0, 12]], at: PEAK_AT + 0.2, initial: { x: cx, y: cy + 32, fillColor: '#ffd166', alpha: 0 },
|
|
227
229
|
keyframes: [{ at: 0, to: { alpha: 1 }, duration: 0.3 }] });
|
|
228
|
-
sequences.push({ type: 'text', text, at:
|
|
230
|
+
sequences.push({ type: 'text', text, at: PEAK_AT + 0.2, style: { fontSize: FS, fontWeight: 'bold', fill: '#1b1b2f' }, initial: { x: cx, y: cy, anchorX: 0.5, anchorY: 0.5, alpha: 0 },
|
|
229
231
|
keyframes: [{ at: 0, to: { alpha: 1 }, duration: 0.3 }] });
|
|
230
232
|
return sequences;
|
|
231
233
|
```
|
|
@@ -356,6 +358,23 @@ return { sequences, transitions, duration: total };
|
|
|
356
358
|
|
|
357
359
|
Source images should be at least canvas-sized (`kenBurns` zooms in): draw generated images at 1920×1080 for a 1280×720 movie.
|
|
358
360
|
|
|
361
|
+
**A plain cut instead of a transition, generated images, music with no file:** scenes are layers one after the other (`at` = `i × L`) with no `transitions`; the images are the assets `p1…p3` (draw them with `placeholderPhoto`, recipe `placeholder-images`). The tune is written as text: at 96 bpm a beat is 0.625 s, so 24 beats are 15 s and every track has to add up to 24 (the check's `music` read-back shows each track's length).
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
// @recipe slideshow-cuts
|
|
365
|
+
const N = 3, L = 5, total = N * L;
|
|
366
|
+
const sequences = Array.from({ length: N }, (_, i) =>
|
|
367
|
+
kenBurns({ asset: 'p' + (i + 1), name: 'scene' + i, at: i * L, duration: L, motion: i % 2 ? 'position' : 'scale' }));
|
|
368
|
+
sequences.push({ type: 'audio', at: 0, volume: 0.7, duration: total, music: {
|
|
369
|
+
bpm: 96,
|
|
370
|
+
tracks: [
|
|
371
|
+
{ inst: 'pad', notes: 'Am:4 F:4 C:4 G:4 Am:4 F:4' }, // 6 chords x 4 beats = 24 beats = 15 s
|
|
372
|
+
{ inst: 'bass', notes: 'a1:4 f1:4 c2:4 g1:4 a1:4 f1:4' },
|
|
373
|
+
],
|
|
374
|
+
} });
|
|
375
|
+
return { sequences, duration: total };
|
|
376
|
+
```
|
|
377
|
+
|
|
359
378
|
---
|
|
360
379
|
|
|
361
380
|
## Music with no file (`music`)
|
|
@@ -656,7 +675,7 @@ return { duration: d.duration, sequences: d.composition.sequences, transitions:
|
|
|
656
675
|
```
|
|
657
676
|
|
|
658
677
|
```js
|
|
659
|
-
// @docs-only
|
|
678
|
+
// @docs-only deck-in-page
|
|
660
679
|
// in the page: the deck is spread into init; the Presenter (or the Controller, which then shows the stops and a Present button) plays it
|
|
661
680
|
import { Movie } from 'pixi-effects';
|
|
662
681
|
import { deck, Presenter } from 'pixi-effects/present'; // (add "pixi-effects/present" to the import map)
|
|
@@ -1025,7 +1044,7 @@ return [{ type: 'composition', name: 'film', width: 'GW', height: 'GH', sequence
|
|
|
1025
1044
|
Draw on a canvas and register `canvas.toDataURL()` as an asset (`data:` URLs work). Make the image at least canvas-sized (1920×1080 for a 1280×720 movie) so `kenBurns` zooms stay sharp.
|
|
1026
1045
|
|
|
1027
1046
|
```js
|
|
1028
|
-
// @docs-only
|
|
1047
|
+
// @docs-only placeholder-images
|
|
1029
1048
|
function placeholderPhoto(name, hueA, hueB, w = 1920, h = 1080) {
|
|
1030
1049
|
const c = Object.assign(document.createElement('canvas'), { width: w, height: h });
|
|
1031
1050
|
const g = c.getContext('2d');
|
|
@@ -1044,7 +1063,7 @@ function placeholderPhoto(name, hueA, hueB, w = 1920, h = 1080) {
|
|
|
1044
1063
|
`ctx.renderer` is a normal `WebGLRenderer`, so an environment map works; lights alone make `metalness: 1` look black. `three/addons` is not in the importmap: build the environment yourself. Size the layer to the area you want (it clips at its own rectangle) and pull the camera back (`z ≈ 5.4` for a radius-1 torus knot at fov 50).
|
|
1045
1064
|
|
|
1046
1065
|
```js
|
|
1047
|
-
// @docs-only
|
|
1066
|
+
// @docs-only metallic-three
|
|
1048
1067
|
// registerThree(); const THREE = await import('three');
|
|
1049
1068
|
three({
|
|
1050
1069
|
type: 'three', width: 'GW * 0.56', height: 'GH * 0.9', initial: { x: 'GW * 0.77', y: 'GH/2', anchorX: 0.5, anchorY: 0.5 },
|
|
@@ -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.30.
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/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.30.
|
|
41
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.
|
|
42
|
-
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.
|
|
43
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.
|
|
40
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/dist/index.js",
|
|
41
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/dist/Controller.js",
|
|
42
|
+
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/dist/present.js",
|
|
43
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/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.30.
|
|
49
|
+
<link rel="modulepreload" href="https://cdn.jsdelivr.net/npm/pixi-effects@0.30.1/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';
|