@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.
@@ -0,0 +1,612 @@
1
+ import { Rect, Vec2 } from '@flare-engine/math';
2
+ import { SkImageLike, SpriteFrame } from '@flare-engine/assets';
3
+ import { ComponentDef, World, Query } from '@flare-engine/ecs';
4
+
5
+ /** A drawable item with a z-order for sorting. */
6
+ interface Drawable {
7
+ /** Z-index for draw ordering. Higher values draw on top. */
8
+ z: number;
9
+ }
10
+ /**
11
+ * Sort an array of drawables by z-index (ascending = back to front).
12
+ * Uses insertion sort which is fast for nearly-sorted arrays
13
+ * (typical in game rendering where order rarely changes).
14
+ *
15
+ * Mutates the input array in place. Returns the same array.
16
+ *
17
+ * @param items The array to sort in place.
18
+ * @param length Optional upper bound — when provided, only sorts `items[0..length)`.
19
+ * Use this when passing a pooled array that may hold stale tail entries.
20
+ */
21
+ declare function sortByDrawOrder<T extends Drawable>(items: T[], length?: number): T[];
22
+ /**
23
+ * Compare function for use with Array.sort when insertion sort isn't desired.
24
+ * Stable relative order for equal z values (returns 0).
25
+ */
26
+ declare function drawOrderCompare(a: Drawable, b: Drawable): number;
27
+
28
+ /** Camera follow smoothing options. */
29
+ interface FollowOptions {
30
+ /** Smoothing factor (0 = instant, lower = faster, default 0.1). */
31
+ smoothing?: number;
32
+ /** Offset from target (e.g. look-ahead). */
33
+ offset?: Vec2;
34
+ /** Dead zone rect — camera won't move while target is inside. */
35
+ deadZone?: Rect;
36
+ }
37
+ /**
38
+ * Camera2D — 2D camera with position, zoom, rotation, follow, and shake.
39
+ * Pure data + methods — no Skia dependency. The renderer reads camera state
40
+ * to build the view transform.
41
+ */
42
+ declare class Camera2D {
43
+ /** Camera position (world-space center). */
44
+ x: number;
45
+ y: number;
46
+ /** Zoom level (1 = 100%). */
47
+ zoom: number;
48
+ /** Rotation in radians. */
49
+ rotation: number;
50
+ /** Viewport width in pixels. */
51
+ viewportWidth: number;
52
+ /** Viewport height in pixels. */
53
+ viewportHeight: number;
54
+ /** Optional world bounds to clamp camera position. */
55
+ bounds: Rect | null;
56
+ private _follow;
57
+ private _shake;
58
+ private _shakePerlin;
59
+ private _shakeSeed;
60
+ private _shakePerlinSeed;
61
+ private _motionScale;
62
+ /** Set viewport dimensions. */
63
+ setViewport(width: number, height: number): void;
64
+ /** Start following a target position. */
65
+ follow(target: Vec2, options?: FollowOptions): void;
66
+ /** Stop following. */
67
+ unfollow(): void;
68
+ /**
69
+ * Global motion multiplier applied to subsequent `shake()` calls. Scales
70
+ * both amplitude and duration. Default 1 (no-op). 0 fully suppresses
71
+ * shake. Intended to be driven by `@flare-engine/a11y` reduced-motion so
72
+ * callers don't have to guard every shake site.
73
+ *
74
+ * Only affects future `shake()` calls — does not retroactively modify an
75
+ * in-flight shake.
76
+ *
77
+ * Clamped to `>= 0`.
78
+ */
79
+ setMotionScale(scale: number): void;
80
+ /** Current motion scale (default 1). */
81
+ get motionScale(): number;
82
+ /** Start camera shake. */
83
+ shake(intensity: number, duration: number): void;
84
+ /**
85
+ * Start a Perlin-noise-based camera shake. Overwrites any in-flight
86
+ * shakePerlin (same semantics as shake()). Respects motionScale.
87
+ */
88
+ shakePerlin(intensity: number, duration: number): void;
89
+ /**
90
+ * Update camera. Call once per frame.
91
+ * @param dt Delta time in seconds.
92
+ */
93
+ update(dt: number): void;
94
+ /** Get the effective camera X (including shake + shakePerlin offsets). */
95
+ get effectiveX(): number;
96
+ /** Get the effective camera Y (including shake + shakePerlin offsets). */
97
+ get effectiveY(): number;
98
+ /** Convert world coordinates to screen coordinates. */
99
+ worldToScreen(worldX: number, worldY: number): Vec2;
100
+ /** Zero-alloc variant — writes screen coordinates into `out`. */
101
+ worldToScreenInto(worldX: number, worldY: number, out: Vec2): Vec2;
102
+ /** Convert screen coordinates to world coordinates. */
103
+ screenToWorld(screenX: number, screenY: number): Vec2;
104
+ /** Zero-alloc variant — writes world coordinates into `out`. */
105
+ screenToWorldInto(screenX: number, screenY: number, out: Vec2): Vec2;
106
+ /** Get the visible world-space bounding rect (allocates a fresh Rect). */
107
+ getBounds(): Rect;
108
+ /** Zero-alloc variant — writes the visible bounds into `out`. */
109
+ getBoundsInto(out: Rect): Rect;
110
+ private _clampToBounds;
111
+ }
112
+
113
+ /** Minimal Skia Canvas-like interface for the Renderer. */
114
+ interface CanvasLike {
115
+ save(): number;
116
+ restore(): void;
117
+ translate(x: number, y: number): void;
118
+ scale(sx: number, sy: number): void;
119
+ rotate(radians: number, cx: number, cy: number): void;
120
+ clear(color: number): void;
121
+ }
122
+ /** Stats tracked per frame. */
123
+ interface RenderStats {
124
+ drawCalls: number;
125
+ batchCount: number;
126
+ }
127
+ /**
128
+ * Renderer — manages the Skia Canvas lifecycle for a frame.
129
+ * `begin()` to start, draw your content, `end()` to finish.
130
+ */
131
+ declare class Renderer {
132
+ /** Current frame stats. */
133
+ readonly stats: RenderStats;
134
+ private _canvas;
135
+ private _inFrame;
136
+ /** Set the canvas to render to. Called once when canvas is available. */
137
+ setCanvas(canvas: CanvasLike): void;
138
+ /** Get the current canvas. Throws if not in a frame. */
139
+ get canvas(): CanvasLike;
140
+ /** Whether we are currently between begin() and end(). */
141
+ get inFrame(): boolean;
142
+ /**
143
+ * Begin a new frame. Clears the canvas and resets stats.
144
+ * @param clearColor 32-bit ARGB color to clear with (default black = 0xFF000000).
145
+ */
146
+ begin(clearColor?: number): void;
147
+ /**
148
+ * End the current frame. Restores canvas state.
149
+ */
150
+ end(): void;
151
+ /**
152
+ * Apply camera transform to the canvas.
153
+ * Call after begin() and before drawing.
154
+ */
155
+ applyCamera(cameraX: number, cameraY: number, zoom: number, rotation: number, viewportWidth: number, viewportHeight: number): void;
156
+ /** Increment draw call counter. Called by batching systems. */
157
+ recordDrawCall(): void;
158
+ /** Increment batch counter. */
159
+ recordBatch(): void;
160
+ }
161
+
162
+ /** Color as RGBA (0-1 per channel). */
163
+ interface SpriteColor {
164
+ readonly r: number;
165
+ readonly g: number;
166
+ readonly b: number;
167
+ readonly a: number;
168
+ }
169
+ /** A sprite effect that can be applied during rendering. */
170
+ interface SpriteEffect {
171
+ readonly type: string;
172
+ }
173
+ /** Tint effect — blend a color onto the sprite. */
174
+ interface TintEffect extends SpriteEffect {
175
+ readonly type: 'tint';
176
+ readonly color: SpriteColor;
177
+ readonly amount: number;
178
+ }
179
+ /** Flash effect — overlay a solid color that decays. */
180
+ interface FlashEffect extends SpriteEffect {
181
+ readonly type: 'flash';
182
+ readonly color: SpriteColor;
183
+ readonly duration: number;
184
+ readonly elapsed: number;
185
+ }
186
+ /** Grayscale effect — desaturate the sprite. */
187
+ interface GrayscaleEffect extends SpriteEffect {
188
+ readonly type: 'grayscale';
189
+ readonly amount: number;
190
+ }
191
+ /** Outline effect — draw an outline around the sprite. */
192
+ interface OutlineEffect extends SpriteEffect {
193
+ readonly type: 'outline';
194
+ readonly color: SpriteColor;
195
+ readonly width: number;
196
+ }
197
+ /**
198
+ * Opaque pre-compiled shader program. Typically a Skia `RuntimeEffect` built
199
+ * once at asset load time via `Skia.RuntimeEffect.Make(sksl)`. The render
200
+ * package stays Skia-agnostic; the `@flare-engine/react` layer narrows this to the
201
+ * concrete Skia type when applying the effect.
202
+ */
203
+ type CompiledShader = object;
204
+ /** Uniform values keyed by name. Scalars for floats, arrays for vec/mat. */
205
+ type ShaderUniforms = Record<string, number | readonly number[]>;
206
+ /** Custom shader effect — apply a pre-compiled runtime shader with uniforms. */
207
+ interface CustomShaderEffect extends SpriteEffect {
208
+ readonly type: 'custom';
209
+ readonly shader: CompiledShader;
210
+ readonly uniforms: ShaderUniforms;
211
+ }
212
+ /** Water surface effect — animated UV-displaced sprite with foam crests. */
213
+ interface WaterEffect extends SpriteEffect {
214
+ readonly type: 'water';
215
+ readonly color: SpriteColor;
216
+ readonly foamColor: SpriteColor;
217
+ readonly waveSpeed: number;
218
+ readonly waveAmplitude: number;
219
+ }
220
+ /** Fog / volumetric haze effect — animated FBM noise layered over a sprite. */
221
+ interface FogEffect extends SpriteEffect {
222
+ readonly type: 'fog';
223
+ readonly color: SpriteColor;
224
+ readonly density: number;
225
+ readonly speed: number;
226
+ readonly scale: number;
227
+ }
228
+ /** Fire / flame effect — value-noise FBM flame rising upward from a sprite. */
229
+ interface FireEffect extends SpriteEffect {
230
+ readonly type: 'fire';
231
+ readonly color: SpriteColor;
232
+ readonly tipColor: SpriteColor;
233
+ readonly speed: number;
234
+ readonly intensity: number;
235
+ }
236
+ /** Rooftop city skyline — AABB silhouettes via SDF hash fields. */
237
+ interface RoofsCityEffect extends SpriteEffect {
238
+ readonly type: 'roofs-city';
239
+ readonly skyColor: SpriteColor;
240
+ readonly roofColor: SpriteColor;
241
+ readonly scrollOffsetY: number;
242
+ readonly density: number;
243
+ /** Destination render width in logical pixels — used as shader resolution.x. */
244
+ readonly resolutionW: number;
245
+ /** Destination render height in logical pixels — used as shader resolution.y. */
246
+ readonly resolutionH: number;
247
+ }
248
+ /** Forest canopy — value-noise tree line. */
249
+ interface ForestCanopyEffect extends SpriteEffect {
250
+ readonly type: 'forest-canopy';
251
+ readonly skyColor: SpriteColor;
252
+ readonly leafColor: SpriteColor;
253
+ readonly scrollOffsetY: number;
254
+ readonly density: number;
255
+ readonly resolutionW: number;
256
+ readonly resolutionH: number;
257
+ }
258
+ /** River surface — 1D height-field wave with foam crests. */
259
+ interface RiverWaveEffect extends SpriteEffect {
260
+ readonly type: 'river-wave';
261
+ readonly waterColor: SpriteColor;
262
+ readonly foamColor: SpriteColor;
263
+ readonly scrollOffsetY: number;
264
+ readonly waveScale: number;
265
+ readonly resolutionW: number;
266
+ readonly resolutionH: number;
267
+ }
268
+ /** Moon surface — polar-SDF craters on a flat disc. */
269
+ interface MoonCraterEffect extends SpriteEffect {
270
+ readonly type: 'moon-crater';
271
+ readonly moonColor: SpriteColor;
272
+ readonly craterColor: SpriteColor;
273
+ readonly scrollOffsetY: number;
274
+ readonly craterCount: number;
275
+ readonly resolutionW: number;
276
+ readonly resolutionH: number;
277
+ }
278
+ /** Cave interior — stalactites hanging from the top edge. */
279
+ interface CaveInteriorEffect extends SpriteEffect {
280
+ readonly type: 'cave-interior';
281
+ readonly caveColor: SpriteColor;
282
+ readonly glowColor: SpriteColor;
283
+ readonly scrollOffsetY: number;
284
+ readonly stalactiteCount: number;
285
+ readonly resolutionW: number;
286
+ readonly resolutionH: number;
287
+ }
288
+ /** Mountain / hill ridge — FBM height-field silhouette. */
289
+ interface MountainHillsEffect extends SpriteEffect {
290
+ readonly type: 'mountain-hills';
291
+ readonly skyColor: SpriteColor;
292
+ readonly hillColor: SpriteColor;
293
+ readonly scrollOffsetY: number;
294
+ readonly ridgeScale: number;
295
+ readonly resolutionW: number;
296
+ readonly resolutionH: number;
297
+ }
298
+ /** Any concrete sprite effect. */
299
+ type AnySpriteEffect = TintEffect | FlashEffect | GrayscaleEffect | OutlineEffect | CustomShaderEffect | WaterEffect | FogEffect | FireEffect | RoofsCityEffect | ForestCanopyEffect | RiverWaveEffect | MoonCraterEffect | CaveInteriorEffect | MountainHillsEffect;
300
+ /** Create a tint effect. */
301
+ declare function tint(color: SpriteColor, amount: number): TintEffect;
302
+ /** Create a flash effect. */
303
+ declare function flash(color: SpriteColor, durationSec: number): FlashEffect;
304
+ /** Create a grayscale effect. */
305
+ declare function grayscale(amount: number): GrayscaleEffect;
306
+ /** Create an outline effect. */
307
+ declare function outline(color: SpriteColor, width: number): OutlineEffect;
308
+ /** Create a custom shader effect. `shader` must be pre-compiled; compiling
309
+ * SkSL per frame is expensive. `uniforms` may be mutated externally between
310
+ * frames to animate parameters. */
311
+ declare function custom(shader: CompiledShader, uniforms: ShaderUniforms): CustomShaderEffect;
312
+ /** Create a water surface effect. Time is passed to the renderer each frame
313
+ * via `beginFrame(w, h, time)` — the effect itself is time-invariant. */
314
+ declare function water(color: SpriteColor, foamColor: SpriteColor, waveSpeed: number, waveAmplitude: number): WaterEffect;
315
+ /** Create a fog / volumetric haze effect. Time is passed to the renderer each
316
+ * frame via `beginFrame(w, h, time)` — the effect itself is time-invariant. */
317
+ declare function fog(color: SpriteColor, density?: number, speed?: number, scale?: number): FogEffect;
318
+ /** Create a fire / flame effect. Time is passed to the renderer each
319
+ * frame via `beginFrame(w, h, time)` — the effect itself is time-invariant. */
320
+ declare function fire(opts: {
321
+ color: SpriteColor;
322
+ tipColor: SpriteColor;
323
+ speed?: number;
324
+ intensity?: number;
325
+ }): FireEffect;
326
+ /** Create a rooftop city skyline effect. */
327
+ declare function roofsCity(opts: {
328
+ skyColor: SpriteColor;
329
+ roofColor: SpriteColor;
330
+ scrollOffsetY?: number;
331
+ density?: number;
332
+ resolutionW?: number;
333
+ resolutionH?: number;
334
+ }): RoofsCityEffect;
335
+ /** Create a forest canopy effect. */
336
+ declare function forestCanopy(opts: {
337
+ skyColor: SpriteColor;
338
+ leafColor: SpriteColor;
339
+ scrollOffsetY?: number;
340
+ density?: number;
341
+ resolutionW?: number;
342
+ resolutionH?: number;
343
+ }): ForestCanopyEffect;
344
+ /** Create a river wave surface effect. */
345
+ declare function riverWave(opts: {
346
+ waterColor: SpriteColor;
347
+ foamColor: SpriteColor;
348
+ scrollOffsetY?: number;
349
+ waveScale?: number;
350
+ resolutionW?: number;
351
+ resolutionH?: number;
352
+ }): RiverWaveEffect;
353
+ /** Create a moon crater surface effect. */
354
+ declare function moonCrater(opts: {
355
+ moonColor: SpriteColor;
356
+ craterColor: SpriteColor;
357
+ scrollOffsetY?: number;
358
+ craterCount?: number;
359
+ resolutionW?: number;
360
+ resolutionH?: number;
361
+ }): MoonCraterEffect;
362
+ /** Create a cave interior effect. */
363
+ declare function caveInterior(opts: {
364
+ caveColor: SpriteColor;
365
+ glowColor: SpriteColor;
366
+ scrollOffsetY?: number;
367
+ stalactiteCount?: number;
368
+ resolutionW?: number;
369
+ resolutionH?: number;
370
+ }): CaveInteriorEffect;
371
+ /** Create a mountain / hill ridge effect. */
372
+ declare function mountainHills(opts: {
373
+ skyColor: SpriteColor;
374
+ hillColor: SpriteColor;
375
+ scrollOffsetY?: number;
376
+ ridgeScale?: number;
377
+ resolutionW?: number;
378
+ resolutionH?: number;
379
+ }): MountainHillsEffect;
380
+ /**
381
+ * Advance a time-based effect by `dt` seconds.
382
+ *
383
+ * Returns a new effect object if state changed (e.g. flash elapsed time
384
+ * advances), the same reference if the effect is time-invariant (tint,
385
+ * grayscale, outline, custom), or `null` if the effect has expired.
386
+ *
387
+ * Effects are immutable — callers must replace their reference with the
388
+ * returned value. Sharing an effect instance across entities is safe.
389
+ */
390
+ declare function updateEffect(effect: AnySpriteEffect, dt: number): AnySpriteEffect | null;
391
+ /**
392
+ * Get the current intensity of a flash effect (1 at start, 0 at end).
393
+ */
394
+ declare function flashIntensity(effect: FlashEffect): number;
395
+
396
+ /** A single sprite draw request in the batch. */
397
+ interface SpriteDrawCommand {
398
+ /** Texture (Skia SkImage). */
399
+ image: SkImageLike;
400
+ /** Source rectangle in the texture. */
401
+ srcX: number;
402
+ srcY: number;
403
+ srcW: number;
404
+ srcH: number;
405
+ /** Destination position and size. */
406
+ dstX: number;
407
+ dstY: number;
408
+ dstW: number;
409
+ dstH: number;
410
+ /** Optional per-sprite effect. Shared by reference across commands in a
411
+ * batch — the render adapter applies it once per batch. */
412
+ effect: AnySpriteEffect | undefined;
413
+ /** Per-instance alpha multiplier. Range [0, 1]. Default 1 (fully opaque).
414
+ * Participates in the batch key — sprites with different alphas are
415
+ * placed in separate batches so the renderer can apply each alpha once
416
+ * per draw call without per-sprite overhead. */
417
+ alpha: number;
418
+ /** Rotation in radians, clockwise (Skia Y-down convention). Default 0.
419
+ * Rotation is applied about the anchor point. */
420
+ rotation: number;
421
+ /** Anchor X fraction stored for the bridge rotation path. Range [0, 1]. */
422
+ anchorX: number;
423
+ /** Anchor Y fraction stored for the bridge rotation path. Range [0, 1]. */
424
+ anchorY: number;
425
+ /**
426
+ * When true, the sprite is drawn via `drawImageRect` so `dstW` and `dstH`
427
+ * are honored independently (non-uniform scale). A 256×256 source image
428
+ * will fill an arbitrary W×H destination rect.
429
+ *
430
+ * Default (false/absent): drawn via the batched `drawAtlas` path (uniform
431
+ * scale — dstW/srcW drives both axes). Stretched sprites are NOT batched
432
+ * with regular sprites; each is an independent `drawImageRect` call.
433
+ *
434
+ * Rotation is unsupported in stretch mode — the RSXform similarity-transform
435
+ * limit does not apply, but the bridge draws a plain axis-aligned rect.
436
+ * Combine with a zero rotation only.
437
+ */
438
+ stretch: boolean;
439
+ }
440
+ /**
441
+ * SpriteBatch — collects sprite draw commands and groups them by texture
442
+ * (and optionally by effect) for efficient batched rendering.
443
+ *
444
+ * Usage:
445
+ * 1. Call `draw()` for each sprite, optionally passing an effect
446
+ * 2. Call `flush()` with an atlas draw callback to execute all batched draws
447
+ * 3. Batching groups by texture identity, then by effect identity — so
448
+ * sharing an effect instance across sprites maximizes batch size
449
+ *
450
+ * The actual Skia Atlas draw call is delegated to a callback so this module
451
+ * remains testable without Skia.
452
+ */
453
+ declare class SpriteBatch {
454
+ private _commands;
455
+ private _pool;
456
+ private _batches;
457
+ /** Number of queued draw commands. */
458
+ get count(): number;
459
+ /** Queue a sprite draw from a frame.
460
+ *
461
+ * `anchorX` / `anchorY` define the pivot point of the sprite as a
462
+ * fraction of its destination size — `(0, 0)` = top-left (default),
463
+ * `(0.5, 0.5)` = center, `(1, 1)` = bottom-right. `dstX` / `dstY`
464
+ * then refer to the anchor point instead of the top-left corner.
465
+ *
466
+ * `alpha` is a per-instance opacity multiplier in [0, 1]. Default 1. */
467
+ draw(image: SkImageLike, frame: SpriteFrame, dstX: number, dstY: number, dstW?: number, dstH?: number, effect?: AnySpriteEffect, anchorX?: number, anchorY?: number, alpha?: number, rotation?: number, stretch?: boolean): void;
468
+ /** Queue a raw sprite draw with explicit src/dst rects. `dstX` / `dstY`
469
+ * refer to the anchor point — offset by `anchorX * dstW` / `anchorY * dstH`.
470
+ *
471
+ * `alpha` is a per-instance opacity multiplier in [0, 1]. Default 1. */
472
+ drawRaw(image: SkImageLike, srcX: number, srcY: number, srcW: number, srcH: number, dstX: number, dstY: number, dstW: number, dstH: number, effect?: AnySpriteEffect, anchorX?: number, anchorY?: number, alpha?: number, rotation?: number, stretch?: boolean): void;
473
+ /**
474
+ * Flush all queued commands, grouped by (texture, effect, alpha) identity.
475
+ *
476
+ * @param drawAtlas Callback invoked once per batch. Receives
477
+ * `(image, commands)` — all commands in a batch share the same texture,
478
+ * the same effect (readable from `commands[0].effect`), and the same
479
+ * alpha (readable from `commands[0].alpha`).
480
+ * @param renderer Optional renderer to record stats.
481
+ */
482
+ flush(drawAtlas: (image: SkImageLike, commands: SpriteDrawCommand[]) => void, renderer?: Renderer): void;
483
+ /** Clear all queued commands without drawing. */
484
+ clear(): void;
485
+ private _acquire;
486
+ }
487
+
488
+ declare const FIRE_SKSL = "\nuniform shader src;\nuniform float2 resolution;\nuniform float3 color;\nuniform float3 tipColor;\nuniform float speed;\nuniform float intensity;\nuniform float time;\n\nfloat hash(float2 p) { return fract(sin(dot(p, float2(127.1, 311.7))) * 43758.5453); }\n\nfloat vnoise(float2 p) {\n float2 i = floor(p);\n float2 f = fract(p);\n float2 u = f * f * (3.0 - 2.0 * f);\n float a = hash(i);\n float b = hash(i + float2(1.0, 0.0));\n float c = hash(i + float2(0.0, 1.0));\n float d = hash(i + float2(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfloat fbm(float2 p) {\n return 0.5 * vnoise(p) + 0.25 * vnoise(p * 2.0);\n}\n\nhalf4 main(float2 coord) {\n half4 srcColor = src.eval(coord);\n float2 uv = coord / max(resolution, float2(1.0));\n float2 np = float2(uv.x * 4.0, uv.y * 6.0 - time * speed * 2.0);\n float n = fbm(np);\n float flameMask = smoothstep(0.0, intensity, n * (1.0 - uv.y));\n float3 fireRgb = mix(color, tipColor, uv.y);\n half4 fireColor = half4(fireRgb.r, fireRgb.g, fireRgb.b, flameMask);\n return half4(fireColor.rgb, fireColor.a * srcColor.a);\n}\n";
489
+ declare const FOG_SKSL = "\nuniform shader src;\nuniform float2 resolution;\nuniform float3 color;\nuniform float density;\nuniform float speed;\nuniform float scale;\nuniform float time;\n\nfloat hash(float2 p) {\n return fract(sin(dot(p, float2(127.1, 311.7))) * 43758.5453);\n}\n\nfloat valueNoise(float2 p) {\n float2 i = floor(p);\n float2 f = fract(p);\n float2 u = f * f * (3.0 - 2.0 * f);\n float a = hash(i);\n float b = hash(i + float2(1.0, 0.0));\n float c = hash(i + float2(0.0, 1.0));\n float d = hash(i + float2(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfloat fbm(float2 p) {\n float value = 0.0;\n float amplitude = 0.5;\n for (int i = 0; i < 4; i++) {\n value += amplitude * valueNoise(p);\n p *= 2.0;\n amplitude *= 0.5;\n }\n return value;\n}\n\nhalf4 main(float2 coord) {\n float2 uv = coord / max(resolution, float2(1.0));\n float2 drift = float2(time * speed, 0.3 * time * speed);\n float n = clamp(fbm(uv * scale + drift), 0.0, 1.0);\n half4 srcColor = src.eval(coord);\n half3 blended = mix(srcColor.rgb, half3(color.r, color.g, color.b), half(density * n));\n return half4(blended, srcColor.a);\n}\n";
490
+ declare const WATER_SKSL = "\nuniform shader src;\nuniform float2 resolution;\nuniform float3 color;\nuniform float3 foamColor;\nuniform float waveSpeed;\nuniform float waveAmplitude;\nuniform float time;\n\nhalf4 main(float2 coord) {\n float2 uv = coord / resolution;\n float wave1 = sin(uv.x * 6.2832 * 3.0 + time * waveSpeed * 2.0) * waveAmplitude;\n float wave2 = sin(uv.x * 6.2832 * 5.0 - time * waveSpeed * 1.3 + 1.5) * waveAmplitude * 0.5;\n float totalWave = wave1 + wave2;\n float2 displaced = float2(uv.x, uv.y + totalWave);\n displaced = clamp(displaced, float2(0.0), float2(1.0));\n half4 c = src.eval(displaced * resolution);\n float waveDeriv = cos(uv.x * 6.2832 * 3.0 + time * waveSpeed * 2.0) * waveAmplitude * 2.0\n + cos(uv.x * 6.2832 * 5.0 - time * waveSpeed * 1.3 + 1.5) * waveAmplitude;\n float foam = smoothstep(waveAmplitude * 0.6, waveAmplitude * 1.5, waveDeriv);\n half3 tinted = mix(c.rgb, half3(color.r, color.g, color.b), half(0.35));\n half3 final = mix(tinted, half3(foamColor.r, foamColor.g, foamColor.b), half(foam));\n return half4(final, c.a);\n}\n";
491
+ declare const ROOFS_CITY_SKSL = "\nuniform float2 resolution;\nuniform float3 skyColor;\nuniform float3 roofColor;\nuniform float scrollOffsetY;\nuniform float density;\n\nfloat hash(float x) { return fract(sin(x * 127.1) * 43758.5453); }\n\nfloat buildingTop(float2 uv, float density) {\n float col = floor(uv.x * density);\n float h = 0.35 + 0.35 * hash(col);\n float w = 0.6 + 0.3 * hash(col + 7.3);\n float edge = step(abs(fract(uv.x * density) - 0.5), w * 0.5);\n return edge * step(uv.y, h);\n}\n\nhalf4 main(float2 coord) {\n float2 uv = coord / max(resolution, float2(1.0));\n uv.y = uv.y + scrollOffsetY / max(resolution.y, 1.0);\n uv.y = fract(uv.y);\n float buildings = buildingTop(uv, density);\n half3 col = mix(half3(skyColor.r, skyColor.g, skyColor.b),\n half3(roofColor.r, roofColor.g, roofColor.b),\n half(buildings));\n return half4(col, 1.0);\n}\n";
492
+ declare const FOREST_CANOPY_SKSL = "\nuniform float2 resolution;\nuniform float3 skyColor;\nuniform float3 leafColor;\nuniform float scrollOffsetY;\nuniform float density;\n\nfloat hash(float2 p) { return fract(sin(dot(p, float2(127.1, 311.7))) * 43758.5453); }\n\nfloat valueNoise(float2 p) {\n float2 i = floor(p);\n float2 f = fract(p);\n float2 u = f * f * (3.0 - 2.0 * f);\n return mix(mix(hash(i), hash(i + float2(1.0, 0.0)), u.x),\n mix(hash(i + float2(0.0, 1.0)), hash(i + float2(1.0, 1.0)), u.x), u.y);\n}\n\nhalf4 main(float2 coord) {\n float2 uv = coord / max(resolution, float2(1.0));\n uv.y = uv.y + scrollOffsetY / max(resolution.y, 1.0);\n float n = valueNoise(float2(uv.x * density, uv.y * 3.0));\n float treeLine = 0.55 + 0.25 * valueNoise(float2(uv.x * density * 0.5, 0.0));\n float canopy = step(uv.y, treeLine) * smoothstep(0.3, 0.7, n);\n half3 col = mix(half3(skyColor.r, skyColor.g, skyColor.b),\n half3(leafColor.r, leafColor.g, leafColor.b),\n half(canopy));\n return half4(col, 1.0);\n}\n";
493
+ declare const RIVER_WAVE_SKSL = "\nuniform float2 resolution;\nuniform float3 waterColor;\nuniform float3 foamColor;\nuniform float scrollOffsetY;\nuniform float waveScale;\n\nhalf4 main(float2 coord) {\n float2 uv = coord / max(resolution, float2(1.0));\n float scroll = scrollOffsetY / max(resolution.y, 1.0);\n float2 p = float2(uv.x, uv.y + scroll);\n float w1 = sin(p.x * 6.2832 * waveScale + p.y * 4.0) * 0.04;\n float w2 = sin(p.x * 6.2832 * waveScale * 1.7 - p.y * 2.5 + 1.2) * 0.02;\n float surface = 0.5 + w1 + w2;\n float inWater = step(uv.y, surface);\n float crest = smoothstep(surface - 0.015, surface, uv.y) * (1.0 - inWater) +\n smoothstep(surface + 0.015, surface, uv.y) * inWater;\n half3 base = mix(half3(foamColor.r, foamColor.g, foamColor.b),\n half3(waterColor.r, waterColor.g, waterColor.b),\n half(inWater));\n half3 col = mix(base, half3(foamColor.r, foamColor.g, foamColor.b), half(crest * 0.6));\n return half4(col, 1.0);\n}\n";
494
+ declare const MOON_CRATER_SKSL = "\nuniform float2 resolution;\nuniform float3 moonColor;\nuniform float3 craterColor;\nuniform float scrollOffsetY;\nuniform float craterCount;\n\nfloat hash(float2 p) { return fract(sin(dot(p, float2(127.1, 311.7))) * 43758.5453); }\n\nfloat crater(float2 uv, float2 center, float r) {\n float d = length(uv - center);\n float rim = smoothstep(r, r * 0.85, d) - smoothstep(r * 0.85, r * 0.65, d);\n float floor_ = smoothstep(r * 0.65, r * 0.4, d);\n return rim * 0.6 + floor_ * 0.25;\n}\n\nhalf4 main(float2 coord) {\n float2 uv = coord / max(resolution, float2(1.0));\n uv.y = uv.y + scrollOffsetY / max(resolution.y, 1.0);\n float c = 0.0;\n for (int i = 0; i < 6; i++) {\n float fi = float(i);\n float2 center = float2(hash(float2(fi, 0.0)), hash(float2(fi, 1.0)));\n float r = 0.04 + 0.07 * hash(float2(fi, 2.0));\n c += crater(fract(uv), center, r) * step(fi, craterCount - 1.0);\n }\n c = clamp(c, 0.0, 1.0);\n half3 col = mix(half3(moonColor.r, moonColor.g, moonColor.b),\n half3(craterColor.r, craterColor.g, craterColor.b),\n half(c));\n return half4(col, 1.0);\n}\n";
495
+ declare const CAVE_INTERIOR_SKSL = "\nuniform float2 resolution;\nuniform float3 caveColor;\nuniform float3 glowColor;\nuniform float scrollOffsetY;\nuniform float stalactiteCount;\n\nfloat hash(float x) { return fract(sin(x * 127.1) * 43758.5453); }\n\nfloat stalactite(float2 uv, float count) {\n float col = floor(uv.x * count);\n float tip = 0.1 + 0.2 * hash(col);\n float w = 0.3 + 0.3 * hash(col + 5.0);\n float dx = abs(fract(uv.x * count) - 0.5) / (w * 0.5);\n float shape = step(dx, 1.0) * step(uv.y, tip * (1.0 - clamp(dx, 0.0, 1.0)));\n return shape;\n}\n\nhalf4 main(float2 coord) {\n float2 uv = coord / max(resolution, float2(1.0));\n uv.y = 1.0 - uv.y;\n uv.y = uv.y + scrollOffsetY / max(resolution.y, 1.0);\n float stalac = stalactite(uv, stalactiteCount);\n float glow = smoothstep(0.8, 1.0, uv.y) * (1.0 - stalac);\n half3 col = mix(half3(caveColor.r, caveColor.g, caveColor.b),\n half3(glowColor.r, glowColor.g, glowColor.b),\n half(glow));\n col = mix(col, half3(caveColor.r, caveColor.g, caveColor.b) * half(0.3), half(stalac));\n return half4(col, 1.0);\n}\n";
496
+ declare const MOUNTAIN_HILLS_SKSL = "\nuniform float2 resolution;\nuniform float3 skyColor;\nuniform float3 hillColor;\nuniform float scrollOffsetY;\nuniform float ridgeScale;\n\nfloat hash(float x) { return fract(sin(x * 127.1) * 43758.5453); }\n\nfloat heightField(float x, float scale) {\n float h = 0.0;\n float amp = 0.5;\n float freq = scale;\n for (int i = 0; i < 4; i++) {\n float ix = floor(x * freq);\n float fx = fract(x * freq);\n fx = fx * fx * (3.0 - 2.0 * fx);\n h += amp * mix(hash(ix), hash(ix + 1.0), fx);\n freq *= 2.0;\n amp *= 0.5;\n }\n return h;\n}\n\nhalf4 main(float2 coord) {\n float2 uv = coord / max(resolution, float2(1.0));\n float scroll = scrollOffsetY / max(resolution.y, 1.0);\n float horizon = 0.4 + 0.3 * heightField(uv.x + scroll * 0.3, ridgeScale);\n float mountains = step(uv.y, horizon);\n half3 col = mix(half3(skyColor.r, skyColor.g, skyColor.b),\n half3(hillColor.r, hillColor.g, hillColor.b),\n half(mountains));\n return half4(col, 1.0);\n}\n";
497
+
498
+ /** A single glyph in a bitmap font. */
499
+ interface Glyph {
500
+ /** Character code (Unicode). */
501
+ id: number;
502
+ /** Source rect in the font texture. */
503
+ x: number;
504
+ y: number;
505
+ width: number;
506
+ height: number;
507
+ /** Draw offset. */
508
+ xOffset: number;
509
+ yOffset: number;
510
+ /** Horizontal advance after this glyph. */
511
+ xAdvance: number;
512
+ }
513
+ /** Kerning pair. */
514
+ interface KerningPair {
515
+ first: number;
516
+ second: number;
517
+ amount: number;
518
+ }
519
+ /** Parsed bitmap font data. */
520
+ interface BitmapFont {
521
+ /** Line height in pixels. */
522
+ lineHeight: number;
523
+ /** Baseline offset. */
524
+ base: number;
525
+ /** Glyph map by character code. */
526
+ glyphs: Map<number, Glyph>;
527
+ /** Kerning map. Key = `first << 16 | second`. */
528
+ kernings: Map<number, number>;
529
+ }
530
+ /**
531
+ * Parse BMFont JSON format (common output from tools like Hiero, BMFont, msdf-bmfont).
532
+ */
533
+ declare function parseBMFontJSON(json: unknown): BitmapFont;
534
+ /** Get kerning between two characters. */
535
+ declare function getKerning(font: BitmapFont, first: number, second: number): number;
536
+ /** Measure the width of a text string in pixels. */
537
+ declare function measureText(font: BitmapFont, text: string): number;
538
+ /**
539
+ * Draw text using a SpriteBatch.
540
+ * Characters are batched as individual sprites from the font texture.
541
+ */
542
+ declare function drawText(batch: SpriteBatch, font: BitmapFont, image: SkImageLike, text: string, x: number, y: number, scale?: number): void;
543
+
544
+ /** Position component data expected by the render system. */
545
+ interface PositionData {
546
+ x: number;
547
+ y: number;
548
+ }
549
+ /** Sprite anchor as a fraction of the destination rect — `(0, 0)` is
550
+ * top-left (default), `(0.5, 0.5)` is center, `(1, 1)` is bottom-right.
551
+ * When present, `Position` refers to the anchor point rather than the
552
+ * sprite's top-left corner. */
553
+ interface SpriteAnchor {
554
+ x: number;
555
+ y: number;
556
+ }
557
+ /** Sprite component data expected by the render system. */
558
+ interface SpriteData {
559
+ image: SkImageLike;
560
+ frame: SpriteFrame;
561
+ z: number;
562
+ width?: number;
563
+ height?: number;
564
+ /** Optional per-sprite effect (tint, flash, grayscale, outline, custom).
565
+ * Shared instances are batched together — prefer reusing an effect
566
+ * reference across entities that should render identically. */
567
+ effect?: AnySpriteEffect;
568
+ /** Optional pivot point for the sprite. Defaults to top-left `(0, 0)`. */
569
+ anchor?: SpriteAnchor;
570
+ /** Per-instance alpha multiplier. Range [0, 1]. Default 1 (fully opaque).
571
+ * Mutate directly each frame — e.g. `sprite.alpha = lerp(1, 0, t)`.
572
+ * Sprites with different alphas are placed in separate draw batches. */
573
+ alpha?: number;
574
+ /** Rotation in radians, clockwise (Skia Y-down convention). Default 0.
575
+ * Applied about the sprite's anchor point. Non-uniform scale combined with
576
+ * rotation is unsupported — scale is inferred uniformly from dstW/srcW. */
577
+ rotation?: number;
578
+ /**
579
+ * When true, draw via `drawImageRect` so `width` and `height` are honored
580
+ * independently (non-uniform scale). Use for background tiles or any sprite
581
+ * that must fill a non-square destination rect exactly.
582
+ *
583
+ * Default (absent/false): drawn via the batched `drawAtlas` uniform-scale
584
+ * path. Stretched sprites are NOT batched with regular sprites.
585
+ *
586
+ * Rotation is unsupported in stretch mode — combine only with `rotation = 0`.
587
+ */
588
+ stretch?: boolean;
589
+ }
590
+ /**
591
+ * Create a render system function.
592
+ *
593
+ * This is not a defineSystem() wrapper because it needs extra parameters
594
+ * (SpriteBatch) beyond what the ECS system interface provides. Instead,
595
+ * it returns a function to call in your game loop.
596
+ *
597
+ * The returned function only **queues** sprite draws into `batch` — it
598
+ * does not call `batch.flush()`. Flushing is owned by the frame owner
599
+ * (e.g. `<GameView>`) so that all draw sources (ECS, particles, custom)
600
+ * can contribute to a single flush at the render phase.
601
+ *
602
+ * @param world The ECS world.
603
+ * @param query Query that matches entities with Position + Sprite.
604
+ * @param positionDef ComponentDef for Position.
605
+ * @param spriteDef ComponentDef for Sprite.
606
+ * @param batch SpriteBatch instance to queue draws into.
607
+ */
608
+ declare function createRenderSystem(world: World, query: Query, positionDef: ComponentDef<PositionData>, spriteDef: ComponentDef<SpriteData>, batch: SpriteBatch): () => void;
609
+ /** Canonical Sprite component — queried by the render system alongside `Position`. */
610
+ declare const Sprite: ComponentDef<SpriteData>;
611
+
612
+ export { type AnySpriteEffect, type BitmapFont, CAVE_INTERIOR_SKSL, Camera2D, type CanvasLike, type CaveInteriorEffect, type CompiledShader, type CustomShaderEffect, type Drawable, FIRE_SKSL, FOG_SKSL, FOREST_CANOPY_SKSL, type FireEffect, type FlashEffect, type FogEffect, type FollowOptions, type ForestCanopyEffect, type Glyph, type GrayscaleEffect, type KerningPair, MOON_CRATER_SKSL, MOUNTAIN_HILLS_SKSL, type MoonCraterEffect, type MountainHillsEffect, type OutlineEffect, type PositionData, RIVER_WAVE_SKSL, ROOFS_CITY_SKSL, type RenderStats, Renderer, type RiverWaveEffect, type RoofsCityEffect, type ShaderUniforms, Sprite, type SpriteAnchor, SpriteBatch, type SpriteColor, type SpriteData, type SpriteDrawCommand, type SpriteEffect, type TintEffect, WATER_SKSL, type WaterEffect, caveInterior, createRenderSystem, custom, drawOrderCompare, drawText, fire, flash, flashIntensity, fog, forestCanopy, getKerning, grayscale, measureText, moonCrater, mountainHills, outline, parseBMFontJSON, riverWave, roofsCity, sortByDrawOrder, tint, updateEffect, water };