@blakron/core 0.4.3 → 0.5.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 (82) hide show
  1. package/dist/blakron/display/DisplayObject.d.ts +1 -1
  2. package/dist/blakron/display/DisplayObject.d.ts.map +1 -1
  3. package/dist/blakron/display/DisplayObject.js +1 -1
  4. package/dist/blakron/display/DisplayObject.js.map +1 -1
  5. package/dist/blakron/player/CanvasRenderer.d.ts +49 -0
  6. package/dist/blakron/player/CanvasRenderer.d.ts.map +1 -0
  7. package/dist/blakron/player/CanvasRenderer.js +792 -0
  8. package/dist/blakron/player/CanvasRenderer.js.map +1 -0
  9. package/dist/blakron/player/DisplayList.d.ts +32 -0
  10. package/dist/blakron/player/DisplayList.d.ts.map +1 -0
  11. package/dist/blakron/player/DisplayList.js +78 -0
  12. package/dist/blakron/player/DisplayList.js.map +1 -0
  13. package/dist/blakron/player/InstructionSet.d.ts +49 -0
  14. package/dist/blakron/player/InstructionSet.d.ts.map +1 -0
  15. package/dist/blakron/player/InstructionSet.js +45 -0
  16. package/dist/blakron/player/InstructionSet.js.map +1 -0
  17. package/dist/blakron/player/Player.d.ts.map +1 -1
  18. package/dist/blakron/player/Player.js +2 -1
  19. package/dist/blakron/player/Player.js.map +1 -1
  20. package/dist/blakron/player/RenderBuffer.d.ts +35 -0
  21. package/dist/blakron/player/RenderBuffer.d.ts.map +1 -0
  22. package/dist/blakron/player/RenderBuffer.js +83 -0
  23. package/dist/blakron/player/RenderBuffer.js.map +1 -0
  24. package/dist/blakron/player/RenderPipe.d.ts +1 -1
  25. package/dist/blakron/player/RenderPipe.d.ts.map +1 -1
  26. package/dist/blakron/player/createPlayer.d.ts.map +1 -1
  27. package/dist/blakron/player/createPlayer.js +2 -1
  28. package/dist/blakron/player/createPlayer.js.map +1 -1
  29. package/dist/blakron/player/index.d.ts +4 -2
  30. package/dist/blakron/player/index.d.ts.map +1 -1
  31. package/dist/blakron/player/index.js +4 -2
  32. package/dist/blakron/player/index.js.map +1 -1
  33. package/dist/blakron/player/pipes/BitmapPipe.d.ts +35 -0
  34. package/dist/blakron/player/pipes/BitmapPipe.d.ts.map +1 -0
  35. package/dist/blakron/player/pipes/BitmapPipe.js +130 -0
  36. package/dist/blakron/player/pipes/BitmapPipe.js.map +1 -0
  37. package/dist/blakron/player/pipes/FilterPipe.d.ts +68 -0
  38. package/dist/blakron/player/pipes/FilterPipe.d.ts.map +1 -0
  39. package/dist/blakron/player/pipes/FilterPipe.js +169 -0
  40. package/dist/blakron/player/pipes/FilterPipe.js.map +1 -0
  41. package/dist/blakron/player/pipes/GraphicsPipe.d.ts +36 -0
  42. package/dist/blakron/player/pipes/GraphicsPipe.d.ts.map +1 -0
  43. package/dist/blakron/player/pipes/GraphicsPipe.js +126 -0
  44. package/dist/blakron/player/pipes/GraphicsPipe.js.map +1 -0
  45. package/dist/blakron/player/pipes/MaskPipe.d.ts +58 -0
  46. package/dist/blakron/player/pipes/MaskPipe.d.ts.map +1 -0
  47. package/dist/blakron/player/pipes/MaskPipe.js +190 -0
  48. package/dist/blakron/player/pipes/MaskPipe.js.map +1 -0
  49. package/dist/blakron/player/pipes/MeshPipe.d.ts +22 -0
  50. package/dist/blakron/player/pipes/MeshPipe.d.ts.map +1 -0
  51. package/dist/blakron/player/pipes/MeshPipe.js +50 -0
  52. package/dist/blakron/player/pipes/MeshPipe.js.map +1 -0
  53. package/dist/blakron/player/pipes/TextPipe.d.ts +52 -0
  54. package/dist/blakron/player/pipes/TextPipe.d.ts.map +1 -0
  55. package/dist/blakron/player/pipes/TextPipe.js +163 -0
  56. package/dist/blakron/player/pipes/TextPipe.js.map +1 -0
  57. package/dist/blakron/player/webgl/ShaderLib.d.ts +34 -0
  58. package/dist/blakron/player/webgl/ShaderLib.d.ts.map +1 -0
  59. package/dist/blakron/player/webgl/ShaderLib.js +268 -0
  60. package/dist/blakron/player/webgl/ShaderLib.js.map +1 -0
  61. package/dist/blakron/player/webgl/WebGLProgram.d.ts +2 -3
  62. package/dist/blakron/player/webgl/WebGLProgram.d.ts.map +1 -1
  63. package/dist/blakron/player/webgl/WebGLProgram.js.map +1 -1
  64. package/dist/blakron/player/webgl/WebGLRenderContext.d.ts +1 -9
  65. package/dist/blakron/player/webgl/WebGLRenderContext.d.ts.map +1 -1
  66. package/dist/blakron/player/webgl/WebGLRenderContext.js +14 -45
  67. package/dist/blakron/player/webgl/WebGLRenderContext.js.map +1 -1
  68. package/dist/blakron/player/webgl/WebGLRenderTarget.d.ts +1 -2
  69. package/dist/blakron/player/webgl/WebGLRenderTarget.d.ts.map +1 -1
  70. package/dist/blakron/player/webgl/WebGLRenderTarget.js.map +1 -1
  71. package/dist/blakron/player/webgl/WebGLRenderer.d.ts.map +1 -1
  72. package/dist/blakron/player/webgl/WebGLRenderer.js +8 -3
  73. package/dist/blakron/player/webgl/WebGLRenderer.js.map +1 -1
  74. package/dist/blakron/player/webgl/WebGLUtils.d.ts +3 -9
  75. package/dist/blakron/player/webgl/WebGLUtils.d.ts.map +1 -1
  76. package/dist/blakron/player/webgl/WebGLUtils.js +2 -2
  77. package/dist/blakron/player/webgl/WebGLUtils.js.map +1 -1
  78. package/dist/blakron/player/webgl/index.d.ts +1 -1
  79. package/dist/blakron/player/webgl/index.d.ts.map +1 -1
  80. package/dist/blakron/player/webgl/index.js +1 -1
  81. package/dist/blakron/player/webgl/index.js.map +1 -1
  82. package/package.json +52 -51
@@ -0,0 +1,792 @@
1
+ import { setBitmapPixelHitTest } from '../display/Bitmap.js';
2
+ import { setGraphicsHitTest } from '../display/Graphics.js';
3
+ import { Rectangle } from '../geom/Rectangle.js';
4
+ import { BlurFilter } from '../filters/BlurFilter.js';
5
+ import { ColorMatrixFilter } from '../filters/ColorMatrixFilter.js';
6
+ import { GlowFilter } from '../filters/GlowFilter.js';
7
+ import { DropShadowFilter } from '../filters/DropShadowFilter.js';
8
+ import { HorizontalAlign } from '../text/enums/HorizontalAlign.js';
9
+ import { VerticalAlign } from '../text/enums/VerticalAlign.js';
10
+ import { TextFieldType } from '../text/enums/TextFieldType.js';
11
+ import { getFontString, measureVerticalCorrection } from '../text/TextMeasurer.js';
12
+ import { RenderBuffer, hitTestBuffer } from './RenderBuffer.js';
13
+ const CAPS_MAP = { none: 'butt', square: 'square', round: 'round' };
14
+ /** Convert a 0xRRGGBB color number to a CSS rgb() string. */
15
+ function colorToString(color) {
16
+ const r = (color >> 16) & 0xff;
17
+ const g = (color >> 8) & 0xff;
18
+ const b = color & 0xff;
19
+ return `rgb(${r},${g},${b})`;
20
+ }
21
+ /**
22
+ * Canvas 2D renderer. Traverses the DisplayObject tree and draws each node
23
+ * using the Canvas 2D API. Equivalent to Egret's `CanvasRenderer`.
24
+ *
25
+ * Performance notes:
26
+ * - Currently does full-tree traversal and full-canvas redraw every frame.
27
+ * Egret used a DisplayList with dirty-region tracking to only redraw changed areas.
28
+ * This should be added when performance becomes a concern.
29
+ * - Egret used a RenderNode intermediate layer (BitmapNode, GraphicsNode, etc.)
30
+ * to cache rendering instructions. Blakron reads DisplayObject data directly,
31
+ * which is simpler but skips the caching benefit. If profiling shows the JS-side
32
+ * traversal is a bottleneck, consider adding a lightweight render command cache.
33
+ * - For high-performance scenarios, a WebGL renderer with batch rendering should
34
+ * be implemented as an alternative backend.
35
+ */
36
+ export class CanvasRenderer {
37
+ // ── Instance fields ───────────────────────────────────────────────────────
38
+ _hasFill = false;
39
+ _hasStroke = false;
40
+ // ── Public methods ────────────────────────────────────────────────────────
41
+ /**
42
+ * Renders a display object tree into the given buffer.
43
+ */
44
+ render(displayObject, buffer, matrix) {
45
+ const ctx = buffer.context;
46
+ if (matrix) {
47
+ ctx.save();
48
+ ctx.transform(matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty);
49
+ }
50
+ const drawCalls = this.drawDisplayObject(displayObject, ctx, 0, 0, true);
51
+ if (matrix)
52
+ ctx.restore();
53
+ return drawCalls;
54
+ }
55
+ /** @internal Used by WebGLRenderer for offscreen DisplayList rendering. */
56
+ renderToContext(displayObject, ctx, offsetX, offsetY) {
57
+ this.drawDisplayObject(displayObject, ctx, offsetX, offsetY, true);
58
+ }
59
+ /** @internal Used by WebGLRenderer to rasterize a Graphics object into a Canvas 2D context. */
60
+ renderGraphicsToContext(graphics, ctx, offsetX, offsetY, forHitTest = false, skipCache = false) {
61
+ this.renderGraphics(graphics, ctx, offsetX, offsetY, forHitTest, skipCache);
62
+ }
63
+ /** @internal Used by WebGL TextPipe to rasterize a TextField into a Canvas 2D context. */
64
+ renderTextFieldToContext(tf, ctx, offsetX, offsetY) {
65
+ this.renderTextField(tf, ctx, offsetX, offsetY);
66
+ }
67
+ /** @internal Used by Bitmap pixel hit test. */
68
+ renderBitmapToContext(bitmap, ctx, offsetX, offsetY) {
69
+ this.renderBitmap(bitmap, ctx, offsetX, offsetY);
70
+ }
71
+ // ── Private: tree traversal ───────────────────────────────────────────────
72
+ drawDisplayObject(displayObject, ctx, offsetX, offsetY, _isStage = false) {
73
+ let drawCalls = 0;
74
+ // DisplayList cache (cacheAsBitmap)
75
+ const displayList = displayObject.displayList;
76
+ if (displayList && !_isStage) {
77
+ if (displayObject.cacheDirty || displayObject.renderDirty) {
78
+ if (displayList.updateSurfaceSize()) {
79
+ displayList.renderBuffer.clear();
80
+ this.drawDisplayObject(displayObject, displayList.renderBuffer.context, displayList.offsetX, displayList.offsetY, true);
81
+ displayList.updateBitmapData();
82
+ }
83
+ displayObject.cacheDirty = false;
84
+ displayObject.renderDirty = false;
85
+ }
86
+ // Draw cached result and skip children
87
+ if (displayList.bitmapData?.source) {
88
+ ctx.drawImage(displayList.bitmapData.source, offsetX - displayList.offsetX, offsetY - displayList.offsetY);
89
+ drawCalls++;
90
+ }
91
+ return drawCalls;
92
+ }
93
+ // Draw self
94
+ drawCalls += this.renderSelf(displayObject, ctx, offsetX, offsetY);
95
+ // Draw children
96
+ const children = displayObject.children;
97
+ if (!children)
98
+ return drawCalls;
99
+ for (let i = 0; i < children.length; i++) {
100
+ const child = children[i];
101
+ if (child.renderMode === 1 /* RenderMode.NONE */)
102
+ continue;
103
+ let childOffsetX;
104
+ let childOffsetY;
105
+ if (child.useTranslate) {
106
+ const m = child.getMatrix();
107
+ childOffsetX = offsetX + child.internalX;
108
+ childOffsetY = offsetY + child.internalY;
109
+ ctx.save();
110
+ ctx.transform(m.a, m.b, m.c, m.d, childOffsetX, childOffsetY);
111
+ childOffsetX = -child.internalAnchorOffsetX;
112
+ childOffsetY = -child.internalAnchorOffsetY;
113
+ }
114
+ else {
115
+ childOffsetX = offsetX + child.internalX - child.internalAnchorOffsetX;
116
+ childOffsetY = offsetY + child.internalY - child.internalAnchorOffsetY;
117
+ }
118
+ let prevAlpha;
119
+ if (child.internalAlpha !== 1) {
120
+ prevAlpha = ctx.globalAlpha;
121
+ ctx.globalAlpha *= child.internalAlpha;
122
+ }
123
+ if (child.renderMode === 2 /* RenderMode.FILTER */) {
124
+ drawCalls += this.drawWithFilter(child, ctx, childOffsetX, childOffsetY);
125
+ }
126
+ else if (child.renderMode === 4 /* RenderMode.SCROLLRECT */) {
127
+ drawCalls += this.drawWithScrollRect(child, ctx, childOffsetX, childOffsetY);
128
+ }
129
+ else if (child.renderMode === 3 /* RenderMode.CLIP */) {
130
+ drawCalls += this.drawWithClip(child, ctx, childOffsetX, childOffsetY);
131
+ }
132
+ else {
133
+ drawCalls += this.drawDisplayObject(child, ctx, childOffsetX, childOffsetY);
134
+ }
135
+ if (child.useTranslate) {
136
+ ctx.restore();
137
+ }
138
+ else if (prevAlpha !== undefined) {
139
+ ctx.globalAlpha = prevAlpha;
140
+ }
141
+ }
142
+ return drawCalls;
143
+ }
144
+ drawWithScrollRect(displayObject, ctx, offsetX, offsetY) {
145
+ const rect = displayObject.internalScrollRect ?? displayObject.internalMaskRect;
146
+ if (!rect || rect.isEmpty())
147
+ return 0;
148
+ if (displayObject.internalScrollRect) {
149
+ offsetX -= rect.x;
150
+ offsetY -= rect.y;
151
+ }
152
+ ctx.save();
153
+ ctx.beginPath();
154
+ ctx.rect(rect.x + offsetX, rect.y + offsetY, rect.width, rect.height);
155
+ ctx.clip();
156
+ const drawCalls = this.drawDisplayObject(displayObject, ctx, offsetX, offsetY);
157
+ ctx.restore();
158
+ return drawCalls;
159
+ }
160
+ drawWithFilter(displayObject, ctx, offsetX, offsetY) {
161
+ const filters = displayObject.internalFilters;
162
+ if (!filters.length)
163
+ return this.drawDisplayObject(displayObject, ctx, offsetX, offsetY);
164
+ const bounds = displayObject.getOriginalBounds();
165
+ if (bounds.width <= 0 || bounds.height <= 0)
166
+ return 0;
167
+ // Build CSS filter string for GPU-accelerated filters.
168
+ // ColorMatrixFilter cannot be expressed as a CSS filter string (arbitrary matrix),
169
+ // so it falls back to the CPU pixel path below.
170
+ const cssFilters = [];
171
+ let hasCpuFilter = false;
172
+ for (const filter of filters) {
173
+ if (filter instanceof BlurFilter) {
174
+ // Average blurX/blurY — CSS blur is isotropic.
175
+ const radius = (filter.blurX + filter.blurY) / 2;
176
+ if (radius > 0)
177
+ cssFilters.push(`blur(${radius}px)`);
178
+ }
179
+ else if (filter instanceof DropShadowFilter) {
180
+ const angleRad = (filter.angle / 180) * Math.PI;
181
+ const dx = Math.round(filter.distance * Math.cos(angleRad));
182
+ const dy = Math.round(filter.distance * Math.sin(angleRad));
183
+ const blur = (filter.blurX + filter.blurY) / 2;
184
+ const r = (filter.color >> 16) & 0xff;
185
+ const g = (filter.color >> 8) & 0xff;
186
+ const b = filter.color & 0xff;
187
+ const a = Math.round(filter.alpha * 255);
188
+ cssFilters.push(`drop-shadow(${dx}px ${dy}px ${blur}px rgba(${r},${g},${b},${a / 255}))`);
189
+ }
190
+ else if (filter instanceof GlowFilter) {
191
+ // Approximate glow as a zero-offset drop-shadow.
192
+ const blur = (filter.blurX + filter.blurY) / 2;
193
+ const r = (filter.color >> 16) & 0xff;
194
+ const g = (filter.color >> 8) & 0xff;
195
+ const b = filter.color & 0xff;
196
+ const a = Math.round(filter.alpha * filter.strength * 255);
197
+ cssFilters.push(`drop-shadow(0px 0px ${blur}px rgba(${r},${g},${b},${a / 255}))`);
198
+ }
199
+ else if (filter instanceof ColorMatrixFilter) {
200
+ hasCpuFilter = true;
201
+ }
202
+ }
203
+ const hasBlendMode = displayObject.internalBlendMode !== 0;
204
+ // ── Fast path: all filters expressible as CSS ─────────────────────────
205
+ if (!hasCpuFilter && cssFilters.length > 0) {
206
+ ctx.save();
207
+ ctx.filter = cssFilters.join(' ');
208
+ if (hasBlendMode)
209
+ ctx.globalCompositeOperation = displayObject.blendMode;
210
+ let drawCalls = 0;
211
+ if (displayObject.internalMask) {
212
+ drawCalls += this.drawWithClip(displayObject, ctx, offsetX, offsetY);
213
+ }
214
+ else if (displayObject.internalScrollRect || displayObject.internalMaskRect) {
215
+ drawCalls += this.drawWithScrollRect(displayObject, ctx, offsetX, offsetY);
216
+ }
217
+ else {
218
+ drawCalls += this.drawDisplayObject(displayObject, ctx, offsetX, offsetY);
219
+ }
220
+ ctx.restore();
221
+ return drawCalls;
222
+ }
223
+ // ── CPU fallback: ColorMatrixFilter or mixed ──────────────────────────
224
+ const bufferW = Math.ceil(bounds.width);
225
+ const bufferH = Math.ceil(bounds.height);
226
+ const offscreen = new RenderBuffer(bufferW, bufferH);
227
+ const offCtx = offscreen.context;
228
+ // Apply CSS-capable filters on the offscreen context before drawing.
229
+ if (cssFilters.length > 0)
230
+ offCtx.filter = cssFilters.join(' ');
231
+ let drawCalls = 0;
232
+ if (displayObject.internalMask) {
233
+ drawCalls += this.drawWithClip(displayObject, offCtx, -bounds.x, -bounds.y);
234
+ }
235
+ else if (displayObject.internalScrollRect || displayObject.internalMaskRect) {
236
+ drawCalls += this.drawWithScrollRect(displayObject, offCtx, -bounds.x, -bounds.y);
237
+ }
238
+ else {
239
+ drawCalls += this.drawDisplayObject(displayObject, offCtx, -bounds.x, -bounds.y);
240
+ }
241
+ if (drawCalls === 0) {
242
+ offscreen.destroy();
243
+ return 0;
244
+ }
245
+ offCtx.filter = 'none';
246
+ // Apply CPU-only filters (ColorMatrixFilter).
247
+ const imageData = offCtx.getImageData(0, 0, offscreen.width, offscreen.height);
248
+ const data = imageData.data;
249
+ for (const filter of filters) {
250
+ if (filter instanceof ColorMatrixFilter) {
251
+ applyColorMatrix(data, filter.matrix);
252
+ }
253
+ }
254
+ offCtx.putImageData(imageData, 0, 0);
255
+ if (hasBlendMode)
256
+ ctx.globalCompositeOperation = displayObject.blendMode;
257
+ ctx.drawImage(offscreen.surface, offsetX + bounds.x, offsetY + bounds.y);
258
+ if (hasBlendMode)
259
+ ctx.globalCompositeOperation = 'source-over';
260
+ offscreen.destroy();
261
+ return drawCalls + 1;
262
+ }
263
+ drawWithClip(displayObject, ctx, offsetX, offsetY) {
264
+ const scrollRect = displayObject.internalScrollRect ?? displayObject.internalMaskRect;
265
+ const mask = displayObject.internalMask;
266
+ const hasBlendMode = displayObject.internalBlendMode !== 0;
267
+ if (hasBlendMode) {
268
+ ctx.globalCompositeOperation = displayObject.blendMode;
269
+ }
270
+ if (scrollRect) {
271
+ ctx.save();
272
+ ctx.beginPath();
273
+ ctx.rect(scrollRect.x + offsetX, scrollRect.y + offsetY, scrollRect.width, scrollRect.height);
274
+ ctx.clip();
275
+ }
276
+ // DisplayObject mask: render content and mask to offscreen buffers,
277
+ // composite with 'destination-in' to produce the masked result.
278
+ if (mask) {
279
+ const bounds = displayObject.getOriginalBounds();
280
+ if (bounds.width <= 0 || bounds.height <= 0) {
281
+ if (scrollRect)
282
+ ctx.restore();
283
+ if (hasBlendMode)
284
+ ctx.globalCompositeOperation = 'source-over';
285
+ return 0;
286
+ }
287
+ const bw = Math.ceil(bounds.width);
288
+ const bh = Math.ceil(bounds.height);
289
+ const bx = bounds.x;
290
+ const by = bounds.y;
291
+ // Render content to offscreen buffer
292
+ const contentBuffer = new RenderBuffer(bw, bh);
293
+ const contentCtx = contentBuffer.context;
294
+ const drawCalls = this.drawDisplayObject(displayObject, contentCtx, -bx, -by);
295
+ // Render mask shape to the same buffer using destination-in
296
+ contentCtx.globalCompositeOperation = 'destination-in';
297
+ const maskMatrix = mask.getConcatenatedMatrix();
298
+ const parentMatrix = displayObject.getConcatenatedMatrix();
299
+ // Transform mask relative to the content's local space
300
+ contentCtx.save();
301
+ const invA = parentMatrix.a, invB = parentMatrix.b, invC = parentMatrix.c, invD = parentMatrix.d;
302
+ const invTx = parentMatrix.tx, invTy = parentMatrix.ty;
303
+ const det = invA * invD - invB * invC;
304
+ if (Math.abs(det) > 1e-6) {
305
+ const ia = invD / det, ib = -invB / det, ic = -invC / det, id = invA / det;
306
+ const itx = (invC * invTy - invD * invTx) / det;
307
+ const ity = (invB * invTx - invA * invTy) / det;
308
+ // Combine: inverse(parent) * mask
309
+ const ra = ia * maskMatrix.a + ic * maskMatrix.b;
310
+ const rb = ib * maskMatrix.a + id * maskMatrix.b;
311
+ const rc = ia * maskMatrix.c + ic * maskMatrix.d;
312
+ const rd = ib * maskMatrix.c + id * maskMatrix.d;
313
+ const rtx = ia * maskMatrix.tx + ic * maskMatrix.ty + itx - bx;
314
+ const rty = ib * maskMatrix.tx + id * maskMatrix.ty + ity - by;
315
+ contentCtx.setTransform(ra, rb, rc, rd, rtx, rty);
316
+ }
317
+ else {
318
+ contentCtx.translate(-bx, -by);
319
+ }
320
+ this.drawDisplayObject(mask, contentCtx, 0, 0);
321
+ contentCtx.restore();
322
+ contentCtx.globalCompositeOperation = 'source-over';
323
+ // Draw the masked result onto the main context
324
+ ctx.drawImage(contentBuffer.surface, offsetX + bx, offsetY + by);
325
+ contentBuffer.destroy();
326
+ if (scrollRect)
327
+ ctx.restore();
328
+ if (hasBlendMode)
329
+ ctx.globalCompositeOperation = 'source-over';
330
+ return drawCalls + 1;
331
+ }
332
+ const drawCalls = this.drawDisplayObject(displayObject, ctx, offsetX, offsetY);
333
+ if (scrollRect)
334
+ ctx.restore();
335
+ if (hasBlendMode)
336
+ ctx.globalCompositeOperation = 'source-over';
337
+ return drawCalls;
338
+ }
339
+ // ── Private: render individual node types ─────────────────────────────────
340
+ renderSelf(displayObject, ctx, offsetX, offsetY) {
341
+ switch (displayObject.renderObjectType) {
342
+ case 2 /* RenderObjectType.MESH */:
343
+ return this.renderMesh(displayObject, ctx, offsetX, offsetY);
344
+ case 1 /* RenderObjectType.BITMAP */:
345
+ return this.renderBitmap(displayObject, ctx, offsetX, offsetY);
346
+ case 3 /* RenderObjectType.SHAPE */:
347
+ return this.renderGraphics(displayObject.graphics, ctx, offsetX, offsetY);
348
+ case 4 /* RenderObjectType.SPRITE */:
349
+ return this.renderGraphics(displayObject.graphics, ctx, offsetX, offsetY);
350
+ case 5 /* RenderObjectType.TEXT */:
351
+ return this.renderTextField(displayObject, ctx, offsetX, offsetY);
352
+ default:
353
+ return 0;
354
+ }
355
+ }
356
+ renderBitmap(bitmap, ctx, offsetX, offsetY) {
357
+ const bd = bitmap.bitmapData;
358
+ if (!bd?.source)
359
+ return 0;
360
+ const destW = !isNaN(bitmap.width) ? bitmap.width : bitmap.textureWidth;
361
+ const destH = !isNaN(bitmap.height) ? bitmap.height : bitmap.textureHeight;
362
+ if (destW <= 0 || destH <= 0)
363
+ return 0;
364
+ ctx.imageSmoothingEnabled = bitmap.smoothing;
365
+ ctx.drawImage(bd.source, bitmap.bitmapX, bitmap.bitmapY, bitmap.bitmapWidth, bitmap.bitmapHeight, offsetX + bitmap.bitmapOffsetX, offsetY + bitmap.bitmapOffsetY, destW, destH);
366
+ return 1;
367
+ }
368
+ renderMesh(mesh, ctx, offsetX, offsetY) {
369
+ const bd = mesh.bitmapData;
370
+ if (!bd?.source || mesh.vertices.length === 0)
371
+ return 0;
372
+ // Canvas 2D doesn't natively support mesh rendering.
373
+ // Fall back to drawing the full texture as a simple bitmap.
374
+ const destW = !isNaN(mesh.width) ? mesh.width : mesh.textureWidth;
375
+ const destH = !isNaN(mesh.height) ? mesh.height : mesh.textureHeight;
376
+ ctx.drawImage(bd.source, mesh.bitmapX, mesh.bitmapY, mesh.bitmapWidth, mesh.bitmapHeight, offsetX + mesh.bitmapOffsetX, offsetY + mesh.bitmapOffsetY, destW, destH);
377
+ return 1;
378
+ }
379
+ renderGraphics(graphics, ctx, offsetX, offsetY, forHitTest = false, skipCache = false) {
380
+ if (graphics.commands.length === 0)
381
+ return 0;
382
+ // ── Offscreen cache (skip for hit-test or when caller manages its own cache) ──
383
+ if (!forHitTest && !skipCache) {
384
+ if (graphics.canvasCacheDirty || !graphics.offscreenCanvas) {
385
+ // Measure bounds to size the offscreen canvas
386
+ const bounds = new Rectangle();
387
+ graphics.measureContentBounds(bounds);
388
+ const cw = Math.ceil(bounds.width) || 1;
389
+ const ch = Math.ceil(bounds.height) || 1;
390
+ // Create or resize offscreen canvas
391
+ if (!graphics.offscreenCanvas) {
392
+ graphics.offscreenCanvas = document.createElement('canvas');
393
+ graphics.offscreenCtx = graphics.offscreenCanvas.getContext('2d', { willReadFrequently: true });
394
+ }
395
+ const oc = graphics.offscreenCanvas;
396
+ if (oc.width !== cw || oc.height !== ch) {
397
+ oc.width = cw;
398
+ oc.height = ch;
399
+ }
400
+ else {
401
+ graphics.offscreenCtx.clearRect(0, 0, cw, ch);
402
+ }
403
+ const oc2d = graphics.offscreenCtx;
404
+ oc2d.save();
405
+ oc2d.translate(-bounds.x, -bounds.y);
406
+ this._hasFill = false;
407
+ this._hasStroke = false;
408
+ for (const cmd of graphics.commands) {
409
+ this.executeGraphicsCommand(cmd, oc2d, false);
410
+ }
411
+ // Flush any open path that wasn't closed by endFill
412
+ this.flushOpenPath(oc2d);
413
+ oc2d.restore();
414
+ graphics.offscreenBoundsX = bounds.x;
415
+ graphics.offscreenBoundsY = bounds.y;
416
+ graphics.canvasCacheDirty = false;
417
+ }
418
+ // Draw cached offscreen canvas
419
+ ctx.drawImage(graphics.offscreenCanvas, offsetX + graphics.offscreenBoundsX, offsetY + graphics.offscreenBoundsY);
420
+ return 1;
421
+ }
422
+ // ── Hit-test path: direct execution, no cache ─────────────────────────
423
+ ctx.save();
424
+ ctx.translate(offsetX, offsetY);
425
+ this._hasFill = false;
426
+ this._hasStroke = false;
427
+ for (const cmd of graphics.commands) {
428
+ this.executeGraphicsCommand(cmd, ctx, forHitTest);
429
+ }
430
+ // Flush any open path that wasn't closed by endFill
431
+ this.flushOpenPath(ctx);
432
+ ctx.restore();
433
+ return 1;
434
+ }
435
+ renderTextField(tf, ctx, offsetX, offsetY) {
436
+ tf.getLinesArr(); // ensure lines are computed
437
+ // INPUT mode while focused: HTML input element owns the display.
438
+ // Only render background/border so the field is visible, but skip
439
+ // text content to avoid the double-text artefact.
440
+ const inputFocused = tf.type === TextFieldType.INPUT && tf.isTyping;
441
+ const width = !isNaN(tf.explicitWidth) ? tf.explicitWidth : tf.textWidth;
442
+ const height = !isNaN(tf.explicitHeight) ? tf.explicitHeight : tf.textHeight;
443
+ if (width <= 0 || height <= 0)
444
+ return 0;
445
+ ctx.save();
446
+ ctx.translate(offsetX, offsetY);
447
+ // ── Background ────────────────────────────────────────────────────────
448
+ if (tf.background) {
449
+ ctx.fillStyle = colorToString(tf.backgroundColor);
450
+ ctx.fillRect(0, 0, width, height);
451
+ }
452
+ // ── Border ────────────────────────────────────────────────────────────
453
+ if (tf.border) {
454
+ ctx.strokeStyle = colorToString(tf.borderColor);
455
+ ctx.lineWidth = 1;
456
+ ctx.strokeRect(0, 0, width, height);
457
+ }
458
+ // While the native input is active, skip text/cursor rendering.
459
+ if (inputFocused) {
460
+ ctx.restore();
461
+ return 0;
462
+ }
463
+ // ── Clip to visible area (for scrollV support) ────────────────────────
464
+ ctx.beginPath();
465
+ ctx.rect(0, 0, width, height);
466
+ ctx.clip();
467
+ // ── Compute vertical offset ───────────────────────────────────────────
468
+ const lines = tf.getLinesArr();
469
+ const lineSpacing = tf.lineSpacing;
470
+ let totalTextHeight = 0;
471
+ for (let i = 0; i < lines.length; i++) {
472
+ totalTextHeight += lines[i].height;
473
+ if (i > 0)
474
+ totalTextHeight += lineSpacing;
475
+ }
476
+ let verticalOffset = 0;
477
+ if (tf.verticalAlign === VerticalAlign.MIDDLE) {
478
+ verticalOffset = Math.max(0, (height - totalTextHeight) / 2);
479
+ }
480
+ else if (tf.verticalAlign === VerticalAlign.BOTTOM) {
481
+ verticalOffset = Math.max(0, height - totalTextHeight);
482
+ }
483
+ // ── Vertical centering correction ─────────────────────────────────────
484
+ // When textBaseline='top', lineHeight=fontSize centers the em-square, not the
485
+ // actual glyphs. Measure font metrics to correct the visual offset.
486
+ let vCorrection = 0;
487
+ if (tf.verticalAlign === VerticalAlign.MIDDLE) {
488
+ // Find first non-empty element to measure
489
+ let sampleText = '';
490
+ let sampleSize = tf.size;
491
+ let sampleFont = tf.fontFamily;
492
+ let sampleBold = tf.bold;
493
+ let sampleItalic = tf.italic;
494
+ outer: for (const line of lines) {
495
+ for (const el of line.elements) {
496
+ if (el.text) {
497
+ sampleText = el.text;
498
+ sampleSize = el.style?.size ?? tf.size;
499
+ sampleFont = el.style?.fontFamily ?? tf.fontFamily;
500
+ sampleBold = el.style?.bold ?? tf.bold;
501
+ sampleItalic = el.style?.italic ?? tf.italic;
502
+ break outer;
503
+ }
504
+ }
505
+ }
506
+ vCorrection = measureVerticalCorrection(sampleText, sampleFont, sampleSize, sampleBold, sampleItalic);
507
+ }
508
+ // ── ScrollV offset: accumulate actual line heights instead of fixed formula ──
509
+ const scrollOffset = tf.getScrollYOffset();
510
+ // ── Draw lines ────────────────────────────────────────────────────────
511
+ let drawY = verticalOffset - scrollOffset + vCorrection;
512
+ let drawCalls = 0;
513
+ for (let i = 0; i < lines.length; i++) {
514
+ const line = lines[i];
515
+ if (drawY + line.height < 0 || drawY > height) {
516
+ drawY += line.height + lineSpacing;
517
+ continue;
518
+ }
519
+ // Horizontal alignment offset
520
+ let lineX = 0;
521
+ if (tf.textAlign === HorizontalAlign.RIGHT) {
522
+ lineX = width - line.width;
523
+ }
524
+ else if (tf.textAlign === HorizontalAlign.CENTER) {
525
+ lineX = (width - line.width) / 2;
526
+ }
527
+ // Draw each element in the line
528
+ for (const el of line.elements) {
529
+ const style = el.style;
530
+ const fontSize = style?.size ?? tf.size;
531
+ const fontFamily = style?.fontFamily ?? tf.fontFamily;
532
+ const bold = style?.bold ?? tf.bold;
533
+ const italic = style?.italic ?? tf.italic;
534
+ const textColor = style?.textColor ?? tf.textColor;
535
+ const strokeColor = style?.strokeColor ?? tf.strokeColor;
536
+ const stroke = style?.stroke ?? tf.stroke;
537
+ const fontStr = getFontString(fontSize, fontFamily, bold, italic);
538
+ ctx.font = fontStr;
539
+ ctx.textBaseline = 'top';
540
+ // Stroke
541
+ if (stroke > 0) {
542
+ ctx.strokeStyle = colorToString(strokeColor);
543
+ ctx.lineWidth = stroke * 2;
544
+ ctx.lineJoin = 'round';
545
+ ctx.strokeText(el.text, lineX, drawY);
546
+ drawCalls++;
547
+ }
548
+ // Fill
549
+ ctx.fillStyle = colorToString(textColor);
550
+ ctx.fillText(el.text, lineX, drawY);
551
+ drawCalls++;
552
+ lineX += el.width;
553
+ }
554
+ drawY += line.height + lineSpacing;
555
+ }
556
+ // ── INPUT cursor ──────────────────────────────────────────────────────
557
+ if (tf.type === TextFieldType.INPUT && tf.isTyping) {
558
+ // Draw blinking cursor at caretIndex position
559
+ const caretIndex = tf.caretIndex;
560
+ const fontStr = getFontString(tf.size, tf.fontFamily, tf.bold, tf.italic);
561
+ ctx.font = fontStr;
562
+ ctx.textBaseline = 'top';
563
+ // Calculate cursor x by measuring text up to caretIndex
564
+ let cursorX = 0;
565
+ let charCount = 0;
566
+ for (const line of lines) {
567
+ for (const el of line.elements) {
568
+ const elLen = el.text.length;
569
+ if (charCount + elLen >= caretIndex) {
570
+ const partial = el.text.substring(0, caretIndex - charCount);
571
+ cursorX += ctx.measureText(partial).width;
572
+ charCount = caretIndex;
573
+ break;
574
+ }
575
+ cursorX += el.width;
576
+ charCount += elLen;
577
+ }
578
+ if (charCount >= caretIndex)
579
+ break;
580
+ cursorX = 0; // reset x for next line
581
+ }
582
+ const cursorY = verticalOffset - scrollOffset;
583
+ ctx.fillStyle = colorToString(tf.textColor);
584
+ ctx.fillRect(cursorX, cursorY, 1, tf.size);
585
+ drawCalls++;
586
+ }
587
+ ctx.restore();
588
+ return drawCalls > 0 ? 1 : 0;
589
+ }
590
+ flushOpenPath(ctx) {
591
+ if (this._hasFill)
592
+ ctx.fill();
593
+ if (this._hasStroke)
594
+ ctx.stroke();
595
+ this._hasFill = false;
596
+ this._hasStroke = false;
597
+ }
598
+ executeGraphicsCommand(cmd, ctx, forHitTest = false) {
599
+ switch (cmd.type) {
600
+ case 9 /* PathCommandType.BeginFill */:
601
+ ctx.fillStyle = forHitTest
602
+ ? '#000'
603
+ : `rgba(${(cmd.color >> 16) & 0xff},${(cmd.color >> 8) & 0xff},${cmd.color & 0xff},${cmd.alpha})`;
604
+ ctx.beginPath();
605
+ this._hasFill = true;
606
+ break;
607
+ case 10 /* PathCommandType.BeginGradientFill */: {
608
+ if (forHitTest) {
609
+ ctx.fillStyle = '#000';
610
+ ctx.beginPath();
611
+ this._hasFill = true;
612
+ break;
613
+ }
614
+ // createGradientBox uses the Flash/Egret convention where the matrix
615
+ // maps gradient space ±819.2 (1638.4/2) to the desired box edges.
616
+ const GH = 819.2;
617
+ let gradient;
618
+ if (cmd.matrix) {
619
+ const m = cmd.matrix;
620
+ if (cmd.gradientType === 'radial') {
621
+ const cx = m.tx;
622
+ const cy = m.ty;
623
+ const rx = Math.sqrt(m.a * m.a + m.b * m.b) * GH;
624
+ const ry = Math.sqrt(m.c * m.c + m.d * m.d) * GH;
625
+ gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(rx, ry));
626
+ }
627
+ else {
628
+ const x0 = m.a * -GH + m.tx;
629
+ const y0 = m.b * -GH + m.ty;
630
+ const x1 = m.a * GH + m.tx;
631
+ const y1 = m.b * GH + m.ty;
632
+ gradient = ctx.createLinearGradient(x0, y0, x1, y1);
633
+ }
634
+ }
635
+ else {
636
+ if (cmd.gradientType === 'radial') {
637
+ gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, GH);
638
+ }
639
+ else {
640
+ gradient = ctx.createLinearGradient(-GH, 0, GH, 0);
641
+ }
642
+ }
643
+ for (let i = 0; i < cmd.colors.length; i++) {
644
+ const c = cmd.colors[i];
645
+ const a = cmd.alphas[i] ?? 1;
646
+ const r = (cmd.ratios[i] ?? 0) / 255;
647
+ gradient.addColorStop(r, `rgba(${(c >> 16) & 0xff},${(c >> 8) & 0xff},${c & 0xff},${a})`);
648
+ }
649
+ ctx.fillStyle = gradient;
650
+ ctx.beginPath();
651
+ this._hasFill = true;
652
+ break;
653
+ }
654
+ case 11 /* PathCommandType.EndFill */:
655
+ if (this._hasFill)
656
+ ctx.fill();
657
+ ctx.closePath();
658
+ if (this._hasStroke)
659
+ ctx.stroke();
660
+ this._hasFill = false;
661
+ break;
662
+ case 12 /* PathCommandType.LineStyle */:
663
+ ctx.lineWidth = cmd.thickness;
664
+ ctx.strokeStyle = forHitTest
665
+ ? '#000'
666
+ : `rgba(${(cmd.color >> 16) & 0xff},${(cmd.color >> 8) & 0xff},${cmd.color & 0xff},${cmd.alpha})`;
667
+ ctx.lineCap = CAPS_MAP[cmd.caps ?? 'none'] ?? 'butt';
668
+ ctx.lineJoin = (cmd.joints ?? 'round');
669
+ ctx.miterLimit = cmd.miterLimit;
670
+ if (cmd.lineDash)
671
+ ctx.setLineDash(cmd.lineDash);
672
+ this._hasStroke = true;
673
+ break;
674
+ case 0 /* PathCommandType.MoveTo */:
675
+ ctx.moveTo(cmd.x, cmd.y);
676
+ break;
677
+ case 1 /* PathCommandType.LineTo */:
678
+ ctx.lineTo(cmd.x, cmd.y);
679
+ break;
680
+ case 2 /* PathCommandType.CurveTo */:
681
+ ctx.quadraticCurveTo(cmd.cx, cmd.cy, cmd.ax, cmd.ay);
682
+ break;
683
+ case 3 /* PathCommandType.CubicCurveTo */:
684
+ ctx.bezierCurveTo(cmd.cx1, cmd.cy1, cmd.cx2, cmd.cy2, cmd.ax, cmd.ay);
685
+ break;
686
+ case 4 /* PathCommandType.DrawRect */:
687
+ ctx.beginPath();
688
+ ctx.rect(cmd.x, cmd.y, cmd.w, cmd.h);
689
+ if (this._hasFill)
690
+ ctx.fill();
691
+ if (this._hasStroke)
692
+ ctx.stroke();
693
+ break;
694
+ case 5 /* PathCommandType.DrawRoundRect */: {
695
+ const { x, y, w, h, ew, eh } = cmd;
696
+ const rx = ew / 2, ry = eh / 2;
697
+ ctx.beginPath();
698
+ ctx.moveTo(x + rx, y);
699
+ ctx.lineTo(x + w - rx, y);
700
+ ctx.ellipse(x + w - rx, y + ry, rx, ry, 0, -Math.PI / 2, 0);
701
+ ctx.lineTo(x + w, y + h - ry);
702
+ ctx.ellipse(x + w - rx, y + h - ry, rx, ry, 0, 0, Math.PI / 2);
703
+ ctx.lineTo(x + rx, y + h);
704
+ ctx.ellipse(x + rx, y + h - ry, rx, ry, 0, Math.PI / 2, Math.PI);
705
+ ctx.lineTo(x, y + ry);
706
+ ctx.ellipse(x + rx, y + ry, rx, ry, 0, Math.PI, Math.PI * 1.5);
707
+ ctx.closePath();
708
+ if (this._hasFill)
709
+ ctx.fill();
710
+ if (this._hasStroke)
711
+ ctx.stroke();
712
+ break;
713
+ }
714
+ case 6 /* PathCommandType.DrawCircle */:
715
+ ctx.beginPath();
716
+ ctx.arc(cmd.x, cmd.y, cmd.r, 0, Math.PI * 2);
717
+ if (this._hasFill)
718
+ ctx.fill();
719
+ if (this._hasStroke)
720
+ ctx.stroke();
721
+ break;
722
+ case 7 /* PathCommandType.DrawEllipse */: {
723
+ const cx = cmd.x + cmd.w / 2;
724
+ const cy = cmd.y + cmd.h / 2;
725
+ ctx.beginPath();
726
+ ctx.ellipse(cx, cy, cmd.w / 2, cmd.h / 2, 0, 0, Math.PI * 2);
727
+ if (this._hasFill)
728
+ ctx.fill();
729
+ if (this._hasStroke)
730
+ ctx.stroke();
731
+ break;
732
+ }
733
+ case 8 /* PathCommandType.DrawArc */:
734
+ ctx.beginPath();
735
+ ctx.arc(cmd.x, cmd.y, cmd.r, cmd.start, cmd.end, cmd.ccw);
736
+ if (this._hasStroke)
737
+ ctx.stroke();
738
+ break;
739
+ case 13 /* PathCommandType.Clear */:
740
+ this._hasFill = false;
741
+ this._hasStroke = false;
742
+ break;
743
+ }
744
+ }
745
+ }
746
+ // ── Filter pixel manipulation helpers ─────────────────────────────────────────
747
+ function applyColorMatrix(data, matrix) {
748
+ for (let i = 0; i < data.length; i += 4) {
749
+ const r = data[i], g = data[i + 1], b = data[i + 2], a = data[i + 3];
750
+ data[i] = Math.max(0, Math.min(255, r * matrix[0] + g * matrix[1] + b * matrix[2] + a * matrix[3] + matrix[4]));
751
+ data[i + 1] = Math.max(0, Math.min(255, r * matrix[5] + g * matrix[6] + b * matrix[7] + a * matrix[8] + matrix[9]));
752
+ data[i + 2] = Math.max(0, Math.min(255, r * matrix[10] + g * matrix[11] + b * matrix[12] + a * matrix[13] + matrix[14]));
753
+ data[i + 3] = Math.max(0, Math.min(255, r * matrix[15] + g * matrix[16] + b * matrix[17] + a * matrix[18] + matrix[19]));
754
+ }
755
+ }
756
+ // ── Graphics pixel-perfect hit test ──────────────────────────────────────────
757
+ // Registered here to avoid circular dependency between display/ and player/.
758
+ // Uses the shared 3×3 hitTestBuffer: translate so the test point lands at (1,1),
759
+ // render with forHitTest=true (all shapes drawn as opaque black), then read alpha.
760
+ const _hitRenderer = new CanvasRenderer();
761
+ setGraphicsHitTest((graphics, localX, localY) => {
762
+ const buf = hitTestBuffer();
763
+ buf.clear();
764
+ const ctx = buf.context;
765
+ // Translate so localX/Y maps to pixel (1,1) in the 3×3 buffer
766
+ ctx.setTransform(1, 0, 0, 1, 1 - localX, 1 - localY);
767
+ _hitRenderer.renderGraphicsToContext(graphics, ctx, 0, 0, true);
768
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
769
+ try {
770
+ return buf.getPixels(1, 1)[3] !== 0;
771
+ }
772
+ catch {
773
+ return false;
774
+ }
775
+ });
776
+ // ── Bitmap pixel-perfect hit test ────────────────────────────────────────────
777
+ // Renders the Bitmap into the shared 3×3 buffer at the test point and reads alpha.
778
+ setBitmapPixelHitTest((bitmap, localX, localY) => {
779
+ const buf = hitTestBuffer();
780
+ buf.clear();
781
+ const ctx = buf.context;
782
+ ctx.setTransform(1, 0, 0, 1, 1 - localX, 1 - localY);
783
+ _hitRenderer.renderBitmapToContext(bitmap, ctx, 0, 0);
784
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
785
+ try {
786
+ return buf.getPixels(1, 1)[3] !== 0;
787
+ }
788
+ catch {
789
+ return false;
790
+ }
791
+ });
792
+ //# sourceMappingURL=CanvasRenderer.js.map