pixi-effects 0.34.0 → 0.35.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 +9 -0
- package/README.md +5 -5
- package/ai/chat-template.html +3 -3
- package/dist/Controller.cjs +130 -1
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +23 -1
- package/dist/Controller.d.ts +23 -1
- package/dist/Controller.js +130 -1
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-CL6SsAWe.d.cts → Movie-Db4QnMIH.d.cts} +24 -1
- package/dist/{Movie-f0tQG9y5.d.ts → Movie-VYtUN4TS.d.ts} +24 -1
- package/dist/{chunk-67LG7MN5.js → chunk-2GNK6DPV.js} +86 -8
- package/dist/chunk-2GNK6DPV.js.map +1 -0
- package/dist/{chunk-FVZEBYLW.cjs → chunk-7TTCPHAV.cjs} +86 -8
- package/dist/chunk-7TTCPHAV.cjs.map +1 -0
- package/dist/index.cjs +24 -24
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/loader.css +1 -1
- package/dist/present.cjs +1 -1
- package/dist/present.d.cts +1 -1
- package/dist/present.d.ts +1 -1
- package/dist/present.js +1 -1
- package/docs/api.md +10 -0
- package/llms-full.txt +11 -1
- package/package.json +1 -1
- package/skills/pixi-effects/SKILL.md +1 -1
- package/skills/pixi-effects/template.html +6 -6
- package/dist/chunk-67LG7MN5.js.map +0 -1
- package/dist/chunk-FVZEBYLW.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.35.0
|
|
4
|
+
|
|
5
|
+
**The player's seek-bar preview (`thumbnails: true`, off by default) and `movie.thumbnail()`**
|
|
6
|
+
|
|
7
|
+
- **`movie.thumbnail(frame, { width? | scale? })`** gives a small canvas of any frame **without moving the playhead or changing the picture**, also while the movie plays. It draws the frame on the side inside one task (nominal time set, 3D / shader layers awaited, the playhead put back and awaited), so the browser never shows the in-between; it equals `snapshot({ motionBlur: false })` at that size. `null` when a seek, render or another thumbnail is running. Seeks, renders and peeks wait for a running thumbnail, so they never interleave with it.
|
|
8
|
+
- **`movie.thumbnailsOff`** is `'variables'` for a movie that declares variables and `'video'` for one with a video layer, else `null`; `thumbnail()` gives `null` then (a video would have to seek twice, a variable movie can be re-laid out under it).
|
|
9
|
+
- **`new Controller(movie, { canvas, thumbnails: true })`**: hover (or move along) the seek bar and a 160 px picture with the time appears above it, kept inside the picture, hidden while dragging; pictures are cached per tenth of a second until a rebuild. Default off.
|
|
10
|
+
- Tests: `tests/tools/thumbnail.test.ts` (exact against `snapshot`, undisturbed canvas and playhead, playback with a thumbnail per frame, the off reasons, options) and `tests/tools/thumbnailPreview.test.ts` (the bar, with real mouse moves).
|
|
11
|
+
|
|
3
12
|
## 0.34.0
|
|
4
13
|
|
|
5
14
|
**From the capability probe of 2026-10-12 (15 kinds of video tried by fresh agents; nothing was impossible, 9 needed hand-written glue; these are the defects that were verified on the real build)**
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pixi-effects
|
|
2
2
|
|
|
3
|
-
> **Status**: experimental — current release `0.
|
|
3
|
+
> **Status**: experimental — current release `0.35.0`. 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.
|
|
53
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
52
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.35.0/dist/index.js",
|
|
53
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.35.0/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.
|
|
64
|
+
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.35.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.
|
|
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.
|
|
80
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.35.0/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.
|
|
63
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
64
|
-
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
62
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.35.0/dist/index.js",
|
|
63
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.35.0/dist/Controller.js",
|
|
64
|
+
"pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.35.0/dist/present.js"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
</script>
|
package/dist/Controller.cjs
CHANGED
|
@@ -63,6 +63,19 @@ var CONTROLLER_CSS = `
|
|
|
63
63
|
.mc-progress:hover .mc-progress-thumb,
|
|
64
64
|
.mc-progress.mc-scrubbing .mc-progress-thumb { transform: translate(-50%, -50%) scale(1); }
|
|
65
65
|
|
|
66
|
+
.movie-controller > .mc-preview {
|
|
67
|
+
position: absolute; bottom: 74px; left: 0;
|
|
68
|
+
transform: translateX(-50%);
|
|
69
|
+
display: flex; flex-direction: column; align-items: center; gap: 4px;
|
|
70
|
+
padding: 3px; border-radius: 4px;
|
|
71
|
+
background: rgba(0,0,0,0.75);
|
|
72
|
+
pointer-events: none;
|
|
73
|
+
opacity: 0; transition: opacity 100ms ease;
|
|
74
|
+
}
|
|
75
|
+
.mc-preview[data-show="1"] { opacity: 1; }
|
|
76
|
+
.mc-preview canvas { display: block; width: 160px; height: auto; border-radius: 2px; }
|
|
77
|
+
.mc-preview-time { font-size: 12px; line-height: 1; color: var(--mc-fg, #fff); font-variant-numeric: tabular-nums; }
|
|
78
|
+
|
|
66
79
|
.mc-bar {
|
|
67
80
|
position: relative;
|
|
68
81
|
display: flex;
|
|
@@ -295,7 +308,7 @@ var THEME_PROPERTIES = {
|
|
|
295
308
|
trackHeight: "--mc-track-height",
|
|
296
309
|
font: "--mc-font"
|
|
297
310
|
};
|
|
298
|
-
var CONTROLLER_OPTION_KEYS = ["canvas", "theme", "exportButton", "keyboard", "clickToPlay", "presentButton", "pauseAtStops", "loopButton", "className"];
|
|
311
|
+
var CONTROLLER_OPTION_KEYS = ["canvas", "theme", "exportButton", "keyboard", "clickToPlay", "presentButton", "pauseAtStops", "loopButton", "thumbnails", "className"];
|
|
299
312
|
var Controller = class _Controller {
|
|
300
313
|
movie;
|
|
301
314
|
options;
|
|
@@ -341,6 +354,17 @@ var Controller = class _Controller {
|
|
|
341
354
|
onReady = null;
|
|
342
355
|
/** A rebuild (setVars, resize, rebuild) may have moved the stops: draw their marks again. */
|
|
343
356
|
onRebuilt = null;
|
|
357
|
+
previewEl = null;
|
|
358
|
+
previewCanvas = null;
|
|
359
|
+
previewTimeEl = null;
|
|
360
|
+
/** Pictures already made, by frame bucket (a frame is a pure function of time; cleared on a rebuild). */
|
|
361
|
+
previewCache = /* @__PURE__ */ new Map();
|
|
362
|
+
previewWanted = null;
|
|
363
|
+
previewRunning = false;
|
|
364
|
+
previewRetry = null;
|
|
365
|
+
previewRetries = 0;
|
|
366
|
+
previewCleanup = null;
|
|
367
|
+
static PREVIEW_WIDTH_PX = 160;
|
|
344
368
|
presenter = null;
|
|
345
369
|
/** `pixi-effects/present`, loaded as soon as the movie has stops (a press of play must not wait for it). */
|
|
346
370
|
present = null;
|
|
@@ -387,6 +411,7 @@ var Controller = class _Controller {
|
|
|
387
411
|
presentButton: options.presentButton ?? true,
|
|
388
412
|
pauseAtStops: options.pauseAtStops ?? true,
|
|
389
413
|
loopButton: options.loopButton ?? true,
|
|
414
|
+
thumbnails: options.thumbnails ?? false,
|
|
390
415
|
className: options.className ?? "movie-controller"
|
|
391
416
|
};
|
|
392
417
|
installStyles();
|
|
@@ -409,6 +434,7 @@ var Controller = class _Controller {
|
|
|
409
434
|
this.bindPlayButton();
|
|
410
435
|
if (this.options.clickToPlay) this.bindCanvasClick();
|
|
411
436
|
this.bindScrubbing();
|
|
437
|
+
if (this.options.thumbnails) this.bindPreview();
|
|
412
438
|
this.bindMuteButton();
|
|
413
439
|
this.bindLoopButton();
|
|
414
440
|
this.bindVolumeSlider();
|
|
@@ -588,6 +614,7 @@ var Controller = class _Controller {
|
|
|
588
614
|
};
|
|
589
615
|
this.onRebuilt = () => {
|
|
590
616
|
this.refreshStops();
|
|
617
|
+
this.previewCache.clear();
|
|
591
618
|
};
|
|
592
619
|
this.movie.on("ready", this.onReady);
|
|
593
620
|
this.movie.on("rebuilt", this.onRebuilt);
|
|
@@ -911,6 +938,104 @@ var Controller = class _Controller {
|
|
|
911
938
|
};
|
|
912
939
|
window.addEventListener("blur", this.windowBlurHandler);
|
|
913
940
|
}
|
|
941
|
+
/** The hover preview above the seek bar (`thumbnails: true`). */
|
|
942
|
+
bindPreview() {
|
|
943
|
+
const el = document.createElement("div");
|
|
944
|
+
el.className = "mc-preview";
|
|
945
|
+
el.setAttribute("aria-hidden", "true");
|
|
946
|
+
const canvas = document.createElement("canvas");
|
|
947
|
+
const time = document.createElement("span");
|
|
948
|
+
time.className = "mc-preview-time";
|
|
949
|
+
el.append(canvas, time);
|
|
950
|
+
this.root.appendChild(el);
|
|
951
|
+
this.previewEl = el;
|
|
952
|
+
this.previewCanvas = canvas;
|
|
953
|
+
this.previewTimeEl = time;
|
|
954
|
+
const hide = () => {
|
|
955
|
+
this.previewWanted = null;
|
|
956
|
+
el.removeAttribute("data-show");
|
|
957
|
+
};
|
|
958
|
+
const onMove = (e) => {
|
|
959
|
+
if (this.isScrubbing || this.movie.thumbnailsOff || e.pointerType === "touch") {
|
|
960
|
+
hide();
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const rect = this.progressEl.getBoundingClientRect();
|
|
964
|
+
const inset = _Controller.PROGRESS_INSET_PX;
|
|
965
|
+
const frame = pxToFrame(e.clientX, { left: rect.left + inset, width: Math.max(0, rect.width - 2 * inset) }, this.movie.totalFrames);
|
|
966
|
+
const rootRect = this.root.getBoundingClientRect();
|
|
967
|
+
const half = el.getBoundingClientRect().width / 2 + 2;
|
|
968
|
+
const x = Math.min(Math.max(e.clientX - rootRect.left, half), Math.max(half, rootRect.width - half));
|
|
969
|
+
el.style.left = `${rootRect.width > 0 ? x / rootRect.width * 100 : 50}%`;
|
|
970
|
+
this.previewRetries = 0;
|
|
971
|
+
time.textContent = formatTime(frame / (this.movie.frameRate || 1));
|
|
972
|
+
this.previewWanted = frame;
|
|
973
|
+
void this.runPreview();
|
|
974
|
+
};
|
|
975
|
+
this.progressEl.addEventListener("pointermove", onMove);
|
|
976
|
+
this.progressEl.addEventListener("pointerleave", hide);
|
|
977
|
+
this.progressEl.addEventListener("pointerdown", hide);
|
|
978
|
+
this.previewCleanup = () => {
|
|
979
|
+
this.progressEl.removeEventListener("pointermove", onMove);
|
|
980
|
+
this.progressEl.removeEventListener("pointerleave", hide);
|
|
981
|
+
this.progressEl.removeEventListener("pointerdown", hide);
|
|
982
|
+
if (this.previewRetry) {
|
|
983
|
+
clearTimeout(this.previewRetry);
|
|
984
|
+
this.previewRetry = null;
|
|
985
|
+
}
|
|
986
|
+
el.remove();
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/** Draws the picture of the wanted frame into the preview; one at a time, and the last wish wins. */
|
|
990
|
+
async runPreview() {
|
|
991
|
+
if (this.previewRunning) return;
|
|
992
|
+
this.previewRunning = true;
|
|
993
|
+
try {
|
|
994
|
+
while (this.previewWanted !== null && !this.destroyed) {
|
|
995
|
+
const wanted = this.previewWanted;
|
|
996
|
+
const fps = this.movie.frameRate || 30;
|
|
997
|
+
const step = Math.max(1, Math.round(fps / 10));
|
|
998
|
+
const key = Math.min(this.movie.totalFrames, Math.round(wanted / step) * step);
|
|
999
|
+
let pic = this.previewCache.get(key) ?? null;
|
|
1000
|
+
if (!pic) {
|
|
1001
|
+
try {
|
|
1002
|
+
pic = await this.movie.thumbnail(key, { width: _Controller.PREVIEW_WIDTH_PX });
|
|
1003
|
+
} catch {
|
|
1004
|
+
this.previewWanted = null;
|
|
1005
|
+
this.previewEl?.removeAttribute("data-show");
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
if (pic) {
|
|
1009
|
+
if (this.previewCache.size >= 120) this.previewCache.delete(this.previewCache.keys().next().value);
|
|
1010
|
+
this.previewCache.set(key, pic);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (this.previewWanted === null || this.destroyed) break;
|
|
1014
|
+
if (pic) {
|
|
1015
|
+
const c = this.previewCanvas;
|
|
1016
|
+
if (c.width !== pic.width || c.height !== pic.height) {
|
|
1017
|
+
c.width = pic.width;
|
|
1018
|
+
c.height = pic.height;
|
|
1019
|
+
}
|
|
1020
|
+
c.getContext("2d").drawImage(pic, 0, 0);
|
|
1021
|
+
this.previewEl.setAttribute("data-show", "1");
|
|
1022
|
+
if (this.previewWanted === wanted) this.previewWanted = null;
|
|
1023
|
+
} else if (this.previewWanted !== wanted) {
|
|
1024
|
+
} else if (!this.movie.thumbnailsOff && this.previewRetries++ < 10) {
|
|
1025
|
+
this.previewRetry = setTimeout(() => {
|
|
1026
|
+
this.previewRetry = null;
|
|
1027
|
+
void this.runPreview();
|
|
1028
|
+
}, 120);
|
|
1029
|
+
break;
|
|
1030
|
+
} else {
|
|
1031
|
+
this.previewWanted = null;
|
|
1032
|
+
this.previewEl?.removeAttribute("data-show");
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
} finally {
|
|
1036
|
+
this.previewRunning = false;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
914
1039
|
seekFromPointer(clientX) {
|
|
915
1040
|
const rect = this.progressEl.getBoundingClientRect();
|
|
916
1041
|
const inset = _Controller.PROGRESS_INSET_PX;
|
|
@@ -1187,6 +1312,10 @@ var Controller = class _Controller {
|
|
|
1187
1312
|
if (this.destroyed) return;
|
|
1188
1313
|
this.destroyed = true;
|
|
1189
1314
|
this.stopPresenting();
|
|
1315
|
+
if (this.previewCleanup) {
|
|
1316
|
+
this.previewCleanup();
|
|
1317
|
+
this.previewCleanup = null;
|
|
1318
|
+
}
|
|
1190
1319
|
if (this.onReady) {
|
|
1191
1320
|
this.movie.off("ready", this.onReady);
|
|
1192
1321
|
this.onReady = null;
|