@flare-engine/render 0.1.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/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/index.d.mts +612 -0
- package/dist/index.d.ts +612 -0
- package/dist/index.js +1154 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1092 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
// src/draw-order.ts
|
|
2
|
+
function sortByDrawOrder(items, length) {
|
|
3
|
+
const n = length ?? items.length;
|
|
4
|
+
for (let i = 1; i < n; i++) {
|
|
5
|
+
const item = items[i];
|
|
6
|
+
let j = i - 1;
|
|
7
|
+
while (j >= 0 && items[j].z > item.z) {
|
|
8
|
+
items[j + 1] = items[j];
|
|
9
|
+
j--;
|
|
10
|
+
}
|
|
11
|
+
items[j + 1] = item;
|
|
12
|
+
}
|
|
13
|
+
return items;
|
|
14
|
+
}
|
|
15
|
+
function drawOrderCompare(a, b) {
|
|
16
|
+
return a.z - b.z;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/camera2d.ts
|
|
20
|
+
function pseudoRandom(seed) {
|
|
21
|
+
const x = Math.sin(seed * 12.9898 + seed * 78.233) * 43758.5453;
|
|
22
|
+
return x - Math.floor(x);
|
|
23
|
+
}
|
|
24
|
+
var PERM = new Uint8Array(256);
|
|
25
|
+
{
|
|
26
|
+
for (let i = 0; i < 256; i++) PERM[i] = i;
|
|
27
|
+
let s = 2654435769;
|
|
28
|
+
for (let i = 255; i > 0; i--) {
|
|
29
|
+
s = (s ^ s >>> 15) >>> 0;
|
|
30
|
+
s = Math.imul(s, 2246822507) >>> 0;
|
|
31
|
+
s = (s ^ s >>> 13) >>> 0;
|
|
32
|
+
const j = s % (i + 1);
|
|
33
|
+
const t = PERM[i];
|
|
34
|
+
PERM[i] = PERM[j];
|
|
35
|
+
PERM[j] = t;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
var S = Math.SQRT1_2;
|
|
39
|
+
var GRADS = [
|
|
40
|
+
[1, 0],
|
|
41
|
+
[S, S],
|
|
42
|
+
[0, 1],
|
|
43
|
+
[-S, S],
|
|
44
|
+
[-1, 0],
|
|
45
|
+
[-S, -S],
|
|
46
|
+
[0, -1],
|
|
47
|
+
[S, -S]
|
|
48
|
+
];
|
|
49
|
+
function grad2(hash, x, y) {
|
|
50
|
+
const g = GRADS[hash & 7];
|
|
51
|
+
return g[0] * x + g[1] * y;
|
|
52
|
+
}
|
|
53
|
+
function fade(t) {
|
|
54
|
+
return t * t * t * (t * (t * 6 - 15) + 10);
|
|
55
|
+
}
|
|
56
|
+
function noise2d(x, y) {
|
|
57
|
+
const xi = Math.floor(x) & 255;
|
|
58
|
+
const yi = Math.floor(y) & 255;
|
|
59
|
+
const xf = x - Math.floor(x);
|
|
60
|
+
const yf = y - Math.floor(y);
|
|
61
|
+
const u = fade(xf);
|
|
62
|
+
const v = fade(yf);
|
|
63
|
+
const aa = PERM[PERM[xi] + yi];
|
|
64
|
+
const ab = PERM[PERM[xi] + yi + 1];
|
|
65
|
+
const ba = PERM[PERM[xi + 1 & 255] + yi];
|
|
66
|
+
const bb = PERM[PERM[xi + 1 & 255] + yi + 1];
|
|
67
|
+
const x1 = xf;
|
|
68
|
+
const y1 = yf;
|
|
69
|
+
const x2 = xf - 1;
|
|
70
|
+
const y2 = yf - 1;
|
|
71
|
+
const n00 = grad2(aa, x1, y1);
|
|
72
|
+
const n10 = grad2(ba, x2, y1);
|
|
73
|
+
const n01 = grad2(ab, x1, y2);
|
|
74
|
+
const n11 = grad2(bb, x2, y2);
|
|
75
|
+
const lerpX0 = n00 + u * (n10 - n00);
|
|
76
|
+
const lerpX1 = n01 + u * (n11 - n01);
|
|
77
|
+
return lerpX0 + v * (lerpX1 - lerpX0);
|
|
78
|
+
}
|
|
79
|
+
function fbmNoise2d(x, y) {
|
|
80
|
+
return 0.6667 * noise2d(x, y) + 0.3333 * noise2d(x * 2, y * 2);
|
|
81
|
+
}
|
|
82
|
+
var Camera2D = class {
|
|
83
|
+
/** Camera position (world-space center). */
|
|
84
|
+
x = 0;
|
|
85
|
+
y = 0;
|
|
86
|
+
/** Zoom level (1 = 100%). */
|
|
87
|
+
zoom = 1;
|
|
88
|
+
/** Rotation in radians. */
|
|
89
|
+
rotation = 0;
|
|
90
|
+
/** Viewport width in pixels. */
|
|
91
|
+
viewportWidth = 0;
|
|
92
|
+
/** Viewport height in pixels. */
|
|
93
|
+
viewportHeight = 0;
|
|
94
|
+
/** Optional world bounds to clamp camera position. */
|
|
95
|
+
bounds = null;
|
|
96
|
+
_follow = null;
|
|
97
|
+
_shake = null;
|
|
98
|
+
_shakePerlin = null;
|
|
99
|
+
_shakeSeed = 0;
|
|
100
|
+
_shakePerlinSeed = 0;
|
|
101
|
+
_motionScale = 1;
|
|
102
|
+
/** Set viewport dimensions. */
|
|
103
|
+
setViewport(width, height) {
|
|
104
|
+
this.viewportWidth = width;
|
|
105
|
+
this.viewportHeight = height;
|
|
106
|
+
}
|
|
107
|
+
/** Start following a target position. */
|
|
108
|
+
follow(target, options) {
|
|
109
|
+
this._follow = { target, options: options ?? {} };
|
|
110
|
+
}
|
|
111
|
+
/** Stop following. */
|
|
112
|
+
unfollow() {
|
|
113
|
+
this._follow = null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Global motion multiplier applied to subsequent `shake()` calls. Scales
|
|
117
|
+
* both amplitude and duration. Default 1 (no-op). 0 fully suppresses
|
|
118
|
+
* shake. Intended to be driven by `@flare-engine/a11y` reduced-motion so
|
|
119
|
+
* callers don't have to guard every shake site.
|
|
120
|
+
*
|
|
121
|
+
* Only affects future `shake()` calls — does not retroactively modify an
|
|
122
|
+
* in-flight shake.
|
|
123
|
+
*
|
|
124
|
+
* Clamped to `>= 0`.
|
|
125
|
+
*/
|
|
126
|
+
setMotionScale(scale) {
|
|
127
|
+
this._motionScale = scale < 0 ? 0 : scale;
|
|
128
|
+
}
|
|
129
|
+
/** Current motion scale (default 1). */
|
|
130
|
+
get motionScale() {
|
|
131
|
+
return this._motionScale;
|
|
132
|
+
}
|
|
133
|
+
/** Start camera shake. */
|
|
134
|
+
shake(intensity, duration) {
|
|
135
|
+
const k = this._motionScale;
|
|
136
|
+
if (k <= 0) {
|
|
137
|
+
this._shake = null;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this._shake = {
|
|
141
|
+
intensity: intensity * k,
|
|
142
|
+
duration: duration * k,
|
|
143
|
+
elapsed: 0,
|
|
144
|
+
offsetX: 0,
|
|
145
|
+
offsetY: 0
|
|
146
|
+
};
|
|
147
|
+
this._shakeSeed++;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Start a Perlin-noise-based camera shake. Overwrites any in-flight
|
|
151
|
+
* shakePerlin (same semantics as shake()). Respects motionScale.
|
|
152
|
+
*/
|
|
153
|
+
shakePerlin(intensity, duration) {
|
|
154
|
+
const k = this._motionScale;
|
|
155
|
+
if (k <= 0) {
|
|
156
|
+
this._shakePerlin = null;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this._shakePerlin = {
|
|
160
|
+
intensity: intensity * k,
|
|
161
|
+
duration: duration * k,
|
|
162
|
+
elapsed: 0,
|
|
163
|
+
offsetX: 0,
|
|
164
|
+
offsetY: 0
|
|
165
|
+
};
|
|
166
|
+
this._shakePerlinSeed++;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Update camera. Call once per frame.
|
|
170
|
+
* @param dt Delta time in seconds.
|
|
171
|
+
*/
|
|
172
|
+
update(dt) {
|
|
173
|
+
if (this._follow) {
|
|
174
|
+
const { target, options } = this._follow;
|
|
175
|
+
const smoothing = options.smoothing ?? 0.1;
|
|
176
|
+
const offsetX = options.offset?.x ?? 0;
|
|
177
|
+
const offsetY = options.offset?.y ?? 0;
|
|
178
|
+
const goalX = target.x + offsetX;
|
|
179
|
+
const goalY = target.y + offsetY;
|
|
180
|
+
if (options.deadZone) {
|
|
181
|
+
const dz = options.deadZone;
|
|
182
|
+
const relX = target.x - this.x;
|
|
183
|
+
const relY = target.y - this.y;
|
|
184
|
+
if (relX < dz.x) {
|
|
185
|
+
this.x += (goalX - this.x - (dz.x - relX)) * (1 - smoothing ** dt);
|
|
186
|
+
} else if (relX > dz.x + dz.width) {
|
|
187
|
+
this.x += (goalX - this.x + (relX - dz.x - dz.width)) * (1 - smoothing ** dt);
|
|
188
|
+
}
|
|
189
|
+
if (relY < dz.y) {
|
|
190
|
+
this.y += (goalY - this.y - (dz.y - relY)) * (1 - smoothing ** dt);
|
|
191
|
+
} else if (relY > dz.y + dz.height) {
|
|
192
|
+
this.y += (goalY - this.y + (relY - dz.y - dz.height)) * (1 - smoothing ** dt);
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
const factor = 1 - smoothing ** dt;
|
|
196
|
+
this.x += (goalX - this.x) * factor;
|
|
197
|
+
this.y += (goalY - this.y) * factor;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (this.bounds) {
|
|
201
|
+
this._clampToBounds();
|
|
202
|
+
}
|
|
203
|
+
if (this._shake) {
|
|
204
|
+
this._shake.elapsed += dt;
|
|
205
|
+
if (this._shake.elapsed >= this._shake.duration) {
|
|
206
|
+
this._shake = null;
|
|
207
|
+
} else {
|
|
208
|
+
const progress = this._shake.elapsed / this._shake.duration;
|
|
209
|
+
const decay = 1 - progress;
|
|
210
|
+
const seed = this._shakeSeed + this._shake.elapsed * 100;
|
|
211
|
+
this._shake.offsetX = (pseudoRandom(seed) * 2 - 1) * this._shake.intensity * decay;
|
|
212
|
+
this._shake.offsetY = (pseudoRandom(seed + 1) * 2 - 1) * this._shake.intensity * decay;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (this._shakePerlin) {
|
|
216
|
+
this._shakePerlin.elapsed += dt;
|
|
217
|
+
if (this._shakePerlin.elapsed >= this._shakePerlin.duration) {
|
|
218
|
+
this._shakePerlin = null;
|
|
219
|
+
} else {
|
|
220
|
+
const progress = this._shakePerlin.elapsed / this._shakePerlin.duration;
|
|
221
|
+
const decay = 1 - progress;
|
|
222
|
+
const t = this._shakePerlin.elapsed * 4.7 + this._shakePerlinSeed * 17.3;
|
|
223
|
+
this._shakePerlin.offsetX = fbmNoise2d(t, 0) * this._shakePerlin.intensity * decay;
|
|
224
|
+
this._shakePerlin.offsetY = fbmNoise2d(0, t + 5.4) * this._shakePerlin.intensity * decay;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Get the effective camera X (including shake + shakePerlin offsets). */
|
|
229
|
+
get effectiveX() {
|
|
230
|
+
return this.x + (this._shake?.offsetX ?? 0) + (this._shakePerlin?.offsetX ?? 0);
|
|
231
|
+
}
|
|
232
|
+
/** Get the effective camera Y (including shake + shakePerlin offsets). */
|
|
233
|
+
get effectiveY() {
|
|
234
|
+
return this.y + (this._shake?.offsetY ?? 0) + (this._shakePerlin?.offsetY ?? 0);
|
|
235
|
+
}
|
|
236
|
+
/** Convert world coordinates to screen coordinates. */
|
|
237
|
+
worldToScreen(worldX, worldY) {
|
|
238
|
+
return this.worldToScreenInto(worldX, worldY, { x: 0, y: 0 });
|
|
239
|
+
}
|
|
240
|
+
/** Zero-alloc variant — writes screen coordinates into `out`. */
|
|
241
|
+
worldToScreenInto(worldX, worldY, out) {
|
|
242
|
+
const cos = Math.cos(-this.rotation);
|
|
243
|
+
const sin = Math.sin(-this.rotation);
|
|
244
|
+
const dx = (worldX - this.effectiveX) * this.zoom;
|
|
245
|
+
const dy = (worldY - this.effectiveY) * this.zoom;
|
|
246
|
+
out.x = cos * dx - sin * dy + this.viewportWidth / 2;
|
|
247
|
+
out.y = sin * dx + cos * dy + this.viewportHeight / 2;
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
/** Convert screen coordinates to world coordinates. */
|
|
251
|
+
screenToWorld(screenX, screenY) {
|
|
252
|
+
return this.screenToWorldInto(screenX, screenY, { x: 0, y: 0 });
|
|
253
|
+
}
|
|
254
|
+
/** Zero-alloc variant — writes world coordinates into `out`. */
|
|
255
|
+
screenToWorldInto(screenX, screenY, out) {
|
|
256
|
+
const cx = screenX - this.viewportWidth / 2;
|
|
257
|
+
const cy = screenY - this.viewportHeight / 2;
|
|
258
|
+
const cos = Math.cos(this.rotation);
|
|
259
|
+
const sin = Math.sin(this.rotation);
|
|
260
|
+
const rx = cos * cx - sin * cy;
|
|
261
|
+
const ry = sin * cx + cos * cy;
|
|
262
|
+
out.x = rx / this.zoom + this.effectiveX;
|
|
263
|
+
out.y = ry / this.zoom + this.effectiveY;
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
/** Get the visible world-space bounding rect (allocates a fresh Rect). */
|
|
267
|
+
getBounds() {
|
|
268
|
+
return this.getBoundsInto({ x: 0, y: 0, width: 0, height: 0 });
|
|
269
|
+
}
|
|
270
|
+
/** Zero-alloc variant — writes the visible bounds into `out`. */
|
|
271
|
+
getBoundsInto(out) {
|
|
272
|
+
const halfW = this.viewportWidth / (2 * this.zoom);
|
|
273
|
+
const halfH = this.viewportHeight / (2 * this.zoom);
|
|
274
|
+
out.x = this.effectiveX - halfW;
|
|
275
|
+
out.y = this.effectiveY - halfH;
|
|
276
|
+
out.width = halfW * 2;
|
|
277
|
+
out.height = halfH * 2;
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
_clampToBounds() {
|
|
281
|
+
const b = this.bounds;
|
|
282
|
+
const halfW = this.viewportWidth / (2 * this.zoom);
|
|
283
|
+
const halfH = this.viewportHeight / (2 * this.zoom);
|
|
284
|
+
const minX = b.x + halfW;
|
|
285
|
+
const maxX = b.x + b.width - halfW;
|
|
286
|
+
const minY = b.y + halfH;
|
|
287
|
+
const maxY = b.y + b.height - halfH;
|
|
288
|
+
if (minX <= maxX) {
|
|
289
|
+
this.x = Math.max(minX, Math.min(maxX, this.x));
|
|
290
|
+
} else {
|
|
291
|
+
this.x = b.x + b.width / 2;
|
|
292
|
+
}
|
|
293
|
+
if (minY <= maxY) {
|
|
294
|
+
this.y = Math.max(minY, Math.min(maxY, this.y));
|
|
295
|
+
} else {
|
|
296
|
+
this.y = b.y + b.height / 2;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/renderer.ts
|
|
302
|
+
var Renderer = class {
|
|
303
|
+
/** Current frame stats. */
|
|
304
|
+
stats = { drawCalls: 0, batchCount: 0 };
|
|
305
|
+
_canvas = null;
|
|
306
|
+
_inFrame = false;
|
|
307
|
+
/** Set the canvas to render to. Called once when canvas is available. */
|
|
308
|
+
setCanvas(canvas) {
|
|
309
|
+
this._canvas = canvas;
|
|
310
|
+
}
|
|
311
|
+
/** Get the current canvas. Throws if not in a frame. */
|
|
312
|
+
get canvas() {
|
|
313
|
+
if (!this._canvas) {
|
|
314
|
+
throw new Error("Renderer: no canvas set");
|
|
315
|
+
}
|
|
316
|
+
return this._canvas;
|
|
317
|
+
}
|
|
318
|
+
/** Whether we are currently between begin() and end(). */
|
|
319
|
+
get inFrame() {
|
|
320
|
+
return this._inFrame;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Begin a new frame. Clears the canvas and resets stats.
|
|
324
|
+
* @param clearColor 32-bit ARGB color to clear with (default black = 0xFF000000).
|
|
325
|
+
*/
|
|
326
|
+
begin(clearColor = 4278190080) {
|
|
327
|
+
if (this._inFrame) {
|
|
328
|
+
throw new Error("Renderer: begin() called while already in frame");
|
|
329
|
+
}
|
|
330
|
+
this._inFrame = true;
|
|
331
|
+
this.stats.drawCalls = 0;
|
|
332
|
+
this.stats.batchCount = 0;
|
|
333
|
+
this.canvas.save();
|
|
334
|
+
this.canvas.clear(clearColor);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* End the current frame. Restores canvas state.
|
|
338
|
+
*/
|
|
339
|
+
end() {
|
|
340
|
+
if (!this._inFrame) {
|
|
341
|
+
throw new Error("Renderer: end() called without begin()");
|
|
342
|
+
}
|
|
343
|
+
this.canvas.restore();
|
|
344
|
+
this._inFrame = false;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Apply camera transform to the canvas.
|
|
348
|
+
* Call after begin() and before drawing.
|
|
349
|
+
*/
|
|
350
|
+
applyCamera(cameraX, cameraY, zoom, rotation, viewportWidth, viewportHeight) {
|
|
351
|
+
const c = this.canvas;
|
|
352
|
+
c.translate(viewportWidth / 2, viewportHeight / 2);
|
|
353
|
+
c.scale(zoom, zoom);
|
|
354
|
+
if (rotation !== 0) {
|
|
355
|
+
c.rotate(-rotation, 0, 0);
|
|
356
|
+
}
|
|
357
|
+
c.translate(-cameraX, -cameraY);
|
|
358
|
+
}
|
|
359
|
+
/** Increment draw call counter. Called by batching systems. */
|
|
360
|
+
recordDrawCall() {
|
|
361
|
+
this.stats.drawCalls++;
|
|
362
|
+
}
|
|
363
|
+
/** Increment batch counter. */
|
|
364
|
+
recordBatch() {
|
|
365
|
+
this.stats.batchCount++;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// src/sprite-batch.ts
|
|
370
|
+
var SpriteBatch = class {
|
|
371
|
+
_commands = [];
|
|
372
|
+
_pool = [];
|
|
373
|
+
_batches = [];
|
|
374
|
+
/** Number of queued draw commands. */
|
|
375
|
+
get count() {
|
|
376
|
+
return this._commands.length;
|
|
377
|
+
}
|
|
378
|
+
/** Queue a sprite draw from a frame.
|
|
379
|
+
*
|
|
380
|
+
* `anchorX` / `anchorY` define the pivot point of the sprite as a
|
|
381
|
+
* fraction of its destination size — `(0, 0)` = top-left (default),
|
|
382
|
+
* `(0.5, 0.5)` = center, `(1, 1)` = bottom-right. `dstX` / `dstY`
|
|
383
|
+
* then refer to the anchor point instead of the top-left corner.
|
|
384
|
+
*
|
|
385
|
+
* `alpha` is a per-instance opacity multiplier in [0, 1]. Default 1. */
|
|
386
|
+
draw(image, frame, dstX, dstY, dstW, dstH, effect, anchorX = 0, anchorY = 0, alpha = 1, rotation = 0, stretch = false) {
|
|
387
|
+
const cmd = this._acquire();
|
|
388
|
+
const w = dstW ?? frame.w;
|
|
389
|
+
const h = dstH ?? frame.h;
|
|
390
|
+
cmd.image = image;
|
|
391
|
+
cmd.srcX = frame.x;
|
|
392
|
+
cmd.srcY = frame.y;
|
|
393
|
+
cmd.srcW = frame.w;
|
|
394
|
+
cmd.srcH = frame.h;
|
|
395
|
+
cmd.dstX = dstX - anchorX * w;
|
|
396
|
+
cmd.dstY = dstY - anchorY * h;
|
|
397
|
+
cmd.dstW = w;
|
|
398
|
+
cmd.dstH = h;
|
|
399
|
+
cmd.effect = effect;
|
|
400
|
+
cmd.alpha = alpha;
|
|
401
|
+
cmd.rotation = rotation;
|
|
402
|
+
cmd.anchorX = anchorX;
|
|
403
|
+
cmd.anchorY = anchorY;
|
|
404
|
+
cmd.stretch = stretch;
|
|
405
|
+
this._commands.push(cmd);
|
|
406
|
+
}
|
|
407
|
+
/** Queue a raw sprite draw with explicit src/dst rects. `dstX` / `dstY`
|
|
408
|
+
* refer to the anchor point — offset by `anchorX * dstW` / `anchorY * dstH`.
|
|
409
|
+
*
|
|
410
|
+
* `alpha` is a per-instance opacity multiplier in [0, 1]. Default 1. */
|
|
411
|
+
drawRaw(image, srcX, srcY, srcW, srcH, dstX, dstY, dstW, dstH, effect, anchorX = 0, anchorY = 0, alpha = 1, rotation = 0, stretch = false) {
|
|
412
|
+
const cmd = this._acquire();
|
|
413
|
+
cmd.image = image;
|
|
414
|
+
cmd.srcX = srcX;
|
|
415
|
+
cmd.srcY = srcY;
|
|
416
|
+
cmd.srcW = srcW;
|
|
417
|
+
cmd.srcH = srcH;
|
|
418
|
+
cmd.dstX = dstX - anchorX * dstW;
|
|
419
|
+
cmd.dstY = dstY - anchorY * dstH;
|
|
420
|
+
cmd.dstW = dstW;
|
|
421
|
+
cmd.dstH = dstH;
|
|
422
|
+
cmd.effect = effect;
|
|
423
|
+
cmd.alpha = alpha;
|
|
424
|
+
cmd.rotation = rotation;
|
|
425
|
+
cmd.anchorX = anchorX;
|
|
426
|
+
cmd.anchorY = anchorY;
|
|
427
|
+
cmd.stretch = stretch;
|
|
428
|
+
this._commands.push(cmd);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Flush all queued commands, grouped by (texture, effect, alpha) identity.
|
|
432
|
+
*
|
|
433
|
+
* @param drawAtlas Callback invoked once per batch. Receives
|
|
434
|
+
* `(image, commands)` — all commands in a batch share the same texture,
|
|
435
|
+
* the same effect (readable from `commands[0].effect`), and the same
|
|
436
|
+
* alpha (readable from `commands[0].alpha`).
|
|
437
|
+
* @param renderer Optional renderer to record stats.
|
|
438
|
+
*/
|
|
439
|
+
flush(drawAtlas, renderer) {
|
|
440
|
+
if (this._commands.length === 0) return;
|
|
441
|
+
const batches = this._batches;
|
|
442
|
+
for (const cmd of this._commands) {
|
|
443
|
+
let batch;
|
|
444
|
+
for (let i = 0; i < batches.length; i++) {
|
|
445
|
+
const b = batches[i];
|
|
446
|
+
if (b.image === cmd.image && b.effect === cmd.effect && b.alpha === cmd.alpha) {
|
|
447
|
+
batch = b;
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (!batch) {
|
|
452
|
+
batch = { image: cmd.image, effect: cmd.effect, alpha: cmd.alpha, commands: [] };
|
|
453
|
+
batches.push(batch);
|
|
454
|
+
}
|
|
455
|
+
batch.commands.push(cmd);
|
|
456
|
+
}
|
|
457
|
+
for (let i = 0; i < batches.length; i++) {
|
|
458
|
+
const batch = batches[i];
|
|
459
|
+
drawAtlas(batch.image, batch.commands);
|
|
460
|
+
if (renderer) {
|
|
461
|
+
renderer.recordDrawCall();
|
|
462
|
+
renderer.recordBatch();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
for (const cmd of this._commands) {
|
|
466
|
+
this._pool.push(cmd);
|
|
467
|
+
}
|
|
468
|
+
this._commands.length = 0;
|
|
469
|
+
for (let i = 0; i < batches.length; i++) {
|
|
470
|
+
batches[i].commands.length = 0;
|
|
471
|
+
}
|
|
472
|
+
batches.length = 0;
|
|
473
|
+
}
|
|
474
|
+
/** Clear all queued commands without drawing. */
|
|
475
|
+
clear() {
|
|
476
|
+
for (const cmd of this._commands) {
|
|
477
|
+
this._pool.push(cmd);
|
|
478
|
+
}
|
|
479
|
+
this._commands.length = 0;
|
|
480
|
+
}
|
|
481
|
+
_acquire() {
|
|
482
|
+
if (this._pool.length > 0) {
|
|
483
|
+
return this._pool.pop();
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
image: null,
|
|
487
|
+
srcX: 0,
|
|
488
|
+
srcY: 0,
|
|
489
|
+
srcW: 0,
|
|
490
|
+
srcH: 0,
|
|
491
|
+
dstX: 0,
|
|
492
|
+
dstY: 0,
|
|
493
|
+
dstW: 0,
|
|
494
|
+
dstH: 0,
|
|
495
|
+
effect: void 0,
|
|
496
|
+
alpha: 1,
|
|
497
|
+
rotation: 0,
|
|
498
|
+
anchorX: 0,
|
|
499
|
+
anchorY: 0,
|
|
500
|
+
stretch: false
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// src/sprite-effects.ts
|
|
506
|
+
function tint(color, amount) {
|
|
507
|
+
return { type: "tint", color, amount };
|
|
508
|
+
}
|
|
509
|
+
function flash(color, durationSec) {
|
|
510
|
+
return { type: "flash", color, duration: durationSec, elapsed: 0 };
|
|
511
|
+
}
|
|
512
|
+
function grayscale(amount) {
|
|
513
|
+
return { type: "grayscale", amount };
|
|
514
|
+
}
|
|
515
|
+
function outline(color, width) {
|
|
516
|
+
return { type: "outline", color, width };
|
|
517
|
+
}
|
|
518
|
+
function custom(shader, uniforms) {
|
|
519
|
+
return { type: "custom", shader, uniforms };
|
|
520
|
+
}
|
|
521
|
+
function water(color, foamColor, waveSpeed, waveAmplitude) {
|
|
522
|
+
return { type: "water", color, foamColor, waveSpeed, waveAmplitude };
|
|
523
|
+
}
|
|
524
|
+
function fog(color, density = 0.5, speed = 0.3, scale = 2) {
|
|
525
|
+
return { type: "fog", color, density, speed, scale };
|
|
526
|
+
}
|
|
527
|
+
function fire(opts) {
|
|
528
|
+
return {
|
|
529
|
+
type: "fire",
|
|
530
|
+
color: opts.color,
|
|
531
|
+
tipColor: opts.tipColor,
|
|
532
|
+
speed: opts.speed ?? 1,
|
|
533
|
+
intensity: opts.intensity ?? 0.8
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function roofsCity(opts) {
|
|
537
|
+
return {
|
|
538
|
+
type: "roofs-city",
|
|
539
|
+
skyColor: opts.skyColor,
|
|
540
|
+
roofColor: opts.roofColor,
|
|
541
|
+
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
542
|
+
density: opts.density ?? 6,
|
|
543
|
+
resolutionW: opts.resolutionW ?? 0,
|
|
544
|
+
resolutionH: opts.resolutionH ?? 0
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function forestCanopy(opts) {
|
|
548
|
+
return {
|
|
549
|
+
type: "forest-canopy",
|
|
550
|
+
skyColor: opts.skyColor,
|
|
551
|
+
leafColor: opts.leafColor,
|
|
552
|
+
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
553
|
+
density: opts.density ?? 5,
|
|
554
|
+
resolutionW: opts.resolutionW ?? 0,
|
|
555
|
+
resolutionH: opts.resolutionH ?? 0
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function riverWave(opts) {
|
|
559
|
+
return {
|
|
560
|
+
type: "river-wave",
|
|
561
|
+
waterColor: opts.waterColor,
|
|
562
|
+
foamColor: opts.foamColor,
|
|
563
|
+
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
564
|
+
waveScale: opts.waveScale ?? 2,
|
|
565
|
+
resolutionW: opts.resolutionW ?? 0,
|
|
566
|
+
resolutionH: opts.resolutionH ?? 0
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function moonCrater(opts) {
|
|
570
|
+
return {
|
|
571
|
+
type: "moon-crater",
|
|
572
|
+
moonColor: opts.moonColor,
|
|
573
|
+
craterColor: opts.craterColor,
|
|
574
|
+
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
575
|
+
craterCount: opts.craterCount ?? 4,
|
|
576
|
+
resolutionW: opts.resolutionW ?? 0,
|
|
577
|
+
resolutionH: opts.resolutionH ?? 0
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function caveInterior(opts) {
|
|
581
|
+
return {
|
|
582
|
+
type: "cave-interior",
|
|
583
|
+
caveColor: opts.caveColor,
|
|
584
|
+
glowColor: opts.glowColor,
|
|
585
|
+
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
586
|
+
stalactiteCount: opts.stalactiteCount ?? 8,
|
|
587
|
+
resolutionW: opts.resolutionW ?? 0,
|
|
588
|
+
resolutionH: opts.resolutionH ?? 0
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function mountainHills(opts) {
|
|
592
|
+
return {
|
|
593
|
+
type: "mountain-hills",
|
|
594
|
+
skyColor: opts.skyColor,
|
|
595
|
+
hillColor: opts.hillColor,
|
|
596
|
+
scrollOffsetY: opts.scrollOffsetY ?? 0,
|
|
597
|
+
ridgeScale: opts.ridgeScale ?? 1.5,
|
|
598
|
+
resolutionW: opts.resolutionW ?? 0,
|
|
599
|
+
resolutionH: opts.resolutionH ?? 0
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function updateEffect(effect, dt) {
|
|
603
|
+
if (effect.type === "flash") {
|
|
604
|
+
const elapsed = effect.elapsed + dt;
|
|
605
|
+
if (elapsed >= effect.duration) return null;
|
|
606
|
+
return { type: "flash", color: effect.color, duration: effect.duration, elapsed };
|
|
607
|
+
}
|
|
608
|
+
return effect;
|
|
609
|
+
}
|
|
610
|
+
function flashIntensity(effect) {
|
|
611
|
+
if (effect.duration <= 0) return 0;
|
|
612
|
+
const t = effect.elapsed / effect.duration;
|
|
613
|
+
return Math.max(0, 1 - t);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/shaders.ts
|
|
617
|
+
var FIRE_SKSL = (
|
|
618
|
+
/* sksl */
|
|
619
|
+
`
|
|
620
|
+
uniform shader src;
|
|
621
|
+
uniform float2 resolution;
|
|
622
|
+
uniform float3 color;
|
|
623
|
+
uniform float3 tipColor;
|
|
624
|
+
uniform float speed;
|
|
625
|
+
uniform float intensity;
|
|
626
|
+
uniform float time;
|
|
627
|
+
|
|
628
|
+
float hash(float2 p) { return fract(sin(dot(p, float2(127.1, 311.7))) * 43758.5453); }
|
|
629
|
+
|
|
630
|
+
float vnoise(float2 p) {
|
|
631
|
+
float2 i = floor(p);
|
|
632
|
+
float2 f = fract(p);
|
|
633
|
+
float2 u = f * f * (3.0 - 2.0 * f);
|
|
634
|
+
float a = hash(i);
|
|
635
|
+
float b = hash(i + float2(1.0, 0.0));
|
|
636
|
+
float c = hash(i + float2(0.0, 1.0));
|
|
637
|
+
float d = hash(i + float2(1.0, 1.0));
|
|
638
|
+
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
float fbm(float2 p) {
|
|
642
|
+
return 0.5 * vnoise(p) + 0.25 * vnoise(p * 2.0);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
half4 main(float2 coord) {
|
|
646
|
+
half4 srcColor = src.eval(coord);
|
|
647
|
+
float2 uv = coord / max(resolution, float2(1.0));
|
|
648
|
+
float2 np = float2(uv.x * 4.0, uv.y * 6.0 - time * speed * 2.0);
|
|
649
|
+
float n = fbm(np);
|
|
650
|
+
float flameMask = smoothstep(0.0, intensity, n * (1.0 - uv.y));
|
|
651
|
+
float3 fireRgb = mix(color, tipColor, uv.y);
|
|
652
|
+
half4 fireColor = half4(fireRgb.r, fireRgb.g, fireRgb.b, flameMask);
|
|
653
|
+
return half4(fireColor.rgb, fireColor.a * srcColor.a);
|
|
654
|
+
}
|
|
655
|
+
`
|
|
656
|
+
);
|
|
657
|
+
var FOG_SKSL = (
|
|
658
|
+
/* sksl */
|
|
659
|
+
`
|
|
660
|
+
uniform shader src;
|
|
661
|
+
uniform float2 resolution;
|
|
662
|
+
uniform float3 color;
|
|
663
|
+
uniform float density;
|
|
664
|
+
uniform float speed;
|
|
665
|
+
uniform float scale;
|
|
666
|
+
uniform float time;
|
|
667
|
+
|
|
668
|
+
float hash(float2 p) {
|
|
669
|
+
return fract(sin(dot(p, float2(127.1, 311.7))) * 43758.5453);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
float valueNoise(float2 p) {
|
|
673
|
+
float2 i = floor(p);
|
|
674
|
+
float2 f = fract(p);
|
|
675
|
+
float2 u = f * f * (3.0 - 2.0 * f);
|
|
676
|
+
float a = hash(i);
|
|
677
|
+
float b = hash(i + float2(1.0, 0.0));
|
|
678
|
+
float c = hash(i + float2(0.0, 1.0));
|
|
679
|
+
float d = hash(i + float2(1.0, 1.0));
|
|
680
|
+
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
float fbm(float2 p) {
|
|
684
|
+
float value = 0.0;
|
|
685
|
+
float amplitude = 0.5;
|
|
686
|
+
for (int i = 0; i < 4; i++) {
|
|
687
|
+
value += amplitude * valueNoise(p);
|
|
688
|
+
p *= 2.0;
|
|
689
|
+
amplitude *= 0.5;
|
|
690
|
+
}
|
|
691
|
+
return value;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
half4 main(float2 coord) {
|
|
695
|
+
float2 uv = coord / max(resolution, float2(1.0));
|
|
696
|
+
float2 drift = float2(time * speed, 0.3 * time * speed);
|
|
697
|
+
float n = clamp(fbm(uv * scale + drift), 0.0, 1.0);
|
|
698
|
+
half4 srcColor = src.eval(coord);
|
|
699
|
+
half3 blended = mix(srcColor.rgb, half3(color.r, color.g, color.b), half(density * n));
|
|
700
|
+
return half4(blended, srcColor.a);
|
|
701
|
+
}
|
|
702
|
+
`
|
|
703
|
+
);
|
|
704
|
+
var WATER_SKSL = (
|
|
705
|
+
/* sksl */
|
|
706
|
+
`
|
|
707
|
+
uniform shader src;
|
|
708
|
+
uniform float2 resolution;
|
|
709
|
+
uniform float3 color;
|
|
710
|
+
uniform float3 foamColor;
|
|
711
|
+
uniform float waveSpeed;
|
|
712
|
+
uniform float waveAmplitude;
|
|
713
|
+
uniform float time;
|
|
714
|
+
|
|
715
|
+
half4 main(float2 coord) {
|
|
716
|
+
float2 uv = coord / resolution;
|
|
717
|
+
float wave1 = sin(uv.x * 6.2832 * 3.0 + time * waveSpeed * 2.0) * waveAmplitude;
|
|
718
|
+
float wave2 = sin(uv.x * 6.2832 * 5.0 - time * waveSpeed * 1.3 + 1.5) * waveAmplitude * 0.5;
|
|
719
|
+
float totalWave = wave1 + wave2;
|
|
720
|
+
float2 displaced = float2(uv.x, uv.y + totalWave);
|
|
721
|
+
displaced = clamp(displaced, float2(0.0), float2(1.0));
|
|
722
|
+
half4 c = src.eval(displaced * resolution);
|
|
723
|
+
float waveDeriv = cos(uv.x * 6.2832 * 3.0 + time * waveSpeed * 2.0) * waveAmplitude * 2.0
|
|
724
|
+
+ cos(uv.x * 6.2832 * 5.0 - time * waveSpeed * 1.3 + 1.5) * waveAmplitude;
|
|
725
|
+
float foam = smoothstep(waveAmplitude * 0.6, waveAmplitude * 1.5, waveDeriv);
|
|
726
|
+
half3 tinted = mix(c.rgb, half3(color.r, color.g, color.b), half(0.35));
|
|
727
|
+
half3 final = mix(tinted, half3(foamColor.r, foamColor.g, foamColor.b), half(foam));
|
|
728
|
+
return half4(final, c.a);
|
|
729
|
+
}
|
|
730
|
+
`
|
|
731
|
+
);
|
|
732
|
+
var ROOFS_CITY_SKSL = (
|
|
733
|
+
/* sksl */
|
|
734
|
+
`
|
|
735
|
+
uniform float2 resolution;
|
|
736
|
+
uniform float3 skyColor;
|
|
737
|
+
uniform float3 roofColor;
|
|
738
|
+
uniform float scrollOffsetY;
|
|
739
|
+
uniform float density;
|
|
740
|
+
|
|
741
|
+
float hash(float x) { return fract(sin(x * 127.1) * 43758.5453); }
|
|
742
|
+
|
|
743
|
+
float buildingTop(float2 uv, float density) {
|
|
744
|
+
float col = floor(uv.x * density);
|
|
745
|
+
float h = 0.35 + 0.35 * hash(col);
|
|
746
|
+
float w = 0.6 + 0.3 * hash(col + 7.3);
|
|
747
|
+
float edge = step(abs(fract(uv.x * density) - 0.5), w * 0.5);
|
|
748
|
+
return edge * step(uv.y, h);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
half4 main(float2 coord) {
|
|
752
|
+
float2 uv = coord / max(resolution, float2(1.0));
|
|
753
|
+
uv.y = uv.y + scrollOffsetY / max(resolution.y, 1.0);
|
|
754
|
+
uv.y = fract(uv.y);
|
|
755
|
+
float buildings = buildingTop(uv, density);
|
|
756
|
+
half3 col = mix(half3(skyColor.r, skyColor.g, skyColor.b),
|
|
757
|
+
half3(roofColor.r, roofColor.g, roofColor.b),
|
|
758
|
+
half(buildings));
|
|
759
|
+
return half4(col, 1.0);
|
|
760
|
+
}
|
|
761
|
+
`
|
|
762
|
+
);
|
|
763
|
+
var FOREST_CANOPY_SKSL = (
|
|
764
|
+
/* sksl */
|
|
765
|
+
`
|
|
766
|
+
uniform float2 resolution;
|
|
767
|
+
uniform float3 skyColor;
|
|
768
|
+
uniform float3 leafColor;
|
|
769
|
+
uniform float scrollOffsetY;
|
|
770
|
+
uniform float density;
|
|
771
|
+
|
|
772
|
+
float hash(float2 p) { return fract(sin(dot(p, float2(127.1, 311.7))) * 43758.5453); }
|
|
773
|
+
|
|
774
|
+
float valueNoise(float2 p) {
|
|
775
|
+
float2 i = floor(p);
|
|
776
|
+
float2 f = fract(p);
|
|
777
|
+
float2 u = f * f * (3.0 - 2.0 * f);
|
|
778
|
+
return mix(mix(hash(i), hash(i + float2(1.0, 0.0)), u.x),
|
|
779
|
+
mix(hash(i + float2(0.0, 1.0)), hash(i + float2(1.0, 1.0)), u.x), u.y);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
half4 main(float2 coord) {
|
|
783
|
+
float2 uv = coord / max(resolution, float2(1.0));
|
|
784
|
+
uv.y = uv.y + scrollOffsetY / max(resolution.y, 1.0);
|
|
785
|
+
float n = valueNoise(float2(uv.x * density, uv.y * 3.0));
|
|
786
|
+
float treeLine = 0.55 + 0.25 * valueNoise(float2(uv.x * density * 0.5, 0.0));
|
|
787
|
+
float canopy = step(uv.y, treeLine) * smoothstep(0.3, 0.7, n);
|
|
788
|
+
half3 col = mix(half3(skyColor.r, skyColor.g, skyColor.b),
|
|
789
|
+
half3(leafColor.r, leafColor.g, leafColor.b),
|
|
790
|
+
half(canopy));
|
|
791
|
+
return half4(col, 1.0);
|
|
792
|
+
}
|
|
793
|
+
`
|
|
794
|
+
);
|
|
795
|
+
var RIVER_WAVE_SKSL = (
|
|
796
|
+
/* sksl */
|
|
797
|
+
`
|
|
798
|
+
uniform float2 resolution;
|
|
799
|
+
uniform float3 waterColor;
|
|
800
|
+
uniform float3 foamColor;
|
|
801
|
+
uniform float scrollOffsetY;
|
|
802
|
+
uniform float waveScale;
|
|
803
|
+
|
|
804
|
+
half4 main(float2 coord) {
|
|
805
|
+
float2 uv = coord / max(resolution, float2(1.0));
|
|
806
|
+
float scroll = scrollOffsetY / max(resolution.y, 1.0);
|
|
807
|
+
float2 p = float2(uv.x, uv.y + scroll);
|
|
808
|
+
float w1 = sin(p.x * 6.2832 * waveScale + p.y * 4.0) * 0.04;
|
|
809
|
+
float w2 = sin(p.x * 6.2832 * waveScale * 1.7 - p.y * 2.5 + 1.2) * 0.02;
|
|
810
|
+
float surface = 0.5 + w1 + w2;
|
|
811
|
+
float inWater = step(uv.y, surface);
|
|
812
|
+
float crest = smoothstep(surface - 0.015, surface, uv.y) * (1.0 - inWater) +
|
|
813
|
+
smoothstep(surface + 0.015, surface, uv.y) * inWater;
|
|
814
|
+
half3 base = mix(half3(foamColor.r, foamColor.g, foamColor.b),
|
|
815
|
+
half3(waterColor.r, waterColor.g, waterColor.b),
|
|
816
|
+
half(inWater));
|
|
817
|
+
half3 col = mix(base, half3(foamColor.r, foamColor.g, foamColor.b), half(crest * 0.6));
|
|
818
|
+
return half4(col, 1.0);
|
|
819
|
+
}
|
|
820
|
+
`
|
|
821
|
+
);
|
|
822
|
+
var MOON_CRATER_SKSL = (
|
|
823
|
+
/* sksl */
|
|
824
|
+
`
|
|
825
|
+
uniform float2 resolution;
|
|
826
|
+
uniform float3 moonColor;
|
|
827
|
+
uniform float3 craterColor;
|
|
828
|
+
uniform float scrollOffsetY;
|
|
829
|
+
uniform float craterCount;
|
|
830
|
+
|
|
831
|
+
float hash(float2 p) { return fract(sin(dot(p, float2(127.1, 311.7))) * 43758.5453); }
|
|
832
|
+
|
|
833
|
+
float crater(float2 uv, float2 center, float r) {
|
|
834
|
+
float d = length(uv - center);
|
|
835
|
+
float rim = smoothstep(r, r * 0.85, d) - smoothstep(r * 0.85, r * 0.65, d);
|
|
836
|
+
float floor_ = smoothstep(r * 0.65, r * 0.4, d);
|
|
837
|
+
return rim * 0.6 + floor_ * 0.25;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
half4 main(float2 coord) {
|
|
841
|
+
float2 uv = coord / max(resolution, float2(1.0));
|
|
842
|
+
uv.y = uv.y + scrollOffsetY / max(resolution.y, 1.0);
|
|
843
|
+
float c = 0.0;
|
|
844
|
+
for (int i = 0; i < 6; i++) {
|
|
845
|
+
float fi = float(i);
|
|
846
|
+
float2 center = float2(hash(float2(fi, 0.0)), hash(float2(fi, 1.0)));
|
|
847
|
+
float r = 0.04 + 0.07 * hash(float2(fi, 2.0));
|
|
848
|
+
c += crater(fract(uv), center, r) * step(fi, craterCount - 1.0);
|
|
849
|
+
}
|
|
850
|
+
c = clamp(c, 0.0, 1.0);
|
|
851
|
+
half3 col = mix(half3(moonColor.r, moonColor.g, moonColor.b),
|
|
852
|
+
half3(craterColor.r, craterColor.g, craterColor.b),
|
|
853
|
+
half(c));
|
|
854
|
+
return half4(col, 1.0);
|
|
855
|
+
}
|
|
856
|
+
`
|
|
857
|
+
);
|
|
858
|
+
var CAVE_INTERIOR_SKSL = (
|
|
859
|
+
/* sksl */
|
|
860
|
+
`
|
|
861
|
+
uniform float2 resolution;
|
|
862
|
+
uniform float3 caveColor;
|
|
863
|
+
uniform float3 glowColor;
|
|
864
|
+
uniform float scrollOffsetY;
|
|
865
|
+
uniform float stalactiteCount;
|
|
866
|
+
|
|
867
|
+
float hash(float x) { return fract(sin(x * 127.1) * 43758.5453); }
|
|
868
|
+
|
|
869
|
+
float stalactite(float2 uv, float count) {
|
|
870
|
+
float col = floor(uv.x * count);
|
|
871
|
+
float tip = 0.1 + 0.2 * hash(col);
|
|
872
|
+
float w = 0.3 + 0.3 * hash(col + 5.0);
|
|
873
|
+
float dx = abs(fract(uv.x * count) - 0.5) / (w * 0.5);
|
|
874
|
+
float shape = step(dx, 1.0) * step(uv.y, tip * (1.0 - clamp(dx, 0.0, 1.0)));
|
|
875
|
+
return shape;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
half4 main(float2 coord) {
|
|
879
|
+
float2 uv = coord / max(resolution, float2(1.0));
|
|
880
|
+
uv.y = 1.0 - uv.y;
|
|
881
|
+
uv.y = uv.y + scrollOffsetY / max(resolution.y, 1.0);
|
|
882
|
+
float stalac = stalactite(uv, stalactiteCount);
|
|
883
|
+
float glow = smoothstep(0.8, 1.0, uv.y) * (1.0 - stalac);
|
|
884
|
+
half3 col = mix(half3(caveColor.r, caveColor.g, caveColor.b),
|
|
885
|
+
half3(glowColor.r, glowColor.g, glowColor.b),
|
|
886
|
+
half(glow));
|
|
887
|
+
col = mix(col, half3(caveColor.r, caveColor.g, caveColor.b) * half(0.3), half(stalac));
|
|
888
|
+
return half4(col, 1.0);
|
|
889
|
+
}
|
|
890
|
+
`
|
|
891
|
+
);
|
|
892
|
+
var MOUNTAIN_HILLS_SKSL = (
|
|
893
|
+
/* sksl */
|
|
894
|
+
`
|
|
895
|
+
uniform float2 resolution;
|
|
896
|
+
uniform float3 skyColor;
|
|
897
|
+
uniform float3 hillColor;
|
|
898
|
+
uniform float scrollOffsetY;
|
|
899
|
+
uniform float ridgeScale;
|
|
900
|
+
|
|
901
|
+
float hash(float x) { return fract(sin(x * 127.1) * 43758.5453); }
|
|
902
|
+
|
|
903
|
+
float heightField(float x, float scale) {
|
|
904
|
+
float h = 0.0;
|
|
905
|
+
float amp = 0.5;
|
|
906
|
+
float freq = scale;
|
|
907
|
+
for (int i = 0; i < 4; i++) {
|
|
908
|
+
float ix = floor(x * freq);
|
|
909
|
+
float fx = fract(x * freq);
|
|
910
|
+
fx = fx * fx * (3.0 - 2.0 * fx);
|
|
911
|
+
h += amp * mix(hash(ix), hash(ix + 1.0), fx);
|
|
912
|
+
freq *= 2.0;
|
|
913
|
+
amp *= 0.5;
|
|
914
|
+
}
|
|
915
|
+
return h;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
half4 main(float2 coord) {
|
|
919
|
+
float2 uv = coord / max(resolution, float2(1.0));
|
|
920
|
+
float scroll = scrollOffsetY / max(resolution.y, 1.0);
|
|
921
|
+
float horizon = 0.4 + 0.3 * heightField(uv.x + scroll * 0.3, ridgeScale);
|
|
922
|
+
float mountains = step(uv.y, horizon);
|
|
923
|
+
half3 col = mix(half3(skyColor.r, skyColor.g, skyColor.b),
|
|
924
|
+
half3(hillColor.r, hillColor.g, hillColor.b),
|
|
925
|
+
half(mountains));
|
|
926
|
+
return half4(col, 1.0);
|
|
927
|
+
}
|
|
928
|
+
`
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
// src/sprite-font.ts
|
|
932
|
+
function parseBMFontJSON(json) {
|
|
933
|
+
const data = json;
|
|
934
|
+
const glyphs = /* @__PURE__ */ new Map();
|
|
935
|
+
for (const c of data.chars) {
|
|
936
|
+
glyphs.set(c.id, {
|
|
937
|
+
id: c.id,
|
|
938
|
+
x: c.x,
|
|
939
|
+
y: c.y,
|
|
940
|
+
width: c.width,
|
|
941
|
+
height: c.height,
|
|
942
|
+
xOffset: c.xoffset,
|
|
943
|
+
yOffset: c.yoffset,
|
|
944
|
+
xAdvance: c.xadvance
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
const kernings = /* @__PURE__ */ new Map();
|
|
948
|
+
if (data.kernings) {
|
|
949
|
+
for (const k of data.kernings) {
|
|
950
|
+
kernings.set(k.first << 16 | k.second, k.amount);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
lineHeight: data.common.lineHeight,
|
|
955
|
+
base: data.common.base,
|
|
956
|
+
glyphs,
|
|
957
|
+
kernings
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function getKerning(font, first, second) {
|
|
961
|
+
return font.kernings.get(first << 16 | second) ?? 0;
|
|
962
|
+
}
|
|
963
|
+
function measureText(font, text) {
|
|
964
|
+
let width = 0;
|
|
965
|
+
let prevCode = -1;
|
|
966
|
+
for (let i = 0; i < text.length; i++) {
|
|
967
|
+
const code = text.charCodeAt(i);
|
|
968
|
+
const glyph = font.glyphs.get(code);
|
|
969
|
+
if (!glyph) continue;
|
|
970
|
+
if (prevCode >= 0) {
|
|
971
|
+
width += getKerning(font, prevCode, code);
|
|
972
|
+
}
|
|
973
|
+
width += glyph.xAdvance;
|
|
974
|
+
prevCode = code;
|
|
975
|
+
}
|
|
976
|
+
return width;
|
|
977
|
+
}
|
|
978
|
+
function drawText(batch, font, image, text, x, y, scale = 1) {
|
|
979
|
+
let cursorX = x;
|
|
980
|
+
let prevCode = -1;
|
|
981
|
+
for (let i = 0; i < text.length; i++) {
|
|
982
|
+
const code = text.charCodeAt(i);
|
|
983
|
+
const glyph = font.glyphs.get(code);
|
|
984
|
+
if (!glyph) continue;
|
|
985
|
+
if (prevCode >= 0) {
|
|
986
|
+
cursorX += getKerning(font, prevCode, code) * scale;
|
|
987
|
+
}
|
|
988
|
+
batch.drawRaw(
|
|
989
|
+
image,
|
|
990
|
+
glyph.x,
|
|
991
|
+
glyph.y,
|
|
992
|
+
glyph.width,
|
|
993
|
+
glyph.height,
|
|
994
|
+
cursorX + glyph.xOffset * scale,
|
|
995
|
+
y + glyph.yOffset * scale,
|
|
996
|
+
glyph.width * scale,
|
|
997
|
+
glyph.height * scale
|
|
998
|
+
);
|
|
999
|
+
cursorX += glyph.xAdvance * scale;
|
|
1000
|
+
prevCode = code;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/render-system.ts
|
|
1005
|
+
import { defineComponent } from "@flare-engine/ecs";
|
|
1006
|
+
function createRenderSystem(world, query, positionDef, spriteDef, batch) {
|
|
1007
|
+
const entries = [];
|
|
1008
|
+
return () => {
|
|
1009
|
+
let i = 0;
|
|
1010
|
+
for (const entityId of query) {
|
|
1011
|
+
const sprite = world.get(entityId, spriteDef);
|
|
1012
|
+
if (!sprite) continue;
|
|
1013
|
+
if (i >= entries.length) {
|
|
1014
|
+
entries.push({ entityId, z: 0 });
|
|
1015
|
+
}
|
|
1016
|
+
const entry = entries[i];
|
|
1017
|
+
entry.entityId = entityId;
|
|
1018
|
+
entry.z = sprite.z;
|
|
1019
|
+
i++;
|
|
1020
|
+
}
|
|
1021
|
+
const count = i;
|
|
1022
|
+
if (entries.length > count * 2 && entries.length > 64) {
|
|
1023
|
+
entries.length = count;
|
|
1024
|
+
}
|
|
1025
|
+
sortByDrawOrder(entries, count);
|
|
1026
|
+
for (let j = 0; j < count; j++) {
|
|
1027
|
+
const entry = entries[j];
|
|
1028
|
+
const pos = world.get(entry.entityId, positionDef);
|
|
1029
|
+
const sprite = world.get(entry.entityId, spriteDef);
|
|
1030
|
+
if (!pos || !sprite) continue;
|
|
1031
|
+
const anchor = sprite.anchor;
|
|
1032
|
+
batch.draw(
|
|
1033
|
+
sprite.image,
|
|
1034
|
+
sprite.frame,
|
|
1035
|
+
pos.x,
|
|
1036
|
+
pos.y,
|
|
1037
|
+
sprite.width,
|
|
1038
|
+
sprite.height,
|
|
1039
|
+
sprite.effect,
|
|
1040
|
+
anchor ? anchor.x : 0,
|
|
1041
|
+
anchor ? anchor.y : 0,
|
|
1042
|
+
sprite.alpha ?? 1,
|
|
1043
|
+
sprite.rotation ?? 0,
|
|
1044
|
+
sprite.stretch ?? false
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
var Sprite = defineComponent("Sprite", () => ({
|
|
1050
|
+
image: null,
|
|
1051
|
+
frame: { name: "", x: 0, y: 0, w: 0, h: 0 },
|
|
1052
|
+
z: 0
|
|
1053
|
+
}));
|
|
1054
|
+
export {
|
|
1055
|
+
CAVE_INTERIOR_SKSL,
|
|
1056
|
+
Camera2D,
|
|
1057
|
+
FIRE_SKSL,
|
|
1058
|
+
FOG_SKSL,
|
|
1059
|
+
FOREST_CANOPY_SKSL,
|
|
1060
|
+
MOON_CRATER_SKSL,
|
|
1061
|
+
MOUNTAIN_HILLS_SKSL,
|
|
1062
|
+
RIVER_WAVE_SKSL,
|
|
1063
|
+
ROOFS_CITY_SKSL,
|
|
1064
|
+
Renderer,
|
|
1065
|
+
Sprite,
|
|
1066
|
+
SpriteBatch,
|
|
1067
|
+
WATER_SKSL,
|
|
1068
|
+
caveInterior,
|
|
1069
|
+
createRenderSystem,
|
|
1070
|
+
custom,
|
|
1071
|
+
drawOrderCompare,
|
|
1072
|
+
drawText,
|
|
1073
|
+
fire,
|
|
1074
|
+
flash,
|
|
1075
|
+
flashIntensity,
|
|
1076
|
+
fog,
|
|
1077
|
+
forestCanopy,
|
|
1078
|
+
getKerning,
|
|
1079
|
+
grayscale,
|
|
1080
|
+
measureText,
|
|
1081
|
+
moonCrater,
|
|
1082
|
+
mountainHills,
|
|
1083
|
+
outline,
|
|
1084
|
+
parseBMFontJSON,
|
|
1085
|
+
riverWave,
|
|
1086
|
+
roofsCity,
|
|
1087
|
+
sortByDrawOrder,
|
|
1088
|
+
tint,
|
|
1089
|
+
updateEffect,
|
|
1090
|
+
water
|
|
1091
|
+
};
|
|
1092
|
+
//# sourceMappingURL=index.mjs.map
|