hayao 0.3.0 → 0.4.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/README.md +10 -0
- package/bin/create-hayao.mjs +1 -1
- package/dist/app/browser.d.ts +33 -3
- package/dist/app/game.d.ts +15 -11
- package/dist/audio/audio.d.ts +18 -1
- package/dist/audio/synth.d.ts +7 -0
- package/dist/audio/zzfx.d.ts +14 -0
- package/dist/core/clock.d.ts +1 -1
- package/dist/core/projection.d.ts +36 -0
- package/dist/core/rng.d.ts +9 -0
- package/dist/hayao.global.js +190 -5
- package/dist/index.d.ts +10 -1
- package/dist/index.js +2341 -328
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +190 -5
- package/dist/input/actions.d.ts +60 -6
- package/dist/input/gamepad.d.ts +101 -0
- package/dist/input/source.d.ts +85 -4
- package/dist/logic/coroutine.d.ts +68 -0
- package/dist/render/canvas.d.ts +3 -7
- package/dist/render/canvas2d-core.d.ts +9 -0
- package/dist/render/commands.d.ts +36 -2
- package/dist/render/paint.d.ts +8 -0
- package/dist/render/renderer.d.ts +25 -0
- package/dist/render/svg.d.ts +2 -1
- package/dist/render/webgl.d.ts +176 -0
- package/dist/scene/iso.d.ts +73 -0
- package/dist/scene/node.d.ts +81 -2
- package/dist/scene/nodes.d.ts +34 -0
- package/dist/scene/particles.d.ts +19 -0
- package/dist/scene/verletChain.d.ts +76 -0
- package/dist/ui/touch.d.ts +51 -0
- package/dist/verify/dom.d.ts +26 -0
- package/dist/world.d.ts +72 -7
- package/docs/API.md +79 -26
- package/docs/CONVENTIONS.md +290 -2
- package/docs/EMBED.md +5 -0
- package/docs/QUICKSTART.md +13 -1
- package/docs/VERIFICATION.md +34 -4
- package/package.json +2 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { DrawCommand } from './commands';
|
|
2
|
+
import { type Renderer, type RendererConfig, type Viewport } from './renderer';
|
|
3
|
+
import type { Vec2 } from '../core/math';
|
|
4
|
+
/** Value for a GLSL uniform: float, vec2, vec3, or vec4. */
|
|
5
|
+
export type UniformValue = number | [number, number] | [number, number, number] | [number, number, number, number];
|
|
6
|
+
/**
|
|
7
|
+
* One pass in a post-processing pipeline. Each pass is a GLSL fragment shader
|
|
8
|
+
* that receives:
|
|
9
|
+
* uniform sampler2D u_scene — the original Canvas2D rasterized frame
|
|
10
|
+
* uniform sampler2D u_prev — output of the previous pass (= u_scene for pass 0)
|
|
11
|
+
* uniform float u_time — wall-clock seconds (cosmetic only, never hashed)
|
|
12
|
+
* uniform vec2 u_resolution — display canvas size in pixels
|
|
13
|
+
* in vec2 v_uv — 0..1 UV with (0,0) at top-left
|
|
14
|
+
* out vec4 fragColor — output pixel
|
|
15
|
+
*
|
|
16
|
+
* Plus any custom uniforms added via setUniform() or the pass's own `uniforms` map.
|
|
17
|
+
*/
|
|
18
|
+
export interface PostProcessPass {
|
|
19
|
+
/** GLSL #version 300 es fragment shader source. */
|
|
20
|
+
shader: string;
|
|
21
|
+
/** Per-pass custom uniforms (merged with the renderer-level ones, pass overrides renderer). */
|
|
22
|
+
uniforms?: Record<string, UniformValue>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Ready-made GLSL fragment shaders for common post-processing effects.
|
|
26
|
+
* Use in setPostProcess() or as passes in setPipeline().
|
|
27
|
+
*
|
|
28
|
+
* Each effect references u_scene / u_prev / u_time / u_resolution as needed.
|
|
29
|
+
* Custom tuning: pass a custom uniform via setUniform() — names are noted below.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { WebGL2Renderer, WEBGL_EFFECTS } from '@hayao';
|
|
34
|
+
* const renderer = new WebGL2Renderer({ width, height, postProcess: WEBGL_EFFECTS.vignette });
|
|
35
|
+
* // Tune: renderer.setUniform('u_vignette', 0.5);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const WEBGL_EFFECTS: {
|
|
39
|
+
/** Passthrough — no effect. */
|
|
40
|
+
readonly passthrough: "#version 300 es\nprecision mediump float;\nin vec2 v_uv;\nuniform sampler2D u_scene;\nuniform sampler2D u_prev;\nout vec4 fragColor;\nvoid main() { fragColor = texture(u_prev, v_uv); }";
|
|
41
|
+
/** Pixelate: blockSize in pixels (default 4). setUniform('u_block', N) */
|
|
42
|
+
readonly pixelate: "#version 300 es\nprecision mediump float;\nin vec2 v_uv;\nuniform sampler2D u_prev;\nuniform vec2 u_resolution;\nuniform float u_block;\nout vec4 fragColor;\nvoid main() {\n float block = max(1.0, u_block > 0.0 ? u_block : 4.0);\n vec2 px = floor(v_uv * u_resolution / block) * block / u_resolution;\n fragColor = texture(u_prev, px);\n}";
|
|
43
|
+
/** Vignette: darkens edges. setUniform('u_vignette', 0.0..1.0, default 0.4) */
|
|
44
|
+
readonly vignette: "#version 300 es\nprecision mediump float;\nin vec2 v_uv;\nuniform sampler2D u_prev;\nuniform float u_vignette;\nout vec4 fragColor;\nvoid main() {\n vec4 col = texture(u_prev, v_uv);\n vec2 uv = v_uv - 0.5;\n float d = dot(uv, uv);\n float strength = u_vignette > 0.0 ? u_vignette : 0.4;\n fragColor = col * (1.0 - d * strength * 4.0);\n}";
|
|
45
|
+
/** CRT scanlines + slight barrel distortion. u_time drives the scan roll. */
|
|
46
|
+
readonly crt: "#version 300 es\nprecision mediump float;\nin vec2 v_uv;\nuniform sampler2D u_prev;\nuniform vec2 u_resolution;\nuniform float u_time;\nuniform float u_crt_lines; // scanline density, default 600\nuniform float u_crt_warp; // barrel strength, default 0.08\nout vec4 fragColor;\nvoid main() {\n float lines = u_crt_lines > 0.0 ? u_crt_lines : 600.0;\n float warp = u_crt_warp > 0.0 ? u_crt_warp : 0.08;\n // barrel distortion\n vec2 uv = v_uv - 0.5;\n float r2 = dot(uv, uv);\n uv *= 1.0 + warp * r2;\n uv += 0.5;\n if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {\n fragColor = vec4(0.0, 0.0, 0.0, 1.0);\n return;\n }\n vec4 col = texture(u_prev, uv);\n // rolling scanlines\n float scan = sin((uv.y * lines + u_time * 40.0) * 3.14159) * 0.5 + 0.5;\n col.rgb *= 0.75 + 0.25 * scan;\n // slight green phosphor tint\n col.rgb = mix(col.rgb, col.rgb * vec3(0.9, 1.05, 0.85), 0.3);\n fragColor = col;\n}";
|
|
47
|
+
/** Chromatic aberration / RGB fringe. setUniform('u_aberration', 0..0.01, default 0.004) */
|
|
48
|
+
readonly chromaticAberration: "#version 300 es\nprecision mediump float;\nin vec2 v_uv;\nuniform sampler2D u_prev;\nuniform float u_aberration;\nout vec4 fragColor;\nvoid main() {\n float amt = u_aberration > 0.0 ? u_aberration : 0.004;\n vec2 dir = v_uv - 0.5;\n float r = texture(u_prev, v_uv + dir * amt * 2.0).r;\n float g = texture(u_prev, v_uv).g;\n float b = texture(u_prev, v_uv - dir * amt * 2.0).b;\n fragColor = vec4(r, g, b, 1.0);\n}";
|
|
49
|
+
/** Palette-shift / hue rotation. u_hue_shift in radians, driven by u_time for animation. */
|
|
50
|
+
readonly hueRotate: "#version 300 es\nprecision mediump float;\nin vec2 v_uv;\nuniform sampler2D u_prev;\nuniform float u_hue_shift; // radians; if 0 uses u_time\nuniform float u_time;\nout vec4 fragColor;\nvoid main() {\n float angle = u_hue_shift != 0.0 ? u_hue_shift : u_time * 0.5;\n vec4 col = texture(u_prev, v_uv);\n // Hue rotate via Rodrigues in YIQ-ish space\n float c = cos(angle), s = sin(angle);\n mat3 m = mat3(\n 0.299+0.701*c+0.168*s, 0.587-0.587*c+0.330*s, 0.114-0.114*c-0.497*s,\n 0.299-0.299*c-0.328*s, 0.587+0.413*c+0.035*s, 0.114-0.114*c+0.292*s,\n 0.299-0.300*c+1.250*s, 0.587-0.588*c-1.050*s, 0.114+0.886*c-0.203*s\n );\n fragColor = vec4(m * col.rgb, col.a);\n}";
|
|
51
|
+
/** Wave distortion (heat haze, underwater). setUniform('u_wave_amp', default 0.003) */
|
|
52
|
+
readonly wave: "#version 300 es\nprecision mediump float;\nin vec2 v_uv;\nuniform sampler2D u_prev;\nuniform float u_time;\nuniform float u_wave_amp;\nuniform float u_wave_freq;\nout vec4 fragColor;\nvoid main() {\n float amp = u_wave_amp > 0.0 ? u_wave_amp : 0.003;\n float freq = u_wave_freq > 0.0 ? u_wave_freq : 8.0;\n vec2 uv = v_uv;\n uv.x += sin(uv.y * freq + u_time * 2.5) * amp;\n uv.y += cos(uv.x * freq + u_time * 2.5) * amp;\n fragColor = texture(u_prev, clamp(uv, 0.0, 1.0));\n}";
|
|
53
|
+
/** Bloom pass 1/4: extract bright pixels above threshold (default 0.6). */
|
|
54
|
+
readonly bloomBrightpass: "#version 300 es\nprecision mediump float;\nin vec2 v_uv;\nuniform sampler2D u_prev;\nuniform float u_bloom_threshold;\nout vec4 fragColor;\nvoid main() {\n vec4 col = texture(u_prev, v_uv);\n float lum = dot(col.rgb, vec3(0.2126, 0.7152, 0.0722));\n float thresh = u_bloom_threshold > 0.0 ? u_bloom_threshold : 0.55;\n float weight = smoothstep(thresh - 0.05, thresh + 0.05, lum);\n fragColor = col * weight;\n}";
|
|
55
|
+
/** Bloom pass 2/4: horizontal gaussian blur (13-tap). */
|
|
56
|
+
readonly bloomHBlur: "#version 300 es\nprecision mediump float;\nin vec2 v_uv;\nuniform sampler2D u_prev;\nuniform vec2 u_resolution;\nuniform float u_bloom_spread;\nout vec4 fragColor;\nvoid main() {\n float spread = u_bloom_spread > 0.0 ? u_bloom_spread : 1.0;\n vec2 texel = vec2(spread / u_resolution.x, 0.0);\n const float W[7] = float[](0.0625, 0.125, 0.1875, 0.25, 0.1875, 0.125, 0.0625);\n vec4 sum = vec4(0.0);\n for (int i = 0; i < 7; i++)\n sum += texture(u_prev, clamp(v_uv + float(i-3) * texel, 0.0, 1.0)) * W[i];\n fragColor = sum;\n}";
|
|
57
|
+
/** Bloom pass 3/4: vertical gaussian blur (13-tap). */
|
|
58
|
+
readonly bloomVBlur: "#version 300 es\nprecision mediump float;\nin vec2 v_uv;\nuniform sampler2D u_prev;\nuniform vec2 u_resolution;\nuniform float u_bloom_spread;\nout vec4 fragColor;\nvoid main() {\n float spread = u_bloom_spread > 0.0 ? u_bloom_spread : 1.0;\n vec2 texel = vec2(0.0, spread / u_resolution.y);\n const float W[7] = float[](0.0625, 0.125, 0.1875, 0.25, 0.1875, 0.125, 0.0625);\n vec4 sum = vec4(0.0);\n for (int i = 0; i < 7; i++)\n sum += texture(u_prev, clamp(v_uv + float(i-3) * texel, 0.0, 1.0)) * W[i];\n fragColor = sum;\n}";
|
|
59
|
+
/** Bloom pass 4/4: composite original + blurred bloom. u_bloom_intensity controls strength. */
|
|
60
|
+
readonly bloomComposite: "#version 300 es\nprecision mediump float;\nin vec2 v_uv;\nuniform sampler2D u_scene;\nuniform sampler2D u_prev;\nuniform float u_bloom_intensity;\nout vec4 fragColor;\nvoid main() {\n vec4 scene = texture(u_scene, v_uv);\n vec4 bloom = texture(u_prev, v_uv);\n float intensity = u_bloom_intensity > 0.0 ? u_bloom_intensity : 0.8;\n fragColor = vec4(scene.rgb + bloom.rgb * intensity, scene.a);\n}";
|
|
61
|
+
};
|
|
62
|
+
export declare const BLOOM_PIPELINE: PostProcessPass[];
|
|
63
|
+
export interface WebGLRendererConfig extends RendererConfig {
|
|
64
|
+
/**
|
|
65
|
+
* Initial post-processing shader or pipeline. Pass a GLSL string for a
|
|
66
|
+
* single-pass effect, or use setPipeline() for multi-pass. Defaults to
|
|
67
|
+
* passthrough.
|
|
68
|
+
*/
|
|
69
|
+
postProcess?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* GPU-accelerated renderer with a programmable GLSL post-processing pipeline.
|
|
73
|
+
*
|
|
74
|
+
* ## Quick start
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { WebGL2Renderer, WEBGL_EFFECTS, BLOOM_PIPELINE } from '@hayao';
|
|
77
|
+
*
|
|
78
|
+
* // Single-pass:
|
|
79
|
+
* const renderer = new WebGL2Renderer({ width, height,
|
|
80
|
+
* postProcess: WEBGL_EFFECTS.vignette });
|
|
81
|
+
* renderer.setUniform('u_vignette', 0.5);
|
|
82
|
+
*
|
|
83
|
+
* // Multi-pass bloom:
|
|
84
|
+
* const renderer = new WebGL2Renderer({ width, height });
|
|
85
|
+
* renderer.setPipeline(BLOOM_PIPELINE);
|
|
86
|
+
* renderer.setUniform('u_bloom_intensity', 1.2);
|
|
87
|
+
*
|
|
88
|
+
* // Runtime effect swap:
|
|
89
|
+
* renderer.setPostProcess(WEBGL_EFFECTS.crt);
|
|
90
|
+
* renderer.clearPostProcess(); // back to passthrough
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* ## Auto-uniforms (available in every shader, every pass)
|
|
94
|
+
* - `uniform float u_time` — wall-clock seconds (cosmetic, never hashed)
|
|
95
|
+
* - `uniform vec2 u_resolution` — display canvas size in px (w, h)
|
|
96
|
+
* - `uniform sampler2D u_scene` — original Canvas2D rasterized frame
|
|
97
|
+
* - `uniform sampler2D u_prev` — output of the previous pass (= u_scene for pass 0)
|
|
98
|
+
*
|
|
99
|
+
* ## Determinism
|
|
100
|
+
* `u_time` is wall-clock and NOT part of `world.hash()`. The renderer is a
|
|
101
|
+
* pure display-list consumer — it never writes state back into the engine.
|
|
102
|
+
* All verify passes run against `HeadlessRenderer` / `SvgRenderer` and see
|
|
103
|
+
* the same DrawCommand[] regardless of post-processing.
|
|
104
|
+
*/
|
|
105
|
+
export declare class WebGL2Renderer implements Renderer {
|
|
106
|
+
readonly width: number;
|
|
107
|
+
readonly height: number;
|
|
108
|
+
private background;
|
|
109
|
+
private glCanvas;
|
|
110
|
+
private gl;
|
|
111
|
+
private offscreen;
|
|
112
|
+
private ctx2d;
|
|
113
|
+
private dpr;
|
|
114
|
+
/** Original rasterized scene texture. */
|
|
115
|
+
private sceneTex;
|
|
116
|
+
/** Intermediate pass framebuffers (one per non-final pass). */
|
|
117
|
+
private fbos;
|
|
118
|
+
/** Compiled programs, one per pass. */
|
|
119
|
+
private programs;
|
|
120
|
+
/** Per-pass uniform maps (merged with global uniforms). */
|
|
121
|
+
private passUniforms;
|
|
122
|
+
/** Empty VAO for gl_VertexID-only draws. */
|
|
123
|
+
private vao;
|
|
124
|
+
/** Frames drawn, driving u_time at a nominal 60 fps (cosmetic, but deterministic). */
|
|
125
|
+
private framesDrawn;
|
|
126
|
+
/** Global custom uniforms applied to every pass. */
|
|
127
|
+
private uniforms;
|
|
128
|
+
constructor(config: WebGLRendererConfig);
|
|
129
|
+
mount(parent: HTMLElement): void;
|
|
130
|
+
private resize;
|
|
131
|
+
private buildPipeline;
|
|
132
|
+
draw(commands: DrawCommand[]): void;
|
|
133
|
+
/**
|
|
134
|
+
* Set a custom uniform applied to every pass. Useful for knob-driven effect
|
|
135
|
+
* parameters (intensity, scale, colour) without recompiling the shader.
|
|
136
|
+
* The value is set on every draw() until cleared or overridden.
|
|
137
|
+
*
|
|
138
|
+
* ```ts
|
|
139
|
+
* renderer.setUniform('u_vignette', 0.5);
|
|
140
|
+
* renderer.setUniform('u_bloom_intensity', [1.0, 0.8]);
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
setUniform(name: string, value: UniformValue): this;
|
|
144
|
+
/** Remove a custom uniform. */
|
|
145
|
+
clearUniform(name: string): this;
|
|
146
|
+
/**
|
|
147
|
+
* Replace the post-processing pipeline. Accepts an array of passes; each
|
|
148
|
+
* pass reads `u_prev` (previous output) and `u_scene` (original frame).
|
|
149
|
+
* Throws on shader compile error — previous pipeline stays active.
|
|
150
|
+
*
|
|
151
|
+
* For single-pass effects, prefer `setPostProcess(shader)`. For the standard
|
|
152
|
+
* bloom setup, use `setPipeline(BLOOM_PIPELINE)`.
|
|
153
|
+
*
|
|
154
|
+
* ```ts
|
|
155
|
+
* renderer.setPipeline([
|
|
156
|
+
* { shader: WEBGL_EFFECTS.bloomBrightpass },
|
|
157
|
+
* { shader: WEBGL_EFFECTS.bloomHBlur },
|
|
158
|
+
* { shader: WEBGL_EFFECTS.bloomVBlur },
|
|
159
|
+
* { shader: WEBGL_EFFECTS.bloomComposite },
|
|
160
|
+
* ]);
|
|
161
|
+
* renderer.setUniform('u_bloom_intensity', 1.0);
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
setPipeline(passes: PostProcessPass[]): this;
|
|
165
|
+
/**
|
|
166
|
+
* Convenience: set a single-pass post-processing shader.
|
|
167
|
+
* Throws on compile error — previous effect stays active.
|
|
168
|
+
*/
|
|
169
|
+
setPostProcess(fragmentShader: string): this;
|
|
170
|
+
/** Restore passthrough (no post-processing). */
|
|
171
|
+
clearPostProcess(): this;
|
|
172
|
+
get element(): HTMLCanvasElement;
|
|
173
|
+
toDesign(clientX: number, clientY: number): Vec2;
|
|
174
|
+
viewport(): Viewport;
|
|
175
|
+
dispose(): void;
|
|
176
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Transform } from '../core/math';
|
|
2
|
+
import type { DrawCommand, Paint } from '../render/commands';
|
|
3
|
+
import { Node, type NodeConfig } from './node';
|
|
4
|
+
/**
|
|
5
|
+
* A raised isometric tile = a cube: a top diamond + a front-left and front-right
|
|
6
|
+
* parallelogram. `pos` is the GROUND cell centre (where an elev-0 diamond would
|
|
7
|
+
* sit); the block rises `height` screen-px toward −y. Give it a base `fill` and
|
|
8
|
+
* the two side faces auto-shade (left darkest, right mid) — or override any face
|
|
9
|
+
* with `top` / `left` / `right`. Emits the three faces in back-to-front order so
|
|
10
|
+
* the top always paints over the sides within the prism.
|
|
11
|
+
*/
|
|
12
|
+
export interface IsoPrismConfig extends NodeConfig {
|
|
13
|
+
/** Footprint diamond width (horizontal diagonal) in screen px. */
|
|
14
|
+
tileW: number;
|
|
15
|
+
/** Footprint diamond height (vertical diagonal) in screen px. */
|
|
16
|
+
tileH: number;
|
|
17
|
+
/** Block height in screen px (0 = a flat tile: only the top diamond). */
|
|
18
|
+
height: number;
|
|
19
|
+
/** Base colour; side faces derive from it unless overridden. */
|
|
20
|
+
fill: string;
|
|
21
|
+
/** Explicit face paints (override the auto-shade). */
|
|
22
|
+
top?: Paint;
|
|
23
|
+
left?: Paint;
|
|
24
|
+
right?: Paint;
|
|
25
|
+
/** Shared stroke for all faces (seams between tiles). */
|
|
26
|
+
stroke?: string;
|
|
27
|
+
strokeWidth?: number;
|
|
28
|
+
opacity?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Multiply a #rgb / #rrggbb colour's channels by `f` (clamped) for face shading. */
|
|
31
|
+
export declare function shadeHex(hex: string, f: number): string;
|
|
32
|
+
export declare class IsoPrism extends Node {
|
|
33
|
+
readonly type = "IsoPrism";
|
|
34
|
+
tileW: number;
|
|
35
|
+
tileH: number;
|
|
36
|
+
height: number;
|
|
37
|
+
fill: string;
|
|
38
|
+
topPaint?: Paint;
|
|
39
|
+
leftPaint?: Paint;
|
|
40
|
+
rightPaint?: Paint;
|
|
41
|
+
stroke?: string;
|
|
42
|
+
strokeWidth?: number;
|
|
43
|
+
opacity?: number;
|
|
44
|
+
constructor(config: IsoPrismConfig);
|
|
45
|
+
protected draw(out: DrawCommand[], world: Transform): void;
|
|
46
|
+
protected serializeProps(): Record<string, unknown>;
|
|
47
|
+
applyProps(props: Record<string, unknown>): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A container that auto-assigns each child's painter `z` from a depth accessor,
|
|
51
|
+
* so overlapping sprites stack correctly without every mover re-setting `z` in
|
|
52
|
+
* its own update. The key is read at DRAW time (positions are final for the
|
|
53
|
+
* frame), integrating these children into the same global z-order as the rest of
|
|
54
|
+
* the scene. Godot's `YSort`, generalised to any axis:
|
|
55
|
+
* new DepthSort({ key: (n) => n.pos.y }) // top-down overlap
|
|
56
|
+
* new DepthSort({ key: (n) => n.userDepth }) // iso gx+gy
|
|
57
|
+
* See docs/CONVENTIONS.md §Depth for the z-from-depth convention.
|
|
58
|
+
*/
|
|
59
|
+
export interface DepthSortConfig extends NodeConfig {
|
|
60
|
+
/** Depth of a child → its `z`. Larger = drawn on top (painter's order). */
|
|
61
|
+
key: (n: Node) => number;
|
|
62
|
+
/** Multiplier applied to the key before it becomes `z` (default 1). */
|
|
63
|
+
depthScale?: number;
|
|
64
|
+
}
|
|
65
|
+
export declare class DepthSort extends Node {
|
|
66
|
+
readonly type = "DepthSort";
|
|
67
|
+
key: (n: Node) => number;
|
|
68
|
+
depthScale: number;
|
|
69
|
+
/** DepthSort is a pure ordering container — its z assignments are view-only. */
|
|
70
|
+
cosmetic: boolean;
|
|
71
|
+
constructor(config: DepthSortConfig);
|
|
72
|
+
collectDraw(out: DrawCommand[], parentWorld?: Transform): void;
|
|
73
|
+
}
|
package/dist/scene/node.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Rng } from '../core/rng';
|
|
2
2
|
import type { Clock } from '../core/clock';
|
|
3
3
|
import type { InputState } from '../input/actions';
|
|
4
|
-
import { Signal } from '../core/events';
|
|
4
|
+
import { Signal, type EventBus } from '../core/events';
|
|
5
5
|
import { type Transform, type Vec2 } from '../core/math';
|
|
6
6
|
import type { DrawCommand } from '../render/commands';
|
|
7
7
|
/**
|
|
@@ -19,6 +19,33 @@ export interface WorldContext {
|
|
|
19
19
|
requestFree(node: Node): void;
|
|
20
20
|
/** Seconds elapsed in sim time. */
|
|
21
21
|
readonly time: number;
|
|
22
|
+
/**
|
|
23
|
+
* Canonical out-of-tree game state (hashed + snapshotted) — the same object
|
|
24
|
+
* as `world.state`, so behaviours read/write shared sim data without closing
|
|
25
|
+
* over the world or stashing it in module variables.
|
|
26
|
+
*/
|
|
27
|
+
readonly state: Record<string, unknown>;
|
|
28
|
+
/** The world event bus — emit/subscribe without a `world` closure. */
|
|
29
|
+
readonly events: EventBus<Record<string, unknown>>;
|
|
30
|
+
/** Design-space dimensions (what the camera letterboxes to). */
|
|
31
|
+
readonly width: number;
|
|
32
|
+
readonly height: number;
|
|
33
|
+
/** True while the world is paused (only `pauseMode: 'always'` subtrees update). */
|
|
34
|
+
readonly paused: boolean;
|
|
35
|
+
/** Sim-time multiplier (1 = realtime); `'always'` subtrees receive unscaled dt. */
|
|
36
|
+
readonly timeScale: number;
|
|
37
|
+
/**
|
|
38
|
+
* The active camera's world position + zoom, or null when none is current.
|
|
39
|
+
* Structural (no Camera2D import needed) — enough for parallax, culling, and
|
|
40
|
+
* screen-edge logic without a sibling search through the tree.
|
|
41
|
+
*/
|
|
42
|
+
camera(): {
|
|
43
|
+
pos: {
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
};
|
|
47
|
+
zoom: number;
|
|
48
|
+
} | null;
|
|
22
49
|
}
|
|
23
50
|
/** A composable update unit attached to a node (favour over deep inheritance). */
|
|
24
51
|
export interface Behavior {
|
|
@@ -29,6 +56,15 @@ export interface Behavior {
|
|
|
29
56
|
/** Optional tag for lookup/serialization. */
|
|
30
57
|
readonly kind?: string;
|
|
31
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* How a node behaves while the world is paused. `'inherit'` (default) takes the
|
|
61
|
+
* parent's effective mode (the root default is pausable), `'always'` keeps its
|
|
62
|
+
* subtree updating through a pause (pause menus, transitions), `'stopped'`
|
|
63
|
+
* halts its subtree even when the world is running.
|
|
64
|
+
*/
|
|
65
|
+
export type PauseMode = 'inherit' | 'always' | 'stopped';
|
|
66
|
+
/** A resolved pause mode — what 'inherit' collapses to as it flows down the tree. */
|
|
67
|
+
type EffectivePauseMode = 'pausable' | 'always' | 'stopped';
|
|
32
68
|
/** Deterministic id source — reset by the World on load so ids are reproducible. */
|
|
33
69
|
export declare function resetNodeIds(start?: number): void;
|
|
34
70
|
export interface NodeConfig {
|
|
@@ -56,6 +92,24 @@ export declare class Node {
|
|
|
56
92
|
* particles, tweened positions) never pollutes the canonical, verifiable state.
|
|
57
93
|
*/
|
|
58
94
|
cosmetic: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Pause behaviour for this subtree (see PauseMode). Serialized only when
|
|
97
|
+
* non-default, so existing trees keep their pinned hashes.
|
|
98
|
+
*/
|
|
99
|
+
pauseMode: PauseMode;
|
|
100
|
+
/**
|
|
101
|
+
* Screen-space overlay: this subtree ignores the camera — its transforms
|
|
102
|
+
* compose from IDENTITY (design-space coordinates) — and every command it
|
|
103
|
+
* emits is tagged `layer: 1` so HUD/overlay content always paints above the
|
|
104
|
+
* world, whatever the z values. Serialized only when true.
|
|
105
|
+
*/
|
|
106
|
+
screenSpace: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Optional local anchor: when set, rotation/scale pivot around this point in
|
|
109
|
+
* local space instead of the node origin (the local transform gains a
|
|
110
|
+
* trailing translation of −pivot). Serialized only when set.
|
|
111
|
+
*/
|
|
112
|
+
pivot?: Vec2;
|
|
59
113
|
parent: Node | null;
|
|
60
114
|
readonly children: Node[];
|
|
61
115
|
world: WorldContext | null;
|
|
@@ -70,19 +124,41 @@ export declare class Node {
|
|
|
70
124
|
private _ready;
|
|
71
125
|
private _freed;
|
|
72
126
|
constructor(config?: NodeConfig);
|
|
127
|
+
/** Shorthand for `pos.x` — reads and writes the same vector. */
|
|
128
|
+
get x(): number;
|
|
129
|
+
set x(v: number);
|
|
130
|
+
/** Shorthand for `pos.y` — reads and writes the same vector. */
|
|
131
|
+
get y(): number;
|
|
132
|
+
set y(v: number);
|
|
73
133
|
addChild<T extends Node>(child: T): T;
|
|
74
134
|
removeChild(child: Node): void;
|
|
75
135
|
/** Deferred free: runs exit hooks and detaches at the end of the step. */
|
|
76
136
|
free(): void;
|
|
137
|
+
/**
|
|
138
|
+
* Immediately exit + detach ALL children. Unlike `free()` this is NOT
|
|
139
|
+
* deferred — the children are gone when the call returns (safe here because
|
|
140
|
+
* it iterates a snapshot of the array). Use it to rebuild a container's
|
|
141
|
+
* contents wholesale; prefer `free()` for removals during an update.
|
|
142
|
+
*/
|
|
143
|
+
clearChildren(): void;
|
|
77
144
|
/** Depth-first find by name. */
|
|
78
145
|
find(name: string): Node | null;
|
|
146
|
+
/** Depth-first find of the first node (self included) that is an instance of `ctor` — typed. */
|
|
147
|
+
findOfType<T extends Node>(ctor: new (...args: never[]) => T): T | null;
|
|
79
148
|
/** All descendants (and self) of a given type tag. */
|
|
80
149
|
query(type: string, out?: Node[]): Node[];
|
|
81
150
|
addBehavior(b: Behavior): this;
|
|
82
151
|
signal<T = void>(name: string): Signal<T>;
|
|
83
152
|
emit<T>(name: string, payload: T): void;
|
|
84
153
|
enterTree(world: WorldContext): void;
|
|
85
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Advance this subtree. `dt` is the (time-scaled) step delta; `unscaledDt`
|
|
156
|
+
* and `paused` come from the world, and `parentMode` is the effective pause
|
|
157
|
+
* mode flowing down the tree. The walk always descends — a paused node just
|
|
158
|
+
* skips its callbacks — so an `'always'` descendant (pause menu, transition)
|
|
159
|
+
* keeps running inside a paused parent.
|
|
160
|
+
*/
|
|
161
|
+
updateTree(dt: number, unscaledDt?: number, parentMode?: EffectivePauseMode, paused?: boolean): void;
|
|
86
162
|
exitTree(): void;
|
|
87
163
|
get isFreed(): boolean;
|
|
88
164
|
localTransform(): Transform;
|
|
@@ -108,6 +184,9 @@ export interface SerializedNode {
|
|
|
108
184
|
scale: Vec2;
|
|
109
185
|
z: number;
|
|
110
186
|
visible: boolean;
|
|
187
|
+
/** Local rotation/scale anchor (absent when unset). */
|
|
188
|
+
pivot?: Vec2;
|
|
111
189
|
props: Record<string, unknown>;
|
|
112
190
|
children: SerializedNode[];
|
|
113
191
|
}
|
|
192
|
+
export {};
|
package/dist/scene/nodes.d.ts
CHANGED
|
@@ -8,11 +8,22 @@ import { Node, type NodeConfig } from './node';
|
|
|
8
8
|
* middle. Pass `anchor: 'topLeft'` to place `pos` at the top-left
|
|
9
9
|
* corner instead (the canvas mental model) — no half-size offset.
|
|
10
10
|
* - `circle` → CENTER-anchored: `pos` is the center.
|
|
11
|
+
* - `ellipse`→ CENTER-anchored: `pos` is the center; `rx`/`ry` are the semi-axes.
|
|
12
|
+
* - `arc` → CENTER-anchored: `pos` is the arc's center. Radians, clockwise
|
|
13
|
+
* from +x (y-down screen convention), running `start` → `end`.
|
|
14
|
+
* `sector: true` closes through the center (a pie slice).
|
|
11
15
|
* - `glyph` → CENTER-anchored (align 'center').
|
|
12
16
|
* - `poly` → LOCAL coordinates: `points` are relative to `pos` as-is (put your
|
|
13
17
|
* own 0,0 wherever you like).
|
|
14
18
|
* - `path` → LOCAL coordinates: `d` is drawn relative to `pos`.
|
|
19
|
+
* - `diamond`→ CENTER-anchored: a `w×h` rhombus (iso tile). `pos` is its middle.
|
|
20
|
+
* - `regularPoly` → CENTER-anchored: `sides`-gon of circumradius `r`. `rotation`
|
|
21
|
+
* (radians) turns it; 0 puts the first vertex straight up.
|
|
15
22
|
* (`Text` is anchored by its `align`, not centered — see the Text node.)
|
|
23
|
+
*
|
|
24
|
+
* `diamond` and `regularPoly` are pure sugar over `poly` (resolved at draw time,
|
|
25
|
+
* no renderer change) — they exist so an iso tile or a hex reads as intent, not
|
|
26
|
+
* as four hand-typed corner numbers.
|
|
16
27
|
*/
|
|
17
28
|
export type Shape = {
|
|
18
29
|
kind: 'rect';
|
|
@@ -23,6 +34,16 @@ export type Shape = {
|
|
|
23
34
|
} | {
|
|
24
35
|
kind: 'circle';
|
|
25
36
|
radius: number;
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'ellipse';
|
|
39
|
+
rx: number;
|
|
40
|
+
ry: number;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'arc';
|
|
43
|
+
radius: number;
|
|
44
|
+
start: number;
|
|
45
|
+
end: number;
|
|
46
|
+
sector?: boolean;
|
|
26
47
|
} | {
|
|
27
48
|
kind: 'poly';
|
|
28
49
|
points: number[];
|
|
@@ -34,7 +55,20 @@ export type Shape = {
|
|
|
34
55
|
kind: 'glyph';
|
|
35
56
|
char: string;
|
|
36
57
|
size: number;
|
|
58
|
+
} | {
|
|
59
|
+
kind: 'diamond';
|
|
60
|
+
w: number;
|
|
61
|
+
h: number;
|
|
62
|
+
} | {
|
|
63
|
+
kind: 'regularPoly';
|
|
64
|
+
sides: number;
|
|
65
|
+
r: number;
|
|
66
|
+
rotation?: number;
|
|
37
67
|
};
|
|
68
|
+
/** Local poly points for a center-anchored `w×h` diamond (top, right, bottom, left). */
|
|
69
|
+
export declare function diamondPoints(w: number, h: number): number[];
|
|
70
|
+
/** Local poly points for a center-anchored regular `sides`-gon of circumradius `r`. */
|
|
71
|
+
export declare function regularPolyPoints(sides: number, r: number, rotation?: number): number[];
|
|
38
72
|
export interface SpriteConfig extends NodeConfig, Paint {
|
|
39
73
|
shape: Shape;
|
|
40
74
|
}
|
|
@@ -42,6 +42,25 @@ export declare class Particles extends Node {
|
|
|
42
42
|
private gravity;
|
|
43
43
|
private drag;
|
|
44
44
|
private shrink;
|
|
45
|
+
private emitRate;
|
|
46
|
+
private emitAcc;
|
|
47
|
+
private emitStyle;
|
|
48
|
+
private emitOrigin;
|
|
49
|
+
/**
|
|
50
|
+
* Start (or retune) continuous emission at `ratePerSec` particles per second.
|
|
51
|
+
* `style.at` sets the node-local origin (default 0,0); the rest styles
|
|
52
|
+
* particles exactly like `burst()`. Omitting `style` keeps the previous
|
|
53
|
+
* emitter style (falling back to the `burst` preset if none was ever set).
|
|
54
|
+
*
|
|
55
|
+
* The signal form `emit(name, payload)` inherited from Node still works —
|
|
56
|
+
* the overload dispatches on the first argument's type.
|
|
57
|
+
*/
|
|
58
|
+
emit(ratePerSec: number, style?: ParticleStyle & {
|
|
59
|
+
at?: Vec2;
|
|
60
|
+
}): void;
|
|
61
|
+
emit<T>(name: string, payload: T): void;
|
|
62
|
+
/** Stop continuous emission. Live particles finish their lives naturally. */
|
|
63
|
+
stopEmit(): void;
|
|
45
64
|
protected onProcess(dt: number): void;
|
|
46
65
|
protected draw(out: DrawCommand[], world: Transform): void;
|
|
47
66
|
get liveCount(): number;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type Transform, type Vec2 } from '../core/math';
|
|
2
|
+
import type { DrawCommand } from '../render/commands';
|
|
3
|
+
import { Node, type NodeConfig } from './node';
|
|
4
|
+
export interface VerletChainConfig extends NodeConfig {
|
|
5
|
+
/** Number of segments (points = segments + 1). */
|
|
6
|
+
segments: number;
|
|
7
|
+
/** Total rest length of the chain, px. */
|
|
8
|
+
length: number;
|
|
9
|
+
/** Acceleration applied to free points, px/s² in WORLD space (+y is down). */
|
|
10
|
+
gravity?: Vec2;
|
|
11
|
+
/** Per-step velocity retention 0..1 (1 = undamped). Default 0.985. */
|
|
12
|
+
damping?: number;
|
|
13
|
+
/** Constraint-relaxation passes per step. Default 3. */
|
|
14
|
+
iterations?: number;
|
|
15
|
+
/** Pin the tail too (set `tailTarget` for where). Default false. */
|
|
16
|
+
pinTail?: boolean;
|
|
17
|
+
/** Stroke color for draw(). Default '#333'. */
|
|
18
|
+
stroke?: string;
|
|
19
|
+
/** Stroke width at the head, px. Default 3. */
|
|
20
|
+
strokeWidth?: number;
|
|
21
|
+
/** Taper the stroke toward the tail (draws per-segment polys). Default false. */
|
|
22
|
+
taper?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The rope node. Read `points` (node-local) to attach child sprites along the
|
|
26
|
+
* chain, `segmentAngle(i)` to orient them, and call `impulse(v)` to flick it.
|
|
27
|
+
*/
|
|
28
|
+
export declare class VerletChain extends Node {
|
|
29
|
+
readonly type: string;
|
|
30
|
+
gravity: Vec2;
|
|
31
|
+
damping: number;
|
|
32
|
+
iterations: number;
|
|
33
|
+
pinTail: boolean;
|
|
34
|
+
stroke: string;
|
|
35
|
+
strokeWidth: number;
|
|
36
|
+
taper: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Where the tail is pinned when `pinTail` is true, in NODE-LOCAL space
|
|
39
|
+
* (same space as `points`). null leaves the tail free even with pinTail set.
|
|
40
|
+
*/
|
|
41
|
+
tailTarget: Vec2 | null;
|
|
42
|
+
readonly segments: number;
|
|
43
|
+
/** Rest length of one segment. */
|
|
44
|
+
readonly segmentLength: number;
|
|
45
|
+
private px;
|
|
46
|
+
private py;
|
|
47
|
+
private prevx;
|
|
48
|
+
private prevy;
|
|
49
|
+
/** Node-local mirror of the sim points, updated after each step. */
|
|
50
|
+
private readonly local;
|
|
51
|
+
private initialized;
|
|
52
|
+
private impX;
|
|
53
|
+
private impY;
|
|
54
|
+
constructor(config: VerletChainConfig);
|
|
55
|
+
/**
|
|
56
|
+
* Chain points in NODE-LOCAL space, head first (`points[0]` ≈ {0,0}).
|
|
57
|
+
* The Vec2 objects are reused across steps — read, don't retain copies-by-ref.
|
|
58
|
+
*/
|
|
59
|
+
get points(): readonly Vec2[];
|
|
60
|
+
/**
|
|
61
|
+
* Direction of segment i (0..segments-1) in node-local space, radians
|
|
62
|
+
* (0 = +x, y-down screen convention). Map child sprites to segments with
|
|
63
|
+
* `sprite.pos = chain.points[i]; sprite.rotation = chain.segmentAngle(i)`.
|
|
64
|
+
*/
|
|
65
|
+
segmentAngle(i: number): number;
|
|
66
|
+
/** Flick the chain: add a velocity (world px/s) to every free point next step. */
|
|
67
|
+
impulse(v: Vec2): void;
|
|
68
|
+
protected onProcess(dt: number): void;
|
|
69
|
+
/**
|
|
70
|
+
* Stroked open poly of the chain. `points` are node-local and `world` is the
|
|
71
|
+
* self-inclusive world transform handed to draw(), so commands emit with it
|
|
72
|
+
* directly. With `taper`, each segment is its own 2-point poly at a linearly
|
|
73
|
+
* decreasing width (segments are few, so this stays cheap); otherwise one poly.
|
|
74
|
+
*/
|
|
75
|
+
protected draw(out: DrawCommand[], world: Transform): void;
|
|
76
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { GameHandle } from '../app/browser';
|
|
2
|
+
/** A virtual stick. Give it 4 directional actions, and/or an analog axis prefix. */
|
|
3
|
+
export interface TouchStick {
|
|
4
|
+
/** Directional actions held while tilted past the deadzone. Array = [up,down,left,right]. */
|
|
5
|
+
dirs?: [string, string, string, string] | {
|
|
6
|
+
up: string;
|
|
7
|
+
down: string;
|
|
8
|
+
left: string;
|
|
9
|
+
right: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Emit live analog axes `${prefix}x`, `${prefix}y` (−1..1) and `${prefix}angle`
|
|
13
|
+
* (radians), quantized to `buckets`. NOTE: these are LIVE axes (feel), not part
|
|
14
|
+
* of the input log — for replay-exact analog aim, thread them through
|
|
15
|
+
* `world.step(actions, axes)` (see docs/CONVENTIONS.md §Pointer).
|
|
16
|
+
*/
|
|
17
|
+
prefix?: string;
|
|
18
|
+
/** Axis quantization levels (default 32). */
|
|
19
|
+
buckets?: number;
|
|
20
|
+
/** Dead-zone as a fraction of the stick radius, 0..1 (default 0.28). */
|
|
21
|
+
deadzone?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface TouchButton {
|
|
24
|
+
action: string;
|
|
25
|
+
/** Glyph/label drawn on the button (default the action name). */
|
|
26
|
+
label?: string;
|
|
27
|
+
/** Hold the action while pressed (default true); false = one-shot tap via press(). */
|
|
28
|
+
hold?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface TouchControlsLayout {
|
|
31
|
+
/** Left-corner stick (movement). Array shorthand → [up,down,left,right]. */
|
|
32
|
+
left?: TouchStick | [string, string, string, string];
|
|
33
|
+
/** Right-corner stick (aim). */
|
|
34
|
+
right?: TouchStick | [string, string, string, string];
|
|
35
|
+
/** Action buttons, stacked at the lower-right. */
|
|
36
|
+
buttons?: TouchButton[];
|
|
37
|
+
/** Only mount when the device has a coarse (touch) pointer (default true). */
|
|
38
|
+
touchOnly?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare class TouchControls {
|
|
41
|
+
private handle;
|
|
42
|
+
private root;
|
|
43
|
+
private disposers;
|
|
44
|
+
/** Every action this control set can hold — released together on dispose/reset. */
|
|
45
|
+
private owned;
|
|
46
|
+
constructor(handle: GameHandle, layout: TouchControlsLayout);
|
|
47
|
+
private addStick;
|
|
48
|
+
private addButtons;
|
|
49
|
+
private hold;
|
|
50
|
+
dispose(): void;
|
|
51
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type GameHandle } from '../app/browser';
|
|
2
|
+
import type { GameDefinition } from '../app/game';
|
|
3
|
+
export interface DomHarness {
|
|
4
|
+
/** The live game handle (world, input, pointer, renderer, viewport…). */
|
|
5
|
+
handle: GameHandle;
|
|
6
|
+
/** Press a finger/pointer down at design (x,y) with a stable id (default 1). */
|
|
7
|
+
touchDown(x: number, y: number, id?: number): void;
|
|
8
|
+
/** Move a tracked finger/pointer to design (x,y). */
|
|
9
|
+
touchMove(x: number, y: number, id?: number): void;
|
|
10
|
+
/** Lift a finger/pointer (default id 1). */
|
|
11
|
+
touchUp(id?: number): void;
|
|
12
|
+
/**
|
|
13
|
+
* Sample the live pointer + keyboard input into the sim, then run `n` fixed
|
|
14
|
+
* steps — the same order `runBrowser` uses, minus wall-clock. Held/virtual
|
|
15
|
+
* actions and quantized axes flow through exactly as in production.
|
|
16
|
+
*/
|
|
17
|
+
step(n?: number, axes?: Record<string, number>): void;
|
|
18
|
+
dispose(): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Boot a game's real host wiring under the DOM for a host-layer test. The loop is
|
|
22
|
+
* frozen (`isHeld`) so stepping is deterministic and test-driven — call `step()`
|
|
23
|
+
* after firing input. Attach a `TouchControls` to `harness.handle` to prove a
|
|
24
|
+
* virtual gamepad end-to-end.
|
|
25
|
+
*/
|
|
26
|
+
export declare function bootDom(def: GameDefinition, mount?: HTMLElement): DomHarness;
|