@flare-engine/render 0.2.1 → 0.3.1
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/LICENSE +201 -21
- package/NOTICE +2 -0
- package/README.md +19 -8
- package/dist/index.d.mts +564 -503
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +564 -503
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1538 -1491
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1498 -1427
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -12
package/dist/index.mjs
CHANGED
|
@@ -1,632 +1,324 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
for (let i = 1; i < n; i++) {
|
|
5
|
-
const item = items[i];
|
|
6
|
-
let j = i - 1;
|
|
7
|
-
while (j >= 0 && items[j].z > item.z) {
|
|
8
|
-
items[j + 1] = items[j];
|
|
9
|
-
j--;
|
|
10
|
-
}
|
|
11
|
-
items[j + 1] = item;
|
|
12
|
-
}
|
|
13
|
-
return items;
|
|
14
|
-
}
|
|
15
|
-
function drawOrderCompare(a, b) {
|
|
16
|
-
return a.z - b.z;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// src/camera2d.ts
|
|
1
|
+
import { defineComponent } from "@flare-engine/ecs";
|
|
2
|
+
//#region src/camera2d.ts
|
|
3
|
+
/** Simple pseudo-random for deterministic shake. Avoids Math.random() allocs. */
|
|
20
4
|
function pseudoRandom(seed) {
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
const x = Math.sin(seed * 12.9898 + seed * 78.233) * 43758.5453;
|
|
6
|
+
return x - Math.floor(x);
|
|
23
7
|
}
|
|
24
|
-
|
|
8
|
+
const PERM = /* @__PURE__ */ new Uint8Array(256);
|
|
25
9
|
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
10
|
+
for (let i = 0; i < 256; i++) PERM[i] = i;
|
|
11
|
+
let s = 2654435769;
|
|
12
|
+
for (let i = 255; i > 0; i--) {
|
|
13
|
+
s = (s ^ s >>> 15) >>> 0;
|
|
14
|
+
s = Math.imul(s, 2246822507) >>> 0;
|
|
15
|
+
s = (s ^ s >>> 13) >>> 0;
|
|
16
|
+
const j = s % (i + 1);
|
|
17
|
+
const t = PERM[i];
|
|
18
|
+
PERM[i] = PERM[j];
|
|
19
|
+
PERM[j] = t;
|
|
20
|
+
}
|
|
37
21
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
22
|
+
const S = Math.SQRT1_2;
|
|
23
|
+
const GRADS = [
|
|
24
|
+
[1, 0],
|
|
25
|
+
[S, S],
|
|
26
|
+
[0, 1],
|
|
27
|
+
[-S, S],
|
|
28
|
+
[-1, 0],
|
|
29
|
+
[-S, -S],
|
|
30
|
+
[0, -1],
|
|
31
|
+
[S, -S]
|
|
48
32
|
];
|
|
49
33
|
function grad2(hash, x, y) {
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
const g = GRADS[hash & 7];
|
|
35
|
+
return g[0] * x + g[1] * y;
|
|
52
36
|
}
|
|
53
37
|
function fade(t) {
|
|
54
|
-
|
|
38
|
+
return t * t * t * (t * (t * 6 - 15) + 10);
|
|
55
39
|
}
|
|
40
|
+
/** 2D value-gradient noise in [-1, 1]. */
|
|
56
41
|
function noise2d(x, y) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return lerpX0 + v * (lerpX1 - lerpX0);
|
|
42
|
+
const xi = Math.floor(x) & 255;
|
|
43
|
+
const yi = Math.floor(y) & 255;
|
|
44
|
+
const xf = x - Math.floor(x);
|
|
45
|
+
const yf = y - Math.floor(y);
|
|
46
|
+
const u = fade(xf);
|
|
47
|
+
const v = fade(yf);
|
|
48
|
+
const aa = PERM[PERM[xi] + yi];
|
|
49
|
+
const ab = PERM[PERM[xi] + yi + 1];
|
|
50
|
+
const ba = PERM[PERM[xi + 1 & 255] + yi];
|
|
51
|
+
const bb = PERM[PERM[xi + 1 & 255] + yi + 1];
|
|
52
|
+
const x1 = xf;
|
|
53
|
+
const y1 = yf;
|
|
54
|
+
const x2 = xf - 1;
|
|
55
|
+
const y2 = yf - 1;
|
|
56
|
+
const n00 = grad2(aa, x1, y1);
|
|
57
|
+
const n10 = grad2(ba, x2, y1);
|
|
58
|
+
const n01 = grad2(ab, x1, y2);
|
|
59
|
+
const n11 = grad2(bb, x2, y2);
|
|
60
|
+
const lerpX0 = n00 + u * (n10 - n00);
|
|
61
|
+
return lerpX0 + v * (n01 + u * (n11 - n01) - lerpX0);
|
|
78
62
|
}
|
|
63
|
+
/** 2-octave FBM for shake displacement — result in [-1, 1]. */
|
|
79
64
|
function fbmNoise2d(x, y) {
|
|
80
|
-
|
|
65
|
+
return .6667 * noise2d(x, y) + .3333 * noise2d(x * 2, y * 2);
|
|
81
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Camera2D — 2D camera with position, zoom, rotation, follow, and shake.
|
|
69
|
+
* Pure data + methods — no Skia dependency. The renderer reads camera state
|
|
70
|
+
* to build the view transform.
|
|
71
|
+
*/
|
|
82
72
|
var Camera2D = class {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
} else {
|
|
296
|
-
this.y = b.y + b.height / 2;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
// src/renderer.ts
|
|
302
|
-
var Renderer = class {
|
|
303
|
-
/** Current frame stats. */
|
|
304
|
-
stats = { drawCalls: 0, batchCount: 0 };
|
|
305
|
-
_canvas = null;
|
|
306
|
-
_inFrame = false;
|
|
307
|
-
/** Set the canvas to render to. Called once when canvas is available. */
|
|
308
|
-
setCanvas(canvas) {
|
|
309
|
-
this._canvas = canvas;
|
|
310
|
-
}
|
|
311
|
-
/** Get the current canvas. Throws if not in a frame. */
|
|
312
|
-
get canvas() {
|
|
313
|
-
if (!this._canvas) {
|
|
314
|
-
throw new Error("Renderer: no canvas set");
|
|
315
|
-
}
|
|
316
|
-
return this._canvas;
|
|
317
|
-
}
|
|
318
|
-
/** Whether we are currently between begin() and end(). */
|
|
319
|
-
get inFrame() {
|
|
320
|
-
return this._inFrame;
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Begin a new frame. Clears the canvas and resets stats.
|
|
324
|
-
* @param clearColor 32-bit ARGB color to clear with (default black = 0xFF000000).
|
|
325
|
-
*/
|
|
326
|
-
begin(clearColor = 4278190080) {
|
|
327
|
-
if (this._inFrame) {
|
|
328
|
-
throw new Error("Renderer: begin() called while already in frame");
|
|
329
|
-
}
|
|
330
|
-
this._inFrame = true;
|
|
331
|
-
this.stats.drawCalls = 0;
|
|
332
|
-
this.stats.batchCount = 0;
|
|
333
|
-
this.canvas.save();
|
|
334
|
-
this.canvas.clear(clearColor);
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* End the current frame. Restores canvas state.
|
|
338
|
-
*/
|
|
339
|
-
end() {
|
|
340
|
-
if (!this._inFrame) {
|
|
341
|
-
throw new Error("Renderer: end() called without begin()");
|
|
342
|
-
}
|
|
343
|
-
this.canvas.restore();
|
|
344
|
-
this._inFrame = false;
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Apply camera transform to the canvas.
|
|
348
|
-
* Call after begin() and before drawing.
|
|
349
|
-
*/
|
|
350
|
-
applyCamera(cameraX, cameraY, zoom, rotation, viewportWidth, viewportHeight) {
|
|
351
|
-
const c = this.canvas;
|
|
352
|
-
c.translate(viewportWidth / 2, viewportHeight / 2);
|
|
353
|
-
c.scale(zoom, zoom);
|
|
354
|
-
if (rotation !== 0) {
|
|
355
|
-
c.rotate(-rotation, 0, 0);
|
|
356
|
-
}
|
|
357
|
-
c.translate(-cameraX, -cameraY);
|
|
358
|
-
}
|
|
359
|
-
/** Increment draw call counter. Called by batching systems. */
|
|
360
|
-
recordDrawCall() {
|
|
361
|
-
this.stats.drawCalls++;
|
|
362
|
-
}
|
|
363
|
-
/** Increment batch counter. */
|
|
364
|
-
recordBatch() {
|
|
365
|
-
this.stats.batchCount++;
|
|
366
|
-
}
|
|
73
|
+
/** Camera position (world-space center). */
|
|
74
|
+
x = 0;
|
|
75
|
+
y = 0;
|
|
76
|
+
/** Zoom level (1 = 100%). */
|
|
77
|
+
zoom = 1;
|
|
78
|
+
/** Rotation in radians. */
|
|
79
|
+
rotation = 0;
|
|
80
|
+
/** Viewport width in pixels. */
|
|
81
|
+
viewportWidth = 0;
|
|
82
|
+
/** Viewport height in pixels. */
|
|
83
|
+
viewportHeight = 0;
|
|
84
|
+
/** Optional world bounds to clamp camera position. */
|
|
85
|
+
bounds = null;
|
|
86
|
+
_follow = null;
|
|
87
|
+
_shake = null;
|
|
88
|
+
_shakePerlin = null;
|
|
89
|
+
_shakeSeed = 0;
|
|
90
|
+
_shakePerlinSeed = 0;
|
|
91
|
+
_motionScale = 1;
|
|
92
|
+
/** Set viewport dimensions. */
|
|
93
|
+
setViewport(width, height) {
|
|
94
|
+
this.viewportWidth = width;
|
|
95
|
+
this.viewportHeight = height;
|
|
96
|
+
}
|
|
97
|
+
/** Start following a target position. */
|
|
98
|
+
follow(target, options) {
|
|
99
|
+
this._follow = {
|
|
100
|
+
target,
|
|
101
|
+
options: options ?? {}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Stop following. */
|
|
105
|
+
unfollow() {
|
|
106
|
+
this._follow = null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Global motion multiplier applied to subsequent `shake()` calls. Scales
|
|
110
|
+
* both amplitude and duration. Default 1 (no-op). 0 fully suppresses
|
|
111
|
+
* shake. Intended to be driven by `@flare-engine/a11y` reduced-motion so
|
|
112
|
+
* callers don't have to guard every shake site.
|
|
113
|
+
*
|
|
114
|
+
* Only affects future `shake()` calls — does not retroactively modify an
|
|
115
|
+
* in-flight shake.
|
|
116
|
+
*
|
|
117
|
+
* Clamped to `>= 0`.
|
|
118
|
+
*/
|
|
119
|
+
setMotionScale(scale) {
|
|
120
|
+
this._motionScale = scale < 0 ? 0 : scale;
|
|
121
|
+
}
|
|
122
|
+
/** Current motion scale (default 1). */
|
|
123
|
+
get motionScale() {
|
|
124
|
+
return this._motionScale;
|
|
125
|
+
}
|
|
126
|
+
/** Start camera shake. */
|
|
127
|
+
shake(intensity, duration) {
|
|
128
|
+
const k = this._motionScale;
|
|
129
|
+
if (k <= 0) {
|
|
130
|
+
this._shake = null;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this._shake = {
|
|
134
|
+
intensity: intensity * k,
|
|
135
|
+
duration: duration * k,
|
|
136
|
+
elapsed: 0,
|
|
137
|
+
offsetX: 0,
|
|
138
|
+
offsetY: 0
|
|
139
|
+
};
|
|
140
|
+
this._shakeSeed++;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Start a Perlin-noise-based camera shake. Overwrites any in-flight
|
|
144
|
+
* shakePerlin (same semantics as shake()). Respects motionScale.
|
|
145
|
+
*/
|
|
146
|
+
shakePerlin(intensity, duration) {
|
|
147
|
+
const k = this._motionScale;
|
|
148
|
+
if (k <= 0) {
|
|
149
|
+
this._shakePerlin = null;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this._shakePerlin = {
|
|
153
|
+
intensity: intensity * k,
|
|
154
|
+
duration: duration * k,
|
|
155
|
+
elapsed: 0,
|
|
156
|
+
offsetX: 0,
|
|
157
|
+
offsetY: 0
|
|
158
|
+
};
|
|
159
|
+
this._shakePerlinSeed++;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Update camera. Call once per frame.
|
|
163
|
+
* @param dt Delta time in seconds.
|
|
164
|
+
*/
|
|
165
|
+
update(dt) {
|
|
166
|
+
if (this._follow) {
|
|
167
|
+
const { target, options } = this._follow;
|
|
168
|
+
const smoothing = options.smoothing ?? .1;
|
|
169
|
+
const offsetX = options.offset?.x ?? 0;
|
|
170
|
+
const offsetY = options.offset?.y ?? 0;
|
|
171
|
+
const goalX = target.x + offsetX;
|
|
172
|
+
const goalY = target.y + offsetY;
|
|
173
|
+
if (options.deadZone) {
|
|
174
|
+
const dz = options.deadZone;
|
|
175
|
+
const relX = target.x - this.x;
|
|
176
|
+
const relY = target.y - this.y;
|
|
177
|
+
if (relX < dz.x) this.x += (relX + offsetX - dz.x) * (1 - smoothing ** dt);
|
|
178
|
+
else if (relX > dz.x + dz.width) this.x += (relX + offsetX - dz.x - dz.width) * (1 - smoothing ** dt);
|
|
179
|
+
if (relY < dz.y) this.y += (relY + offsetY - dz.y) * (1 - smoothing ** dt);
|
|
180
|
+
else if (relY > dz.y + dz.height) this.y += (relY + offsetY - dz.y - dz.height) * (1 - smoothing ** dt);
|
|
181
|
+
} else {
|
|
182
|
+
const factor = 1 - smoothing ** dt;
|
|
183
|
+
this.x += (goalX - this.x) * factor;
|
|
184
|
+
this.y += (goalY - this.y) * factor;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (this.bounds) this._clampToBounds();
|
|
188
|
+
if (this._shake) {
|
|
189
|
+
this._shake.elapsed += dt;
|
|
190
|
+
if (this._shake.elapsed >= this._shake.duration) this._shake = null;
|
|
191
|
+
else {
|
|
192
|
+
const decay = 1 - this._shake.elapsed / this._shake.duration;
|
|
193
|
+
const seed = this._shakeSeed + this._shake.elapsed * 100;
|
|
194
|
+
this._shake.offsetX = (pseudoRandom(seed) * 2 - 1) * this._shake.intensity * decay;
|
|
195
|
+
this._shake.offsetY = (pseudoRandom(seed + 1) * 2 - 1) * this._shake.intensity * decay;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (this._shakePerlin) {
|
|
199
|
+
this._shakePerlin.elapsed += dt;
|
|
200
|
+
if (this._shakePerlin.elapsed >= this._shakePerlin.duration) this._shakePerlin = null;
|
|
201
|
+
else {
|
|
202
|
+
const decay = 1 - this._shakePerlin.elapsed / this._shakePerlin.duration;
|
|
203
|
+
const t = this._shakePerlin.elapsed * 4.7 + this._shakePerlinSeed * 17.3;
|
|
204
|
+
this._shakePerlin.offsetX = fbmNoise2d(t, 0) * this._shakePerlin.intensity * decay;
|
|
205
|
+
this._shakePerlin.offsetY = fbmNoise2d(0, t + 5.4) * this._shakePerlin.intensity * decay;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Get the effective camera X (including shake + shakePerlin offsets). */
|
|
210
|
+
get effectiveX() {
|
|
211
|
+
return this.x + (this._shake?.offsetX ?? 0) + (this._shakePerlin?.offsetX ?? 0);
|
|
212
|
+
}
|
|
213
|
+
/** Get the effective camera Y (including shake + shakePerlin offsets). */
|
|
214
|
+
get effectiveY() {
|
|
215
|
+
return this.y + (this._shake?.offsetY ?? 0) + (this._shakePerlin?.offsetY ?? 0);
|
|
216
|
+
}
|
|
217
|
+
/** Convert world coordinates to screen coordinates. */
|
|
218
|
+
worldToScreen(worldX, worldY) {
|
|
219
|
+
return this.worldToScreenInto(worldX, worldY, {
|
|
220
|
+
x: 0,
|
|
221
|
+
y: 0
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/** Zero-alloc variant — writes screen coordinates into `out`. */
|
|
225
|
+
worldToScreenInto(worldX, worldY, out) {
|
|
226
|
+
const cos = Math.cos(-this.rotation);
|
|
227
|
+
const sin = Math.sin(-this.rotation);
|
|
228
|
+
const dx = (worldX - this.effectiveX) * this.zoom;
|
|
229
|
+
const dy = (worldY - this.effectiveY) * this.zoom;
|
|
230
|
+
out.x = cos * dx - sin * dy + this.viewportWidth / 2;
|
|
231
|
+
out.y = sin * dx + cos * dy + this.viewportHeight / 2;
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
/** Convert screen coordinates to world coordinates. */
|
|
235
|
+
screenToWorld(screenX, screenY) {
|
|
236
|
+
return this.screenToWorldInto(screenX, screenY, {
|
|
237
|
+
x: 0,
|
|
238
|
+
y: 0
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/** Zero-alloc variant — writes world coordinates into `out`. */
|
|
242
|
+
screenToWorldInto(screenX, screenY, out) {
|
|
243
|
+
const cx = screenX - this.viewportWidth / 2;
|
|
244
|
+
const cy = screenY - this.viewportHeight / 2;
|
|
245
|
+
const cos = Math.cos(this.rotation);
|
|
246
|
+
const sin = Math.sin(this.rotation);
|
|
247
|
+
const rx = cos * cx - sin * cy;
|
|
248
|
+
const ry = sin * cx + cos * cy;
|
|
249
|
+
out.x = rx / this.zoom + this.effectiveX;
|
|
250
|
+
out.y = ry / this.zoom + this.effectiveY;
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
/** Get the visible world-space bounding rect (allocates a fresh Rect). */
|
|
254
|
+
getBounds() {
|
|
255
|
+
return this.getBoundsInto({
|
|
256
|
+
x: 0,
|
|
257
|
+
y: 0,
|
|
258
|
+
width: 0,
|
|
259
|
+
height: 0
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/** Zero-alloc variant — writes the visible bounds into `out`. */
|
|
263
|
+
getBoundsInto(out) {
|
|
264
|
+
const halfW = this.viewportWidth / (2 * this.zoom);
|
|
265
|
+
const halfH = this.viewportHeight / (2 * this.zoom);
|
|
266
|
+
out.x = this.effectiveX - halfW;
|
|
267
|
+
out.y = this.effectiveY - halfH;
|
|
268
|
+
out.width = halfW * 2;
|
|
269
|
+
out.height = halfH * 2;
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
_clampToBounds() {
|
|
273
|
+
const b = this.bounds;
|
|
274
|
+
const halfW = this.viewportWidth / (2 * this.zoom);
|
|
275
|
+
const halfH = this.viewportHeight / (2 * this.zoom);
|
|
276
|
+
const minX = b.x + halfW;
|
|
277
|
+
const maxX = b.x + b.width - halfW;
|
|
278
|
+
const minY = b.y + halfH;
|
|
279
|
+
const maxY = b.y + b.height - halfH;
|
|
280
|
+
if (minX <= maxX) this.x = Math.max(minX, Math.min(maxX, this.x));
|
|
281
|
+
else this.x = b.x + b.width / 2;
|
|
282
|
+
if (minY <= maxY) this.y = Math.max(minY, Math.min(maxY, this.y));
|
|
283
|
+
else this.y = b.y + b.height / 2;
|
|
284
|
+
}
|
|
367
285
|
};
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
cmd.srcW = frame.w;
|
|
394
|
-
cmd.srcH = frame.h;
|
|
395
|
-
cmd.dstX = dstX - anchorX * w;
|
|
396
|
-
cmd.dstY = dstY - anchorY * h;
|
|
397
|
-
cmd.dstW = w;
|
|
398
|
-
cmd.dstH = h;
|
|
399
|
-
cmd.effect = effect;
|
|
400
|
-
cmd.alpha = alpha;
|
|
401
|
-
cmd.rotation = rotation;
|
|
402
|
-
cmd.anchorX = anchorX;
|
|
403
|
-
cmd.anchorY = anchorY;
|
|
404
|
-
cmd.stretch = stretch;
|
|
405
|
-
this._commands.push(cmd);
|
|
406
|
-
}
|
|
407
|
-
/** Queue a raw sprite draw with explicit src/dst rects. `dstX` / `dstY`
|
|
408
|
-
* refer to the anchor point — offset by `anchorX * dstW` / `anchorY * dstH`.
|
|
409
|
-
*
|
|
410
|
-
* `alpha` is a per-instance opacity multiplier in [0, 1]. Default 1. */
|
|
411
|
-
drawRaw(image, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH, effect, anchorX = 0, anchorY = 0, alpha = 1, rotation = 0, stretch = false) {
|
|
412
|
-
const cmd = this._acquire();
|
|
413
|
-
cmd.image = image;
|
|
414
|
-
cmd.srcX = srcX;
|
|
415
|
-
cmd.srcY = srcY;
|
|
416
|
-
cmd.srcW = srcW;
|
|
417
|
-
cmd.srcH = srcH;
|
|
418
|
-
cmd.dstX = dstX - anchorX * dstW;
|
|
419
|
-
cmd.dstY = dstY - anchorY * dstH;
|
|
420
|
-
cmd.dstW = dstW;
|
|
421
|
-
cmd.dstH = dstH;
|
|
422
|
-
cmd.effect = effect;
|
|
423
|
-
cmd.alpha = alpha;
|
|
424
|
-
cmd.rotation = rotation;
|
|
425
|
-
cmd.anchorX = anchorX;
|
|
426
|
-
cmd.anchorY = anchorY;
|
|
427
|
-
cmd.stretch = stretch;
|
|
428
|
-
this._commands.push(cmd);
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* Flush all queued commands, grouped by (texture, effect, alpha) identity.
|
|
432
|
-
*
|
|
433
|
-
* @param drawAtlas Callback invoked once per batch. Receives
|
|
434
|
-
* `(image, commands)` — all commands in a batch share the same texture,
|
|
435
|
-
* the same effect (readable from `commands[0].effect`), and the same
|
|
436
|
-
* alpha (readable from `commands[0].alpha`).
|
|
437
|
-
* @param renderer Optional renderer to record stats.
|
|
438
|
-
*/
|
|
439
|
-
flush(drawAtlas, renderer) {
|
|
440
|
-
if (this._commands.length === 0) return;
|
|
441
|
-
const batches = this._batches;
|
|
442
|
-
for (const cmd of this._commands) {
|
|
443
|
-
let batch;
|
|
444
|
-
for (let i = 0; i < batches.length; i++) {
|
|
445
|
-
const b = batches[i];
|
|
446
|
-
if (b.image === cmd.image && b.effect === cmd.effect && b.alpha === cmd.alpha) {
|
|
447
|
-
batch = b;
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
if (!batch) {
|
|
452
|
-
batch = { image: cmd.image, effect: cmd.effect, alpha: cmd.alpha, commands: [] };
|
|
453
|
-
batches.push(batch);
|
|
454
|
-
}
|
|
455
|
-
batch.commands.push(cmd);
|
|
456
|
-
}
|
|
457
|
-
for (let i = 0; i < batches.length; i++) {
|
|
458
|
-
const batch = batches[i];
|
|
459
|
-
drawAtlas(batch.image, batch.commands);
|
|
460
|
-
if (renderer) {
|
|
461
|
-
renderer.recordDrawCall();
|
|
462
|
-
renderer.recordBatch();
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
for (const cmd of this._commands) {
|
|
466
|
-
this._pool.push(cmd);
|
|
467
|
-
}
|
|
468
|
-
this._commands.length = 0;
|
|
469
|
-
for (let i = 0; i < batches.length; i++) {
|
|
470
|
-
batches[i].commands.length = 0;
|
|
471
|
-
}
|
|
472
|
-
batches.length = 0;
|
|
473
|
-
}
|
|
474
|
-
/** Clear all queued commands without drawing. */
|
|
475
|
-
clear() {
|
|
476
|
-
for (const cmd of this._commands) {
|
|
477
|
-
this._pool.push(cmd);
|
|
478
|
-
}
|
|
479
|
-
this._commands.length = 0;
|
|
480
|
-
}
|
|
481
|
-
_acquire() {
|
|
482
|
-
if (this._pool.length > 0) {
|
|
483
|
-
return this._pool.pop();
|
|
484
|
-
}
|
|
485
|
-
return {
|
|
486
|
-
image: null,
|
|
487
|
-
srcX: 0,
|
|
488
|
-
srcY: 0,
|
|
489
|
-
srcW: 0,
|
|
490
|
-
srcH: 0,
|
|
491
|
-
dstX: 0,
|
|
492
|
-
dstY: 0,
|
|
493
|
-
dstW: 0,
|
|
494
|
-
dstH: 0,
|
|
495
|
-
effect: void 0,
|
|
496
|
-
alpha: 1,
|
|
497
|
-
rotation: 0,
|
|
498
|
-
anchorX: 0,
|
|
499
|
-
anchorY: 0,
|
|
500
|
-
stretch: false
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
// src/sprite-effects.ts
|
|
506
|
-
function tint(color, amount) {
|
|
507
|
-
return { type: "tint", color, amount };
|
|
508
|
-
}
|
|
509
|
-
function flash(color, durationSec) {
|
|
510
|
-
return { type: "flash", color, duration: durationSec, elapsed: 0 };
|
|
511
|
-
}
|
|
512
|
-
function grayscale(amount) {
|
|
513
|
-
return { type: "grayscale", amount };
|
|
514
|
-
}
|
|
515
|
-
function outline(color, width) {
|
|
516
|
-
return { type: "outline", color, width };
|
|
517
|
-
}
|
|
518
|
-
function custom(shader, uniforms) {
|
|
519
|
-
return { type: "custom", shader, uniforms };
|
|
520
|
-
}
|
|
521
|
-
function water(color, foamColor, waveSpeed, waveAmplitude) {
|
|
522
|
-
return { type: "water", color, foamColor, waveSpeed, waveAmplitude };
|
|
523
|
-
}
|
|
524
|
-
function fog(color, density = 0.5, speed = 0.3, scale = 2) {
|
|
525
|
-
return { type: "fog", color, density, speed, scale };
|
|
526
|
-
}
|
|
527
|
-
function fire(opts) {
|
|
528
|
-
return {
|
|
529
|
-
type: "fire",
|
|
530
|
-
color: opts.color,
|
|
531
|
-
tipColor: opts.tipColor,
|
|
532
|
-
speed: opts.speed ?? 1,
|
|
533
|
-
intensity: opts.intensity ?? 0.8
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
function dropShadow(opts) {
|
|
537
|
-
return {
|
|
538
|
-
type: "drop-shadow",
|
|
539
|
-
color: opts.color,
|
|
540
|
-
offsetX: opts.offsetX ?? 0,
|
|
541
|
-
offsetY: opts.offsetY ?? 6,
|
|
542
|
-
blur: opts.blur ?? 4
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
function roofsCity(opts) {
|
|
546
|
-
return {
|
|
547
|
-
type: "roofs-city",
|
|
548
|
-
skyColor: opts.skyColor,
|
|
549
|
-
roofColor: opts.roofColor,
|
|
550
|
-
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
551
|
-
density: opts.density ?? 6,
|
|
552
|
-
resolutionW: opts.resolutionW ?? 0,
|
|
553
|
-
resolutionH: opts.resolutionH ?? 0
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
function forestCanopy(opts) {
|
|
557
|
-
return {
|
|
558
|
-
type: "forest-canopy",
|
|
559
|
-
skyColor: opts.skyColor,
|
|
560
|
-
leafColor: opts.leafColor,
|
|
561
|
-
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
562
|
-
density: opts.density ?? 5,
|
|
563
|
-
resolutionW: opts.resolutionW ?? 0,
|
|
564
|
-
resolutionH: opts.resolutionH ?? 0
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
function riverWave(opts) {
|
|
568
|
-
return {
|
|
569
|
-
type: "river-wave",
|
|
570
|
-
waterColor: opts.waterColor,
|
|
571
|
-
foamColor: opts.foamColor,
|
|
572
|
-
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
573
|
-
waveScale: opts.waveScale ?? 2,
|
|
574
|
-
resolutionW: opts.resolutionW ?? 0,
|
|
575
|
-
resolutionH: opts.resolutionH ?? 0
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
function moonCrater(opts) {
|
|
579
|
-
return {
|
|
580
|
-
type: "moon-crater",
|
|
581
|
-
moonColor: opts.moonColor,
|
|
582
|
-
craterColor: opts.craterColor,
|
|
583
|
-
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
584
|
-
craterCount: opts.craterCount ?? 4,
|
|
585
|
-
resolutionW: opts.resolutionW ?? 0,
|
|
586
|
-
resolutionH: opts.resolutionH ?? 0
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
function caveInterior(opts) {
|
|
590
|
-
return {
|
|
591
|
-
type: "cave-interior",
|
|
592
|
-
caveColor: opts.caveColor,
|
|
593
|
-
glowColor: opts.glowColor,
|
|
594
|
-
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
595
|
-
stalactiteCount: opts.stalactiteCount ?? 8,
|
|
596
|
-
resolutionW: opts.resolutionW ?? 0,
|
|
597
|
-
resolutionH: opts.resolutionH ?? 0
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
function mountainHills(opts) {
|
|
601
|
-
return {
|
|
602
|
-
type: "mountain-hills",
|
|
603
|
-
skyColor: opts.skyColor,
|
|
604
|
-
hillColor: opts.hillColor,
|
|
605
|
-
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
606
|
-
ridgeScale: opts.ridgeScale ?? 1.5,
|
|
607
|
-
resolutionW: opts.resolutionW ?? 0,
|
|
608
|
-
resolutionH: opts.resolutionH ?? 0
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
function updateEffect(effect, dt) {
|
|
612
|
-
if (effect.type === "flash") {
|
|
613
|
-
const elapsed = effect.elapsed + dt;
|
|
614
|
-
if (elapsed >= effect.duration) return null;
|
|
615
|
-
return { type: "flash", color: effect.color, duration: effect.duration, elapsed };
|
|
616
|
-
}
|
|
617
|
-
return effect;
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/draw-order.ts
|
|
288
|
+
/**
|
|
289
|
+
* Sort an array of drawables by z-index (ascending = back to front).
|
|
290
|
+
* Uses insertion sort which is fast for nearly-sorted arrays
|
|
291
|
+
* (typical in game rendering where order rarely changes).
|
|
292
|
+
*
|
|
293
|
+
* Mutates the input array in place. Returns the same array.
|
|
294
|
+
*
|
|
295
|
+
* @param items The array to sort in place.
|
|
296
|
+
* @param length Optional upper bound — when provided, only sorts `items[0..length)`.
|
|
297
|
+
* Use this when passing a pooled array that may hold stale tail entries.
|
|
298
|
+
*/
|
|
299
|
+
function sortByDrawOrder(items, length) {
|
|
300
|
+
const n = length ?? items.length;
|
|
301
|
+
for (let i = 1; i < n; i++) {
|
|
302
|
+
const item = items[i];
|
|
303
|
+
let j = i - 1;
|
|
304
|
+
while (j >= 0 && items[j].z > item.z) {
|
|
305
|
+
items[j + 1] = items[j];
|
|
306
|
+
j--;
|
|
307
|
+
}
|
|
308
|
+
items[j + 1] = item;
|
|
309
|
+
}
|
|
310
|
+
return items;
|
|
618
311
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
312
|
+
/**
|
|
313
|
+
* Compare function for use with Array.sort when insertion sort isn't desired.
|
|
314
|
+
* Stable relative order for equal z values (returns 0).
|
|
315
|
+
*/
|
|
316
|
+
function drawOrderCompare(a, b) {
|
|
317
|
+
return a.z - b.z;
|
|
623
318
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
/* sksl */
|
|
628
|
-
`
|
|
629
|
-
uniform shader src;
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/shaders.ts
|
|
321
|
+
const FIRE_SKSL = `
|
|
630
322
|
uniform float2 resolution;
|
|
631
323
|
uniform float3 color;
|
|
632
324
|
uniform float3 tipColor;
|
|
@@ -652,21 +344,15 @@ float fbm(float2 p) {
|
|
|
652
344
|
}
|
|
653
345
|
|
|
654
346
|
half4 main(float2 coord) {
|
|
655
|
-
half4 srcColor = src.eval(coord);
|
|
656
347
|
float2 uv = coord / max(resolution, float2(1.0));
|
|
657
348
|
float2 np = float2(uv.x * 4.0, uv.y * 6.0 - time * speed * 2.0);
|
|
658
349
|
float n = fbm(np);
|
|
659
350
|
float flameMask = smoothstep(0.0, intensity, n * (1.0 - uv.y));
|
|
660
351
|
float3 fireRgb = mix(color, tipColor, uv.y);
|
|
661
|
-
|
|
662
|
-
return half4(fireColor.rgb, fireColor.a * srcColor.a);
|
|
352
|
+
return half4(fireRgb.r, fireRgb.g, fireRgb.b, flameMask);
|
|
663
353
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
var FOG_SKSL = (
|
|
667
|
-
/* sksl */
|
|
668
|
-
`
|
|
669
|
-
uniform shader src;
|
|
354
|
+
`;
|
|
355
|
+
const FOG_SKSL = `
|
|
670
356
|
uniform float2 resolution;
|
|
671
357
|
uniform float3 color;
|
|
672
358
|
uniform float density;
|
|
@@ -704,16 +390,10 @@ half4 main(float2 coord) {
|
|
|
704
390
|
float2 uv = coord / max(resolution, float2(1.0));
|
|
705
391
|
float2 drift = float2(time * speed, 0.3 * time * speed);
|
|
706
392
|
float n = clamp(fbm(uv * scale + drift), 0.0, 1.0);
|
|
707
|
-
half4
|
|
708
|
-
half3 blended = mix(srcColor.rgb, half3(color.r, color.g, color.b), half(density * n));
|
|
709
|
-
return half4(blended, srcColor.a);
|
|
393
|
+
return half4(color.r, color.g, color.b, clamp(density * n, 0.0, 1.0));
|
|
710
394
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
var WATER_SKSL = (
|
|
714
|
-
/* sksl */
|
|
715
|
-
`
|
|
716
|
-
uniform shader src;
|
|
395
|
+
`;
|
|
396
|
+
const WATER_SKSL = `
|
|
717
397
|
uniform float2 resolution;
|
|
718
398
|
uniform float3 color;
|
|
719
399
|
uniform float3 foamColor;
|
|
@@ -722,25 +402,16 @@ uniform float waveAmplitude;
|
|
|
722
402
|
uniform float time;
|
|
723
403
|
|
|
724
404
|
half4 main(float2 coord) {
|
|
725
|
-
float2 uv = coord / resolution;
|
|
726
|
-
float wave1 = sin(uv.x * 6.2832 * 3.0 + time * waveSpeed * 2.0) * waveAmplitude;
|
|
727
|
-
float wave2 = sin(uv.x * 6.2832 * 5.0 - time * waveSpeed * 1.3 + 1.5) * waveAmplitude * 0.5;
|
|
728
|
-
float totalWave = wave1 + wave2;
|
|
729
|
-
float2 displaced = float2(uv.x, uv.y + totalWave);
|
|
730
|
-
displaced = clamp(displaced, float2(0.0), float2(1.0));
|
|
731
|
-
half4 c = src.eval(displaced * resolution);
|
|
405
|
+
float2 uv = coord / max(resolution, float2(1.0));
|
|
732
406
|
float waveDeriv = cos(uv.x * 6.2832 * 3.0 + time * waveSpeed * 2.0) * waveAmplitude * 2.0
|
|
733
407
|
+ cos(uv.x * 6.2832 * 5.0 - time * waveSpeed * 1.3 + 1.5) * waveAmplitude;
|
|
734
408
|
float foam = smoothstep(waveAmplitude * 0.6, waveAmplitude * 1.5, waveDeriv);
|
|
735
|
-
half3 tinted =
|
|
409
|
+
half3 tinted = half3(color.r, color.g, color.b);
|
|
736
410
|
half3 final = mix(tinted, half3(foamColor.r, foamColor.g, foamColor.b), half(foam));
|
|
737
|
-
return half4(final,
|
|
411
|
+
return half4(final, 1.0);
|
|
738
412
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
var ROOFS_CITY_SKSL = (
|
|
742
|
-
/* sksl */
|
|
743
|
-
`
|
|
413
|
+
`;
|
|
414
|
+
const ROOFS_CITY_SKSL = `
|
|
744
415
|
uniform float2 resolution;
|
|
745
416
|
uniform float3 skyColor;
|
|
746
417
|
uniform float3 roofColor;
|
|
@@ -767,11 +438,8 @@ half4 main(float2 coord) {
|
|
|
767
438
|
half(buildings));
|
|
768
439
|
return half4(col, 1.0);
|
|
769
440
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
var FOREST_CANOPY_SKSL = (
|
|
773
|
-
/* sksl */
|
|
774
|
-
`
|
|
441
|
+
`;
|
|
442
|
+
const FOREST_CANOPY_SKSL = `
|
|
775
443
|
uniform float2 resolution;
|
|
776
444
|
uniform float3 skyColor;
|
|
777
445
|
uniform float3 leafColor;
|
|
@@ -799,11 +467,8 @@ half4 main(float2 coord) {
|
|
|
799
467
|
half(canopy));
|
|
800
468
|
return half4(col, 1.0);
|
|
801
469
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
var RIVER_WAVE_SKSL = (
|
|
805
|
-
/* sksl */
|
|
806
|
-
`
|
|
470
|
+
`;
|
|
471
|
+
const RIVER_WAVE_SKSL = `
|
|
807
472
|
uniform float2 resolution;
|
|
808
473
|
uniform float3 waterColor;
|
|
809
474
|
uniform float3 foamColor;
|
|
@@ -826,11 +491,8 @@ half4 main(float2 coord) {
|
|
|
826
491
|
half3 col = mix(base, half3(foamColor.r, foamColor.g, foamColor.b), half(crest * 0.6));
|
|
827
492
|
return half4(col, 1.0);
|
|
828
493
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
var MOON_CRATER_SKSL = (
|
|
832
|
-
/* sksl */
|
|
833
|
-
`
|
|
494
|
+
`;
|
|
495
|
+
const MOON_CRATER_SKSL = `
|
|
834
496
|
uniform float2 resolution;
|
|
835
497
|
uniform float3 moonColor;
|
|
836
498
|
uniform float3 craterColor;
|
|
@@ -862,11 +524,8 @@ half4 main(float2 coord) {
|
|
|
862
524
|
half(c));
|
|
863
525
|
return half4(col, 1.0);
|
|
864
526
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
var CAVE_INTERIOR_SKSL = (
|
|
868
|
-
/* sksl */
|
|
869
|
-
`
|
|
527
|
+
`;
|
|
528
|
+
const CAVE_INTERIOR_SKSL = `
|
|
870
529
|
uniform float2 resolution;
|
|
871
530
|
uniform float3 caveColor;
|
|
872
531
|
uniform float3 glowColor;
|
|
@@ -896,11 +555,8 @@ half4 main(float2 coord) {
|
|
|
896
555
|
col = mix(col, half3(caveColor.r, caveColor.g, caveColor.b) * half(0.3), half(stalac));
|
|
897
556
|
return half4(col, 1.0);
|
|
898
557
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
var MOUNTAIN_HILLS_SKSL = (
|
|
902
|
-
/* sksl */
|
|
903
|
-
`
|
|
558
|
+
`;
|
|
559
|
+
const MOUNTAIN_HILLS_SKSL = `
|
|
904
560
|
uniform float2 resolution;
|
|
905
561
|
uniform float3 skyColor;
|
|
906
562
|
uniform float3 hillColor;
|
|
@@ -934,780 +590,1195 @@ half4 main(float2 coord) {
|
|
|
934
590
|
half(mountains));
|
|
935
591
|
return half4(col, 1.0);
|
|
936
592
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
593
|
+
`;
|
|
594
|
+
//#endregion
|
|
595
|
+
//#region src/sprite-effects.ts
|
|
596
|
+
/** Create a tint effect. */
|
|
597
|
+
function tint(color, amount) {
|
|
598
|
+
return {
|
|
599
|
+
type: "tint",
|
|
600
|
+
color,
|
|
601
|
+
amount
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/** Create a flash effect. */
|
|
605
|
+
function flash(color, durationSec) {
|
|
606
|
+
return {
|
|
607
|
+
type: "flash",
|
|
608
|
+
color,
|
|
609
|
+
duration: durationSec,
|
|
610
|
+
elapsed: 0
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
/** Create a grayscale effect. */
|
|
614
|
+
function grayscale(amount) {
|
|
615
|
+
return {
|
|
616
|
+
type: "grayscale",
|
|
617
|
+
amount
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/** Create an outline effect. */
|
|
621
|
+
function outline(color, width) {
|
|
622
|
+
return {
|
|
623
|
+
type: "outline",
|
|
624
|
+
color,
|
|
625
|
+
width
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
/** Create a custom shader effect. `shader` must be pre-compiled; compiling
|
|
629
|
+
* SkSL per frame is expensive. `uniforms` may be mutated externally between
|
|
630
|
+
* frames to animate parameters. */
|
|
631
|
+
function custom(shader, uniforms) {
|
|
632
|
+
return {
|
|
633
|
+
type: "custom",
|
|
634
|
+
shader,
|
|
635
|
+
uniforms
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
/** Create a water surface effect. Time is passed to the renderer each frame
|
|
639
|
+
* via `beginFrame(w, h, time)` — the effect itself is time-invariant. */
|
|
640
|
+
function water(color, foamColor, waveSpeed, waveAmplitude) {
|
|
641
|
+
return {
|
|
642
|
+
type: "water",
|
|
643
|
+
color,
|
|
644
|
+
foamColor,
|
|
645
|
+
waveSpeed,
|
|
646
|
+
waveAmplitude
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
/** Create a fog / volumetric haze effect. Time is passed to the renderer each
|
|
650
|
+
* frame via `beginFrame(w, h, time)` — the effect itself is time-invariant. */
|
|
651
|
+
function fog(color, density = .5, speed = .3, scale = 2) {
|
|
652
|
+
return {
|
|
653
|
+
type: "fog",
|
|
654
|
+
color,
|
|
655
|
+
density,
|
|
656
|
+
speed,
|
|
657
|
+
scale
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
/** Create a fire / flame effect. Time is passed to the renderer each
|
|
661
|
+
* frame via `beginFrame(w, h, time)` — the effect itself is time-invariant. */
|
|
662
|
+
function fire(opts) {
|
|
663
|
+
return {
|
|
664
|
+
type: "fire",
|
|
665
|
+
color: opts.color,
|
|
666
|
+
tipColor: opts.tipColor,
|
|
667
|
+
speed: opts.speed ?? 1,
|
|
668
|
+
intensity: opts.intensity ?? .8
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
/** Create a drop-shadow effect. Defaults to a soft shadow offset downward for
|
|
672
|
+
* grounding; `color.a` is the shadow opacity. */
|
|
673
|
+
function dropShadow(opts) {
|
|
674
|
+
return {
|
|
675
|
+
type: "drop-shadow",
|
|
676
|
+
color: opts.color,
|
|
677
|
+
offsetX: opts.offsetX ?? 0,
|
|
678
|
+
offsetY: opts.offsetY ?? 6,
|
|
679
|
+
blur: opts.blur ?? 4
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
/** Create a rooftop city skyline effect. */
|
|
683
|
+
function roofsCity(opts) {
|
|
684
|
+
return {
|
|
685
|
+
type: "roofs-city",
|
|
686
|
+
skyColor: opts.skyColor,
|
|
687
|
+
roofColor: opts.roofColor,
|
|
688
|
+
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
689
|
+
density: opts.density ?? 6,
|
|
690
|
+
resolutionW: opts.resolutionW ?? 0,
|
|
691
|
+
resolutionH: opts.resolutionH ?? 0
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/** Create a forest canopy effect. */
|
|
695
|
+
function forestCanopy(opts) {
|
|
696
|
+
return {
|
|
697
|
+
type: "forest-canopy",
|
|
698
|
+
skyColor: opts.skyColor,
|
|
699
|
+
leafColor: opts.leafColor,
|
|
700
|
+
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
701
|
+
density: opts.density ?? 5,
|
|
702
|
+
resolutionW: opts.resolutionW ?? 0,
|
|
703
|
+
resolutionH: opts.resolutionH ?? 0
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
/** Create a river wave surface effect. */
|
|
707
|
+
function riverWave(opts) {
|
|
708
|
+
return {
|
|
709
|
+
type: "river-wave",
|
|
710
|
+
waterColor: opts.waterColor,
|
|
711
|
+
foamColor: opts.foamColor,
|
|
712
|
+
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
713
|
+
waveScale: opts.waveScale ?? 2,
|
|
714
|
+
resolutionW: opts.resolutionW ?? 0,
|
|
715
|
+
resolutionH: opts.resolutionH ?? 0
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/** Create a moon crater surface effect. */
|
|
719
|
+
function moonCrater(opts) {
|
|
720
|
+
return {
|
|
721
|
+
type: "moon-crater",
|
|
722
|
+
moonColor: opts.moonColor,
|
|
723
|
+
craterColor: opts.craterColor,
|
|
724
|
+
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
725
|
+
craterCount: opts.craterCount ?? 4,
|
|
726
|
+
resolutionW: opts.resolutionW ?? 0,
|
|
727
|
+
resolutionH: opts.resolutionH ?? 0
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/** Create a cave interior effect. */
|
|
731
|
+
function caveInterior(opts) {
|
|
732
|
+
return {
|
|
733
|
+
type: "cave-interior",
|
|
734
|
+
caveColor: opts.caveColor,
|
|
735
|
+
glowColor: opts.glowColor,
|
|
736
|
+
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
737
|
+
stalactiteCount: opts.stalactiteCount ?? 8,
|
|
738
|
+
resolutionW: opts.resolutionW ?? 0,
|
|
739
|
+
resolutionH: opts.resolutionH ?? 0
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
/** Create a mountain / hill ridge effect. */
|
|
743
|
+
function mountainHills(opts) {
|
|
744
|
+
return {
|
|
745
|
+
type: "mountain-hills",
|
|
746
|
+
skyColor: opts.skyColor,
|
|
747
|
+
hillColor: opts.hillColor,
|
|
748
|
+
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
749
|
+
ridgeScale: opts.ridgeScale ?? 1.5,
|
|
750
|
+
resolutionW: opts.resolutionW ?? 0,
|
|
751
|
+
resolutionH: opts.resolutionH ?? 0
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Advance a time-based effect by `dt` seconds.
|
|
756
|
+
*
|
|
757
|
+
* Returns a new effect object if state changed (e.g. flash elapsed time
|
|
758
|
+
* advances), the same reference if the effect is time-invariant (tint,
|
|
759
|
+
* grayscale, outline, custom), or `null` if the effect has expired.
|
|
760
|
+
*
|
|
761
|
+
* Effects are immutable — callers must replace their reference with the
|
|
762
|
+
* returned value. Sharing an effect instance across entities is safe.
|
|
763
|
+
*/
|
|
764
|
+
function updateEffect(effect, dt) {
|
|
765
|
+
if (effect.type === "flash") {
|
|
766
|
+
const elapsed = effect.elapsed + dt;
|
|
767
|
+
if (elapsed >= effect.duration) return null;
|
|
768
|
+
return {
|
|
769
|
+
type: "flash",
|
|
770
|
+
color: effect.color,
|
|
771
|
+
duration: effect.duration,
|
|
772
|
+
elapsed
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
return effect;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Get the current intensity of a flash effect (1 at start, 0 at end).
|
|
779
|
+
*/
|
|
780
|
+
function flashIntensity(effect) {
|
|
781
|
+
if (effect.duration <= 0) return 0;
|
|
782
|
+
const t = effect.elapsed / effect.duration;
|
|
783
|
+
return Math.max(0, 1 - t);
|
|
784
|
+
}
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/frame-renderer.ts
|
|
787
|
+
/** See the dev-flag note at the top of this file. */
|
|
788
|
+
const G = globalThis;
|
|
789
|
+
const BLEND_SRC_OVER = 3;
|
|
790
|
+
const BLEND_SRC_IN = 5;
|
|
791
|
+
const OUTLINE_OFFSETS = [
|
|
792
|
+
[1, 0],
|
|
793
|
+
[-1, 0],
|
|
794
|
+
[0, 1],
|
|
795
|
+
[0, -1],
|
|
796
|
+
[Math.SQRT1_2, Math.SQRT1_2],
|
|
797
|
+
[Math.SQRT1_2, -Math.SQRT1_2],
|
|
798
|
+
[-Math.SQRT1_2, Math.SQRT1_2],
|
|
799
|
+
[-Math.SQRT1_2, -Math.SQRT1_2]
|
|
952
800
|
];
|
|
801
|
+
let _xformUnrotatedCount = 0;
|
|
802
|
+
let _xformRotatedCount = 0;
|
|
803
|
+
/**
|
|
804
|
+
* Create a `FrameRenderer` backed by the given structural Skia module.
|
|
805
|
+
*
|
|
806
|
+
* Both `@flare-engine/react` (passing the real RN-Skia module) and
|
|
807
|
+
* `@flare-engine/render-web` (passing a CanvasKit shim) use this factory.
|
|
808
|
+
*/
|
|
953
809
|
function createFrameRenderer(Skia) {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
810
|
+
const defaultPaint = Skia.Paint();
|
|
811
|
+
let tintPaint = null;
|
|
812
|
+
let flashPaint = null;
|
|
813
|
+
let grayscalePaint = null;
|
|
814
|
+
let outlinePaint = null;
|
|
815
|
+
let customPaint = null;
|
|
816
|
+
let waterPaint = null;
|
|
817
|
+
let waterRuntimeEffect = null;
|
|
818
|
+
let fogPaint = null;
|
|
819
|
+
let fogRuntimeEffect = null;
|
|
820
|
+
let firePaint = null;
|
|
821
|
+
let fireRuntimeEffect = null;
|
|
822
|
+
let dropShadowPaint = null;
|
|
823
|
+
let roofsCityPaint = null;
|
|
824
|
+
let roofsCityEffect = null;
|
|
825
|
+
let forestCanopyPaint = null;
|
|
826
|
+
let forestCanopyEffect = null;
|
|
827
|
+
let riverWavePaint = null;
|
|
828
|
+
let riverWaveEffect = null;
|
|
829
|
+
let moonCraterPaint = null;
|
|
830
|
+
let moonCraterEffect = null;
|
|
831
|
+
let caveInteriorPaint = null;
|
|
832
|
+
let caveInteriorEffect = null;
|
|
833
|
+
let mountainHillsPaint = null;
|
|
834
|
+
let mountainHillsEffect = null;
|
|
835
|
+
const roofsCityUniforms = new Array(10).fill(0);
|
|
836
|
+
const forestCanopyUniforms = new Array(10).fill(0);
|
|
837
|
+
const riverWaveUniforms = new Array(10).fill(0);
|
|
838
|
+
const moonCraterUniforms = new Array(10).fill(0);
|
|
839
|
+
const caveInteriorUniforms = new Array(10).fill(0);
|
|
840
|
+
const mountainHillsUniforms = new Array(10).fill(0);
|
|
841
|
+
const recorder = Skia.PictureRecorder();
|
|
842
|
+
const recordingBounds = Skia.XYWHRect(0, 0, 0, 0);
|
|
843
|
+
const srcRects = [];
|
|
844
|
+
const xforms = [];
|
|
845
|
+
const stretchSrcRects = [];
|
|
846
|
+
const stretchDstRects = [];
|
|
847
|
+
let stretchPaint = null;
|
|
848
|
+
const zoneBgRects = [];
|
|
849
|
+
let _cachedSrcSlice = null;
|
|
850
|
+
let _cachedSrcSliceLen = 0;
|
|
851
|
+
let _cachedXformSlice = null;
|
|
852
|
+
let _cachedXformSliceLen = 0;
|
|
853
|
+
const clearColor = /* @__PURE__ */ new Float32Array(4);
|
|
854
|
+
const tintColor = /* @__PURE__ */ new Float32Array(4);
|
|
855
|
+
const flashColor = /* @__PURE__ */ new Float32Array(4);
|
|
856
|
+
const outlineColor = /* @__PURE__ */ new Float32Array(4);
|
|
857
|
+
const dropShadowColor = /* @__PURE__ */ new Float32Array(4);
|
|
858
|
+
const grayMatrix = [
|
|
859
|
+
1,
|
|
860
|
+
0,
|
|
861
|
+
0,
|
|
862
|
+
0,
|
|
863
|
+
0,
|
|
864
|
+
0,
|
|
865
|
+
1,
|
|
866
|
+
0,
|
|
867
|
+
0,
|
|
868
|
+
0,
|
|
869
|
+
0,
|
|
870
|
+
0,
|
|
871
|
+
1,
|
|
872
|
+
0,
|
|
873
|
+
0,
|
|
874
|
+
0,
|
|
875
|
+
0,
|
|
876
|
+
0,
|
|
877
|
+
1,
|
|
878
|
+
0
|
|
879
|
+
];
|
|
880
|
+
const customUniforms = [];
|
|
881
|
+
const waterUniforms = new Array(11).fill(0);
|
|
882
|
+
const fogUniforms = new Array(9).fill(0);
|
|
883
|
+
const fireUniforms = new Array(11).fill(0);
|
|
884
|
+
let currentFrameTime = 0;
|
|
885
|
+
let currentSpriteW = 0;
|
|
886
|
+
let currentSpriteH = 0;
|
|
887
|
+
let current = null;
|
|
888
|
+
let _boundRuntime = null;
|
|
889
|
+
let _boundChain = null;
|
|
890
|
+
const _loggedOnce = /* @__PURE__ */ new Set();
|
|
891
|
+
function logOnce(msg) {
|
|
892
|
+
if (_loggedOnce.has(msg)) return;
|
|
893
|
+
_loggedOnce.add(msg);
|
|
894
|
+
console.warn(`[postfx] ${msg}`);
|
|
895
|
+
}
|
|
896
|
+
const canvas = {
|
|
897
|
+
save: () => current?.save() ?? 0,
|
|
898
|
+
restore: () => {
|
|
899
|
+
current?.restore();
|
|
900
|
+
},
|
|
901
|
+
translate: (x, y) => {
|
|
902
|
+
current?.translate(x, y);
|
|
903
|
+
},
|
|
904
|
+
scale: (sx, sy) => {
|
|
905
|
+
current?.scale(sx, sy);
|
|
906
|
+
},
|
|
907
|
+
rotate: (degrees, cx, cy) => {
|
|
908
|
+
current?.rotate(degrees, cx, cy);
|
|
909
|
+
},
|
|
910
|
+
clear: (argb) => {
|
|
911
|
+
if (current === null) return;
|
|
912
|
+
clearColor[0] = (argb >>> 16 & 255) / 255;
|
|
913
|
+
clearColor[1] = (argb >>> 8 & 255) / 255;
|
|
914
|
+
clearColor[2] = (argb & 255) / 255;
|
|
915
|
+
clearColor[3] = (argb >>> 24 & 255) / 255;
|
|
916
|
+
current.clear(clearColor);
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
function ensurePoolCapacity(n) {
|
|
920
|
+
while (srcRects.length < n) {
|
|
921
|
+
srcRects.push(Skia.XYWHRect(0, 0, 0, 0));
|
|
922
|
+
xforms.push(Skia.RSXform(1, 0, 0, 0));
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function ensureStretchPoolCapacity(n) {
|
|
926
|
+
while (stretchSrcRects.length < n) {
|
|
927
|
+
stretchSrcRects.push(Skia.XYWHRect(0, 0, 0, 0));
|
|
928
|
+
stretchDstRects.push(Skia.XYWHRect(0, 0, 0, 0));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
function ensureZoneBgPoolCapacity(n) {
|
|
932
|
+
while (zoneBgRects.length < n) zoneBgRects.push(Skia.XYWHRect(0, 0, 0, 0));
|
|
933
|
+
}
|
|
934
|
+
function getCachedSrcSlice(n) {
|
|
935
|
+
if (_cachedSrcSlice !== null && _cachedSrcSliceLen === n) return _cachedSrcSlice;
|
|
936
|
+
_cachedSrcSlice = srcRects.length === n ? srcRects : srcRects.slice(0, n);
|
|
937
|
+
_cachedSrcSliceLen = n;
|
|
938
|
+
return _cachedSrcSlice;
|
|
939
|
+
}
|
|
940
|
+
function getCachedXformSlice(n) {
|
|
941
|
+
if (_cachedXformSlice !== null && _cachedXformSliceLen === n) return _cachedXformSlice;
|
|
942
|
+
_cachedXformSlice = xforms.length === n ? xforms : xforms.slice(0, n);
|
|
943
|
+
_cachedXformSliceLen = n;
|
|
944
|
+
return _cachedXformSlice;
|
|
945
|
+
}
|
|
946
|
+
function fillSrcRects(commands, n) {
|
|
947
|
+
for (let i = 0; i < n; i++) {
|
|
948
|
+
const cmd = commands[i];
|
|
949
|
+
srcRects[i]?.setXYWH(cmd.srcX, cmd.srcY, cmd.srcW, cmd.srcH);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function fillXforms(commands, n, dx, dy) {
|
|
953
|
+
for (let i = 0; i < n; i++) {
|
|
954
|
+
const cmd = commands[i];
|
|
955
|
+
const scale = cmd.srcW === 0 ? 1 : cmd.dstW / cmd.srcW;
|
|
956
|
+
if (cmd.rotation === 0) {
|
|
957
|
+
xforms[i]?.set(scale, 0, cmd.dstX + dx, cmd.dstY + dy);
|
|
958
|
+
if (G.__DEV__) _xformUnrotatedCount++;
|
|
959
|
+
} else {
|
|
960
|
+
const c = Math.cos(cmd.rotation);
|
|
961
|
+
const s = Math.sin(cmd.rotation);
|
|
962
|
+
const scos = scale * c;
|
|
963
|
+
const ssin = scale * s;
|
|
964
|
+
const px = cmd.dstX + cmd.anchorX * cmd.dstW + dx;
|
|
965
|
+
const py = cmd.dstY + cmd.anchorY * cmd.dstH + dy;
|
|
966
|
+
const axSrc = cmd.anchorX * cmd.srcW;
|
|
967
|
+
const aySrc = cmd.anchorY * cmd.srcH;
|
|
968
|
+
xforms[i]?.set(scos, ssin, px - scos * axSrc + ssin * aySrc, py - ssin * axSrc - scos * aySrc);
|
|
969
|
+
if (G.__DEV__) _xformRotatedCount++;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function applyTint(effect) {
|
|
974
|
+
if (tintPaint === null) tintPaint = Skia.Paint();
|
|
975
|
+
writeColor(tintColor, effect.color, effect.amount);
|
|
976
|
+
tintPaint.setColorFilter(Skia.ColorFilter.MakeBlend(tintColor, BLEND_SRC_OVER));
|
|
977
|
+
return tintPaint;
|
|
978
|
+
}
|
|
979
|
+
function applyFlash(effect) {
|
|
980
|
+
if (flashPaint === null) flashPaint = Skia.Paint();
|
|
981
|
+
writeColor(flashColor, effect.color, flashIntensity(effect));
|
|
982
|
+
flashPaint.setColorFilter(Skia.ColorFilter.MakeBlend(flashColor, BLEND_SRC_OVER));
|
|
983
|
+
return flashPaint;
|
|
984
|
+
}
|
|
985
|
+
function applyGrayscale(effect) {
|
|
986
|
+
if (grayscalePaint === null) grayscalePaint = Skia.Paint();
|
|
987
|
+
writeGrayscaleMatrix(grayMatrix, effect.amount);
|
|
988
|
+
grayscalePaint.setColorFilter(Skia.ColorFilter.MakeMatrix(grayMatrix));
|
|
989
|
+
return grayscalePaint;
|
|
990
|
+
}
|
|
991
|
+
function applyOutlinePaint(effect) {
|
|
992
|
+
if (outlinePaint === null) outlinePaint = Skia.Paint();
|
|
993
|
+
writeColor(outlineColor, effect.color, 1);
|
|
994
|
+
outlinePaint.setColorFilter(Skia.ColorFilter.MakeBlend(outlineColor, BLEND_SRC_IN));
|
|
995
|
+
return outlinePaint;
|
|
996
|
+
}
|
|
997
|
+
function applyCustom(effect) {
|
|
998
|
+
if (customPaint === null) customPaint = Skia.Paint();
|
|
999
|
+
const shader = effect.shader;
|
|
1000
|
+
const floatCount = shader.getUniformFloatCount();
|
|
1001
|
+
while (customUniforms.length < floatCount) customUniforms.push(0);
|
|
1002
|
+
customUniforms.length = floatCount;
|
|
1003
|
+
fillCustomUniforms(effect, shader, customUniforms);
|
|
1004
|
+
customPaint.setShader(shader.makeShader(customUniforms));
|
|
1005
|
+
return customPaint;
|
|
1006
|
+
}
|
|
1007
|
+
function applyWater(effect, time) {
|
|
1008
|
+
if (Skia.RuntimeEffect === void 0) return defaultPaint;
|
|
1009
|
+
if (waterRuntimeEffect === null) {
|
|
1010
|
+
const compiled = Skia.RuntimeEffect.Make(WATER_SKSL);
|
|
1011
|
+
if (compiled === null) return defaultPaint;
|
|
1012
|
+
waterRuntimeEffect = compiled;
|
|
1013
|
+
}
|
|
1014
|
+
if (waterPaint === null) waterPaint = Skia.Paint();
|
|
1015
|
+
waterUniforms[0] = currentSpriteW;
|
|
1016
|
+
waterUniforms[1] = currentSpriteH;
|
|
1017
|
+
waterUniforms[2] = effect.color.r;
|
|
1018
|
+
waterUniforms[3] = effect.color.g;
|
|
1019
|
+
waterUniforms[4] = effect.color.b;
|
|
1020
|
+
waterUniforms[5] = effect.foamColor.r;
|
|
1021
|
+
waterUniforms[6] = effect.foamColor.g;
|
|
1022
|
+
waterUniforms[7] = effect.foamColor.b;
|
|
1023
|
+
waterUniforms[8] = effect.waveSpeed;
|
|
1024
|
+
waterUniforms[9] = effect.waveAmplitude;
|
|
1025
|
+
waterUniforms[10] = time;
|
|
1026
|
+
waterPaint.setShader(waterRuntimeEffect.makeShader(waterUniforms));
|
|
1027
|
+
return waterPaint;
|
|
1028
|
+
}
|
|
1029
|
+
function applyFog(effect, time) {
|
|
1030
|
+
if (Skia.RuntimeEffect === void 0) return defaultPaint;
|
|
1031
|
+
if (fogRuntimeEffect === null) {
|
|
1032
|
+
const compiled = Skia.RuntimeEffect.Make(FOG_SKSL);
|
|
1033
|
+
if (compiled === null) return defaultPaint;
|
|
1034
|
+
fogRuntimeEffect = compiled;
|
|
1035
|
+
}
|
|
1036
|
+
if (fogPaint === null) fogPaint = Skia.Paint();
|
|
1037
|
+
fogUniforms[0] = currentSpriteW;
|
|
1038
|
+
fogUniforms[1] = currentSpriteH;
|
|
1039
|
+
fogUniforms[2] = effect.color.r;
|
|
1040
|
+
fogUniforms[3] = effect.color.g;
|
|
1041
|
+
fogUniforms[4] = effect.color.b;
|
|
1042
|
+
fogUniforms[5] = effect.density;
|
|
1043
|
+
fogUniforms[6] = effect.speed;
|
|
1044
|
+
fogUniforms[7] = effect.scale;
|
|
1045
|
+
fogUniforms[8] = time;
|
|
1046
|
+
fogPaint.setShader(fogRuntimeEffect.makeShader(fogUniforms));
|
|
1047
|
+
return fogPaint;
|
|
1048
|
+
}
|
|
1049
|
+
function applyFire(effect, time) {
|
|
1050
|
+
if (Skia.RuntimeEffect === void 0) return defaultPaint;
|
|
1051
|
+
if (fireRuntimeEffect === null) {
|
|
1052
|
+
const compiled = Skia.RuntimeEffect.Make(FIRE_SKSL);
|
|
1053
|
+
if (compiled === null) return defaultPaint;
|
|
1054
|
+
fireRuntimeEffect = compiled;
|
|
1055
|
+
}
|
|
1056
|
+
if (firePaint === null) firePaint = Skia.Paint();
|
|
1057
|
+
fireUniforms[0] = currentSpriteW;
|
|
1058
|
+
fireUniforms[1] = currentSpriteH;
|
|
1059
|
+
fireUniforms[2] = effect.color.r;
|
|
1060
|
+
fireUniforms[3] = effect.color.g;
|
|
1061
|
+
fireUniforms[4] = effect.color.b;
|
|
1062
|
+
fireUniforms[5] = effect.tipColor.r;
|
|
1063
|
+
fireUniforms[6] = effect.tipColor.g;
|
|
1064
|
+
fireUniforms[7] = effect.tipColor.b;
|
|
1065
|
+
fireUniforms[8] = effect.speed;
|
|
1066
|
+
fireUniforms[9] = effect.intensity;
|
|
1067
|
+
fireUniforms[10] = time;
|
|
1068
|
+
firePaint.setShader(fireRuntimeEffect.makeShader(fireUniforms));
|
|
1069
|
+
return firePaint;
|
|
1070
|
+
}
|
|
1071
|
+
function applyDropShadow(effect) {
|
|
1072
|
+
if (Skia.ImageFilter === void 0) return defaultPaint;
|
|
1073
|
+
if (dropShadowPaint === null) dropShadowPaint = Skia.Paint();
|
|
1074
|
+
writeColor(dropShadowColor, effect.color, 1);
|
|
1075
|
+
dropShadowPaint.setImageFilter(Skia.ImageFilter.MakeDropShadow(effect.offsetX, effect.offsetY, effect.blur, effect.blur, dropShadowColor, null));
|
|
1076
|
+
return dropShadowPaint;
|
|
1077
|
+
}
|
|
1078
|
+
function applyRoofsCity(effect) {
|
|
1079
|
+
if (Skia.RuntimeEffect === void 0) return defaultPaint;
|
|
1080
|
+
if (roofsCityEffect === null) {
|
|
1081
|
+
const compiled = Skia.RuntimeEffect.Make(ROOFS_CITY_SKSL);
|
|
1082
|
+
if (compiled === null) return defaultPaint;
|
|
1083
|
+
roofsCityEffect = compiled;
|
|
1084
|
+
}
|
|
1085
|
+
if (roofsCityPaint === null) roofsCityPaint = Skia.Paint();
|
|
1086
|
+
roofsCityUniforms[0] = effect.resolutionW;
|
|
1087
|
+
roofsCityUniforms[1] = effect.resolutionH;
|
|
1088
|
+
roofsCityUniforms[2] = effect.skyColor.r;
|
|
1089
|
+
roofsCityUniforms[3] = effect.skyColor.g;
|
|
1090
|
+
roofsCityUniforms[4] = effect.skyColor.b;
|
|
1091
|
+
roofsCityUniforms[5] = effect.roofColor.r;
|
|
1092
|
+
roofsCityUniforms[6] = effect.roofColor.g;
|
|
1093
|
+
roofsCityUniforms[7] = effect.roofColor.b;
|
|
1094
|
+
roofsCityUniforms[8] = effect.scrollOffsetY;
|
|
1095
|
+
roofsCityUniforms[9] = effect.density;
|
|
1096
|
+
roofsCityPaint.setShader(roofsCityEffect.makeShader(roofsCityUniforms));
|
|
1097
|
+
return roofsCityPaint;
|
|
1098
|
+
}
|
|
1099
|
+
function applyForestCanopy(effect) {
|
|
1100
|
+
if (Skia.RuntimeEffect === void 0) return defaultPaint;
|
|
1101
|
+
if (forestCanopyEffect === null) {
|
|
1102
|
+
const compiled = Skia.RuntimeEffect.Make(FOREST_CANOPY_SKSL);
|
|
1103
|
+
if (compiled === null) return defaultPaint;
|
|
1104
|
+
forestCanopyEffect = compiled;
|
|
1105
|
+
}
|
|
1106
|
+
if (forestCanopyPaint === null) forestCanopyPaint = Skia.Paint();
|
|
1107
|
+
forestCanopyUniforms[0] = effect.resolutionW;
|
|
1108
|
+
forestCanopyUniforms[1] = effect.resolutionH;
|
|
1109
|
+
forestCanopyUniforms[2] = effect.skyColor.r;
|
|
1110
|
+
forestCanopyUniforms[3] = effect.skyColor.g;
|
|
1111
|
+
forestCanopyUniforms[4] = effect.skyColor.b;
|
|
1112
|
+
forestCanopyUniforms[5] = effect.leafColor.r;
|
|
1113
|
+
forestCanopyUniforms[6] = effect.leafColor.g;
|
|
1114
|
+
forestCanopyUniforms[7] = effect.leafColor.b;
|
|
1115
|
+
forestCanopyUniforms[8] = effect.scrollOffsetY;
|
|
1116
|
+
forestCanopyUniforms[9] = effect.density;
|
|
1117
|
+
forestCanopyPaint.setShader(forestCanopyEffect.makeShader(forestCanopyUniforms));
|
|
1118
|
+
return forestCanopyPaint;
|
|
1119
|
+
}
|
|
1120
|
+
function applyRiverWave(effect) {
|
|
1121
|
+
if (Skia.RuntimeEffect === void 0) return defaultPaint;
|
|
1122
|
+
if (riverWaveEffect === null) {
|
|
1123
|
+
const compiled = Skia.RuntimeEffect.Make(RIVER_WAVE_SKSL);
|
|
1124
|
+
if (compiled === null) return defaultPaint;
|
|
1125
|
+
riverWaveEffect = compiled;
|
|
1126
|
+
}
|
|
1127
|
+
if (riverWavePaint === null) riverWavePaint = Skia.Paint();
|
|
1128
|
+
riverWaveUniforms[0] = effect.resolutionW;
|
|
1129
|
+
riverWaveUniforms[1] = effect.resolutionH;
|
|
1130
|
+
riverWaveUniforms[2] = effect.waterColor.r;
|
|
1131
|
+
riverWaveUniforms[3] = effect.waterColor.g;
|
|
1132
|
+
riverWaveUniforms[4] = effect.waterColor.b;
|
|
1133
|
+
riverWaveUniforms[5] = effect.foamColor.r;
|
|
1134
|
+
riverWaveUniforms[6] = effect.foamColor.g;
|
|
1135
|
+
riverWaveUniforms[7] = effect.foamColor.b;
|
|
1136
|
+
riverWaveUniforms[8] = effect.scrollOffsetY;
|
|
1137
|
+
riverWaveUniforms[9] = effect.waveScale;
|
|
1138
|
+
riverWavePaint.setShader(riverWaveEffect.makeShader(riverWaveUniforms));
|
|
1139
|
+
return riverWavePaint;
|
|
1140
|
+
}
|
|
1141
|
+
function applyMoonCrater(effect) {
|
|
1142
|
+
if (Skia.RuntimeEffect === void 0) return defaultPaint;
|
|
1143
|
+
if (moonCraterEffect === null) {
|
|
1144
|
+
const compiled = Skia.RuntimeEffect.Make(MOON_CRATER_SKSL);
|
|
1145
|
+
if (compiled === null) return defaultPaint;
|
|
1146
|
+
moonCraterEffect = compiled;
|
|
1147
|
+
}
|
|
1148
|
+
if (moonCraterPaint === null) moonCraterPaint = Skia.Paint();
|
|
1149
|
+
moonCraterUniforms[0] = effect.resolutionW;
|
|
1150
|
+
moonCraterUniforms[1] = effect.resolutionH;
|
|
1151
|
+
moonCraterUniforms[2] = effect.moonColor.r;
|
|
1152
|
+
moonCraterUniforms[3] = effect.moonColor.g;
|
|
1153
|
+
moonCraterUniforms[4] = effect.moonColor.b;
|
|
1154
|
+
moonCraterUniforms[5] = effect.craterColor.r;
|
|
1155
|
+
moonCraterUniforms[6] = effect.craterColor.g;
|
|
1156
|
+
moonCraterUniforms[7] = effect.craterColor.b;
|
|
1157
|
+
moonCraterUniforms[8] = effect.scrollOffsetY;
|
|
1158
|
+
moonCraterUniforms[9] = effect.craterCount;
|
|
1159
|
+
moonCraterPaint.setShader(moonCraterEffect.makeShader(moonCraterUniforms));
|
|
1160
|
+
return moonCraterPaint;
|
|
1161
|
+
}
|
|
1162
|
+
function applyCaveInterior(effect) {
|
|
1163
|
+
if (Skia.RuntimeEffect === void 0) return defaultPaint;
|
|
1164
|
+
if (caveInteriorEffect === null) {
|
|
1165
|
+
const compiled = Skia.RuntimeEffect.Make(CAVE_INTERIOR_SKSL);
|
|
1166
|
+
if (compiled === null) return defaultPaint;
|
|
1167
|
+
caveInteriorEffect = compiled;
|
|
1168
|
+
}
|
|
1169
|
+
if (caveInteriorPaint === null) caveInteriorPaint = Skia.Paint();
|
|
1170
|
+
caveInteriorUniforms[0] = effect.resolutionW;
|
|
1171
|
+
caveInteriorUniforms[1] = effect.resolutionH;
|
|
1172
|
+
caveInteriorUniforms[2] = effect.caveColor.r;
|
|
1173
|
+
caveInteriorUniforms[3] = effect.caveColor.g;
|
|
1174
|
+
caveInteriorUniforms[4] = effect.caveColor.b;
|
|
1175
|
+
caveInteriorUniforms[5] = effect.glowColor.r;
|
|
1176
|
+
caveInteriorUniforms[6] = effect.glowColor.g;
|
|
1177
|
+
caveInteriorUniforms[7] = effect.glowColor.b;
|
|
1178
|
+
caveInteriorUniforms[8] = effect.scrollOffsetY;
|
|
1179
|
+
caveInteriorUniforms[9] = effect.stalactiteCount;
|
|
1180
|
+
caveInteriorPaint.setShader(caveInteriorEffect.makeShader(caveInteriorUniforms));
|
|
1181
|
+
return caveInteriorPaint;
|
|
1182
|
+
}
|
|
1183
|
+
function applyMountainHills(effect) {
|
|
1184
|
+
if (Skia.RuntimeEffect === void 0) return defaultPaint;
|
|
1185
|
+
if (mountainHillsEffect === null) {
|
|
1186
|
+
const compiled = Skia.RuntimeEffect.Make(MOUNTAIN_HILLS_SKSL);
|
|
1187
|
+
if (compiled === null) return defaultPaint;
|
|
1188
|
+
mountainHillsEffect = compiled;
|
|
1189
|
+
}
|
|
1190
|
+
if (mountainHillsPaint === null) mountainHillsPaint = Skia.Paint();
|
|
1191
|
+
mountainHillsUniforms[0] = effect.resolutionW;
|
|
1192
|
+
mountainHillsUniforms[1] = effect.resolutionH;
|
|
1193
|
+
mountainHillsUniforms[2] = effect.skyColor.r;
|
|
1194
|
+
mountainHillsUniforms[3] = effect.skyColor.g;
|
|
1195
|
+
mountainHillsUniforms[4] = effect.skyColor.b;
|
|
1196
|
+
mountainHillsUniforms[5] = effect.hillColor.r;
|
|
1197
|
+
mountainHillsUniforms[6] = effect.hillColor.g;
|
|
1198
|
+
mountainHillsUniforms[7] = effect.hillColor.b;
|
|
1199
|
+
mountainHillsUniforms[8] = effect.scrollOffsetY;
|
|
1200
|
+
mountainHillsUniforms[9] = effect.ridgeScale;
|
|
1201
|
+
mountainHillsPaint.setShader(mountainHillsEffect.makeShader(mountainHillsUniforms));
|
|
1202
|
+
return mountainHillsPaint;
|
|
1203
|
+
}
|
|
1204
|
+
function drawOutlined(cvs, image, commands, n, effect, alpha) {
|
|
1205
|
+
const paint = applyOutlinePaint(effect);
|
|
1206
|
+
paint.setAlphaf(alpha);
|
|
1207
|
+
const srcs = getCachedSrcSlice(n);
|
|
1208
|
+
for (let k = 0; k < OUTLINE_OFFSETS.length; k++) {
|
|
1209
|
+
const offset = OUTLINE_OFFSETS[k];
|
|
1210
|
+
fillXforms(commands, n, offset[0] * effect.width, offset[1] * effect.width);
|
|
1211
|
+
const dsts = getCachedXformSlice(n);
|
|
1212
|
+
cvs.drawAtlas(image, srcs, dsts, paint);
|
|
1213
|
+
}
|
|
1214
|
+
paint.setAlphaf(1);
|
|
1215
|
+
defaultPaint.setAlphaf(alpha);
|
|
1216
|
+
fillXforms(commands, n, 0, 0);
|
|
1217
|
+
const mainDsts = getCachedXformSlice(n);
|
|
1218
|
+
cvs.drawAtlas(image, srcs, mainDsts, defaultPaint);
|
|
1219
|
+
defaultPaint.setAlphaf(1);
|
|
1220
|
+
}
|
|
1221
|
+
function resolveEffectPaint(effect) {
|
|
1222
|
+
switch (effect.type) {
|
|
1223
|
+
case "tint": return applyTint(effect);
|
|
1224
|
+
case "flash": return applyFlash(effect);
|
|
1225
|
+
case "grayscale": return applyGrayscale(effect);
|
|
1226
|
+
case "custom": return applyCustom(effect);
|
|
1227
|
+
case "water": return applyWater(effect, currentFrameTime);
|
|
1228
|
+
case "fog": return applyFog(effect, currentFrameTime);
|
|
1229
|
+
case "fire": return applyFire(effect, currentFrameTime);
|
|
1230
|
+
case "drop-shadow": return applyDropShadow(effect);
|
|
1231
|
+
case "outline": return applyOutlinePaint(effect);
|
|
1232
|
+
case "roofs-city": return applyRoofsCity(effect);
|
|
1233
|
+
case "forest-canopy": return applyForestCanopy(effect);
|
|
1234
|
+
case "river-wave": return applyRiverWave(effect);
|
|
1235
|
+
case "moon-crater": return applyMoonCrater(effect);
|
|
1236
|
+
case "cave-interior": return applyCaveInterior(effect);
|
|
1237
|
+
case "mountain-hills": return applyMountainHills(effect);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
canvas,
|
|
1242
|
+
beginFrame(viewportWidth, viewportHeight, time = 0, pixelDensity = 1) {
|
|
1243
|
+
if (viewportWidth === 0 || viewportHeight === 0) return;
|
|
1244
|
+
_boundRuntime = null;
|
|
1245
|
+
_boundChain = null;
|
|
1246
|
+
currentFrameTime = time;
|
|
1247
|
+
const pd = pixelDensity;
|
|
1248
|
+
recordingBounds.setXYWH(0, 0, viewportWidth * pd, viewportHeight * pd);
|
|
1249
|
+
current = recorder.beginRecording(recordingBounds);
|
|
1250
|
+
if (pd !== 1) current.scale(pd, pd);
|
|
1251
|
+
},
|
|
1252
|
+
drawAtlas(image, commands) {
|
|
1253
|
+
if (current === null) return;
|
|
1254
|
+
const n = commands.length;
|
|
1255
|
+
if (n === 0) return;
|
|
1256
|
+
const effect = commands[0]?.effect;
|
|
1257
|
+
const alpha = commands[0]?.alpha ?? 1;
|
|
1258
|
+
currentSpriteW = commands[0]?.dstW ?? 0;
|
|
1259
|
+
currentSpriteH = commands[0]?.dstH ?? 0;
|
|
1260
|
+
if (effect?.type === "roofs-city" || effect?.type === "forest-canopy" || effect?.type === "river-wave" || effect?.type === "moon-crater" || effect?.type === "cave-interior" || effect?.type === "mountain-hills") {
|
|
1261
|
+
const paint = resolveEffectPaint(effect);
|
|
1262
|
+
paint.setAlphaf(alpha);
|
|
1263
|
+
ensureZoneBgPoolCapacity(n);
|
|
1264
|
+
for (let i = 0; i < n; i++) {
|
|
1265
|
+
const cmd = commands[i];
|
|
1266
|
+
zoneBgRects[i].setXYWH(cmd.dstX, cmd.dstY, cmd.dstW, cmd.dstH);
|
|
1267
|
+
current.drawRect(zoneBgRects[i], paint);
|
|
1268
|
+
}
|
|
1269
|
+
paint.setAlphaf(1);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
if (commands[0]?.stretch) {
|
|
1273
|
+
ensureStretchPoolCapacity(n);
|
|
1274
|
+
if (stretchPaint === null) stretchPaint = Skia.Paint();
|
|
1275
|
+
const sp = stretchPaint;
|
|
1276
|
+
const sPaint = effect === void 0 || effect.type === "outline" ? sp : resolveEffectPaint(effect);
|
|
1277
|
+
sPaint.setAlphaf(alpha);
|
|
1278
|
+
for (let i = 0; i < n; i++) {
|
|
1279
|
+
const cmd = commands[i];
|
|
1280
|
+
const srcR = stretchSrcRects[i];
|
|
1281
|
+
const dstR = stretchDstRects[i];
|
|
1282
|
+
srcR.setXYWH(cmd.srcX, cmd.srcY, cmd.srcW, cmd.srcH);
|
|
1283
|
+
dstR.setXYWH(cmd.dstX, cmd.dstY, cmd.dstW, cmd.dstH);
|
|
1284
|
+
current.drawImageRect(image, srcR, dstR, sPaint);
|
|
1285
|
+
}
|
|
1286
|
+
sPaint.setAlphaf(1);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
ensurePoolCapacity(n);
|
|
1290
|
+
fillSrcRects(commands, n);
|
|
1291
|
+
if (effect?.type === "outline") {
|
|
1292
|
+
drawOutlined(current, image, commands, n, effect, alpha);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
fillXforms(commands, n, 0, 0);
|
|
1296
|
+
const srcs = getCachedSrcSlice(n);
|
|
1297
|
+
const dsts = getCachedXformSlice(n);
|
|
1298
|
+
const paint = effect === void 0 ? defaultPaint : resolveEffectPaint(effect);
|
|
1299
|
+
paint.setAlphaf(alpha);
|
|
1300
|
+
current.drawAtlas(image, srcs, dsts, paint);
|
|
1301
|
+
paint.setAlphaf(1);
|
|
1302
|
+
},
|
|
1303
|
+
setPostFxBinding(runtime, chain) {
|
|
1304
|
+
_boundRuntime = runtime;
|
|
1305
|
+
_boundChain = chain;
|
|
1306
|
+
},
|
|
1307
|
+
hasPostFxBinding() {
|
|
1308
|
+
return _boundRuntime !== null && !!_boundRuntime.supported && _boundRuntime.buildPass !== void 0 && _boundChain !== null;
|
|
1309
|
+
},
|
|
1310
|
+
buildPostFxPasses(runtime, chain, width, height) {
|
|
1311
|
+
const rt = runtime ?? _boundRuntime;
|
|
1312
|
+
const ch = chain ?? _boundChain;
|
|
1313
|
+
if (rt === null || !rt.supported) return [];
|
|
1314
|
+
if (rt.buildPass === void 0) {
|
|
1315
|
+
logOnce("buildPostFxPasses: runtime.supported but buildPass absent — no passes");
|
|
1316
|
+
return [];
|
|
1317
|
+
}
|
|
1318
|
+
if (ch === null) return [];
|
|
1319
|
+
const passes = rt.buildPass(ch, width, height);
|
|
1320
|
+
if (passes.length === 0) return [];
|
|
1321
|
+
const out = [];
|
|
1322
|
+
for (const pass of passes) {
|
|
1323
|
+
const shader = pass.shader;
|
|
1324
|
+
const uniformCount = typeof shader.getUniformCount === "function" ? shader.getUniformCount() : 0;
|
|
1325
|
+
const uniforms = {};
|
|
1326
|
+
for (let i = 0; i < uniformCount; i++) {
|
|
1327
|
+
const name = typeof shader.getUniformName === "function" ? shader.getUniformName(i) : String(i);
|
|
1328
|
+
const info = typeof shader.getUniform === "function" ? shader.getUniform(i) : {
|
|
1329
|
+
columns: 1,
|
|
1330
|
+
rows: 1,
|
|
1331
|
+
slot: i
|
|
1332
|
+
};
|
|
1333
|
+
const size = info.columns * info.rows;
|
|
1334
|
+
if (size === 1) uniforms[name] = pass.uniforms[info.slot] ?? 0;
|
|
1335
|
+
else {
|
|
1336
|
+
const vec = [];
|
|
1337
|
+
for (let k = 0; k < size; k++) vec.push(pass.uniforms[info.slot + k] ?? 0);
|
|
1338
|
+
uniforms[name] = vec;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
out.push({
|
|
1342
|
+
id: pass.id,
|
|
1343
|
+
source: shader,
|
|
1344
|
+
uniforms,
|
|
1345
|
+
children: pass.children
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
return out;
|
|
1349
|
+
},
|
|
1350
|
+
endFrame() {
|
|
1351
|
+
if (current === null) return null;
|
|
1352
|
+
current = null;
|
|
1353
|
+
return recorder.finishRecordingAsPicture();
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
/** Zero both branch counters unconditionally (no __DEV__ guard). */
|
|
1358
|
+
function resetXformCounters() {
|
|
1359
|
+
_xformUnrotatedCount = 0;
|
|
1360
|
+
_xformRotatedCount = 0;
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Cumulative `fillXforms` branch hit counts since the last reset. Both stay 0
|
|
1364
|
+
* when `__DEV__` is falsy or absent.
|
|
1365
|
+
*
|
|
1366
|
+
* These count transform WRITES, not sprites: a run drawn with an outline effect
|
|
1367
|
+
* passes through `fillXforms` 9 times (8 offset passes + 1 main pass), so it
|
|
1368
|
+
* contributes 9 hits per command — the transform work the renderer actually
|
|
1369
|
+
* performs. The counters are process-global: every `createFrameRenderer`
|
|
1370
|
+
* instance increments the same pair, so readings sum across live renderers.
|
|
1371
|
+
*
|
|
1372
|
+
* Allocates one object per call — never call it from the render loop.
|
|
1373
|
+
*/
|
|
1374
|
+
function getXformCounters() {
|
|
1375
|
+
return {
|
|
1376
|
+
rotated: _xformRotatedCount,
|
|
1377
|
+
unrotated: _xformUnrotatedCount
|
|
1378
|
+
};
|
|
1498
1379
|
}
|
|
1499
1380
|
function writeColor(out, color, alphaMul) {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1381
|
+
out[0] = color.r;
|
|
1382
|
+
out[1] = color.g;
|
|
1383
|
+
out[2] = color.b;
|
|
1384
|
+
out[3] = color.a * alphaMul;
|
|
1504
1385
|
}
|
|
1505
1386
|
function writeGrayscaleMatrix(out, amount) {
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1387
|
+
const inv = 1 - amount;
|
|
1388
|
+
const lr = .299 * amount;
|
|
1389
|
+
const lg = .587 * amount;
|
|
1390
|
+
const lb = .114 * amount;
|
|
1391
|
+
out[0] = inv + lr;
|
|
1392
|
+
out[1] = lg;
|
|
1393
|
+
out[2] = lb;
|
|
1394
|
+
out[3] = 0;
|
|
1395
|
+
out[4] = 0;
|
|
1396
|
+
out[5] = lr;
|
|
1397
|
+
out[6] = inv + lg;
|
|
1398
|
+
out[7] = lb;
|
|
1399
|
+
out[8] = 0;
|
|
1400
|
+
out[9] = 0;
|
|
1401
|
+
out[10] = lr;
|
|
1402
|
+
out[11] = lg;
|
|
1403
|
+
out[12] = inv + lb;
|
|
1404
|
+
out[13] = 0;
|
|
1405
|
+
out[14] = 0;
|
|
1406
|
+
out[15] = 0;
|
|
1407
|
+
out[16] = 0;
|
|
1408
|
+
out[17] = 0;
|
|
1409
|
+
out[18] = 1;
|
|
1410
|
+
out[19] = 0;
|
|
1530
1411
|
}
|
|
1531
1412
|
function fillCustomUniforms(effect, shader, out) {
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1413
|
+
const count = shader.getUniformCount();
|
|
1414
|
+
for (let i = 0; i < out.length; i++) out[i] = 0;
|
|
1415
|
+
for (let i = 0; i < count; i++) {
|
|
1416
|
+
const info = shader.getUniform(i);
|
|
1417
|
+
const name = shader.getUniformName(i);
|
|
1418
|
+
const value = effect.uniforms[name];
|
|
1419
|
+
const size = info.columns * info.rows;
|
|
1420
|
+
if (value === void 0) continue;
|
|
1421
|
+
if (typeof value === "number") {
|
|
1422
|
+
out[info.slot] = value;
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
for (let k = 0; k < size; k++) out[info.slot + k] = value[k] ?? 0;
|
|
1426
|
+
}
|
|
1548
1427
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1428
|
+
//#endregion
|
|
1429
|
+
//#region src/render-system.ts
|
|
1430
|
+
/**
|
|
1431
|
+
* Create a render system function.
|
|
1432
|
+
*
|
|
1433
|
+
* This is not a defineSystem() wrapper because it needs extra parameters
|
|
1434
|
+
* (SpriteBatch) beyond what the ECS system interface provides. Instead,
|
|
1435
|
+
* it returns a function to call in your game loop.
|
|
1436
|
+
*
|
|
1437
|
+
* The returned function only **queues** sprite draws into `batch` — it
|
|
1438
|
+
* does not call `batch.flush()`. Flushing is owned by the frame owner
|
|
1439
|
+
* (e.g. `<GameView>`) so that all draw sources (ECS, particles, custom)
|
|
1440
|
+
* can contribute to a single flush at the render phase.
|
|
1441
|
+
*
|
|
1442
|
+
* @param world The ECS world.
|
|
1443
|
+
* @param query Query that matches entities with Position + Sprite.
|
|
1444
|
+
* @param positionDef ComponentDef for Position.
|
|
1445
|
+
* @param spriteDef ComponentDef for Sprite.
|
|
1446
|
+
* @param batch SpriteBatch instance to queue draws into.
|
|
1447
|
+
*/
|
|
1448
|
+
function createRenderSystem(world, query, positionDef, spriteDef, batch) {
|
|
1449
|
+
const entries = [];
|
|
1450
|
+
return () => {
|
|
1451
|
+
let i = 0;
|
|
1452
|
+
for (const entityId of query) {
|
|
1453
|
+
const sprite = world.get(entityId, spriteDef);
|
|
1454
|
+
if (!sprite) continue;
|
|
1455
|
+
if (i >= entries.length) entries.push({
|
|
1456
|
+
entityId,
|
|
1457
|
+
z: 0
|
|
1458
|
+
});
|
|
1459
|
+
const entry = entries[i];
|
|
1460
|
+
entry.entityId = entityId;
|
|
1461
|
+
entry.z = sprite.z;
|
|
1462
|
+
i++;
|
|
1463
|
+
}
|
|
1464
|
+
const count = i;
|
|
1465
|
+
if (entries.length > count * 2 && entries.length > 64) entries.length = count;
|
|
1466
|
+
sortByDrawOrder(entries, count);
|
|
1467
|
+
for (let j = 0; j < count; j++) {
|
|
1468
|
+
const entry = entries[j];
|
|
1469
|
+
const pos = world.get(entry.entityId, positionDef);
|
|
1470
|
+
const sprite = world.get(entry.entityId, spriteDef);
|
|
1471
|
+
if (!pos || !sprite) continue;
|
|
1472
|
+
const anchor = sprite.anchor;
|
|
1473
|
+
batch.draw(sprite.image, sprite.frame, pos.x, pos.y, sprite.width, sprite.height, sprite.effect, anchor ? anchor.x : 0, anchor ? anchor.y : 0, sprite.alpha ?? 1, sprite.rotation ?? 0, sprite.stretch ?? false);
|
|
1474
|
+
}
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
/** Canonical Sprite component — queried by the render system alongside `Position`. */
|
|
1478
|
+
const Sprite = defineComponent("Sprite", () => ({
|
|
1479
|
+
image: null,
|
|
1480
|
+
frame: {
|
|
1481
|
+
name: "",
|
|
1482
|
+
x: 0,
|
|
1483
|
+
y: 0,
|
|
1484
|
+
w: 0,
|
|
1485
|
+
h: 0
|
|
1486
|
+
},
|
|
1487
|
+
z: 0
|
|
1488
|
+
}));
|
|
1489
|
+
//#endregion
|
|
1490
|
+
//#region src/renderer.ts
|
|
1491
|
+
/**
|
|
1492
|
+
* Renderer — manages the Skia Canvas lifecycle for a frame.
|
|
1493
|
+
* `begin()` to start, draw your content, `end()` to finish.
|
|
1494
|
+
*/
|
|
1495
|
+
var Renderer = class {
|
|
1496
|
+
/** Current frame stats. */
|
|
1497
|
+
stats = {
|
|
1498
|
+
drawCalls: 0,
|
|
1499
|
+
batchCount: 0
|
|
1500
|
+
};
|
|
1501
|
+
_canvas = null;
|
|
1502
|
+
_inFrame = false;
|
|
1503
|
+
/** Set the canvas to render to. Called once when canvas is available. */
|
|
1504
|
+
setCanvas(canvas) {
|
|
1505
|
+
this._canvas = canvas;
|
|
1506
|
+
}
|
|
1507
|
+
/** Get the current canvas. Throws if not in a frame. */
|
|
1508
|
+
get canvas() {
|
|
1509
|
+
if (!this._canvas) throw new Error("Renderer: no canvas set");
|
|
1510
|
+
return this._canvas;
|
|
1511
|
+
}
|
|
1512
|
+
/** Whether we are currently between begin() and end(). */
|
|
1513
|
+
get inFrame() {
|
|
1514
|
+
return this._inFrame;
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Begin a new frame. Clears the canvas and resets stats.
|
|
1518
|
+
* @param clearColor 32-bit ARGB color to clear with (default black = 0xFF000000).
|
|
1519
|
+
*/
|
|
1520
|
+
begin(clearColor = 4278190080) {
|
|
1521
|
+
if (this._inFrame) throw new Error("Renderer: begin() called while already in frame");
|
|
1522
|
+
this._inFrame = true;
|
|
1523
|
+
this.stats.drawCalls = 0;
|
|
1524
|
+
this.stats.batchCount = 0;
|
|
1525
|
+
this.canvas.save();
|
|
1526
|
+
this.canvas.clear(clearColor);
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* End the current frame. Restores canvas state.
|
|
1530
|
+
*/
|
|
1531
|
+
end() {
|
|
1532
|
+
if (!this._inFrame) throw new Error("Renderer: end() called without begin()");
|
|
1533
|
+
this.canvas.restore();
|
|
1534
|
+
this._inFrame = false;
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Apply camera transform to the canvas.
|
|
1538
|
+
* Call after begin() and before drawing.
|
|
1539
|
+
*/
|
|
1540
|
+
applyCamera(cameraX, cameraY, zoom, rotation, viewportWidth, viewportHeight) {
|
|
1541
|
+
const c = this.canvas;
|
|
1542
|
+
c.translate(viewportWidth / 2, viewportHeight / 2);
|
|
1543
|
+
c.scale(zoom, zoom);
|
|
1544
|
+
if (rotation !== 0) c.rotate(-rotation * 180 / Math.PI, 0, 0);
|
|
1545
|
+
c.translate(-cameraX, -cameraY);
|
|
1546
|
+
}
|
|
1547
|
+
/** Increment draw call counter. Invoked in tandem with recordBatch, once per
|
|
1548
|
+
* flush run in SpriteBatch.flush. */
|
|
1549
|
+
recordDrawCall() {
|
|
1550
|
+
this.stats.drawCalls++;
|
|
1551
|
+
}
|
|
1552
|
+
/** Increment batch counter. Invoked in tandem with recordDrawCall, once per
|
|
1553
|
+
* flush run in SpriteBatch.flush. */
|
|
1554
|
+
recordBatch() {
|
|
1555
|
+
this.stats.batchCount++;
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
//#endregion
|
|
1559
|
+
//#region src/sprite-batch.ts
|
|
1560
|
+
/**
|
|
1561
|
+
* SpriteBatch — collects sprite draw commands and groups them by texture
|
|
1562
|
+
* (and optionally by effect) for efficient batched rendering.
|
|
1563
|
+
*
|
|
1564
|
+
* Usage:
|
|
1565
|
+
* 1. Call `draw()` for each sprite, optionally passing an effect
|
|
1566
|
+
* 2. Call `flush()` with an atlas draw callback to execute all batched draws
|
|
1567
|
+
* 3. Batching groups consecutive commands by `(image, effect, alpha, stretch)`
|
|
1568
|
+
* identity, preserving global insertion (z-sorted) order. Sharing an effect
|
|
1569
|
+
* reference and the same alpha across consecutive entities maximizes batch
|
|
1570
|
+
* size while keeping sprites in correct draw order.
|
|
1571
|
+
*
|
|
1572
|
+
* The actual Skia Atlas draw call is delegated to a callback so this module
|
|
1573
|
+
* remains testable without Skia.
|
|
1574
|
+
*/
|
|
1575
|
+
var SpriteBatch = class {
|
|
1576
|
+
_commands = [];
|
|
1577
|
+
_pool = [];
|
|
1578
|
+
_batches = [];
|
|
1579
|
+
_allocCount = 0;
|
|
1580
|
+
/** Number of queued draw commands. */
|
|
1581
|
+
get count() {
|
|
1582
|
+
return this._commands.length;
|
|
1583
|
+
}
|
|
1584
|
+
/** Cumulative number of SpriteDrawCommand objects allocated because the internal
|
|
1585
|
+
* command pool was empty. Steady state should be 0 after warm-up. */
|
|
1586
|
+
get allocCount() {
|
|
1587
|
+
return this._allocCount;
|
|
1588
|
+
}
|
|
1589
|
+
/** Queue a sprite draw from a frame.
|
|
1590
|
+
*
|
|
1591
|
+
* `anchorX` / `anchorY` define the pivot point of the sprite as a
|
|
1592
|
+
* fraction of its destination size — `(0, 0)` = top-left (default),
|
|
1593
|
+
* `(0.5, 0.5)` = center, `(1, 1)` = bottom-right. `dstX` / `dstY`
|
|
1594
|
+
* then refer to the anchor point instead of the top-left corner.
|
|
1595
|
+
*
|
|
1596
|
+
* `alpha` is a per-instance opacity multiplier in [0, 1]. Default 1. */
|
|
1597
|
+
draw(image, frame, dstX, dstY, dstW, dstH, effect, anchorX = 0, anchorY = 0, alpha = 1, rotation = 0, stretch = false) {
|
|
1598
|
+
const cmd = this._acquire();
|
|
1599
|
+
const w = dstW ?? frame.w;
|
|
1600
|
+
const h = dstH ?? frame.h;
|
|
1601
|
+
cmd.image = image;
|
|
1602
|
+
cmd.srcX = frame.x;
|
|
1603
|
+
cmd.srcY = frame.y;
|
|
1604
|
+
cmd.srcW = frame.w;
|
|
1605
|
+
cmd.srcH = frame.h;
|
|
1606
|
+
cmd.dstX = dstX - anchorX * w;
|
|
1607
|
+
cmd.dstY = dstY - anchorY * h;
|
|
1608
|
+
cmd.dstW = w;
|
|
1609
|
+
cmd.dstH = h;
|
|
1610
|
+
cmd.effect = effect;
|
|
1611
|
+
cmd.alpha = alpha;
|
|
1612
|
+
cmd.rotation = rotation;
|
|
1613
|
+
cmd.anchorX = anchorX;
|
|
1614
|
+
cmd.anchorY = anchorY;
|
|
1615
|
+
cmd.stretch = stretch;
|
|
1616
|
+
this._commands.push(cmd);
|
|
1617
|
+
}
|
|
1618
|
+
/** Queue a raw sprite draw with explicit src/dst rects. `dstX` / `dstY`
|
|
1619
|
+
* refer to the anchor point — offset by `anchorX * dstW` / `anchorY * dstH`.
|
|
1620
|
+
*
|
|
1621
|
+
* `alpha` is a per-instance opacity multiplier in [0, 1]. Default 1. */
|
|
1622
|
+
drawRaw(image, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH, effect, anchorX = 0, anchorY = 0, alpha = 1, rotation = 0, stretch = false) {
|
|
1623
|
+
const cmd = this._acquire();
|
|
1624
|
+
cmd.image = image;
|
|
1625
|
+
cmd.srcX = srcX;
|
|
1626
|
+
cmd.srcY = srcY;
|
|
1627
|
+
cmd.srcW = srcW;
|
|
1628
|
+
cmd.srcH = srcH;
|
|
1629
|
+
cmd.dstX = dstX - anchorX * dstW;
|
|
1630
|
+
cmd.dstY = dstY - anchorY * dstH;
|
|
1631
|
+
cmd.dstW = dstW;
|
|
1632
|
+
cmd.dstH = dstH;
|
|
1633
|
+
cmd.effect = effect;
|
|
1634
|
+
cmd.alpha = alpha;
|
|
1635
|
+
cmd.rotation = rotation;
|
|
1636
|
+
cmd.anchorX = anchorX;
|
|
1637
|
+
cmd.anchorY = anchorY;
|
|
1638
|
+
cmd.stretch = stretch;
|
|
1639
|
+
this._commands.push(cmd);
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Flush all queued commands in insertion order, grouping maximal consecutive
|
|
1643
|
+
* runs that share the same `(image, effect, alpha, stretch)` key into a
|
|
1644
|
+
* single `drawAtlas` call. This preserves global z-order (the caller is
|
|
1645
|
+
* responsible for queuing in z-sorted order) while still batching runs of
|
|
1646
|
+
* same-key sprites for efficiency.
|
|
1647
|
+
*
|
|
1648
|
+
* @param drawAtlas Callback invoked once per run. Receives
|
|
1649
|
+
* `(image, commands)` — all commands in a run share the same key and are
|
|
1650
|
+
* contiguous in the original insertion order.
|
|
1651
|
+
* @param renderer Optional renderer to record stats.
|
|
1652
|
+
*/
|
|
1653
|
+
flush(drawAtlas, renderer) {
|
|
1654
|
+
if (this._commands.length === 0) return;
|
|
1655
|
+
let run = this._batches[0];
|
|
1656
|
+
if (run === void 0) {
|
|
1657
|
+
run = {
|
|
1658
|
+
image: null,
|
|
1659
|
+
effect: void 0,
|
|
1660
|
+
alpha: 1,
|
|
1661
|
+
stretch: false,
|
|
1662
|
+
commands: []
|
|
1663
|
+
};
|
|
1664
|
+
this._batches[0] = run;
|
|
1665
|
+
}
|
|
1666
|
+
for (const cmd of this._commands) {
|
|
1667
|
+
if (run.commands.length > 0 && (cmd.image !== run.image || cmd.effect !== run.effect || cmd.alpha !== run.alpha || cmd.stretch !== run.stretch)) {
|
|
1668
|
+
drawAtlas(run.image, run.commands);
|
|
1669
|
+
if (renderer) {
|
|
1670
|
+
renderer.recordDrawCall();
|
|
1671
|
+
renderer.recordBatch();
|
|
1672
|
+
}
|
|
1673
|
+
run.commands.length = 0;
|
|
1674
|
+
}
|
|
1675
|
+
run.image = cmd.image;
|
|
1676
|
+
run.effect = cmd.effect;
|
|
1677
|
+
run.alpha = cmd.alpha;
|
|
1678
|
+
run.stretch = cmd.stretch;
|
|
1679
|
+
run.commands.push(cmd);
|
|
1680
|
+
}
|
|
1681
|
+
if (run.commands.length > 0) {
|
|
1682
|
+
drawAtlas(run.image, run.commands);
|
|
1683
|
+
if (renderer) {
|
|
1684
|
+
renderer.recordDrawCall();
|
|
1685
|
+
renderer.recordBatch();
|
|
1686
|
+
}
|
|
1687
|
+
run.commands.length = 0;
|
|
1688
|
+
}
|
|
1689
|
+
for (const cmd of this._commands) this._pool.push(cmd);
|
|
1690
|
+
this._commands.length = 0;
|
|
1691
|
+
}
|
|
1692
|
+
/** Clear all queued commands without drawing. */
|
|
1693
|
+
clear() {
|
|
1694
|
+
for (const cmd of this._commands) this._pool.push(cmd);
|
|
1695
|
+
this._commands.length = 0;
|
|
1696
|
+
}
|
|
1697
|
+
_acquire() {
|
|
1698
|
+
if (this._pool.length > 0) return this._pool.pop();
|
|
1699
|
+
this._allocCount++;
|
|
1700
|
+
return {
|
|
1701
|
+
image: null,
|
|
1702
|
+
srcX: 0,
|
|
1703
|
+
srcY: 0,
|
|
1704
|
+
srcW: 0,
|
|
1705
|
+
srcH: 0,
|
|
1706
|
+
dstX: 0,
|
|
1707
|
+
dstY: 0,
|
|
1708
|
+
dstW: 0,
|
|
1709
|
+
dstH: 0,
|
|
1710
|
+
effect: void 0,
|
|
1711
|
+
alpha: 1,
|
|
1712
|
+
rotation: 0,
|
|
1713
|
+
anchorX: 0,
|
|
1714
|
+
anchorY: 0,
|
|
1715
|
+
stretch: false
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
//#endregion
|
|
1720
|
+
//#region src/sprite-font.ts
|
|
1721
|
+
/**
|
|
1722
|
+
* Parse BMFont JSON format (common output from tools like Hiero, BMFont, msdf-bmfont).
|
|
1723
|
+
*/
|
|
1551
1724
|
function parseBMFontJSON(json) {
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
return {
|
|
1573
|
-
lineHeight: data.common.lineHeight,
|
|
1574
|
-
base: data.common.base,
|
|
1575
|
-
glyphs,
|
|
1576
|
-
kernings
|
|
1577
|
-
};
|
|
1725
|
+
const data = json;
|
|
1726
|
+
const glyphs = /* @__PURE__ */ new Map();
|
|
1727
|
+
for (const c of data.chars) glyphs.set(c.id, {
|
|
1728
|
+
id: c.id,
|
|
1729
|
+
x: c.x,
|
|
1730
|
+
y: c.y,
|
|
1731
|
+
width: c.width,
|
|
1732
|
+
height: c.height,
|
|
1733
|
+
xOffset: c.xoffset,
|
|
1734
|
+
yOffset: c.yoffset,
|
|
1735
|
+
xAdvance: c.xadvance
|
|
1736
|
+
});
|
|
1737
|
+
const kernings = /* @__PURE__ */ new Map();
|
|
1738
|
+
if (data.kernings) for (const k of data.kernings) kernings.set(k.first << 16 | k.second, k.amount);
|
|
1739
|
+
return {
|
|
1740
|
+
lineHeight: data.common.lineHeight,
|
|
1741
|
+
base: data.common.base,
|
|
1742
|
+
glyphs,
|
|
1743
|
+
kernings
|
|
1744
|
+
};
|
|
1578
1745
|
}
|
|
1746
|
+
/** Get kerning between two characters. */
|
|
1579
1747
|
function getKerning(font, first, second) {
|
|
1580
|
-
|
|
1748
|
+
return font.kernings.get(first << 16 | second) ?? 0;
|
|
1581
1749
|
}
|
|
1750
|
+
/** Measure the width of a text string in pixels. */
|
|
1582
1751
|
function measureText(font, text) {
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
}
|
|
1595
|
-
return width;
|
|
1752
|
+
let width = 0;
|
|
1753
|
+
let prevCode = -1;
|
|
1754
|
+
for (let i = 0; i < text.length; i++) {
|
|
1755
|
+
const code = text.charCodeAt(i);
|
|
1756
|
+
const glyph = font.glyphs.get(code);
|
|
1757
|
+
if (!glyph) continue;
|
|
1758
|
+
if (prevCode >= 0) width += getKerning(font, prevCode, code);
|
|
1759
|
+
width += glyph.xAdvance;
|
|
1760
|
+
prevCode = code;
|
|
1761
|
+
}
|
|
1762
|
+
return width;
|
|
1596
1763
|
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Draw text using a SpriteBatch.
|
|
1766
|
+
* Characters are batched as individual sprites from the font texture.
|
|
1767
|
+
*/
|
|
1597
1768
|
function drawText(batch, font, image, text, x, y, scale = 1) {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
glyph.x,
|
|
1610
|
-
glyph.y,
|
|
1611
|
-
glyph.width,
|
|
1612
|
-
glyph.height,
|
|
1613
|
-
cursorX + glyph.xOffset * scale,
|
|
1614
|
-
y + glyph.yOffset * scale,
|
|
1615
|
-
glyph.width * scale,
|
|
1616
|
-
glyph.height * scale
|
|
1617
|
-
);
|
|
1618
|
-
cursorX += glyph.xAdvance * scale;
|
|
1619
|
-
prevCode = code;
|
|
1620
|
-
}
|
|
1769
|
+
let cursorX = x;
|
|
1770
|
+
let prevCode = -1;
|
|
1771
|
+
for (let i = 0; i < text.length; i++) {
|
|
1772
|
+
const code = text.charCodeAt(i);
|
|
1773
|
+
const glyph = font.glyphs.get(code);
|
|
1774
|
+
if (!glyph) continue;
|
|
1775
|
+
if (prevCode >= 0) cursorX += getKerning(font, prevCode, code) * scale;
|
|
1776
|
+
batch.drawRaw(image, glyph.x, glyph.y, glyph.width, glyph.height, cursorX + glyph.xOffset * scale, y + glyph.yOffset * scale, glyph.width * scale, glyph.height * scale);
|
|
1777
|
+
cursorX += glyph.xAdvance * scale;
|
|
1778
|
+
prevCode = code;
|
|
1779
|
+
}
|
|
1621
1780
|
}
|
|
1781
|
+
//#endregion
|
|
1782
|
+
export { CAVE_INTERIOR_SKSL, Camera2D, FIRE_SKSL, FOG_SKSL, FOREST_CANOPY_SKSL, MOON_CRATER_SKSL, MOUNTAIN_HILLS_SKSL, RIVER_WAVE_SKSL, ROOFS_CITY_SKSL, Renderer, Sprite, SpriteBatch, WATER_SKSL, caveInterior, createFrameRenderer, createRenderSystem, custom, drawOrderCompare, drawText, dropShadow, fire, flash, flashIntensity, fog, forestCanopy, getKerning, getXformCounters, grayscale, measureText, moonCrater, mountainHills, outline, parseBMFontJSON, resetXformCounters, riverWave, roofsCity, sortByDrawOrder, tint, updateEffect, water };
|
|
1622
1783
|
|
|
1623
|
-
// src/render-system.ts
|
|
1624
|
-
import { defineComponent } from "@flare-engine/ecs";
|
|
1625
|
-
function createRenderSystem(world, query, positionDef, spriteDef, batch) {
|
|
1626
|
-
const entries = [];
|
|
1627
|
-
return () => {
|
|
1628
|
-
let i = 0;
|
|
1629
|
-
for (const entityId of query) {
|
|
1630
|
-
const sprite = world.get(entityId, spriteDef);
|
|
1631
|
-
if (!sprite) continue;
|
|
1632
|
-
if (i >= entries.length) {
|
|
1633
|
-
entries.push({ entityId, z: 0 });
|
|
1634
|
-
}
|
|
1635
|
-
const entry = entries[i];
|
|
1636
|
-
entry.entityId = entityId;
|
|
1637
|
-
entry.z = sprite.z;
|
|
1638
|
-
i++;
|
|
1639
|
-
}
|
|
1640
|
-
const count = i;
|
|
1641
|
-
if (entries.length > count * 2 && entries.length > 64) {
|
|
1642
|
-
entries.length = count;
|
|
1643
|
-
}
|
|
1644
|
-
sortByDrawOrder(entries, count);
|
|
1645
|
-
for (let j = 0; j < count; j++) {
|
|
1646
|
-
const entry = entries[j];
|
|
1647
|
-
const pos = world.get(entry.entityId, positionDef);
|
|
1648
|
-
const sprite = world.get(entry.entityId, spriteDef);
|
|
1649
|
-
if (!pos || !sprite) continue;
|
|
1650
|
-
const anchor = sprite.anchor;
|
|
1651
|
-
batch.draw(
|
|
1652
|
-
sprite.image,
|
|
1653
|
-
sprite.frame,
|
|
1654
|
-
pos.x,
|
|
1655
|
-
pos.y,
|
|
1656
|
-
sprite.width,
|
|
1657
|
-
sprite.height,
|
|
1658
|
-
sprite.effect,
|
|
1659
|
-
anchor ? anchor.x : 0,
|
|
1660
|
-
anchor ? anchor.y : 0,
|
|
1661
|
-
sprite.alpha ?? 1,
|
|
1662
|
-
sprite.rotation ?? 0,
|
|
1663
|
-
sprite.stretch ?? false
|
|
1664
|
-
);
|
|
1665
|
-
}
|
|
1666
|
-
};
|
|
1667
|
-
}
|
|
1668
|
-
var Sprite = defineComponent("Sprite", () => ({
|
|
1669
|
-
image: null,
|
|
1670
|
-
frame: { name: "", x: 0, y: 0, w: 0, h: 0 },
|
|
1671
|
-
z: 0
|
|
1672
|
-
}));
|
|
1673
|
-
export {
|
|
1674
|
-
CAVE_INTERIOR_SKSL,
|
|
1675
|
-
Camera2D,
|
|
1676
|
-
FIRE_SKSL,
|
|
1677
|
-
FOG_SKSL,
|
|
1678
|
-
FOREST_CANOPY_SKSL,
|
|
1679
|
-
MOON_CRATER_SKSL,
|
|
1680
|
-
MOUNTAIN_HILLS_SKSL,
|
|
1681
|
-
RIVER_WAVE_SKSL,
|
|
1682
|
-
ROOFS_CITY_SKSL,
|
|
1683
|
-
Renderer,
|
|
1684
|
-
Sprite,
|
|
1685
|
-
SpriteBatch,
|
|
1686
|
-
WATER_SKSL,
|
|
1687
|
-
caveInterior,
|
|
1688
|
-
createFrameRenderer,
|
|
1689
|
-
createRenderSystem,
|
|
1690
|
-
custom,
|
|
1691
|
-
drawOrderCompare,
|
|
1692
|
-
drawText,
|
|
1693
|
-
dropShadow,
|
|
1694
|
-
fire,
|
|
1695
|
-
flash,
|
|
1696
|
-
flashIntensity,
|
|
1697
|
-
fog,
|
|
1698
|
-
forestCanopy,
|
|
1699
|
-
getKerning,
|
|
1700
|
-
grayscale,
|
|
1701
|
-
measureText,
|
|
1702
|
-
moonCrater,
|
|
1703
|
-
mountainHills,
|
|
1704
|
-
outline,
|
|
1705
|
-
parseBMFontJSON,
|
|
1706
|
-
riverWave,
|
|
1707
|
-
roofsCity,
|
|
1708
|
-
sortByDrawOrder,
|
|
1709
|
-
tint,
|
|
1710
|
-
updateEffect,
|
|
1711
|
-
water
|
|
1712
|
-
};
|
|
1713
1784
|
//# sourceMappingURL=index.mjs.map
|