zimporter-html 1.0.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -0
  3. package/dist/ParticleConverter.d.ts +26 -0
  4. package/dist/ParticleConverter.d.ts.map +1 -0
  5. package/dist/ParticleConverter.js +129 -0
  6. package/dist/ParticleConverter.js.map +1 -0
  7. package/dist/SceneData.d.ts +189 -0
  8. package/dist/SceneData.d.ts.map +1 -0
  9. package/dist/SceneData.js +16 -0
  10. package/dist/SceneData.js.map +1 -0
  11. package/dist/ZButton.d.ts +37 -0
  12. package/dist/ZButton.d.ts.map +1 -0
  13. package/dist/ZButton.js +258 -0
  14. package/dist/ZButton.js.map +1 -0
  15. package/dist/ZContainer.d.ts +154 -0
  16. package/dist/ZContainer.d.ts.map +1 -0
  17. package/dist/ZContainer.js +540 -0
  18. package/dist/ZContainer.js.map +1 -0
  19. package/dist/ZCuePointsManager.d.ts +8 -0
  20. package/dist/ZCuePointsManager.d.ts.map +1 -0
  21. package/dist/ZCuePointsManager.js +26 -0
  22. package/dist/ZCuePointsManager.js.map +1 -0
  23. package/dist/ZNineSlice.d.ts +23 -0
  24. package/dist/ZNineSlice.d.ts.map +1 -0
  25. package/dist/ZNineSlice.js +72 -0
  26. package/dist/ZNineSlice.js.map +1 -0
  27. package/dist/ZPool.d.ts +16 -0
  28. package/dist/ZPool.d.ts.map +1 -0
  29. package/dist/ZPool.js +54 -0
  30. package/dist/ZPool.js.map +1 -0
  31. package/dist/ZResizeables.d.ts +8 -0
  32. package/dist/ZResizeables.d.ts.map +1 -0
  33. package/dist/ZResizeables.js +16 -0
  34. package/dist/ZResizeables.js.map +1 -0
  35. package/dist/ZScene.d.ts +82 -0
  36. package/dist/ZScene.d.ts.map +1 -0
  37. package/dist/ZScene.js +663 -0
  38. package/dist/ZScene.js.map +1 -0
  39. package/dist/ZSceneStack.d.ts +19 -0
  40. package/dist/ZSceneStack.d.ts.map +1 -0
  41. package/dist/ZSceneStack.js +47 -0
  42. package/dist/ZSceneStack.js.map +1 -0
  43. package/dist/ZScroll.d.ts +72 -0
  44. package/dist/ZScroll.d.ts.map +1 -0
  45. package/dist/ZScroll.js +276 -0
  46. package/dist/ZScroll.js.map +1 -0
  47. package/dist/ZSlider.d.ts +45 -0
  48. package/dist/ZSlider.d.ts.map +1 -0
  49. package/dist/ZSlider.js +176 -0
  50. package/dist/ZSlider.js.map +1 -0
  51. package/dist/ZSpine.d.ts +54 -0
  52. package/dist/ZSpine.d.ts.map +1 -0
  53. package/dist/ZSpine.js +166 -0
  54. package/dist/ZSpine.js.map +1 -0
  55. package/dist/ZState.d.ts +15 -0
  56. package/dist/ZState.d.ts.map +1 -0
  57. package/dist/ZState.js +50 -0
  58. package/dist/ZState.js.map +1 -0
  59. package/dist/ZTextInput.d.ts +24 -0
  60. package/dist/ZTextInput.d.ts.map +1 -0
  61. package/dist/ZTextInput.js +109 -0
  62. package/dist/ZTextInput.js.map +1 -0
  63. package/dist/ZTimeline.d.ts +30 -0
  64. package/dist/ZTimeline.d.ts.map +1 -0
  65. package/dist/ZTimeline.js +123 -0
  66. package/dist/ZTimeline.js.map +1 -0
  67. package/dist/ZToggle.d.ts +19 -0
  68. package/dist/ZToggle.d.ts.map +1 -0
  69. package/dist/ZToggle.js +56 -0
  70. package/dist/ZToggle.js.map +1 -0
  71. package/dist/ZUpdatables.d.ts +24 -0
  72. package/dist/ZUpdatables.d.ts.map +1 -0
  73. package/dist/ZUpdatables.js +50 -0
  74. package/dist/ZUpdatables.js.map +1 -0
  75. package/dist/index.d.ts +18 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +18 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/zimporter-html.min.js +2 -0
  80. package/dist/zimporter-html.min.js.map +1 -0
  81. package/package.json +35 -0
package/dist/ZScene.js ADDED
@@ -0,0 +1,663 @@
1
+ import { ZButton } from './ZButton';
2
+ import { ZContainer } from './ZContainer';
3
+ import { ZTimeline } from './ZTimeline';
4
+ import { ZNineSlice } from './ZNineSlice';
5
+ import { ZScroll } from './ZScroll';
6
+ import { ZSlider } from './ZSlider';
7
+ import { ZTextInput } from './ZTextInput';
8
+ import { ZSpine } from './ZSpine';
9
+ import { ZState } from './ZState';
10
+ import { ZToggle } from './ZToggle';
11
+ /**
12
+ * HTML div–based ZScene.
13
+ *
14
+ * Loads `placements.json` from an asset base path, constructs the scene
15
+ * hierarchy as nested `ZContainer` divs, scales and centres the stage to
16
+ * fit the viewport, and exposes the same public API as the PIXI / Phaser
17
+ * versions of zImporter.
18
+ *
19
+ * Spine and particle assets are silently skipped (not supported in HTML).
20
+ */
21
+ export class ZScene {
22
+ static assetTypes = new Map([
23
+ ['btn', ZButton],
24
+ ['asset', ZContainer],
25
+ ['state', ZState],
26
+ ['toggle', ZToggle],
27
+ ['slider', ZSlider],
28
+ ['scrollBar', ZScroll],
29
+ ['fullScreen', ZContainer],
30
+ ['animation', ZTimeline],
31
+ ]);
32
+ assetBasePath = '';
33
+ data;
34
+ _sceneStage = new ZContainer();
35
+ resizeMap = new Map();
36
+ static SceneMap = new Map();
37
+ sceneId;
38
+ orientation = 'portrait';
39
+ /** Atlas frame rects keyed by frame name (no _IMG suffix). Populated when atlas:true. */
40
+ atlasFrames = {};
41
+ /** Full URL of the atlas image (ta.png). */
42
+ atlasImageUrl = '';
43
+ /** Full atlas image dimensions (needed for background-size). */
44
+ atlasSize = { w: 0, h: 0 };
45
+ /** Parsed bitmap fonts keyed by uniqueFontName (e.g. "Arial_1e00ffa19b9b_53"). */
46
+ bitmapFonts = {};
47
+ // ── Accessors ─────────────────────────────────────────────────────────────
48
+ get sceneStage() {
49
+ return this._sceneStage;
50
+ }
51
+ get sceneWidth() {
52
+ return this.orientation === 'portrait'
53
+ ? this.data.resolution.y : this.data.resolution.x;
54
+ }
55
+ get sceneHeight() {
56
+ return this.orientation === 'portrait'
57
+ ? this.data.resolution.x : this.data.resolution.y;
58
+ }
59
+ // ── Construction ──────────────────────────────────────────────────────────
60
+ constructor(sceneId) {
61
+ this.sceneId = sceneId;
62
+ this.setOrientation();
63
+ ZScene.SceneMap.set(sceneId, this);
64
+ }
65
+ setOrientation() {
66
+ this.orientation = window.innerWidth > window.innerHeight
67
+ ? 'landscape' : 'portrait';
68
+ }
69
+ static getSceneById(sceneId) {
70
+ return ZScene.SceneMap.get(sceneId);
71
+ }
72
+ // ── Loading ───────────────────────────────────────────────────────────────
73
+ /**
74
+ * Fetches `placements.json` (and `ta.json` when atlas:true) from
75
+ * `assetBasePath` and builds the scene.
76
+ */
77
+ async load(assetBasePath, onComplete) {
78
+ this.assetBasePath = assetBasePath.endsWith('/') ? assetBasePath : assetBasePath + '/';
79
+ const url = this.assetBasePath + 'placements.json?rnd=' + Math.random();
80
+ try {
81
+ const res = await fetch(url);
82
+ if (!res.ok)
83
+ throw new Error(`HTTP ${res.status}`);
84
+ const data = await res.json();
85
+ // If the scene uses a texture atlas, load ta.json so we can do
86
+ // CSS-sprite cropping for every img/9slice frame.
87
+ if (data.atlas === true) {
88
+ await this._loadAtlas();
89
+ }
90
+ // Pre-load any bitmap fonts declared in the scene.
91
+ if (data.fonts && data.fonts.length > 0) {
92
+ await this._loadFonts(data.fonts);
93
+ }
94
+ this.initScene(data);
95
+ onComplete();
96
+ }
97
+ catch (err) {
98
+ console.error('[ZScene] Failed to load placements.json:', err);
99
+ }
100
+ }
101
+ /** Fetches ta.json and ta.png metadata and caches the frame rects. */
102
+ async _loadAtlas() {
103
+ const atlasUrl = this.assetBasePath + 'ta.json?rnd=' + Math.random();
104
+ try {
105
+ const res = await fetch(atlasUrl);
106
+ if (!res.ok)
107
+ return; // no atlas available — fall back to individual images
108
+ const json = await res.json();
109
+ // TexturePacker JSON-hash format: { frames: { name: { frame:{x,y,w,h} } }, meta: { image, size } }
110
+ const frames = json.frames;
111
+ for (const key in frames) {
112
+ const f = frames[key].frame ?? frames[key];
113
+ this.atlasFrames[key] = { x: f.x, y: f.y, w: f.w, h: f.h };
114
+ }
115
+ const meta = json.meta ?? {};
116
+ const imageFile = meta.image ?? 'ta.png';
117
+ this.atlasImageUrl = this.assetBasePath + imageFile;
118
+ this.atlasSize = { w: meta.size?.w ?? 0, h: meta.size?.h ?? 0 };
119
+ }
120
+ catch (err) {
121
+ console.warn('[ZScene] Could not load atlas ta.json:', err);
122
+ }
123
+ }
124
+ // ── Bitmap font loading ───────────────────────────────────────────────────
125
+ async _loadFonts(fontNames) {
126
+ await Promise.all(fontNames.map(n => this._loadBitmapFont(n)));
127
+ }
128
+ async _loadBitmapFont(fontName) {
129
+ try {
130
+ const fntUrl = this.assetBasePath + "bitmapFonts/" + fontName + '.fnt?rnd=' + Math.random();
131
+ const res = await fetch(fntUrl);
132
+ if (!res.ok)
133
+ return;
134
+ const xml = await res.text();
135
+ const doc = new DOMParser().parseFromString(xml, 'text/xml');
136
+ const lineHeight = parseInt(doc.querySelector('common')?.getAttribute('lineHeight') ?? '64', 10);
137
+ const chars = new Map();
138
+ doc.querySelectorAll('char').forEach(el => {
139
+ const id = parseInt(el.getAttribute('id'), 10);
140
+ chars.set(id, {
141
+ x: parseInt(el.getAttribute('x'), 10),
142
+ y: parseInt(el.getAttribute('y'), 10),
143
+ w: parseInt(el.getAttribute('width'), 10),
144
+ h: parseInt(el.getAttribute('height'), 10),
145
+ xoffset: parseInt(el.getAttribute('xoffset'), 10),
146
+ yoffset: parseInt(el.getAttribute('yoffset'), 10),
147
+ xadvance: parseInt(el.getAttribute('xadvance'), 10),
148
+ });
149
+ });
150
+ const img = new Image();
151
+ await new Promise(resolve => {
152
+ img.onload = () => resolve();
153
+ img.onerror = () => resolve();
154
+ img.src = this.assetBasePath + "bitmapFonts/" + fontName + '.png?rnd=' + Math.random();
155
+ });
156
+ this.bitmapFonts[fontName] = { img, lineHeight, chars };
157
+ }
158
+ catch (e) {
159
+ console.warn('[ZScene] Could not load bitmap font:', fontName, e);
160
+ }
161
+ }
162
+ // ── Bitmap text canvas renderer ───────────────────────────────────────────
163
+ /**
164
+ * Renders a bitmapText node onto a <canvas> using the pre-parsed glyph
165
+ * atlas. Supports solid-colour and vertical-gradient fills, plus a
166
+ * shadow-based stroke outline that matches PIXI's strokeThickness.
167
+ */
168
+ _createBitmapTextCanvas(data, fontData) {
169
+ const text = data.text ?? '';
170
+ const lineH = fontData.lineHeight;
171
+ const strokeThick = data.strokeThickness ?? 0;
172
+ const pad = Math.ceil(strokeThick);
173
+ // Measure total advance width.
174
+ let textW = 0;
175
+ for (const ch of text) {
176
+ const cd = fontData.chars.get(ch.charCodeAt(0));
177
+ if (cd)
178
+ textW += cd.xadvance;
179
+ }
180
+ const canvasW = textW + pad * 2;
181
+ const canvasH = lineH + pad * 2;
182
+ // ── Offscreen canvas A: raw glyph alpha mask ──────────────────────────
183
+ const maskCanvas = document.createElement('canvas');
184
+ maskCanvas.width = canvasW;
185
+ maskCanvas.height = canvasH;
186
+ const maskCtx = maskCanvas.getContext('2d');
187
+ let cx = pad;
188
+ for (const ch of text) {
189
+ const cd = fontData.chars.get(ch.charCodeAt(0));
190
+ if (!cd)
191
+ continue;
192
+ if (cd.w > 0 && cd.h > 0) {
193
+ maskCtx.drawImage(fontData.img, cd.x, cd.y, cd.w, cd.h, cx + cd.xoffset, pad + cd.yoffset, cd.w, cd.h);
194
+ }
195
+ cx += cd.xadvance;
196
+ }
197
+ // ── Offscreen canvas B: fill tint (gradient or solid) ─────────────────
198
+ const fillCanvas = document.createElement('canvas');
199
+ fillCanvas.width = canvasW;
200
+ fillCanvas.height = canvasH;
201
+ const fillCtx = fillCanvas.getContext('2d');
202
+ // Draw the glyph mask first, then apply fill colour on top via
203
+ // source-in so only the glyph shape is coloured.
204
+ fillCtx.drawImage(maskCanvas, 0, 0);
205
+ fillCtx.globalCompositeOperation = 'source-in';
206
+ if (data.fillType === 'gradient' && data.gradientData) {
207
+ const gd = data.gradientData;
208
+ const horiz = gd.fillGradientType === 1;
209
+ const grad = horiz
210
+ ? fillCtx.createLinearGradient(pad, 0, textW + pad, 0)
211
+ : fillCtx.createLinearGradient(0, pad, 0, lineH + pad);
212
+ gd.colors.forEach((c, i) => {
213
+ grad.addColorStop(gd.percentages[i] ?? i / (gd.colors.length - 1), '#' + c.toString(16).padStart(6, '0'));
214
+ });
215
+ fillCtx.fillStyle = grad;
216
+ }
217
+ else {
218
+ const c = data.color;
219
+ fillCtx.fillStyle = c == null ? '#ffffff'
220
+ : typeof c === 'number' ? '#' + c.toString(16).padStart(6, '0')
221
+ : c;
222
+ }
223
+ fillCtx.fillRect(0, 0, canvasW, canvasH);
224
+ // ── Final canvas: stroke shadow behind, fill on top ───────────────────
225
+ const canvas = document.createElement('canvas');
226
+ canvas.width = canvasW;
227
+ canvas.height = canvasH;
228
+ const ctx = canvas.getContext('2d');
229
+ if (strokeThick > 0 && data.stroke) {
230
+ const strokeC = typeof data.stroke === 'number'
231
+ ? '#' + data.stroke.toString(16).padStart(6, '0')
232
+ : data.stroke;
233
+ // Draw mask with shadow to produce an outline behind the fill.
234
+ ctx.save();
235
+ ctx.shadowColor = strokeC;
236
+ ctx.shadowBlur = strokeThick;
237
+ ctx.drawImage(maskCanvas, 0, 0);
238
+ ctx.restore();
239
+ // Clear the glyph pixels themselves (leave only the shadow halo).
240
+ ctx.globalCompositeOperation = 'destination-out';
241
+ ctx.drawImage(maskCanvas, 0, 0);
242
+ ctx.globalCompositeOperation = 'source-over';
243
+ }
244
+ // Composite coloured fill on top.
245
+ ctx.drawImage(fillCanvas, 0, 0);
246
+ return canvas;
247
+ }
248
+ initScene(data) {
249
+ this.data = data;
250
+ // Give the stage element an id matching its scene name.
251
+ if (data.stage?.name) {
252
+ this._sceneStage.name = data.stage.name;
253
+ }
254
+ }
255
+ // ── Stage / resize ────────────────────────────────────────────────────────
256
+ /**
257
+ * Builds the scene's div hierarchy and appends it to `hostElement`.
258
+ * @param hostElement - The DOM node that will contain the stage (default: document.body).
259
+ */
260
+ loadStage(hostElement = document.body, loadChildren = true) {
261
+ // Style the stage element to have an explicit internal resolution
262
+ // so child coordinates match scene units.
263
+ const stageEl = this._sceneStage.el;
264
+ stageEl.style.position = 'absolute';
265
+ stageEl.style.transformOrigin = '0 0';
266
+ // Do NOT set overflow:hidden here — fullscreen backgrounds need to
267
+ // bleed past the stage bounds to cover the whole viewport.
268
+ this.resize(window.innerWidth, window.innerHeight);
269
+ if (loadChildren && this.data?.stage?.children) {
270
+ for (const child of this.data.stage.children) {
271
+ const instanceData = child;
272
+ if (instanceData.guide)
273
+ continue;
274
+ const mc = this.spawn(instanceData.name);
275
+ if (mc) {
276
+ // addChild before setInstanceData so parent is set when
277
+ // _applyAnchor runs inside applyTransform().
278
+ this._sceneStage.addChild(mc);
279
+ mc.setInstanceData(instanceData, this.orientation);
280
+ this.addToResizeMap(mc);
281
+ this._sceneStage[mc.name] = mc;
282
+ }
283
+ }
284
+ }
285
+ hostElement.appendChild(stageEl);
286
+ this.resize(window.innerWidth, window.innerHeight);
287
+ }
288
+ addToResizeMap(mc) {
289
+ this.resizeMap.set(mc, true);
290
+ }
291
+ removeFromResizeMap(mc) {
292
+ this.resizeMap.delete(mc);
293
+ }
294
+ getInnerDimensions() {
295
+ return { width: this.sceneWidth, height: this.sceneHeight };
296
+ }
297
+ /**
298
+ * Scales and centres the stage div to fill the viewport while preserving
299
+ * the scene's internal aspect ratio. Mirrors ZScene.resize in PIXI.
300
+ */
301
+ resize(width, height) {
302
+ if (!this.data?.resolution)
303
+ return;
304
+ this.setOrientation();
305
+ const baseW = this.sceneWidth;
306
+ const baseH = this.sceneHeight;
307
+ const scale = Math.min(width / baseW, height / baseH);
308
+ const stageEl = this._sceneStage.el;
309
+ stageEl.style.width = baseW + 'px';
310
+ stageEl.style.height = baseH + 'px';
311
+ // Use CSS transform for scale + centering
312
+ const offsetX = (width - baseW * scale) / 2;
313
+ const offsetY = (height - baseH * scale) / 2;
314
+ stageEl.style.transform =
315
+ `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
316
+ // Publish metrics so ZContainer._applyAnchor can convert viewport% → scene coords.
317
+ ZContainer.stageOffsetX = offsetX;
318
+ ZContainer.stageOffsetY = offsetY;
319
+ ZContainer.stageScale = scale;
320
+ for (const [mc] of this.resizeMap) {
321
+ if (mc._fitToScreen) {
322
+ mc.executeFitToScreen(width, height, offsetX, offsetY, scale);
323
+ }
324
+ else {
325
+ mc.resize(width, height, this.orientation);
326
+ }
327
+ }
328
+ // Second pass: re-apply anchors now that ALL containers in the resize
329
+ // map have been updated to the new orientation. This prevents the
330
+ // chain-inversion from seeing stale _x/_y values on ancestor containers
331
+ // that hadn't been processed yet in the first pass.
332
+ for (const [mc] of this.resizeMap) {
333
+ mc.reapplyAnchor();
334
+ }
335
+ }
336
+ // ── Template spawning ─────────────────────────────────────────────────────
337
+ spawn(tempName) {
338
+ const templates = this.data.templates;
339
+ const baseNode = templates[tempName];
340
+ if (!baseNode)
341
+ return undefined;
342
+ const frames = this.getChildrenFrames(tempName);
343
+ let mc;
344
+ if (Object.keys(frames).length > 0) {
345
+ mc = new ZTimeline();
346
+ this.createAsset(mc, baseNode);
347
+ mc.setFrames(frames);
348
+ if (this.data.cuePoints?.[tempName]) {
349
+ mc.setCuePoints(this.data.cuePoints[tempName]);
350
+ }
351
+ mc.gotoAndStop(0);
352
+ }
353
+ else {
354
+ const Ctor = ZScene.getAssetType(baseNode.type) ?? ZContainer;
355
+ mc = new Ctor();
356
+ this.createAsset(mc, baseNode);
357
+ mc.init();
358
+ }
359
+ return mc;
360
+ }
361
+ // ─────────────────────────────────────────────────────────────────────────
362
+ // Asset creation
363
+ // ─────────────────────────────────────────────────────────────────────────
364
+ createAsset(mc, baseNode) {
365
+ for (const childNode of baseNode.children) {
366
+ const _name = childNode.name;
367
+ const type = childNode.type;
368
+ // ── 1. Image ────────────────────────────────────────────────────
369
+ if (type === 'img') {
370
+ const spriteData = childNode;
371
+ const img = this._createImageElement(spriteData);
372
+ img.style.position = 'absolute';
373
+ img.style.left = (spriteData.x || 0) + 'px';
374
+ img.style.top = (spriteData.y || 0) + 'px';
375
+ img.style.width = (spriteData.width || 0) + 'px';
376
+ img.style.height = (spriteData.height || 0) + 'px';
377
+ img.style.transformOrigin = '0 0';
378
+ if (spriteData.pivotX || spriteData.pivotY) {
379
+ img.style.transform =
380
+ `translate(${-(spriteData.pivotX || 0)}px, ${-(spriteData.pivotY || 0)}px)`;
381
+ }
382
+ img.id = _name;
383
+ img.dataset.name = _name;
384
+ mc[_name.replace(/_IMG$/, '')] = img;
385
+ mc.el.appendChild(img);
386
+ }
387
+ // ── 2. Nine-slice ────────────────────────────────────────────────
388
+ else if (type === '9slice') {
389
+ const nsData = childNode;
390
+ const ns = new ZNineSlice(nsData, this.orientation, this.assetBasePath);
391
+ ns.name = _name.replace(/_9S$/, '');
392
+ mc[ns.name] = ns;
393
+ mc.addChild(ns);
394
+ this.addToResizeMap(ns);
395
+ }
396
+ // ── 3. Text / bitmap text ─────────────────────────────────────────
397
+ else if (type === 'textField' || type === 'bitmapText' || type === 'bitmapFontLocked') {
398
+ const textData = childNode;
399
+ const container = this._createTextElement(textData);
400
+ container.el.id = _name;
401
+ mc[_name] = container;
402
+ mc.el.appendChild(container.el);
403
+ }
404
+ // ── 4. Sub-containers / buttons / states / timelines ──────────────
405
+ else if (ZScene.isAssetType(type)) {
406
+ const instanceData = childNode;
407
+ if (instanceData.guide)
408
+ continue;
409
+ const frames = this.getChildrenFrames(_name);
410
+ let asset;
411
+ if (Object.keys(frames).length > 0) {
412
+ asset = new ZTimeline();
413
+ asset.setFrames(frames);
414
+ if (this.data.cuePoints?.[_name]) {
415
+ asset.setCuePoints(this.data.cuePoints[_name]);
416
+ }
417
+ }
418
+ else {
419
+ const Ctor = ZScene.getAssetType(type) ?? ZContainer;
420
+ asset = new Ctor();
421
+ }
422
+ asset.name = instanceData.instanceName;
423
+ if (!asset.name)
424
+ continue;
425
+ mc[asset.name] = asset;
426
+ // addChild BEFORE setInstanceData so asset.parent is set when
427
+ // _applyAnchor() runs inside applyTransform().
428
+ mc.addChild(asset);
429
+ asset.setInstanceData(instanceData, this.orientation);
430
+ this.addToResizeMap(asset);
431
+ // recurse into child template if it exists
432
+ const childTemplate = this.data.templates[_name];
433
+ if (childTemplate?.children) {
434
+ this.createAsset(asset, childTemplate);
435
+ }
436
+ asset.init();
437
+ }
438
+ // ── 5. Input field ────────────────────────────────────────────────
439
+ else if (type === 'inputField') {
440
+ const inputData = childNode;
441
+ const textInput = new ZTextInput(inputData);
442
+ textInput.name = _name;
443
+ textInput.x = inputData.x || 0;
444
+ textInput.y = inputData.y || 0;
445
+ mc[_name] = textInput;
446
+ mc.addChild(textInput);
447
+ this.addToResizeMap(textInput);
448
+ }
449
+ // ── 6. Spine ──────────────────────────────────────────────────────
450
+ else if (type === 'spine') {
451
+ const spineData = childNode;
452
+ const spineObj = new ZSpine();
453
+ spineObj.name = _name;
454
+ mc[_name] = spineObj;
455
+ mc.addChild(spineObj);
456
+ this.addToResizeMap(spineObj);
457
+ spineObj.load(this.assetBasePath, spineData.spineJson, spineData.spineAtlas, spineData.pngFiles ?? [], spineData.skin ?? 'default').then(() => {
458
+ if (spineData.playOnStart?.value) {
459
+ spineObj.play(spineData.playOnStart.animation, true);
460
+ }
461
+ });
462
+ }
463
+ // ── 7. Particle — not supported in HTML ───────────────────────────
464
+ else if (type === 'particle') {
465
+ console.warn('[ZScene] particle assets are not supported in the HTML renderer.');
466
+ }
467
+ // ── Check for a child template (for non-asset-type children) ─────
468
+ const childTemplate = this.data.templates?.[_name];
469
+ if (childTemplate?.children && !ZScene.isAssetType(type)) {
470
+ // Create a wrapper container and recurse
471
+ const asset = new ZContainer();
472
+ asset.name = _name;
473
+ mc[_name] = asset;
474
+ mc.addChild(asset);
475
+ this.createAsset(asset, childTemplate);
476
+ }
477
+ }
478
+ }
479
+ // ─────────────────────────────────────────────────────────────────────────
480
+ // DOM element factories
481
+ // ─────────────────────────────────────────────────────────────────────────
482
+ _createImageElement(data) {
483
+ // Frame name in the atlas is the sprite name without the _IMG suffix.
484
+ const frameName = data.name.replace(/_IMG$/, '');
485
+ const atlasFrame = this.atlasFrames[frameName];
486
+ if (atlasFrame && this.atlasImageUrl) {
487
+ // ── Atlas path: CSS-sprite crop from ta.png ────────────────────
488
+ // The frame is at (atlasFrame.x, atlasFrame.y) inside the atlas.
489
+ // We want to display it at the sprite's intended size (data.width × data.height).
490
+ // CSS background-size scales the whole atlas sheet; we compute the
491
+ // scale factor so the frame region fills the div exactly.
492
+ const scaleX = (data.width || atlasFrame.w) / atlasFrame.w;
493
+ const scaleY = (data.height || atlasFrame.h) / atlasFrame.h;
494
+ const bgW = this.atlasSize.w * scaleX;
495
+ const bgH = this.atlasSize.h * scaleY;
496
+ const bgX = -atlasFrame.x * scaleX;
497
+ const bgY = -atlasFrame.y * scaleY;
498
+ const div = document.createElement('div');
499
+ div.style.userSelect = 'none';
500
+ div.style.backgroundImage = `url(${this.atlasImageUrl})`;
501
+ div.style.backgroundRepeat = 'no-repeat';
502
+ div.style.backgroundSize = `${bgW}px ${bgH}px`;
503
+ div.style.backgroundPosition = `${bgX}px ${bgY}px`;
504
+ div.dataset.frameName = frameName;
505
+ // Store the raw atlas values so executeFitToScreen can recompute
506
+ // the background-size/position when the container is stretched to
507
+ // fill the viewport at a different size.
508
+ div.dataset.atlasFrameX = String(atlasFrame.x);
509
+ div.dataset.atlasFrameY = String(atlasFrame.y);
510
+ div.dataset.atlasFrameW = String(atlasFrame.w);
511
+ div.dataset.atlasFrameH = String(atlasFrame.h);
512
+ div.dataset.atlasTotalW = String(this.atlasSize.w);
513
+ div.dataset.atlasTotalH = String(this.atlasSize.h);
514
+ return div;
515
+ }
516
+ // ── Individual image path ──────────────────────────────────────────
517
+ const img = document.createElement('img');
518
+ img.style.userSelect = 'none';
519
+ img.draggable = false;
520
+ if (data.filePath) {
521
+ const cleanPath = data.filePath.replace(/^\.\//, '');
522
+ img.src = this.assetBasePath + cleanPath;
523
+ }
524
+ return img;
525
+ }
526
+ _createTextElement(data) {
527
+ const wrapper = new ZContainer();
528
+ wrapper.name = data.name;
529
+ wrapper.x = data.x || 0;
530
+ wrapper.y = data.y || 0;
531
+ wrapper._applyTransformPublic?.();
532
+ // ── BitmapText path: render to <canvas> ───────────────────────────────
533
+ const fontKey = data.uniqueFontName ?? (Array.isArray(data.fontName) ? data.fontName[0] : data.fontName);
534
+ const fontData = fontKey ? this.bitmapFonts[fontKey] : undefined;
535
+ if ((data.type === 'bitmapText' || data.type === 'bitmapFontLocked') && fontData) {
536
+ const canvas = this._createBitmapTextCanvas(data, fontData);
537
+ canvas.style.position = 'absolute';
538
+ canvas.style.imageRendering = 'pixelated';
539
+ // Apply anchor offset (same logic as span below).
540
+ const ancX = data.textAnchorX ?? 0;
541
+ const ancY = data.textAnchorY ?? 0;
542
+ if (ancX !== 0 || ancY !== 0) {
543
+ canvas.style.transform = `translate(${-ancX * 100}%, ${-ancY * 100}%)`;
544
+ }
545
+ wrapper.el.appendChild(canvas);
546
+ return wrapper;
547
+ }
548
+ const span = document.createElement('span');
549
+ span.classList.add('z-text');
550
+ span.textContent = data.text ?? '';
551
+ // Font size
552
+ const fontSize = typeof data.size === 'number' ? data.size : parseFloat(String(data.size));
553
+ if (!isNaN(fontSize))
554
+ span.style.fontSize = fontSize + 'px';
555
+ // Color
556
+ // Always set both `color` AND `-webkit-text-fill-color`.
557
+ // When `-webkit-text-stroke` is applied later, WebKit requires an
558
+ // explicit `-webkit-text-fill-color` to keep the fill visible;
559
+ // without it the stroke colour can bleed into the fill.
560
+ const fillColor = data.color != null
561
+ ? (typeof data.color === 'number'
562
+ ? '#' + data.color.toString(16).padStart(6, '0')
563
+ : data.color)
564
+ : null;
565
+ if (fillColor) {
566
+ span.style.color = fillColor;
567
+ span.style.setProperty('-webkit-text-fill-color', fillColor);
568
+ }
569
+ // Font family
570
+ if (data.fontName) {
571
+ span.style.fontFamily = Array.isArray(data.fontName)
572
+ ? data.fontName.join(', ')
573
+ : data.fontName;
574
+ }
575
+ // font-weight: CSS keyword values are lowercase ('normal', 'bold')
576
+ if (data.fontWeight)
577
+ span.style.fontWeight = data.fontWeight.toLowerCase();
578
+ if (data.lineHeight)
579
+ span.style.lineHeight = data.lineHeight + 'px';
580
+ // Set an explicit width when available so text-align works correctly.
581
+ // Also used as the box for percentage-based translate centering.
582
+ if (data.width)
583
+ span.style.width = data.width + 'px';
584
+ if (data.align)
585
+ span.style.textAlign = data.align;
586
+ // Word wrap
587
+ if (data.wordWrap) {
588
+ span.style.whiteSpace = 'normal';
589
+ if (data.wordWrapWidth)
590
+ span.style.width = data.wordWrapWidth + 'px';
591
+ }
592
+ else {
593
+ span.style.whiteSpace = 'nowrap';
594
+ }
595
+ // Text stroke.
596
+ // Use setProperty for vendor-prefixed properties to guarantee they are
597
+ // applied. paint-order:stroke fill draws the stroke first so the fill
598
+ // colour sits on top (standard in Chrome/Firefox/Safari 15.4+).
599
+ if (data.stroke && data.strokeThickness) {
600
+ const strokeColor = typeof data.stroke === 'number'
601
+ ? '#' + data.stroke.toString(16).padStart(6, '0')
602
+ : data.stroke;
603
+ span.style.setProperty('-webkit-text-stroke', `${data.strokeThickness}px ${strokeColor}`);
604
+ span.style.setProperty('paint-order', 'stroke fill');
605
+ }
606
+ // Position the span based on anchor.
607
+ // translate(-50%,-50%) centers the span on its origin point when
608
+ // textAnchorX/Y are 0.5.
609
+ span.style.position = 'absolute';
610
+ const ancX = data.textAnchorX ?? 0;
611
+ const ancY = data.textAnchorY ?? 0;
612
+ if (ancX !== 0 || ancY !== 0) {
613
+ span.style.transform = `translate(${-ancX * 100}%, ${-ancY * 100}%)`;
614
+ }
615
+ span.style.display = 'inline-block';
616
+ wrapper.el.appendChild(span);
617
+ return wrapper;
618
+ }
619
+ // ─────────────────────────────────────────────────────────────────────────
620
+ // Frame / animation helpers
621
+ // ─────────────────────────────────────────────────────────────────────────
622
+ getChildrenFrames(templateName) {
623
+ const frames = {};
624
+ const templates = this.data?.templates;
625
+ const animTracks = this.data?.animTracks ?? {};
626
+ const baseNode = templates?.[templateName];
627
+ if (baseNode?.children) {
628
+ for (const child of baseNode.children) {
629
+ const instanceData = child;
630
+ const childInstanceName = instanceData.instanceName;
631
+ if (!childInstanceName)
632
+ continue;
633
+ const combinedName = childInstanceName + '_' + templateName;
634
+ if (animTracks[combinedName]) {
635
+ frames[childInstanceName] = animTracks[combinedName];
636
+ }
637
+ }
638
+ }
639
+ return frames;
640
+ }
641
+ // ─────────────────────────────────────────────────────────────────────────
642
+ // Static helpers
643
+ // ─────────────────────────────────────────────────────────────────────────
644
+ static getAssetType(value) {
645
+ return this.assetTypes.get(value) ?? null;
646
+ }
647
+ static isAssetType(value) {
648
+ return this.assetTypes.has(value);
649
+ }
650
+ degreesToRadians(degrees) {
651
+ return (degrees * Math.PI) / 180;
652
+ }
653
+ /**
654
+ * Removes the scene's stage element from the DOM and clears the scene map.
655
+ */
656
+ destroy() {
657
+ if (this._sceneStage.el.parentElement) {
658
+ this._sceneStage.el.parentElement.removeChild(this._sceneStage.el);
659
+ }
660
+ ZScene.SceneMap.delete(this.sceneId);
661
+ }
662
+ }
663
+ //# sourceMappingURL=ZScene.js.map