pixi-effects 0.48.1 → 0.49.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 (88) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +51 -7
  3. package/ai/chat-template.html +8 -4
  4. package/ai/tools/check.mjs +6 -6
  5. package/dist/3d.cjs +963 -0
  6. package/dist/3d.cjs.map +1 -0
  7. package/dist/3d.d.cts +303 -0
  8. package/dist/3d.d.ts +303 -0
  9. package/dist/3d.js +956 -0
  10. package/dist/3d.js.map +1 -0
  11. package/dist/Canvas-BfZkAFJm.d.cts +12 -0
  12. package/dist/Canvas-BfZkAFJm.d.ts +12 -0
  13. package/dist/Composition-464AWDV6.cjs +16 -0
  14. package/dist/{Composition-ZZEOEETS.cjs.map → Composition-464AWDV6.cjs.map} +1 -1
  15. package/dist/Composition-5U63LMPX.js +7 -0
  16. package/dist/{Composition-BOLWGT2R.js.map → Composition-5U63LMPX.js.map} +1 -1
  17. package/dist/Controller.d.cts +2 -2
  18. package/dist/Controller.d.ts +2 -2
  19. package/dist/{Movie--qSYKgAQ.d.ts → Movie-6EWOz6Yw.d.ts} +21 -5
  20. package/dist/{Movie-svZQF-9Z.d.cts → Movie-BxB7bauG.d.cts} +21 -5
  21. package/dist/{Spans-MWYFG7RI.cjs → Spans-DUR2W5EH.cjs} +11 -11
  22. package/dist/{Spans-MWYFG7RI.cjs.map → Spans-DUR2W5EH.cjs.map} +1 -1
  23. package/dist/{Spans-PGXRMD6J.js → Spans-W5VJRMGC.js} +6 -6
  24. package/dist/{Spans-PGXRMD6J.js.map → Spans-W5VJRMGC.js.map} +1 -1
  25. package/dist/{blendModes-V5XHC7U4.cjs → blendModes-NC76DNVX.cjs} +5 -5
  26. package/dist/{blendModes-V5XHC7U4.cjs.map → blendModes-NC76DNVX.cjs.map} +1 -1
  27. package/dist/{blendModes-535EMYGK.js → blendModes-ZOBLPSR3.js} +4 -4
  28. package/dist/{blendModes-535EMYGK.js.map → blendModes-ZOBLPSR3.js.map} +1 -1
  29. package/dist/{chunk-V3VBHSK6.cjs → chunk-33USHRV5.cjs} +94 -104
  30. package/dist/chunk-33USHRV5.cjs.map +1 -0
  31. package/dist/{chunk-WYR2AU4W.cjs → chunk-5YNME6AH.cjs} +5 -5
  32. package/dist/{chunk-WYR2AU4W.cjs.map → chunk-5YNME6AH.cjs.map} +1 -1
  33. package/dist/{chunk-NGJXVJB7.cjs → chunk-ABWRISRK.cjs} +3 -2
  34. package/dist/chunk-ABWRISRK.cjs.map +1 -0
  35. package/dist/{chunk-M2LFQY2E.cjs → chunk-BSFWVTSR.cjs} +93 -93
  36. package/dist/chunk-BSFWVTSR.cjs.map +1 -0
  37. package/dist/{chunk-L3MEZZAV.js → chunk-DOGY3EAW.js} +25 -17
  38. package/dist/chunk-DOGY3EAW.js.map +1 -0
  39. package/dist/{chunk-L3XC464G.cjs → chunk-GUDLIAIG.cjs} +89 -66
  40. package/dist/chunk-GUDLIAIG.cjs.map +1 -0
  41. package/dist/{chunk-BFRWSV3R.js → chunk-H363KMJR.js} +3 -3
  42. package/dist/chunk-H363KMJR.js.map +1 -0
  43. package/dist/{chunk-JBDBICS2.js → chunk-MXACRKAN.js} +4 -4
  44. package/dist/{chunk-JBDBICS2.js.map → chunk-MXACRKAN.js.map} +1 -1
  45. package/dist/{chunk-IY5YTIK3.js → chunk-PBMDZT2T.js} +47 -24
  46. package/dist/chunk-PBMDZT2T.js.map +1 -0
  47. package/dist/{chunk-AJWUCFGM.js → chunk-USUUAYTE.js} +6 -6
  48. package/dist/chunk-USUUAYTE.js.map +1 -0
  49. package/dist/index.cjs +35 -35
  50. package/dist/index.d.cts +3 -3
  51. package/dist/index.d.ts +3 -3
  52. package/dist/index.js +5 -5
  53. package/dist/loader.css +1 -1
  54. package/dist/{music-P4JRT73P.js → music-ATX22VAT.js} +3 -3
  55. package/dist/{music-P4JRT73P.js.map → music-ATX22VAT.js.map} +1 -1
  56. package/dist/{music-XQEPBIDT.cjs → music-OFVM3PYH.cjs} +3 -3
  57. package/dist/{music-XQEPBIDT.cjs.map → music-OFVM3PYH.cjs.map} +1 -1
  58. package/dist/present.cjs +5 -5
  59. package/dist/present.d.cts +2 -2
  60. package/dist/present.d.ts +2 -2
  61. package/dist/present.js +5 -5
  62. package/dist/three.cjs +7 -8
  63. package/dist/three.cjs.map +1 -1
  64. package/dist/three.d.cts +2 -12
  65. package/dist/three.d.ts +2 -12
  66. package/dist/three.js +2 -3
  67. package/dist/three.js.map +1 -1
  68. package/dist/{types-CTSrPvY3.d.cts → types-kt7DQKfO.d.cts} +1 -1
  69. package/dist/{types-CTSrPvY3.d.ts → types-kt7DQKfO.d.ts} +1 -1
  70. package/docs/api.md +41 -1
  71. package/docs/dsl.md +81 -1
  72. package/llms-full.txt +218 -5
  73. package/package.json +19 -3
  74. package/skills/pixi-effects/SKILL.md +3 -2
  75. package/skills/pixi-effects/reference/cheatsheet.md +2 -0
  76. package/skills/pixi-effects/reference/pitfalls.md +14 -0
  77. package/skills/pixi-effects/reference/recipes.md +77 -1
  78. package/skills/pixi-effects/template.html +13 -8
  79. package/dist/Composition-BOLWGT2R.js +0 -7
  80. package/dist/Composition-ZZEOEETS.cjs +0 -16
  81. package/dist/chunk-AJWUCFGM.js.map +0 -1
  82. package/dist/chunk-BFRWSV3R.js.map +0 -1
  83. package/dist/chunk-IY5YTIK3.js.map +0 -1
  84. package/dist/chunk-L3MEZZAV.js.map +0 -1
  85. package/dist/chunk-L3XC464G.cjs.map +0 -1
  86. package/dist/chunk-M2LFQY2E.cjs.map +0 -1
  87. package/dist/chunk-NGJXVJB7.cjs.map +0 -1
  88. package/dist/chunk-V3VBHSK6.cjs.map +0 -1
package/dist/3d.cjs ADDED
@@ -0,0 +1,963 @@
1
+ 'use strict';
2
+
3
+ var chunk33USHRV5_cjs = require('./chunk-33USHRV5.cjs');
4
+ var chunk5YNME6AH_cjs = require('./chunk-5YNME6AH.cjs');
5
+ var chunkABWRISRK_cjs = require('./chunk-ABWRISRK.cjs');
6
+ var chunkDOLTCXCL_cjs = require('./chunk-DOLTCXCL.cjs');
7
+ var _3d = require('@pixi/3d');
8
+ var pixi_js = require('pixi.js');
9
+
10
+ var NEAR = 1;
11
+ var FAR = 2e4;
12
+ function applyCameraState(camera, s) {
13
+ camera.mode = "perspective";
14
+ camera.zoom = 1;
15
+ camera.fov = s.fov;
16
+ camera.near = NEAR;
17
+ camera.far = FAR;
18
+ camera.position.set(s.x, -s.y, s.z);
19
+ const same = Math.hypot(s.lookAtX - s.x, s.lookAtY - s.y, s.lookAtZ - s.z) < 1e-9;
20
+ camera.lookAt(same ? { x: s.x, y: -s.y, z: s.z - 1 } : { x: s.lookAtX, y: -s.lookAtY, z: s.lookAtZ });
21
+ }
22
+ var LightMirror = class {
23
+ constructor(root, extent = 2e3, say = () => {
24
+ }, center = { x: 0, y: 0, z: 0 }) {
25
+ this.root = root;
26
+ this.extent = extent;
27
+ this.say = say;
28
+ this.center = center;
29
+ }
30
+ root;
31
+ extent;
32
+ say;
33
+ center;
34
+ entries = [];
35
+ said = /* @__PURE__ */ new Set();
36
+ update(lights) {
37
+ lights.forEach((s, i) => {
38
+ let e = this.entries[i];
39
+ if (!e || e.kind !== s.kind) {
40
+ if (e) this.remove(e);
41
+ e = this.entries[i] = this.make(s.kind);
42
+ }
43
+ this.configure(e, s);
44
+ });
45
+ for (let i = lights.length; i < this.entries.length; i++) this.entries[i].light.visible = false;
46
+ }
47
+ dispose() {
48
+ for (const e of this.entries) this.remove(e);
49
+ this.entries = [];
50
+ }
51
+ /** The lights now live under another root (a rebuild moved the scene into a new view): new lights go there. */
52
+ rehome(root) {
53
+ this.root = root;
54
+ }
55
+ once(key, message) {
56
+ if (this.said.has(key)) return;
57
+ this.said.add(key);
58
+ this.say(message);
59
+ }
60
+ make(kind) {
61
+ const light = kind === "ambient" ? new _3d.AmbientLight() : kind === "parallel" ? new _3d.DirectionalLight() : kind === "spot" ? new _3d.SpotLight() : new _3d.PointLight();
62
+ light.label = "pixi-effects:composition-light";
63
+ this.root.addChild(light);
64
+ return { kind, light };
65
+ }
66
+ remove(e) {
67
+ e.light.removeFromParent();
68
+ e.light.destroy();
69
+ }
70
+ configure(e, s) {
71
+ const { light } = e;
72
+ light.visible = true;
73
+ light.color = [s.r, s.g, s.b];
74
+ let intensity = s.intensity * Math.PI;
75
+ if (s.kind !== "ambient") {
76
+ light.position.set(s.x, -s.y, s.z);
77
+ const same = Math.hypot(s.lookAtX - s.x, s.lookAtY - s.y, s.lookAtZ - s.z) < 1e-9;
78
+ if (s.kind !== "point") light.lookAt(same ? { x: s.x, y: -s.y, z: s.z - 1 } : { x: s.lookAtX, y: -s.lookAtY, z: s.lookAtZ });
79
+ }
80
+ if (s.kind === "point" || s.kind === "spot") {
81
+ const l = light;
82
+ const flat = 100 * this.extent;
83
+ const shadowRange = s.kind === "spot" && s.castsShadows ? 20 * this.extent : 0;
84
+ if (s.falloff === "inverseSquare") {
85
+ l.radius = Math.max(s.radius, 1e-3);
86
+ intensity *= l.radius * l.radius;
87
+ l.range = shadowRange;
88
+ } else if (s.falloff === "smooth") {
89
+ const reach = s.radius + Math.max(s.falloffDistance, 1e-3), p = s.radius / reach;
90
+ l.radius = flat;
91
+ intensity *= flat * flat * (SMOOTH_FIT.gain[0] + SMOOTH_FIT.gain[1] * p);
92
+ l.range = reach * (SMOOTH_FIT.range[0] + SMOOTH_FIT.range[1] * p);
93
+ } else {
94
+ l.radius = flat;
95
+ intensity *= flat * flat;
96
+ l.range = shadowRange;
97
+ }
98
+ }
99
+ if (s.kind === "spot") {
100
+ const l = light;
101
+ l.coneAngle = s.coneAngle / 2;
102
+ l.penumbra = s.coneFeather;
103
+ }
104
+ light.intensity = intensity;
105
+ if (s.kind !== "ambient") {
106
+ const casts = s.castsShadows && s.kind !== "point";
107
+ light.castShadow = casts;
108
+ if (casts) {
109
+ const shadow = light.shadow;
110
+ if (s.kind === "spot") shadow.bias = SPOT_SHADOW_BIAS;
111
+ else shadow.bounds = shadow.bounds ??= _3d.Box.fromCenterSize(this.center, this.extent);
112
+ }
113
+ if (s.castsShadows && s.kind === "point") this.once("point", "followLights: a point light casts no shadow in PixiJS 3D; use a spot light (or a parallel one) for the shadow");
114
+ if (casts && (s.shadowDarkness < 1 || s.shadowDiffusion > 0)) {
115
+ this.once("shadowLook", `followLights: shadowDarkness and shadowDiffusion are not mirrored (PixiJS 3D draws a shadow at full darkness with its view's own filter); the shadow is drawn ${s.shadowDarkness < 1 ? "darker" : "harder"} than on the cards`);
116
+ }
117
+ }
118
+ }
119
+ };
120
+ var SPOT_SHADOW_BIAS = 1e-5;
121
+ var SMOOTH_FIT = { range: [0.8, 0.45], gain: [0.87, 0.45] };
122
+ var FogMirror = class {
123
+ constructor(view) {
124
+ this.view = view;
125
+ }
126
+ view;
127
+ own = null;
128
+ update(fog) {
129
+ const current = this.view.fog;
130
+ if (current && current !== this.own) return;
131
+ if (!fog) {
132
+ if (this.own && current === this.own) this.view.fog = null;
133
+ this.own = null;
134
+ return;
135
+ }
136
+ if (!this.own) {
137
+ this.own = { type: "linear", color: [fog.r, fog.g, fog.b], near: fog.near, far: fog.far };
138
+ this.view.fog = this.own;
139
+ }
140
+ this.own.color = [fog.r, fog.g, fog.b];
141
+ this.own.near = fog.near;
142
+ this.own.far = fog.far;
143
+ }
144
+ dispose() {
145
+ this.update(null);
146
+ }
147
+ };
148
+ var HOST = /* @__PURE__ */ Symbol.for("pixi-effects:scene3d-host");
149
+ var label = "pixi-effects: layerTexture";
150
+ async function layerTexture(ctx, name, options = {}) {
151
+ const host = ctx[HOST];
152
+ if (!host) throw new Error(`${label}: call it from the setup of a scene3d layer: const screen = await layerTexture(ctx, 'screen')`);
153
+ if (typeof name !== "string" || !name) throw new Error(`${label}: the second argument is the name of a 2D layer: layerTexture(ctx, 'screen')`);
154
+ const resolution = options.resolution ?? 1;
155
+ if (!(Number.isFinite(resolution) && resolution > 0 && resolution <= 4)) throw new Error(`${label}: resolution is a number from above 0 to 4 (2 = twice the pixels), got ${JSON.stringify(options.resolution)}`);
156
+ const pixi = host.pixiRenderer;
157
+ if (!pixi) throw new Error(`${label}: PixiJS's renderer is not available here (the movie is not ready)`);
158
+ const source = host.findLayer(name);
159
+ if (!source) {
160
+ const names = host.layerNames();
161
+ throw new Error(`${label}: no layer named "${name}" before this scene3d layer${names.length ? ` (the layers built before it: ${names.map((n) => `"${n}"`).join(", ")})` : ""}. A layer shown on a mesh must be listed BEFORE the scene3d layer, in the same composition, and have that name.`);
162
+ }
163
+ if (source.spec.type !== "composition") {
164
+ throw new Error(`${label}: "${name}" is a ${source.spec.type} layer: a layer shown on a mesh must be a composition (wrap it: { type: 'composition', name: '${name}', width: 480, height: 270, sequences: [ \u2026 ] })`);
165
+ }
166
+ const logicalW = Math.max(1, Math.round(source.intrinsicWidth)), logicalH = Math.max(1, Math.round(source.intrinsicHeight));
167
+ const rt = pixi_js.RenderTexture.create({ width: logicalW, height: logicalH, resolution, autoGenerateMipmaps: true });
168
+ const transparent = options.transparent === true;
169
+ const material = new _3d.FlatMaterial({ baseColorTexture: rt, uvTransform: new pixi_js.Matrix(1, 0, 0, -1, 0, 1), ...transparent ? { alphaMode: "blend" } : {} });
170
+ let current = source;
171
+ let display = source.target;
172
+ let sx = logicalW / source.intrinsicWidth, sy = logicalH / source.intrinsicHeight;
173
+ let hidden = false;
174
+ const hide = () => {
175
+ display.visible = false;
176
+ hidden = true;
177
+ };
178
+ hide();
179
+ const uncull = (c) => {
180
+ if (c.culled) c.culled = false;
181
+ for (const k of c.children) uncull(k);
182
+ };
183
+ const refresh = () => {
184
+ display.visible = true;
185
+ uncull(display);
186
+ try {
187
+ pixi.render({ container: display, target: rt, transform: new pixi_js.Matrix().scale(sx, sy), clear: true, clearColor: [0, 0, 0, transparent ? 0 : 1] });
188
+ } finally {
189
+ display.visible = false;
190
+ }
191
+ rt.source.updateMipmaps();
192
+ };
193
+ const stop = host.beforeDraw(refresh);
194
+ refresh();
195
+ let disposed = false;
196
+ const handle = {
197
+ material,
198
+ texture: rt,
199
+ width: Math.round(logicalW * resolution),
200
+ height: Math.round(logicalH * resolution),
201
+ source: () => current,
202
+ sourceAt: (u, v) => ({ x: u, y: 1 - v }),
203
+ // a plane's uv: v runs up; the texture's rows run down
204
+ prepare(next) {
205
+ const again = next.findLayer(name);
206
+ if (disposed) return () => {
207
+ };
208
+ if (!again) throw new Error(`${label}: after a rebuild there is no layer "${name}" before the scene3d layer any more`);
209
+ return () => {
210
+ current = again;
211
+ display = again.target;
212
+ sx = logicalW / again.intrinsicWidth;
213
+ sy = logicalH / again.intrinsicHeight;
214
+ hide();
215
+ refresh();
216
+ };
217
+ },
218
+ keepHidden() {
219
+ if (hidden && !disposed) display.visible = false;
220
+ },
221
+ dispose() {
222
+ if (disposed) return;
223
+ disposed = true;
224
+ untrack();
225
+ stop();
226
+ if (hidden) {
227
+ display.visible = true;
228
+ hidden = false;
229
+ }
230
+ }
231
+ };
232
+ const untrack = host.track(handle);
233
+ host.own(() => {
234
+ try {
235
+ material.destroy?.();
236
+ } catch {
237
+ }
238
+ try {
239
+ rt.destroy(true);
240
+ } catch {
241
+ }
242
+ });
243
+ return handle;
244
+ }
245
+
246
+ // src/3d/Scene3DSequence.ts
247
+ var OWN_KEYS = ["width", "height", "view", "resolution", "setup", "update", "dispose", "followCamera", "followLights", "followFog", "reuse"];
248
+ var THREE_ONLY = ["renderer", "shareGpu", "antialias"];
249
+ var LAYER_OWNED = ["width", "height", "autoResize"];
250
+ var Scene3DSequence = class extends chunk33USHRV5_cjs.Sequence {
251
+ _view = null;
252
+ _ctx = null;
253
+ _objects = {};
254
+ _warned = /* @__PURE__ */ new Set();
255
+ /** The animation players under the root (found after setup). */
256
+ _players = [];
257
+ /** The layer's own last filter: what PixiJS 3D ignores on its view (alpha, its parents' alpha, a basic blend mode) applied to the view's picture. Only there while needed. */
258
+ _look = null;
259
+ /** An advanced blend mode on a layer with no filter of its own: its blend filter, owned by the layer (the view's own blend mode would be shut inside the look filter's pass). */
260
+ _blend = null;
261
+ /** The layer's time, what PixiJS 3D's shader time `uTime` reads once pinned (see `_pinShaderTime`). */
262
+ _time = 0;
263
+ /** Things to do before every frame of the scene is drawn (the 2D layers shown on meshes: `layerTexture`), and their handles. */
264
+ _drawHooks = /* @__PURE__ */ new Set();
265
+ _shown = [];
266
+ /** Resources the 2D layers on meshes leave to this layer to free when it goes (shared, like `_shown`, with a layer that takes the scene over). */
267
+ _owned = [];
268
+ /** What a rebuild matches this layer by (core/reuse.ts); null: never kept (`reuse: false`, no `name`, view options that are not plain data). */
269
+ _key = null;
270
+ /** The layer of the tree being replaced whose scene this one takes over at the swap (`commitReuse`): until then it is not this layer's. */
271
+ _borrowed = null;
272
+ /** True once another layer took this layer's scene: destroying this one must not let it go. */
273
+ _donated = false;
274
+ _pendingRebinds = [];
275
+ _lightMirror = null;
276
+ _fogMirror = null;
277
+ _pinned = false;
278
+ /** The scene may have changed since the meshes were last made pickable (a frame ran `update`): the next pick walks it again, the picks after it do not. */
279
+ _pickStale = true;
280
+ /** The PixiJS 3D view this layer shows (null before build and after destroy). */
281
+ get view() {
282
+ return this._view;
283
+ }
284
+ get _spec() {
285
+ return this.spec;
286
+ }
287
+ _say(key, message) {
288
+ if (this._warned.has(key)) return;
289
+ this._warned.add(key);
290
+ console.warn(`${this._who()}: ${message}`);
291
+ }
292
+ _who() {
293
+ const name = this._spec.name;
294
+ return `pixi-effects: scene3d layer${name ? ` "${name}"` : ""}`;
295
+ }
296
+ pathRouters() {
297
+ return { scene3d: (path) => this._resolvePath(path) };
298
+ }
299
+ async build() {
300
+ const spec = this._spec;
301
+ if (this.duration === void 0) this.duration = this.parent?.duration ?? this.root.duration;
302
+ this._checkKeys();
303
+ this._checkTint();
304
+ if (typeof spec.update === "function" && chunk33USHRV5_cjs.NOT_PURE.test(spec.update.toString())) {
305
+ this._say("pure", "update calls Math.random, Date.now, performance.now or new Date, so it is not deterministic: the same frame would draw differently in playback, in a seek and in an export. Derive the value from t");
306
+ }
307
+ const renderer = this.root.pixiRenderer;
308
+ if (renderer && !renderer.renderPipes.view3d) {
309
+ throw new Error(`${this._who()}: PixiJS 3D was loaded after the renderer started; import 'pixi-effects/3d' (and call register3D()) before movie.init`);
310
+ }
311
+ const scope = chunk33USHRV5_cjs.buildScope(this, this.parent, this.root);
312
+ const width = resolveDim(spec.width, scope) ?? this.parent?.width ?? this.root.width;
313
+ const height = resolveDim(spec.height, scope) ?? this.parent?.height ?? this.root.height;
314
+ const viewOptions = { ...spec.view ?? {} };
315
+ const owned = LAYER_OWNED.filter((k) => k in viewOptions);
316
+ if (owned.length) {
317
+ this._say("owned", `${owned.join(" / ")} belong${owned.length === 1 ? "s" : ""} on the layer, not in view; ignored`);
318
+ for (const k of owned) delete viewOptions[k];
319
+ }
320
+ const view = new _3d.View3D({ cull: "cpu", ...viewOptions, width, height, ...spec.resolution !== void 0 ? { resolution: spec.resolution } : {} });
321
+ view.label = spec.name ?? "scene3d";
322
+ this._view = view;
323
+ this.target = view;
324
+ this.intrinsicWidth = width;
325
+ this.intrinsicHeight = height;
326
+ this._key = spec.reuse === false || !spec.name ? null : reuseKeyOf(spec, width, height);
327
+ const donor = this._key !== null ? this.root.reuse?.claim(this._key) : null;
328
+ if (donor && donor._lendable()) {
329
+ this._borrow(donor, view, renderer ?? null, width, height);
330
+ if (spec.followCamera) this._checkFollowShape(width, height);
331
+ this.buildFilters();
332
+ return;
333
+ }
334
+ const objects = {};
335
+ const ctx = { view, root: view.root, camera: view.camera, renderer, width, height, objects };
336
+ Object.defineProperty(ctx, HOST, { value: this._makeHost(renderer ?? null), enumerable: false });
337
+ let result;
338
+ try {
339
+ result = await spec.setup(ctx);
340
+ } catch (err) {
341
+ this._destroyView();
342
+ throw new Error(`${this._who()}: setup failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
343
+ }
344
+ Object.assign(objects, result?.objects ?? {});
345
+ if (!("camera" in objects)) objects.camera = view.camera;
346
+ if (!("view" in objects)) objects.view = view;
347
+ this._objects = objects;
348
+ this._ctx = ctx;
349
+ this._stopSelfUpdating(view.root);
350
+ if (spec.followCamera) this._checkFollowShape(width, height);
351
+ this.buildFilters();
352
+ }
353
+ /** Reuse (core/reuse.ts): the key this layer is matched by, while it can still lend its scene. */
354
+ reuseKey() {
355
+ return this._lendable() ? this._key : null;
356
+ }
357
+ _lendable() {
358
+ return !!this._view && !!this._ctx && !this._donated && !this._borrowed;
359
+ }
360
+ /**
361
+ * Take over a layer of the tree being replaced. The view is a PixiJS display object with one parent, so the old view stays in the old tree (which goes on drawing while this tree is
362
+ * built); this layer has a view of its own, and at the swap (`commitReuse`) the old view's whole scene, its camera with it, moves into this one. The objects setup made (and every
363
+ * value an author kept from setup: the camera, the meshes) are the same objects.
364
+ */
365
+ _borrow(d, view, renderer, width, height) {
366
+ this._borrowed = d;
367
+ const objects = { ...d._objects, ...d._objects.view === d._view ? { view } : {} };
368
+ this._objects = objects;
369
+ this._players = d._players;
370
+ const ctx = { view, root: view.root, camera: d._view.camera, renderer, width, height, objects };
371
+ const host = this._makeHost(renderer);
372
+ Object.defineProperty(ctx, HOST, { value: host, enumerable: false });
373
+ this._ctx = ctx;
374
+ this._pendingRebinds = d._shown.map((h) => h.prepare(host));
375
+ }
376
+ /** The new tree is on stage: the scene of the layer this one borrowed from moves into this layer's view, for good. */
377
+ commitReuse() {
378
+ const d = this._borrowed, view = this._view, ctx = this._ctx;
379
+ if (!d || !view || !ctx) return;
380
+ const from = d._view;
381
+ const own = view.camera;
382
+ for (const child of [...from.root.children]) view.root.addChild(child);
383
+ view.camera = ctx.camera;
384
+ if (own !== ctx.camera) {
385
+ own.removeFromParent();
386
+ own.destroy();
387
+ }
388
+ const keep = ["background", "environment", "exposure", "toneMapping", "shadowQuality", "shadowFilter", "cull", ...d._fogMirror ? [] : ["fog"]];
389
+ const src = from, dst = view;
390
+ for (const key of keep) {
391
+ try {
392
+ if (key in src && dst[key] !== src[key]) dst[key] = src[key];
393
+ } catch {
394
+ }
395
+ }
396
+ try {
397
+ const passes = src.passes;
398
+ if (Array.isArray(passes) && passes.length) {
399
+ dst.passes = [...passes];
400
+ src.passes = [];
401
+ }
402
+ } catch {
403
+ }
404
+ this._lightMirror = d._lightMirror;
405
+ this._lightMirror?.rehome(view.root);
406
+ this._fogMirror = null;
407
+ this._pickStale = true;
408
+ this._shown = d._shown;
409
+ this._drawHooks = d._drawHooks;
410
+ this._owned = d._owned;
411
+ d._donated = true;
412
+ this._borrowed = null;
413
+ for (const apply of this._pendingRebinds) apply();
414
+ this._pendingRebinds = [];
415
+ }
416
+ /** Unknown keys (with the likely one), and the keys only a three layer has. */
417
+ _checkKeys() {
418
+ const valid = [...chunkABWRISRK_cjs.COMMON, ...OWN_KEYS];
419
+ for (const key of Object.keys(this.spec)) {
420
+ if (valid.includes(key)) continue;
421
+ if (THREE_ONLY.includes(key)) {
422
+ this._say(`key:${key}`, `${key} is about three's own renderer; a scene3d layer is drawn by PixiJS itself, so it has no such option (ignored)`);
423
+ continue;
424
+ }
425
+ const guess = chunkDOLTCXCL_cjs.suggestName(key, valid);
426
+ this._say(`key:${key}`, `unknown key "${key}"${guess ? `; did you mean "${guess}"?` : ""} (a scene3d layer has ${OWN_KEYS.join(", ")} and the keys every layer has)`);
427
+ }
428
+ }
429
+ /**
430
+ * PixiJS 3D (0.1.0) writes `performance.now()` into the shader time `uTime` of every view at every draw: a shader bit that reads it would follow the wall clock.
431
+ * The view's global uniforms exist once PixiJS has drawn it; from then on `uTime` reads the layer's time and the pipe's write is ignored. Tried before every frame
432
+ * until it holds (drawing the view on its own at build to make them sooner broke a stencil mask on it later). This reaches into the view's per-renderer data
433
+ * (`_gpuData[renderer.uid].globalUniformGroup.uniforms`); when that is there but has no `uTime` (another version), say so once.
434
+ */
435
+ _pinShaderTime() {
436
+ if (this._pinned) return;
437
+ const view = this._view, renderer = this.root.pixiRenderer;
438
+ if (!view || !renderer) return;
439
+ const data = view._gpuData?.[renderer.uid];
440
+ if (!data) return;
441
+ this._pinned = true;
442
+ const uniforms = data.globalUniformGroup?.uniforms;
443
+ if (!uniforms || !("uTime" in uniforms)) {
444
+ this._say("uTime", "could not tie PixiJS 3D's shader time uTime to the video's time (no global uniforms found): a custom shader bit that reads uTime follows the wall clock; give it a uniform you set from t instead");
445
+ return;
446
+ }
447
+ Object.defineProperty(uniforms, "uTime", { configurable: true, enumerable: true, get: () => this._time, set: () => {
448
+ } });
449
+ }
450
+ /**
451
+ * `followCamera` (and, later, `followLights` / `followFog`): before each frame, ask the composition for its camera at this moment (the layer's time on the composition's
452
+ * clock) and put the view's camera there. `ctx.space` keeps the raw numbers for `update`, which runs after and may override.
453
+ */
454
+ _followSpace(t) {
455
+ const spec = this._spec;
456
+ if (!spec.followCamera && !spec.followLights && !spec.followFog) return;
457
+ const ctx = this._ctx, view = this._view;
458
+ if (!ctx || !view) return;
459
+ if (!this.parent?.spaceAt) {
460
+ this._say("parent", "followCamera / followLights / followFog need the layer to sit in a composition that has a camera or a light to follow; this layer's parent cannot say (it keeps its own camera and lights)");
461
+ return;
462
+ }
463
+ const space = this.parent.spaceAt((this.absoluteStart ?? this.at) + t);
464
+ ctx.space = space;
465
+ if (spec.followCamera) applyCameraState(view.camera, space.camera);
466
+ if (spec.followLights) (this._lightMirror ??= new LightMirror(view.root, Math.max(ctx.width, ctx.height) * 2, (m) => this._say(`light:${m}`, m), { x: ctx.width / 2, y: -ctx.height / 2, z: 0 })).update(space.lights);
467
+ if (spec.followFog) {
468
+ if (space.fog && space.fog.amount !== 1) this._say("fogAmount", `followFog: the camera's fogAmount is ${space.fog.amount}, and PixiJS 3D's fog has no amount below 1: the layer uses the full amount (set fogColor to the colour you want at the far end, or leave followFog off and set view.fog yourself from ctx.space.fog)`);
469
+ (this._fogMirror ??= new FogMirror(view)).update(space.fog);
470
+ }
471
+ }
472
+ /** followCamera shows the whole composition through the layer's picture: a layer of another shape would show it squeezed or cut. Said once at build. */
473
+ _checkFollowShape(width, height) {
474
+ const p = this.parent;
475
+ if (!p || !(p.width > 0 && p.height > 0)) return;
476
+ if (Math.abs(width * p.height - height * p.width) <= 1e-3 * width * p.height) return;
477
+ this._say("shape", `followCamera: the layer is ${width} x ${height} but the composition is ${p.width} x ${p.height}; the camera shows the whole composition, so the layer needs its aspect ratio (drop width / height to cover the composition)`);
478
+ }
479
+ /** PixiJS 3D draws the view in its materials' colours: a tint does nothing. Said once, at build. */
480
+ _checkTint() {
481
+ const spec = this._spec;
482
+ const uses = spec.initial?.tint !== void 0 || (spec.keyframes ?? []).some((k) => k?.to?.tint !== void 0 || k?.from?.tint !== void 0);
483
+ if (uses) this._say("tint", "tint is ignored on a scene3d layer (PixiJS 3D draws its own colours): colour the materials, or put a filter on the layer");
484
+ }
485
+ /**
486
+ * Every frame, before the draw: PixiJS 3D ignores the view's alpha (and its parents') and a basic blend mode, so when the layer is not at full strength or blends,
487
+ * its picture goes through one AlphaFilter of the layer's own, last in its filter list, that applies them. A layer at alpha 1 with a normal blend has no such filter.
488
+ */
489
+ syncFrame() {
490
+ super.syncFrame();
491
+ for (const h of this._shown) h.keepHidden();
492
+ const view = this._view;
493
+ if (!view) return;
494
+ let alpha = 1;
495
+ for (let c = view; c; c = c.parent) alpha *= c.alpha;
496
+ const current = view.filters ?? [];
497
+ const others = current.filter((f) => f !== this._look && f !== this._blend);
498
+ const authored = String(this.spec.blendMode ?? "normal");
499
+ if (others.length === 0 && chunk5YNME6AH_cjs.isAdvancedBlend(authored) && !this._blend) {
500
+ this._blend = chunk5YNME6AH_cjs.blendFilterFor(authored);
501
+ if (this._blend) view.blendMode = "normal";
502
+ }
503
+ const mode = String(view.blendMode ?? "normal");
504
+ const basic = others.length === 0 && (mode === "add" || mode === "screen" || mode === "multiply");
505
+ const need = alpha < 1 || basic;
506
+ const own = this._blend && others.length === 0 ? [this._blend] : [];
507
+ let next = [...others, ...own];
508
+ if (need) {
509
+ const look = this._look ??= new pixi_js.AlphaFilter({ alpha: 1 });
510
+ look.alpha = alpha;
511
+ look.blendMode = basic ? mode : "normal";
512
+ next = others.length || own.length ? [look, ...others, ...own] : [look];
513
+ }
514
+ if (next.length !== current.length || next.some((f, k) => f !== current[k])) view.filters = next.length ? next : null;
515
+ }
516
+ /**
517
+ * Models, animation players and particle systems update themselves on PixiJS's shared ticker unless told otherwise: their picture would follow the browser's clock,
518
+ * not the video's time. Turn that off on everything under the root, and say once what was turned off (the author drives them from `update`).
519
+ */
520
+ _stopSelfUpdating(root) {
521
+ const stopped = [];
522
+ const visit = (node) => {
523
+ const n = node;
524
+ let hit = false;
525
+ if (n.player && Array.isArray(n.player.animationLabels) && !this._players.includes(n.player)) this._players.push(n.player);
526
+ if (n.player && n.player.autoUpdate === true) {
527
+ n.player.autoUpdate = false;
528
+ hit = true;
529
+ }
530
+ if (n.autoUpdate === true) {
531
+ n.autoUpdate = false;
532
+ hit = true;
533
+ }
534
+ if (hit) stopped.push(n.label || n.constructor?.name || "a node");
535
+ };
536
+ root.traverse(visit);
537
+ if (stopped.length) {
538
+ this._say("autoUpdate", `turned off autoUpdate on ${stopped.join(", ")}: a video draws from the time, so drive them from update(t) (a model's clip: model.player.getState(name).gotoAndStop(t))`);
539
+ }
540
+ }
541
+ /**
542
+ * Per-frame hook, duck-typed by `Movie._awaitVideoFrames` (as the video and three layers are): the single frame path of playback, seek and export.
543
+ * `update` sets the scene from the time; PixiJS draws the view with the rest of the stage.
544
+ */
545
+ async awaitFrameAt(local) {
546
+ if (!this._ctx || this._borrowed) return;
547
+ const t = Math.max(0, Math.min(local, this.duration ?? local));
548
+ this._time = t;
549
+ this._pickStale = true;
550
+ this._pinShaderTime();
551
+ this._followSpace(t);
552
+ const update = this._spec.update;
553
+ if (update) {
554
+ try {
555
+ update(t, this._ctx);
556
+ } catch (err) {
557
+ this._say("update", `update threw (reported once): ${err instanceof Error ? err.message : String(err)}`);
558
+ }
559
+ }
560
+ this._poseClips(t);
561
+ for (const hook of this._drawHooks) hook();
562
+ }
563
+ /**
564
+ * A clip still playing after `update` (a model made with `autoPlay`, a `player.play(name)` in setup) is posed by nobody: it would move with the ticker, or not at all.
565
+ * The layer poses it from its own time (its speed and loop kept) and leaves it playing, so it plays in the video as it would in a page. A clip `update` poses itself
566
+ * (`gotoAndStop(t)` stops it) is left alone. Nothing is remembered from one frame to the next: the frame depends on this frame's `update` only.
567
+ */
568
+ _poseClips(t) {
569
+ for (const player of this._players) {
570
+ for (const label3 of player.animationLabels) {
571
+ const state = player.getState(label3);
572
+ if (!state.playing) continue;
573
+ const length = state.animation.duration;
574
+ const x = t * (state.animationSpeed ?? 1);
575
+ state.gotoAndStop(length > 0 ? state.loop ? (x % length + length) % length : Math.min(Math.max(x, 0), length) : 0);
576
+ state.play();
577
+ }
578
+ }
579
+ }
580
+ /**
581
+ * The determinism check (`check`, `movie.review()`): the scene set by `update` and drawn at a few moments in two orders, with one draw thrown away first, and the pictures
582
+ * compared, as the canvas and three layers are. A scene whose same frame is not the same picture (state kept from call to call, a clock, a player or particle system on its
583
+ * own clock, a motion blur that reads the last frame) is reported. It draws the view alone into a texture of its own size with PixiJS's renderer; the next real frame sets
584
+ * the scene again, so nothing is left behind.
585
+ */
586
+ checkDeterminism(times) {
587
+ const none = { differs: [], drawMs: 0, threw: [] };
588
+ const view = this._view, ctx = this._ctx;
589
+ const renderer = this.root.pixiRenderer;
590
+ if (!view || !ctx || !renderer) return none;
591
+ const w = Math.max(1, Math.ceil(ctx.width)), h = Math.max(1, Math.ceil(ctx.height));
592
+ const rt = pixi_js.RenderTexture.create({ width: w, height: h });
593
+ const wasGroup = view.isRenderGroup;
594
+ const at = new pixi_js.Matrix().translate(view.anchor.x * w, view.anchor.y * h);
595
+ const keep = w * h * 4 * times.length <= 96e6;
596
+ const hash = (d) => {
597
+ let x = 2166136261;
598
+ for (let i = 0; i < d.length; i++) {
599
+ x ^= d[i];
600
+ x = Math.imul(x, 16777619);
601
+ }
602
+ return x >>> 0;
603
+ };
604
+ const ms = [];
605
+ const threw = /* @__PURE__ */ new Set();
606
+ const update = this._spec.update;
607
+ const once = (t, timed) => {
608
+ const from = performance.now();
609
+ let pixels = new Uint8Array(0);
610
+ try {
611
+ this._time = t;
612
+ this._followSpace(t);
613
+ update?.(t, ctx);
614
+ this._poseClips(t);
615
+ renderer.render({ container: view, target: rt, clear: true, transform: at });
616
+ pixels = renderer.extract.pixels(rt).pixels;
617
+ } catch {
618
+ threw.add(t);
619
+ }
620
+ if (timed) ms.push(performance.now() - from);
621
+ return pixels;
622
+ };
623
+ try {
624
+ for (const t of times) once(t, false);
625
+ const first = times.map((t) => {
626
+ const d = once(t, true);
627
+ return keep ? d.slice() : hash(d);
628
+ });
629
+ const second = [...times].reverse().map((t) => once(t, false)).reverse();
630
+ const differs = [];
631
+ times.forEach((t, i) => {
632
+ const a = first[i], b = second[i];
633
+ if (typeof a === "number") {
634
+ if (a !== hash(b)) differs.push({ t, pixels: -1, maxDiff: -1 });
635
+ return;
636
+ }
637
+ let pixels = 0, maxDiff = 0;
638
+ for (let k = 0; k < a.length; k++) {
639
+ const d = Math.abs(a[k] - b[k]);
640
+ if (d) {
641
+ pixels++;
642
+ if (d > maxDiff) maxDiff = d;
643
+ }
644
+ }
645
+ if (pixels) differs.push({ t, pixels, maxDiff });
646
+ });
647
+ ms.sort((x, y) => x - y);
648
+ return { differs: differs.filter((d) => !threw.has(d.t)), drawMs: ms.length ? ms[Math.floor(ms.length / 2)] : 0, threw: times.filter((t) => threw.has(t)) };
649
+ } finally {
650
+ rt.destroy(true);
651
+ if (!wasGroup && view.isRenderGroup) view.disableRenderGroup();
652
+ }
653
+ }
654
+ /** What `layerTexture` asks of this layer: the layers listed before it, PixiJS's renderer, and a place to hook in before every frame. */
655
+ _makeHost(renderer) {
656
+ return {
657
+ findLayer: (name) => this.parent?.findLayer?.(name) ?? null,
658
+ layerNames: () => this.parent?.layerNames?.() ?? [],
659
+ pixiRenderer: renderer,
660
+ beforeDraw: (fn) => {
661
+ this._drawHooks.add(fn);
662
+ return () => {
663
+ this._drawHooks.delete(fn);
664
+ };
665
+ },
666
+ track: (handle) => {
667
+ this._shown.push(handle);
668
+ return () => {
669
+ const i = this._shown.indexOf(handle);
670
+ if (i >= 0) this._shown.splice(i, 1);
671
+ };
672
+ },
673
+ own: (free) => {
674
+ this._owned.push(free);
675
+ }
676
+ };
677
+ }
678
+ /** True when this layer shows 2D layers on its meshes (`layerTexture`): the movie runs its frame after the rest of the frame is ready (projections, filters fed), as for a three layer. */
679
+ get showsLayers() {
680
+ return this._shown.length > 0;
681
+ }
682
+ /** The 2D layer that a material shows (`layerTexture`), for a click on the mesh to reach it (movie.pick); null for any other material. */
683
+ screenOf(material) {
684
+ const handle = this._shown.find((h) => h.material === material);
685
+ return handle ? { source: handle.source(), at: (u, v) => handle.sourceAt(u, v) } : null;
686
+ }
687
+ /** The view's global point at (u, v) of its picture (0..1, v down). */
688
+ _globalAt(u, v) {
689
+ const view = this._view;
690
+ if (!view) return null;
691
+ const b = view.getLocalBounds();
692
+ return view.toGlobal({ x: b.x + u * b.width, y: b.y + v * b.height });
693
+ }
694
+ /**
695
+ * Make the scene's meshes hit by a ray, exactly: PixiJS 3D only hits nodes whose eventMode is not 'passive', and by default against the mesh's bounding box (a sphere's corners
696
+ * count, and the hit has no uv). A mesh the author left as it was gets eventMode 'static' and hitVolume 'geometry' (per triangle: the silhouette, the normal and the uv a 2D
697
+ * layer on the mesh needs); the author's own choice is kept.
698
+ */
699
+ _makePickable() {
700
+ if (!this._pickStale) return;
701
+ this._pickStale = false;
702
+ const root = this._view?.root;
703
+ root?.traverse((node) => {
704
+ const n = node;
705
+ if (n.eventMode !== "passive" || n.geometry === void 0) return;
706
+ n.eventMode = "static";
707
+ if (n.hitVolume === void 0 || n.hitVolume === null) n.hitVolume = "geometry";
708
+ });
709
+ }
710
+ /**
711
+ * `movie.pick`: a ray through (u, v) of the layer's picture (0..1, v down) from the view's camera, and what it meets first: the object's name (its own label, or its nearest
712
+ * labelled ancestor's), where (scene units), how far from the camera, and the node. Uses the transforms of the last render, which is the frame on screen. null when it meets nothing.
713
+ */
714
+ pickAt(u, v) {
715
+ const view = this._view;
716
+ const at = this._globalAt(u, v);
717
+ if (!view || !at) return null;
718
+ this._makePickable();
719
+ const hit = view.raycast(at);
720
+ if (!hit) return null;
721
+ let name = "";
722
+ for (let n = hit.target; n && n !== view.root; n = n.parent) {
723
+ if (n.label && n.label !== n.constructor?.name) {
724
+ name = n.label;
725
+ break;
726
+ }
727
+ }
728
+ return {
729
+ object: name,
730
+ point: [hit.point.x, hit.point.y, hit.point.z],
731
+ distance: hit.distance,
732
+ object3d: hit.target,
733
+ ...hit.uv ? { uv: { u: hit.uv.x, v: hit.uv.y } } : {},
734
+ material: hit.target.material
735
+ };
736
+ }
737
+ /**
738
+ * `hit.scene3d.grab(plane)`: a function of a point (u, v) of the layer's picture that gives where the pointer's ray meets a plane through the grabbed point, and how far that is
739
+ * from it (`delta`): move the object by it. `plane`: 'screen' (facing the camera: the default), 'floor' (horizontal: y up), 'wall' (z constant), or a normal [x, y, z].
740
+ * null where the ray never meets the plane.
741
+ */
742
+ dragPlane(point, plane = "screen") {
743
+ const view = this._view;
744
+ const grabbed = new _3d.Vector3(...point);
745
+ let normal;
746
+ if (plane === "floor") normal = new _3d.Vector3(0, 1, 0);
747
+ else if (plane === "wall") normal = new _3d.Vector3(0, 0, 1);
748
+ else if (Array.isArray(plane)) normal = new _3d.Vector3(...plane).normalize();
749
+ else {
750
+ const m = view?.camera.worldTransform.elements;
751
+ normal = m ? new _3d.Vector3(m[8], m[9], m[10]).normalize() : new _3d.Vector3(0, 0, 1);
752
+ }
753
+ const surface = new _3d.Plane().setFromNormalAndCoplanarPoint(normal, grabbed);
754
+ return (u, v) => {
755
+ const at = this._globalAt(u, v);
756
+ if (!view || !at) return null;
757
+ const meet = view.screenToRay(at).intersectPlane(surface, new _3d.Vector3());
758
+ if (!meet) return null;
759
+ return { point: [meet.x, meet.y, meet.z], delta: [meet.x - grabbed.x, meet.y - grabbed.y, meet.z - grabbed.z] };
760
+ };
761
+ }
762
+ _resolvePath(path) {
763
+ const segs = path.split(".");
764
+ if (segs.length < 2) {
765
+ this._warnPath(path, "needs at least <object>.<prop>");
766
+ return null;
767
+ }
768
+ let obj = this._objects[segs[0]];
769
+ if (obj == null) {
770
+ this._warnPath(path, `unknown object "${segs[0]}" (known: ${Object.keys(this._objects).join(", ")}): expose it in setup's { objects }`);
771
+ return null;
772
+ }
773
+ for (let i = 1; i < segs.length - 1; i++) {
774
+ obj = obj[segs[i]];
775
+ if (obj == null) {
776
+ this._warnPath(path, `"${segs[i]}" is undefined along the path`);
777
+ return null;
778
+ }
779
+ }
780
+ const prop = segs[segs.length - 1];
781
+ if (typeof obj !== "object" || !(prop in obj)) {
782
+ this._warnPath(path, `no property "${prop}"`);
783
+ return null;
784
+ }
785
+ return { target: obj, prop };
786
+ }
787
+ _warnPath(path, why) {
788
+ this._say(`path:${path}`, `keyframe path scene3d.${path}: ${why}; skipped`);
789
+ }
790
+ _destroyView() {
791
+ const view = this._view;
792
+ this._view = null;
793
+ if (this.target === view) this.target = null;
794
+ try {
795
+ view?.destroy({ children: true });
796
+ } catch (err) {
797
+ console.warn(`${this._who()}: destroying the view threw \u2014`, err);
798
+ }
799
+ }
800
+ destroy() {
801
+ const spec = this._spec;
802
+ const kept = this._donated || this._borrowed !== null;
803
+ if (!kept) {
804
+ if (this._ctx && spec.dispose) {
805
+ try {
806
+ spec.dispose(this._ctx);
807
+ } catch (err) {
808
+ console.warn(`${this._who()}: dispose() threw \u2014`, err);
809
+ }
810
+ }
811
+ for (const h of [...this._shown]) {
812
+ try {
813
+ h.dispose();
814
+ } catch {
815
+ }
816
+ }
817
+ this._drawHooks.clear();
818
+ for (const free of this._owned.splice(0)) free();
819
+ this._lightMirror?.dispose();
820
+ }
821
+ if (!kept) {
822
+ this._shown = [];
823
+ this._drawHooks = /* @__PURE__ */ new Set();
824
+ this._owned = [];
825
+ }
826
+ this._pendingRebinds = [];
827
+ this._borrowed = null;
828
+ this._lightMirror = null;
829
+ this._fogMirror = null;
830
+ this._ctx = null;
831
+ this._objects = {};
832
+ this._players = [];
833
+ this._destroyView();
834
+ this._look?.destroy();
835
+ this._look = null;
836
+ this._blend?.destroy();
837
+ this._blend = null;
838
+ super.destroy();
839
+ }
840
+ };
841
+ var ids = /* @__PURE__ */ new WeakMap();
842
+ var lastId = 0;
843
+ function idOf(fn) {
844
+ if (typeof fn !== "function") return 0;
845
+ let id = ids.get(fn);
846
+ if (id === void 0) {
847
+ id = ++lastId;
848
+ ids.set(fn, id);
849
+ }
850
+ return id;
851
+ }
852
+ function reuseKeyOf(spec, width, height) {
853
+ let view;
854
+ try {
855
+ view = JSON.stringify(spec.view ?? null) ?? "null";
856
+ } catch {
857
+ return null;
858
+ }
859
+ return JSON.stringify(["scene3d", spec.name, idOf(spec.setup), width, height, spec.resolution ?? 1, view, !!spec.followCamera, !!spec.followLights, !!spec.followFog]);
860
+ }
861
+ function resolveDim(v, scope) {
862
+ if (v === void 0) return void 0;
863
+ if (typeof v === "number") return v;
864
+ return chunk33USHRV5_cjs.evaluateExpr(v, scope);
865
+ }
866
+ var label2 = "pixi-effects: loadModel";
867
+ async function loadModel(url, options = {}) {
868
+ if (typeof url !== "string" || !url) throw new Error(`${label2}: the first argument is the model's URL: await loadModel('model.glb')`);
869
+ const source = await pixi_js.Assets.load(url);
870
+ const model = new _3d.Model3D({ source, autoUpdate: false, autoPlay: false });
871
+ const player = model.player;
872
+ const clips = player ? [...player.animationLabels] : [];
873
+ const speed = options.speed ?? 1, offset = options.offset ?? 0, loop = options.loop !== false;
874
+ let clip = null;
875
+ if (options.animation === void 0) clip = clips[0] ?? null;
876
+ else if (options.animation === false) clip = null;
877
+ else if (typeof options.animation === "number") {
878
+ clip = clips[options.animation] ?? null;
879
+ if (clip === null) throw new Error(`${label2}: the model has no clip number ${options.animation} (it has ${clips.length}${clips.length ? `: ${clips.map((c) => `"${c}"`).join(", ")}` : ""})`);
880
+ } else {
881
+ if (!clips.includes(options.animation)) throw new Error(`${label2}: the model has no clip "${options.animation}" (its clips: ${clips.length ? clips.map((c) => `"${c}"`).join(", ") : "none"})`);
882
+ clip = options.animation;
883
+ }
884
+ const object = new _3d.Container3D({ label: url.split("/").pop() ?? "model" });
885
+ object.addChild(model);
886
+ const box = model.getLocalBounds();
887
+ model.position.set(-(box.min.x + box.max.x) / 2, -(box.min.y + box.max.y) / 2, -(box.min.z + box.max.z) / 2);
888
+ const base = { x: box.max.x - box.min.x, y: box.max.y - box.min.y, z: box.max.z - box.min.z };
889
+ const state = clip !== null && player ? player.getState(clip) : null;
890
+ const pose = (t) => {
891
+ if (!state) return;
892
+ const length = state.animation.duration;
893
+ const x = t * speed + offset;
894
+ state.gotoAndStop(length > 0 ? loop ? (x % length + length) % length : Math.min(Math.max(x, 0), length) : 0);
895
+ };
896
+ pose(0);
897
+ return {
898
+ object,
899
+ model,
900
+ clips,
901
+ size: () => ({ x: base.x * object.scale.x, y: base.y * object.scale.y, z: base.z * object.scale.z }),
902
+ fit: (n) => {
903
+ const longest = Math.max(base.x, base.y, base.z);
904
+ if (!(n > 0) || !(longest > 0)) throw new Error(`${label2}: fit(n) needs a size above 0 (got ${n}) and a model with a size (its box is ${base.x} x ${base.y} x ${base.z})`);
905
+ const k = n / longest;
906
+ object.scale.set(k, k, k);
907
+ },
908
+ pose
909
+ };
910
+ }
911
+ function fitCamera(ctx, object, options = {}) {
912
+ const camera = ctx.camera;
913
+ const box = object.getLocalBounds().applyMatrix4(object.getGlobalTransform(new _3d.Matrix4()));
914
+ const centre = new _3d.Vector3((box.min.x + box.max.x) / 2, (box.min.y + box.max.y) / 2, (box.min.z + box.max.z) / 2);
915
+ const margin = options.margin ?? 1.1;
916
+ const [dx, dy, dz] = options.direction ?? [0.45, 0.3, 1];
917
+ const toCamera = new _3d.Vector3(dx, dy, dz).normalize();
918
+ const right = new _3d.Vector3(0, 1, 0).cross(toCamera);
919
+ if (right.x * right.x + right.y * right.y + right.z * right.z < 1e-12) right.set(1, 0, 0);
920
+ right.normalize();
921
+ const up = toCamera.clone().cross(right);
922
+ const tanV = Math.tan(camera.fov * Math.PI / 360), tanH = tanV * (ctx.width / ctx.height);
923
+ let distance = 1e-3;
924
+ for (const x of [box.min.x, box.max.x]) for (const y of [box.min.y, box.max.y]) for (const z of [box.min.z, box.max.z]) {
925
+ const r = new _3d.Vector3(x - centre.x, y - centre.y, z - centre.z);
926
+ const along = r.dot(toCamera);
927
+ distance = Math.max(distance, along + Math.abs(r.dot(right)) * margin / tanH, along + Math.abs(r.dot(up)) * margin / tanV);
928
+ }
929
+ camera.mode = "perspective";
930
+ camera.position.set(centre.x + toCamera.x * distance, centre.y + toCamera.y * distance, centre.z + toCamera.z * distance);
931
+ camera.near = Math.max(distance / 100, 1e-4);
932
+ camera.far = distance * 100;
933
+ camera.lookAt(centre);
934
+ }
935
+
936
+ // src/3d/index.ts
937
+ var saidVersion = false;
938
+ function versionAtLeast(v, min) {
939
+ const [a, b] = v.split(".").map(Number);
940
+ if (!Number.isFinite(a) || !Number.isFinite(b)) return true;
941
+ return a > min[0] || a === min[0] && b >= min[1];
942
+ }
943
+ function register3D() {
944
+ if (!saidVersion && !versionAtLeast(String(pixi_js.VERSION), [8, 23])) {
945
+ saidVersion = true;
946
+ console.warn(`pixi-effects: PixiJS 3D needs pixi.js 8.23.0 or later (this page has ${pixi_js.VERSION}): put pixi.js@8.23.0 in the import map`);
947
+ }
948
+ _3d.Model3D.defaultOptions.autoUpdate = false;
949
+ _3d.AnimationPlayer.defaultOptions.autoUpdate = false;
950
+ chunk33USHRV5_cjs.registerSequenceType("scene3d", Scene3DSequence);
951
+ }
952
+ function scene3d(spec) {
953
+ return spec;
954
+ }
955
+
956
+ exports.Scene3DSequence = Scene3DSequence;
957
+ exports.fitCamera = fitCamera;
958
+ exports.layerTexture = layerTexture;
959
+ exports.loadModel = loadModel;
960
+ exports.register3D = register3D;
961
+ exports.scene3d = scene3d;
962
+ //# sourceMappingURL=3d.cjs.map
963
+ //# sourceMappingURL=3d.cjs.map