@flare-engine/render 0.2.0 → 0.3.0

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