@blakron/core 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blakron/display/DisplayObject.d.ts +1 -1
- package/dist/blakron/display/DisplayObject.d.ts.map +1 -1
- package/dist/blakron/display/DisplayObject.js +1 -1
- package/dist/blakron/display/DisplayObject.js.map +1 -1
- package/dist/blakron/player/Player.d.ts.map +1 -1
- package/dist/blakron/player/Player.js +1 -2
- package/dist/blakron/player/Player.js.map +1 -1
- package/dist/blakron/player/RenderPipe.d.ts +1 -1
- package/dist/blakron/player/RenderPipe.d.ts.map +1 -1
- package/dist/blakron/player/canvas/CanvasRenderer.d.ts +49 -0
- package/dist/blakron/player/canvas/CanvasRenderer.d.ts.map +1 -0
- package/dist/blakron/player/canvas/CanvasRenderer.js +792 -0
- package/dist/blakron/player/canvas/CanvasRenderer.js.map +1 -0
- package/dist/blakron/player/canvas/DisplayList.d.ts +32 -0
- package/dist/blakron/player/canvas/DisplayList.d.ts.map +1 -0
- package/dist/blakron/player/canvas/DisplayList.js +78 -0
- package/dist/blakron/player/canvas/DisplayList.js.map +1 -0
- package/dist/blakron/player/canvas/RenderBuffer.d.ts +35 -0
- package/dist/blakron/player/canvas/RenderBuffer.d.ts.map +1 -0
- package/dist/blakron/player/canvas/RenderBuffer.js +83 -0
- package/dist/blakron/player/canvas/RenderBuffer.js.map +1 -0
- package/dist/blakron/player/canvas/index.d.ts +4 -0
- package/dist/blakron/player/canvas/index.d.ts.map +1 -0
- package/dist/blakron/player/canvas/index.js +4 -0
- package/dist/blakron/player/canvas/index.js.map +1 -0
- package/dist/blakron/player/createPlayer.d.ts.map +1 -1
- package/dist/blakron/player/createPlayer.js +1 -2
- package/dist/blakron/player/createPlayer.js.map +1 -1
- package/dist/blakron/player/index.d.ts +2 -4
- package/dist/blakron/player/index.d.ts.map +1 -1
- package/dist/blakron/player/index.js +2 -4
- package/dist/blakron/player/index.js.map +1 -1
- package/dist/blakron/player/webgl/InstructionSet.d.ts +49 -0
- package/dist/blakron/player/webgl/InstructionSet.d.ts.map +1 -0
- package/dist/blakron/player/webgl/InstructionSet.js +45 -0
- package/dist/blakron/player/webgl/InstructionSet.js.map +1 -0
- package/dist/blakron/player/webgl/ShaderLib.d.ts +4 -16
- package/dist/blakron/player/webgl/ShaderLib.d.ts.map +1 -1
- package/dist/blakron/player/webgl/ShaderLib.js +5 -27
- package/dist/blakron/player/webgl/ShaderLib.js.map +1 -1
- package/dist/blakron/player/webgl/WebGLRenderContext.d.ts +6 -0
- package/dist/blakron/player/webgl/WebGLRenderContext.d.ts.map +1 -1
- package/dist/blakron/player/webgl/WebGLRenderContext.js +28 -10
- package/dist/blakron/player/webgl/WebGLRenderContext.js.map +1 -1
- package/dist/blakron/player/webgl/WebGLRenderer.d.ts.map +1 -1
- package/dist/blakron/player/webgl/WebGLRenderer.js +3 -8
- package/dist/blakron/player/webgl/WebGLRenderer.js.map +1 -1
- package/dist/blakron/player/webgl/index.d.ts +1 -1
- package/dist/blakron/player/webgl/index.d.ts.map +1 -1
- package/dist/blakron/player/webgl/index.js +1 -1
- package/dist/blakron/player/webgl/index.js.map +1 -1
- package/dist/blakron/player/webgl/pipes/BitmapPipe.d.ts +35 -0
- package/dist/blakron/player/webgl/pipes/BitmapPipe.d.ts.map +1 -0
- package/dist/blakron/player/webgl/pipes/BitmapPipe.js +130 -0
- package/dist/blakron/player/webgl/pipes/BitmapPipe.js.map +1 -0
- package/dist/blakron/player/webgl/pipes/FilterPipe.d.ts +68 -0
- package/dist/blakron/player/webgl/pipes/FilterPipe.d.ts.map +1 -0
- package/dist/blakron/player/webgl/pipes/FilterPipe.js +169 -0
- package/dist/blakron/player/webgl/pipes/FilterPipe.js.map +1 -0
- package/dist/blakron/player/webgl/pipes/GraphicsPipe.d.ts +36 -0
- package/dist/blakron/player/webgl/pipes/GraphicsPipe.d.ts.map +1 -0
- package/dist/blakron/player/webgl/pipes/GraphicsPipe.js +126 -0
- package/dist/blakron/player/webgl/pipes/GraphicsPipe.js.map +1 -0
- package/dist/blakron/player/webgl/pipes/MaskPipe.d.ts +58 -0
- package/dist/blakron/player/webgl/pipes/MaskPipe.d.ts.map +1 -0
- package/dist/blakron/player/webgl/pipes/MaskPipe.js +190 -0
- package/dist/blakron/player/webgl/pipes/MaskPipe.js.map +1 -0
- package/dist/blakron/player/webgl/pipes/MeshPipe.d.ts +22 -0
- package/dist/blakron/player/webgl/pipes/MeshPipe.d.ts.map +1 -0
- package/dist/blakron/player/webgl/pipes/MeshPipe.js +50 -0
- package/dist/blakron/player/webgl/pipes/MeshPipe.js.map +1 -0
- package/dist/blakron/player/webgl/pipes/TextPipe.d.ts +52 -0
- package/dist/blakron/player/webgl/pipes/TextPipe.d.ts.map +1 -0
- package/dist/blakron/player/webgl/pipes/TextPipe.js +163 -0
- package/dist/blakron/player/webgl/pipes/TextPipe.js.map +1 -0
- package/dist/blakron/player/webgl/pipes/index.d.ts +7 -0
- package/dist/blakron/player/webgl/pipes/index.d.ts.map +1 -0
- package/dist/blakron/player/webgl/pipes/index.js +7 -0
- package/dist/blakron/player/webgl/pipes/index.js.map +1 -0
- package/dist/blakron/player/webgl/shaders/ShaderLib.d.ts +22 -0
- package/dist/blakron/player/webgl/shaders/ShaderLib.d.ts.map +1 -0
- package/dist/blakron/player/webgl/shaders/ShaderLib.js +246 -0
- package/dist/blakron/player/webgl/shaders/ShaderLib.js.map +1 -0
- package/dist/blakron/player/webgl/shaders/ShaderLib2.d.ts +22 -0
- package/dist/blakron/player/webgl/shaders/ShaderLib2.d.ts.map +1 -0
- package/dist/blakron/player/webgl/shaders/ShaderLib2.js +261 -0
- package/dist/blakron/player/webgl/shaders/ShaderLib2.js.map +1 -0
- package/dist/blakron/player/webgl/shaders/index.d.ts +5 -0
- package/dist/blakron/player/webgl/shaders/index.d.ts.map +1 -0
- package/dist/blakron/player/webgl/shaders/index.js +5 -0
- package/dist/blakron/player/webgl/shaders/index.js.map +1 -0
- package/dist/blakron/player/webgl2/ShaderLib2.d.ts +22 -0
- package/dist/blakron/player/webgl2/ShaderLib2.d.ts.map +1 -0
- package/dist/blakron/player/webgl2/ShaderLib2.js +261 -0
- package/dist/blakron/player/webgl2/ShaderLib2.js.map +1 -0
- package/package.json +51 -52
|
@@ -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
|