pixi-effects 0.33.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +5 -5
  3. package/ai/chat-template.html +3 -3
  4. package/dist/Composition-NDXKQHB4.js +7 -0
  5. package/dist/{Composition-7XNPTM7L.js.map → Composition-NDXKQHB4.js.map} +1 -1
  6. package/dist/Composition-OLH7RC5Z.cjs +16 -0
  7. package/dist/{Composition-L6OALWHI.cjs.map → Composition-OLH7RC5Z.cjs.map} +1 -1
  8. package/dist/Controller.cjs +130 -1
  9. package/dist/Controller.cjs.map +1 -1
  10. package/dist/Controller.d.cts +23 -1
  11. package/dist/Controller.d.ts +23 -1
  12. package/dist/Controller.js +130 -1
  13. package/dist/Controller.js.map +1 -1
  14. package/dist/{Movie-CL6SsAWe.d.cts → Movie-Db4QnMIH.d.cts} +24 -1
  15. package/dist/{Movie-f0tQG9y5.d.ts → Movie-VYtUN4TS.d.ts} +24 -1
  16. package/dist/{Spans-KPZ2QDCN.js → Spans-QHHWC2PB.js} +4 -4
  17. package/dist/{Spans-KPZ2QDCN.js.map → Spans-QHHWC2PB.js.map} +1 -1
  18. package/dist/{Spans-VXNQXSI3.cjs → Spans-QRMN2EZO.cjs} +9 -9
  19. package/dist/{Spans-VXNQXSI3.cjs.map → Spans-QRMN2EZO.cjs.map} +1 -1
  20. package/dist/{chunk-WST432AP.js → chunk-2GNK6DPV.js} +88 -10
  21. package/dist/chunk-2GNK6DPV.js.map +1 -0
  22. package/dist/{chunk-SBBXKPBP.cjs → chunk-5IJENZOG.cjs} +62 -62
  23. package/dist/{chunk-SBBXKPBP.cjs.map → chunk-5IJENZOG.cjs.map} +1 -1
  24. package/dist/{chunk-LD4VKNPQ.cjs → chunk-7TTCPHAV.cjs} +120 -42
  25. package/dist/chunk-7TTCPHAV.cjs.map +1 -0
  26. package/dist/{chunk-BVMZUPDU.cjs → chunk-LEECUCTG.cjs} +22 -5
  27. package/dist/chunk-LEECUCTG.cjs.map +1 -0
  28. package/dist/{chunk-BMH4OCDX.js → chunk-XV4L72FW.js} +3 -3
  29. package/dist/{chunk-BMH4OCDX.js.map → chunk-XV4L72FW.js.map} +1 -1
  30. package/dist/{chunk-E53QWHBK.js → chunk-Y253JQOH.js} +22 -5
  31. package/dist/chunk-Y253JQOH.js.map +1 -0
  32. package/dist/index.cjs +31 -31
  33. package/dist/index.d.cts +2 -2
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.js +3 -3
  36. package/dist/loader.css +1 -1
  37. package/dist/present.cjs +3 -3
  38. package/dist/present.d.cts +1 -1
  39. package/dist/present.d.ts +1 -1
  40. package/dist/present.js +3 -3
  41. package/dist/three.cjs +5 -5
  42. package/dist/three.js +1 -1
  43. package/docs/api.md +10 -0
  44. package/docs/dsl.md +7 -2
  45. package/llms-full.txt +20 -3
  46. package/package.json +1 -1
  47. package/skills/pixi-effects/SKILL.md +1 -1
  48. package/skills/pixi-effects/reference/pitfalls.md +2 -0
  49. package/skills/pixi-effects/template.html +6 -6
  50. package/dist/Composition-7XNPTM7L.js +0 -7
  51. package/dist/Composition-L6OALWHI.cjs +0 -16
  52. package/dist/chunk-BVMZUPDU.cjs.map +0 -1
  53. package/dist/chunk-E53QWHBK.js.map +0 -1
  54. package/dist/chunk-LD4VKNPQ.cjs.map +0 -1
  55. package/dist/chunk-WST432AP.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
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
+
12
+ ## 0.34.0
13
+
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)**
15
+
16
+ - **Fixed (docs + a warning): `bulgePinch.center` is a fraction of the filtered layer (0 to 1), not canvas pixels.** The docs said pixels; a pixel value such as `{ x: 900, y: 500 }` moved the bulge out of the layer and nothing showed, silently (a trial lost three runs on it). The docs now say so (`docs/dsl.md`, the filters guide, pitfall 110), and a centre with a component beyond 3 warns: `bulgePinch.center is 0-1 of the layer ({ x: 0.5, y: 0.5 } is its middle), but {"x":900,"y":500} reads as pixels`. `twist.offset`, `zoomBlur.center` and `shockwave.center` were measured and are pixels, as documented.
17
+ - **Docs, facts only: right-to-left and complex scripts** (`docs/dsl.md` Text, pitfall 110). Arabic, Hebrew, Thai and Devanagari are shaped correctly with a font that covers the script; marks that hang below the font's box can be cut off (the dots of an Arabic ي: set `style.padding`; `check` cannot see it); `splitText` / `animateText` place words left to right, so an Arabic line comes out reversed; `visibleChars` shapes each prefix alone. Not changed in the code: nothing asked for it yet.
18
+ - **Fixed: `radialBlur` did nothing on WebGPU, the default backend.** pixi-filters 6.1.5 writes the filter's `kernelSize` as an i32 but its WGSL shader reads an f32, so the value was read as 0 and the blur was skipped, silently, for every user with WebGPU (WebGL was fine). Reproduced with pixi.js and pixi-filters alone (no pixi-effects); the shader is now rebuilt with the field as an i32, and the same blur comes out on both backends (`tests/tools/radialBlur.test.ts`, `examples/_checks/radial-blur.html`). Every other filter of the package was checked for the same mismatch: this is the only one. The workaround can be removed when pixi-filters fixes it.
19
+ - **A test that draws the recommended filters on WebGPU and on WebGL** (`tests/tools/filterSweep.test.ts`, `examples/_checks/filter-sweep.html`): `glow`, `outline`, `dropShadow`, `bevel`, `bloom`, `pixelate`, `crt`, `rgbSplit`, `dot`, `radialBlur`, `glitch` must show and look the same on both. All 38 `pixi-filters` filters were compared once: `multiColorReplace` (whole picture black), `kawaseBlur` (far weaker), `advancedBloom` (sharper) and `ascii` (stripes) differ on WebGPU; they are named in the docs with the filter to use instead (pitfall 111, `docs/dsl.md` Named filters, the filters guide) and the docs say which ones to reach for first. Not fixed in the code: they are not used by any of the repository's 100 example files, and `multiColorReplace` needs pixi-filters' uniform packing reworked.
20
+
3
21
  ## 0.33.0
4
22
 
5
23
  **`check` tells the truth about what a trial found (design: the AI trial of 2026-10-12, 10 fresh agents on 0.32.0; a fresh review of this change found 2 gaps and 3 false alarms, fixed below)**
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pixi-effects
2
2
 
3
- > **Status**: experimental — current release `0.33.0`. The API may still change between minor versions; see the [CHANGELOG](./CHANGELOG.md) for what each release changed.
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.33.0/dist/index.js",
53
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.33.0/dist/Controller.js"
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.33.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.
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.33.0/dist/three.js"
80
+ "pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.35.0/dist/three.js"
81
81
  }
82
82
  }
83
83
  </script>
@@ -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.33.0/dist/index.js",
63
- "pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.33.0/dist/Controller.js",
64
- "pixi-effects/present": "https://cdn.jsdelivr.net/npm/pixi-effects@0.33.0/dist/present.js"
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>
@@ -0,0 +1,7 @@
1
+ export { CompositionSequence } from './chunk-XV4L72FW.js';
2
+ import './chunk-Y253JQOH.js';
3
+ import './chunk-RTJD3ZSW.js';
4
+ import './chunk-ITDYFBB6.js';
5
+ import './chunk-BXOCEZ4U.js';
6
+ //# sourceMappingURL=Composition-NDXKQHB4.js.map
7
+ //# sourceMappingURL=Composition-NDXKQHB4.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-7XNPTM7L.js"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-NDXKQHB4.js"}
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ var chunk5IJENZOG_cjs = require('./chunk-5IJENZOG.cjs');
4
+ require('./chunk-LEECUCTG.cjs');
5
+ require('./chunk-IMQJVRMT.cjs');
6
+ require('./chunk-GL2AQFLQ.cjs');
7
+ require('./chunk-TWZNOUM7.cjs');
8
+
9
+
10
+
11
+ Object.defineProperty(exports, "CompositionSequence", {
12
+ enumerable: true,
13
+ get: function () { return chunk5IJENZOG_cjs.CompositionSequence; }
14
+ });
15
+ //# sourceMappingURL=Composition-OLH7RC5Z.cjs.map
16
+ //# sourceMappingURL=Composition-OLH7RC5Z.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-L6OALWHI.cjs"}
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-OLH7RC5Z.cjs"}
@@ -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;