hayao 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/bin/create-hayao.mjs +1 -1
- package/dist/anim/blend.d.ts +68 -0
- package/dist/anim/clip.d.ts +87 -0
- package/dist/anim/ik.d.ts +40 -0
- package/dist/anim/skeleton.d.ts +64 -0
- 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 +20 -1
- package/dist/index.js +4159 -979
- 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 +13 -0
- package/dist/render/commands.d.ts +55 -2
- package/dist/render/lightRun.d.ts +35 -0
- 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/svgString.d.ts +8 -3
- package/dist/render/webgl.d.ts +176 -0
- package/dist/scene/clipPlayer.d.ts +58 -0
- package/dist/scene/ikTarget.d.ts +25 -0
- package/dist/scene/iso.d.ts +73 -0
- package/dist/scene/light.d.ts +80 -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/shadow2d.d.ts +25 -0
- package/dist/scene/skeletonDebug.d.ts +28 -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/verify/gates.d.ts +25 -0
- package/dist/world.d.ts +72 -7
- package/docs/API.md +138 -27
- package/docs/CONVENTIONS.md +346 -2
- package/docs/EMBED.md +5 -0
- package/docs/QUICKSTART.md +13 -1
- package/docs/VERIFICATION.md +34 -4
- package/package.json +2 -1
package/dist/input/actions.d.ts
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* action name → physical bindings (KeyboardEvent.code strings, or pointer
|
|
3
|
+
* buttons `mouse.left` / `mouse.right` / `mouse.middle`). Readonly on both
|
|
4
|
+
* levels so `as const` maps type-check without casts.
|
|
5
|
+
*/
|
|
6
|
+
export type InputMap = Readonly<Record<string, readonly string[]>>;
|
|
7
|
+
/** Per-frame analog axis values (already quantized). Recorded into the log + hash. */
|
|
8
|
+
export type AxisFrame = Record<string, number>;
|
|
3
9
|
export declare class InputState {
|
|
4
10
|
private down;
|
|
5
11
|
private prev;
|
|
6
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Analog axes (e.g. from pointer or gamepad), sampled per step. By default
|
|
14
|
+
* these are LIVE host samples — NOT hashed or logged (see PointerSource).
|
|
15
|
+
* Axes passed to `beginFrame`'s second argument are the exception: they are
|
|
16
|
+
* *logged* (enter `getState()` → hash, and the input log), so quantized analog
|
|
17
|
+
* aim replays bit-exactly. Both live and logged values read back via `axis()`.
|
|
18
|
+
*/
|
|
7
19
|
readonly axes: Map<string, number>;
|
|
8
|
-
/**
|
|
9
|
-
|
|
20
|
+
/** The subset of axes that are part of the deterministic log + hash this frame. */
|
|
21
|
+
private logged;
|
|
22
|
+
/**
|
|
23
|
+
* Action names some source has declared it can produce (null until the first
|
|
24
|
+
* declareActions call). Headless worlds with no sources never declare, so
|
|
25
|
+
* they never warn — the typo guard only arms once a host wires real input.
|
|
26
|
+
*/
|
|
27
|
+
private declared;
|
|
28
|
+
/** Unknown names already warned about (warn ONCE per name). */
|
|
29
|
+
private warned;
|
|
30
|
+
/**
|
|
31
|
+
* Register action names a source (or input map) can produce. The browser
|
|
32
|
+
* driver declares the keyboard map's actions and the pointer's `mouse.*`
|
|
33
|
+
* buttons; custom sources should declare theirs too. Once at least one
|
|
34
|
+
* declaration has happened, querying an undeclared action name via
|
|
35
|
+
* `isDown`/`justPressed`/`justReleased` logs a console.warn ONCE per name —
|
|
36
|
+
* catching `isDown('jmup')` typos that would otherwise fail silently.
|
|
37
|
+
*/
|
|
38
|
+
declareActions(names: Iterable<string>): void;
|
|
39
|
+
/** Warn-once guard for querying an action no source has declared. O(1). */
|
|
40
|
+
private checkDeclared;
|
|
41
|
+
/**
|
|
42
|
+
* Engine: set which actions are down this step (from live input or replay).
|
|
43
|
+
* `axes` (optional) are QUANTIZED analog values that become part of the log
|
|
44
|
+
* and hash for this frame — use `snapAxis`/`quantizeAngle` at the host edge so
|
|
45
|
+
* replay and lockstep netplay reproduce analog aim exactly.
|
|
46
|
+
*/
|
|
47
|
+
beginFrame(actionsDown: Iterable<string>, axes?: AxisFrame): void;
|
|
10
48
|
isDown(action: string): boolean;
|
|
11
49
|
justPressed(action: string): boolean;
|
|
12
50
|
justReleased(action: string): boolean;
|
|
@@ -16,25 +54,41 @@ export declare class InputState {
|
|
|
16
54
|
getState(): {
|
|
17
55
|
down: string[];
|
|
18
56
|
prev: string[];
|
|
57
|
+
axes?: Array<[string, number]>;
|
|
19
58
|
};
|
|
20
59
|
setState(s: {
|
|
21
60
|
down: string[];
|
|
22
61
|
prev: string[];
|
|
62
|
+
axes?: Array<[string, number]>;
|
|
23
63
|
}): void;
|
|
24
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Quantize an analog value into `buckets` levels across `[min,max]` and return
|
|
67
|
+
* the SNAPPED value (deterministic — same input → same output on every engine).
|
|
68
|
+
* Feed the result through `beginFrame`'s axes so analog input is replay-exact.
|
|
69
|
+
*/
|
|
70
|
+
export declare function snapAxis(value: number, buckets: number, min?: number, max?: number): number;
|
|
71
|
+
/** Snap an angle (radians) to the nearest of `buckets` evenly-spaced headings. */
|
|
72
|
+
export declare function quantizeAngle(radians: number, buckets: number): number;
|
|
25
73
|
/** Map a set of raw key codes to the set of actions they trigger. */
|
|
26
74
|
export declare function keysToActions(map: InputMap, keysDown: Set<string>): string[];
|
|
27
75
|
/** Records the per-frame action-down sets → a compact, replayable input log. */
|
|
28
76
|
export declare class InputRecorder {
|
|
29
77
|
private frames;
|
|
30
|
-
|
|
78
|
+
private axes;
|
|
79
|
+
private anyAxes;
|
|
80
|
+
record(actionsDown: string[], axes?: AxisFrame): void;
|
|
31
81
|
get length(): number;
|
|
32
82
|
toLog(): InputLog;
|
|
33
83
|
}
|
|
34
84
|
export interface InputLog {
|
|
35
85
|
frames: string[][];
|
|
86
|
+
/** Optional per-frame quantized analog axes, aligned to `frames` (absent = none). */
|
|
87
|
+
axes?: Array<AxisFrame | undefined>;
|
|
36
88
|
}
|
|
37
89
|
/** Replays a recorded log: returns the action set for frame index i. */
|
|
38
90
|
export declare function frameActions(log: InputLog, i: number): string[];
|
|
91
|
+
/** The logged analog axes for frame index i, or undefined if none were recorded. */
|
|
92
|
+
export declare function frameAxes(log: InputLog, i: number): AxisFrame | undefined;
|
|
39
93
|
/** A default set of common bindings games can start from. */
|
|
40
94
|
export declare const DEFAULT_INPUT_MAP: InputMap;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type InputState } from './actions';
|
|
2
|
+
import type { KeyboardSource } from './source';
|
|
3
|
+
/** A mapping from Gamepad.buttons indices to action names. */
|
|
4
|
+
export type GamepadMap = Record<number, string>;
|
|
5
|
+
export interface GamepadSourceOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Gamepad index (0–3). Default 0.
|
|
8
|
+
* Tip: listen for the 'gamepadconnected' event to discover the right index;
|
|
9
|
+
* the first connected controller is always index 0.
|
|
10
|
+
*/
|
|
11
|
+
index?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Bucket count for quantizing analog stick values (±1 range).
|
|
14
|
+
* Higher = finer precision, larger axes log. Default 128 (~0.016 resolution).
|
|
15
|
+
*/
|
|
16
|
+
stickBuckets?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Bucket count for quantizing trigger values (0..1 range).
|
|
19
|
+
* Default 64 (~0.016 resolution).
|
|
20
|
+
*/
|
|
21
|
+
triggerBuckets?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Deadzone magnitude radius (0..1) applied to each stick independently before
|
|
24
|
+
* quantization. Values below this threshold are snapped to zero; above it the
|
|
25
|
+
* range is remapped to 0..1 so the output always uses the full scale.
|
|
26
|
+
* Default 0.12 (typical hardware drift).
|
|
27
|
+
*/
|
|
28
|
+
deadzone?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Gamepad button index → action name. Pressed buttons call
|
|
31
|
+
* `keyboard.setHeld(action, on)` so they enter the SAME deterministic action
|
|
32
|
+
* log as keys and are covered by record/replay + netplay.
|
|
33
|
+
* Defaults to DEFAULT_GAMEPAD_MAP (face buttons + d-pad mapped to common actions).
|
|
34
|
+
*/
|
|
35
|
+
buttonMap?: GamepadMap;
|
|
36
|
+
/**
|
|
37
|
+
* The KeyboardSource to drive actions through. Required for button→action
|
|
38
|
+
* routing. Typically `handle.input` from the GameHandle returned by runBrowser.
|
|
39
|
+
*/
|
|
40
|
+
keyboard?: KeyboardSource;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Axis keys written each step:
|
|
44
|
+
*
|
|
45
|
+
* gamepad.lx / gamepad.ly — left stick, ±1 (after deadzone + quantize)
|
|
46
|
+
* gamepad.rx / gamepad.ry — right stick, ±1
|
|
47
|
+
* gamepad.lt / gamepad.rt — left / right trigger, 0..1
|
|
48
|
+
* gamepad.dpad.x — d-pad horizontal, −1 / 0 / +1
|
|
49
|
+
* gamepad.dpad.y — d-pad vertical, −1 / 0 / +1 (−1 = up)
|
|
50
|
+
*
|
|
51
|
+
* Read them in the sim with `world.input.axis('gamepad.lx')`.
|
|
52
|
+
*
|
|
53
|
+
* Determinism note: axes written by sample() are live host samples and are NOT
|
|
54
|
+
* part of the string input log or world.hash() by default. Buttons routed
|
|
55
|
+
* through a KeyboardSource DO enter the log. For bit-exact analog replay, pass
|
|
56
|
+
* quantized axes as the second argument to world.step(actions, axes) — they
|
|
57
|
+
* then enter getState() → hash and InputRecorder.
|
|
58
|
+
*/
|
|
59
|
+
export declare class GamepadSource {
|
|
60
|
+
private index;
|
|
61
|
+
private stickBuckets;
|
|
62
|
+
private triggerBuckets;
|
|
63
|
+
private deadzone;
|
|
64
|
+
private buttonMap;
|
|
65
|
+
private keyboard;
|
|
66
|
+
/** Track which buttons were pressed last frame to detect edge transitions. */
|
|
67
|
+
private prevPressed;
|
|
68
|
+
constructor(opts?: GamepadSourceOptions);
|
|
69
|
+
/**
|
|
70
|
+
* Write the current gamepad state into InputState axes (quantized). Call once
|
|
71
|
+
* per fixed step, before world.advance() — the browser driver does this via
|
|
72
|
+
* the `sources` option in RunOptions. See also: runBrowser, GameHandle.
|
|
73
|
+
*/
|
|
74
|
+
sample(input: InputState): void;
|
|
75
|
+
/**
|
|
76
|
+
* Release all held actions and clean up. Call when removing the source or
|
|
77
|
+
* switching gamepads.
|
|
78
|
+
*/
|
|
79
|
+
dispose(): void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Standard Gamepad API button mapping for a "Standard Gamepad" layout
|
|
83
|
+
* (Xbox / PlayStation / Switch Pro controllers).
|
|
84
|
+
*
|
|
85
|
+
* Index Xbox PlayStation Action
|
|
86
|
+
* 0 A Cross confirm
|
|
87
|
+
* 1 B Circle cancel
|
|
88
|
+
* 2 X Square action2
|
|
89
|
+
* 3 Y Triangle action
|
|
90
|
+
* 8 Select/View Share restart (hold to restart)
|
|
91
|
+
* 9 Start/Menu Options confirm (pause menu OK)
|
|
92
|
+
* 12 D-pad up up
|
|
93
|
+
* 13 D-pad down down
|
|
94
|
+
* 14 D-pad left left
|
|
95
|
+
* 15 D-pad right right
|
|
96
|
+
*
|
|
97
|
+
* Triggers (6/7) and bumpers (4/5) are intentionally omitted — their analog
|
|
98
|
+
* values are exposed as gamepad.lt / gamepad.rt axes instead, and games can
|
|
99
|
+
* add them to a custom map if they want discrete actions.
|
|
100
|
+
*/
|
|
101
|
+
export declare const DEFAULT_GAMEPAD_MAP: GamepadMap;
|
package/dist/input/source.d.ts
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { type InputMap, type InputState } from './actions';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal contract for a host-side input source. Implement this to feed any
|
|
4
|
+
* hardware (gamepad, MIDI, accelerometer, on-screen controls) into the sim's
|
|
5
|
+
* InputState.axes without touching the engine internals.
|
|
6
|
+
*
|
|
7
|
+
* The engine samples each registered source once per fixed step, BEFORE
|
|
8
|
+
* world.advance(). Register via RunOptions.sources or GameHandle.addSource().
|
|
9
|
+
*/
|
|
10
|
+
export interface InputSource {
|
|
11
|
+
/** Write quantized values into input.axes. Called before each world.advance(). */
|
|
12
|
+
sample(input: InputState): void;
|
|
13
|
+
/** Optional cleanup (remove listeners, release held actions). */
|
|
14
|
+
dispose?(): void;
|
|
15
|
+
}
|
|
2
16
|
import type { Vec2 } from '../core/math';
|
|
3
17
|
export declare class KeyboardSource {
|
|
4
18
|
private keysDown;
|
|
5
19
|
/** Virtual action taps (from DOM buttons etc.) pending consumption by a step. */
|
|
6
20
|
private pressed;
|
|
21
|
+
/** Sustained virtual actions (held on-screen controls) — persist until released. */
|
|
22
|
+
private held;
|
|
7
23
|
private map;
|
|
8
24
|
private target;
|
|
9
25
|
private onDown;
|
|
@@ -12,6 +28,8 @@ export declare class KeyboardSource {
|
|
|
12
28
|
constructor(map: InputMap, target?: Document | HTMLElement);
|
|
13
29
|
/** The actions currently held down, as a stable sorted array. */
|
|
14
30
|
currentActions(): string[];
|
|
31
|
+
/** The action names this source's map can produce (for InputState.declareActions). */
|
|
32
|
+
actionNames(): string[];
|
|
15
33
|
/**
|
|
16
34
|
* Virtually tap an action (DOM button, touch control). The tap is held until
|
|
17
35
|
* at least one fixed step has sampled it — the driver calls clearPressed()
|
|
@@ -21,6 +39,20 @@ export declare class KeyboardSource {
|
|
|
21
39
|
press(action: string): void;
|
|
22
40
|
/** Consume pending virtual taps (driver-called after ≥1 step ran). */
|
|
23
41
|
clearPressed(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Sustain an action for as long as an on-screen control is held. Unlike
|
|
44
|
+
* `press()` (an edge-shaped tap cleared after one step), a held action models
|
|
45
|
+
* STATE: it stays in `currentActions()` every step until `setHeld(action,
|
|
46
|
+
* false)` / `releaseHeld(action)`. This is what a virtual joystick or
|
|
47
|
+
* hold-to-fire button wants — set it on drag-start, clear it on release, no
|
|
48
|
+
* re-press-per-frame. Held actions still flow through the same deterministic
|
|
49
|
+
* action set as keys.
|
|
50
|
+
*/
|
|
51
|
+
setHeld(action: string, on?: boolean): void;
|
|
52
|
+
/** Release a sustained action (sugar for `setHeld(action, false)`). */
|
|
53
|
+
releaseHeld(action: string): void;
|
|
54
|
+
/** Drop all sustained actions (e.g. on blur / control teardown). */
|
|
55
|
+
clearHeld(): void;
|
|
24
56
|
setMap(map: InputMap): void;
|
|
25
57
|
dispose(): void;
|
|
26
58
|
}
|
|
@@ -33,19 +65,45 @@ export interface PointerReadout {
|
|
|
33
65
|
down: boolean;
|
|
34
66
|
/** Has the pointer ever produced an event (else x/y are a neutral 0,0)? */
|
|
35
67
|
active: boolean;
|
|
68
|
+
/** Stable per-touch id (from PointerEvent.pointerId). Absent on the primary hover readout. */
|
|
69
|
+
id?: number;
|
|
36
70
|
}
|
|
37
71
|
/** What a PointerSource needs from a renderer: the DOM node + the design mapper. */
|
|
38
72
|
export interface PointerTarget {
|
|
39
73
|
readonly element?: HTMLElement | SVGElement;
|
|
40
74
|
toDesign?(clientX: number, clientY: number): Vec2;
|
|
41
75
|
}
|
|
76
|
+
export interface PointerSourceOptions {
|
|
77
|
+
/**
|
|
78
|
+
* Allow the browser's context menu on the attached element. Default false —
|
|
79
|
+
* the menu is suppressed (preventDefault on 'contextmenu') so right-click is
|
|
80
|
+
* usable as game input.
|
|
81
|
+
*/
|
|
82
|
+
contextMenu?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Route pointer buttons into the deterministic action pipeline: while a
|
|
85
|
+
* button is held, `sample()` calls `keyboard.setHeld('mouse.left' |
|
|
86
|
+
* 'mouse.right' | 'mouse.middle')` so buttons enter the SAME actionsDown log
|
|
87
|
+
* as keys — `justPressed('mouse.right')` works, replays stay exact, and an
|
|
88
|
+
* inputMap can bind them (`shield: ['mouse.right']`). The browser driver
|
|
89
|
+
* wires this to the GameHandle's KeyboardSource automatically.
|
|
90
|
+
*/
|
|
91
|
+
keyboard?: KeyboardSource;
|
|
92
|
+
}
|
|
93
|
+
/** The pointer-button action names PointerSource can hold (via its keyboard). */
|
|
94
|
+
export declare const MOUSE_ACTIONS: readonly ["mouse.left", "mouse.right", "mouse.middle"];
|
|
42
95
|
/**
|
|
43
96
|
* Continuous pointer / touch input — the sibling of KeyboardSource for games
|
|
44
97
|
* driven by *where* the cursor is (slice, aim, drag, point-and-click, placement).
|
|
45
98
|
* It listens on the renderer's canvas, converts every event to DESIGN space via
|
|
46
99
|
* `renderer.toDesign`, and each fixed step writes the sample into InputState's
|
|
47
|
-
* analog axes: `pointer.x`, `pointer.y`, `pointer.down` (1/0)
|
|
48
|
-
*
|
|
100
|
+
* analog axes: `pointer.x`, `pointer.y`, `pointer.down` (1/0), plus
|
|
101
|
+
* `pointer.right` / `pointer.middle` (1/0) for the secondary mouse buttons
|
|
102
|
+
* (the context menu is suppressed by default so right-click is usable — see
|
|
103
|
+
* PointerSourceOptions). Sim code then reads `world.input.axis('pointer.x')`
|
|
104
|
+
* — no out-of-engine letterbox glue. With `keyboard` wired, buttons also enter
|
|
105
|
+
* the action pipeline as `mouse.left/right/middle` (bindable in an inputMap,
|
|
106
|
+
* `justPressed`-able, replay-exact).
|
|
49
107
|
*
|
|
50
108
|
* Determinism note: axes are host-sampled live input and are NOT part of the
|
|
51
109
|
* string input log or the world hash. `sample()` QUANTIZES design coords to a
|
|
@@ -53,23 +111,46 @@ export interface PointerTarget {
|
|
|
53
111
|
* the sim only ever sees the quantized values, live or replayed. For lockstep
|
|
54
112
|
* netplay, still prefer discrete actions (map a tap to a key via
|
|
55
113
|
* KeyboardSource.press, or snap position to a grid cell). See docs/CONVENTIONS.md.
|
|
114
|
+
*
|
|
115
|
+
* Multitouch: `read()`/`sample()` describe the PRIMARY pointer (mouse or the
|
|
116
|
+
* most-recent finger) and are byte-identical to the single-pointer past. For
|
|
117
|
+
* dual-stick / multi-finger schemes, read every live touch via `readAll()` —
|
|
118
|
+
* each carries its stable `id` so you can tell which finger is which.
|
|
56
119
|
*/
|
|
57
120
|
export declare class PointerSource {
|
|
58
121
|
private clientX;
|
|
59
122
|
private clientY;
|
|
60
123
|
private isDown;
|
|
124
|
+
private rightDown;
|
|
125
|
+
private middleDown;
|
|
61
126
|
private seen;
|
|
127
|
+
/** Live touches by pointerId (a pressed finger/button); excludes hover-only mouse. */
|
|
128
|
+
private touches;
|
|
62
129
|
private target;
|
|
63
130
|
private el;
|
|
131
|
+
private keyboard;
|
|
64
132
|
private onMove;
|
|
65
133
|
private onDown;
|
|
66
134
|
private onUp;
|
|
67
|
-
|
|
135
|
+
private onCtx;
|
|
136
|
+
constructor(target: PointerTarget, opts?: PointerSourceOptions);
|
|
68
137
|
/** The current pointer position in design space + press state. */
|
|
69
138
|
read(): PointerReadout;
|
|
139
|
+
/**
|
|
140
|
+
* Every live (pressed) touch in DESIGN space, ordered by ascending id so the
|
|
141
|
+
* array is stable to iterate. Empty when nothing is pressed. This is the
|
|
142
|
+
* multitouch channel — a dual-stick game reads `left = touches[0]`, etc., or
|
|
143
|
+
* partitions by screen half.
|
|
144
|
+
*/
|
|
145
|
+
readAll(): PointerReadout[];
|
|
70
146
|
/**
|
|
71
147
|
* Write the current sample into an InputState's axes as pointer.x / pointer.y /
|
|
72
|
-
* pointer.down
|
|
148
|
+
* pointer.down, plus pointer.right / pointer.middle (0/1) for the secondary
|
|
149
|
+
* buttons. Call once per step (the browser driver does this before advance).
|
|
150
|
+
*
|
|
151
|
+
* When a keyboard is wired (PointerSourceOptions.keyboard), buttons are ALSO
|
|
152
|
+
* held as actions `mouse.left` / `mouse.right` / `mouse.middle` so they flow
|
|
153
|
+
* through the same deterministic actionsDown log as keys.
|
|
73
154
|
*/
|
|
74
155
|
sample(input: InputState): void;
|
|
75
156
|
dispose(): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Resumes after `seconds` of accumulated sim time. */
|
|
2
|
+
export declare function sleep(seconds: number): Wait;
|
|
3
|
+
/** Resumes on the first step where `cond()` is true (checked once per step). */
|
|
4
|
+
export declare function waitFor(cond: () => boolean): Wait;
|
|
5
|
+
/** Resumes on the next step — yields one frame (e.g. to let a spawn render). */
|
|
6
|
+
export declare function nextStep(): Wait;
|
|
7
|
+
/**
|
|
8
|
+
* Resumes when ANY of the waits completes; the yield expression evaluates to
|
|
9
|
+
* the winning index (ties resolve to the lowest index — deterministic).
|
|
10
|
+
*/
|
|
11
|
+
export declare function race(...waits: Wait[]): Wait;
|
|
12
|
+
/** Resumes when ALL of the waits have completed. */
|
|
13
|
+
export declare function all(...waits: Wait[]): Wait;
|
|
14
|
+
interface SleepWait {
|
|
15
|
+
kind: 'sleep';
|
|
16
|
+
left: number;
|
|
17
|
+
}
|
|
18
|
+
interface CondWait {
|
|
19
|
+
kind: 'cond';
|
|
20
|
+
cond: () => boolean;
|
|
21
|
+
/** Memo so a satisfied condition is never re-evaluated (inside all()). */
|
|
22
|
+
met?: boolean;
|
|
23
|
+
}
|
|
24
|
+
interface NextWait {
|
|
25
|
+
kind: 'next';
|
|
26
|
+
passed?: boolean;
|
|
27
|
+
}
|
|
28
|
+
interface RaceWait {
|
|
29
|
+
kind: 'race';
|
|
30
|
+
waits: Wait[];
|
|
31
|
+
}
|
|
32
|
+
interface AllWait {
|
|
33
|
+
kind: 'all';
|
|
34
|
+
waits: Wait[];
|
|
35
|
+
}
|
|
36
|
+
/** What a coroutine may yield. A `CoroutineHandle` yield joins that runner. */
|
|
37
|
+
export type Wait = SleepWait | CondWait | NextWait | RaceWait | AllWait | CoroutineHandle;
|
|
38
|
+
/** A started coroutine. `done` flips when it returns, throws, or is stopped. */
|
|
39
|
+
export interface CoroutineHandle {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly done: boolean;
|
|
42
|
+
/** Kill the runner (its generator's finally blocks run via `return()`). */
|
|
43
|
+
stop(): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A deterministic scheduler for many concurrent coroutines. Owns no clock —
|
|
47
|
+
* call `step(dt)` from wherever your game advances (typically a brain node's
|
|
48
|
+
* `onUpdate`), so coroutine time IS sim time.
|
|
49
|
+
*/
|
|
50
|
+
export declare class Coroutines {
|
|
51
|
+
private runners;
|
|
52
|
+
/** Started since the last step boundary; adopted at the top of the next step. */
|
|
53
|
+
private pending;
|
|
54
|
+
private nextId;
|
|
55
|
+
/**
|
|
56
|
+
* Register a coroutine. Its body runs (up to the first yield) on the NEXT
|
|
57
|
+
* `step()` — never immediately, and never mid-step. Pass a `name` so a
|
|
58
|
+
* thrown generator warns something better than "co3".
|
|
59
|
+
*/
|
|
60
|
+
start(gen: () => Generator<Wait, void, number>, name?: string): CoroutineHandle;
|
|
61
|
+
/** Advance every live runner by one fixed step, in insertion order. */
|
|
62
|
+
step(dt: number): void;
|
|
63
|
+
/** Stop every runner (including ones not yet adopted). */
|
|
64
|
+
stopAll(): void;
|
|
65
|
+
/** Live runner count (started-but-not-yet-stepped ones included). */
|
|
66
|
+
get active(): number;
|
|
67
|
+
}
|
|
68
|
+
export {};
|
package/dist/render/canvas.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type Renderer, type RendererConfig } from './renderer';
|
|
1
|
+
import type { DrawCommand } from './commands';
|
|
2
|
+
import { type Renderer, type RendererConfig, type Viewport } from './renderer';
|
|
3
3
|
import type { Vec2 } from '../core/math';
|
|
4
4
|
export declare class Canvas2DRenderer implements Renderer {
|
|
5
5
|
readonly width: number;
|
|
@@ -12,13 +12,9 @@ export declare class Canvas2DRenderer implements Renderer {
|
|
|
12
12
|
mount(parent: HTMLElement): void;
|
|
13
13
|
private resize;
|
|
14
14
|
draw(commands: DrawCommand[]): void;
|
|
15
|
-
/** Resolve a fill: a gradient (mapped to the shape's bbox) or a solid color. */
|
|
16
|
-
private fillFor;
|
|
17
|
-
private stroke;
|
|
18
|
-
private paint;
|
|
19
|
-
private roundRect;
|
|
20
15
|
get element(): HTMLCanvasElement;
|
|
21
16
|
/** Map a pointer event's clientX/Y into design space (undoes the letterbox). */
|
|
22
17
|
toDesign(clientX: number, clientY: number): Vec2;
|
|
18
|
+
viewport(): Viewport;
|
|
23
19
|
dispose(): void;
|
|
24
20
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type DrawCommand } from './commands';
|
|
2
|
+
type Ctx2D = CanvasRenderingContext2D;
|
|
3
|
+
/**
|
|
4
|
+
* Paint a sorted display list onto an existing Canvas2D context at `scale`
|
|
5
|
+
* (device pixel ratio). The caller is responsible for sizing the canvas to
|
|
6
|
+
* `width * scale` × `height * scale` before calling.
|
|
7
|
+
*
|
|
8
|
+
* A lighting run (commands at `LAYER_LIGHT`) is composited through an offscreen
|
|
9
|
+
* buffer between the world and HUD passes: world → light buffer (multiply) →
|
|
10
|
+
* HUD. When no light run is present this is exactly the old single-pass paint.
|
|
11
|
+
*/
|
|
12
|
+
export declare function drawToCanvas2D(ctx: Ctx2D, commands: DrawCommand[], width: number, height: number, background: string, scale: number): void;
|
|
13
|
+
export {};
|
|
@@ -45,6 +45,19 @@ export interface Paint {
|
|
|
45
45
|
gradient?: Gradient;
|
|
46
46
|
/** A soft outer glow / drop shadow applied to the shape. */
|
|
47
47
|
shadow?: Shadow;
|
|
48
|
+
/**
|
|
49
|
+
* Dash pattern for strokes, in local px (`[dash, gap, …]` — same as
|
|
50
|
+
* Canvas2D `setLineDash` / SVG `stroke-dasharray`). Omit for solid lines.
|
|
51
|
+
*/
|
|
52
|
+
lineDash?: number[];
|
|
53
|
+
/**
|
|
54
|
+
* Blend mode for compositing this command over what is already painted — the
|
|
55
|
+
* Canvas2D/SVG-expressible intersection: `'multiply'` darkens (ambient base,
|
|
56
|
+
* shadow quads), `'screen'` brightens (a light pool). Omit for normal
|
|
57
|
+
* source-over. Consumed by the lighting run (see render/lightRun.ts); Canvas
|
|
58
|
+
* maps it to `globalCompositeOperation`, SVG to `mix-blend-mode`.
|
|
59
|
+
*/
|
|
60
|
+
blend?: 'multiply' | 'screen';
|
|
48
61
|
}
|
|
49
62
|
export type TextAlign = 'left' | 'center' | 'right';
|
|
50
63
|
interface Base extends Paint {
|
|
@@ -52,6 +65,13 @@ interface Base extends Paint {
|
|
|
52
65
|
transform: Transform;
|
|
53
66
|
/** Painter's-order key; ties broken by tree order. Default 0. */
|
|
54
67
|
z: number;
|
|
68
|
+
/**
|
|
69
|
+
* Render layer: commands sort by layer FIRST, then z, then tree order.
|
|
70
|
+
* Layer 0 (default) is the world; layer 1 is the screen-space overlay pass
|
|
71
|
+
* (HUD, transitions) — set automatically for `Node.screenSpace` subtrees, so
|
|
72
|
+
* overlays always paint above world content regardless of z values.
|
|
73
|
+
*/
|
|
74
|
+
layer?: number;
|
|
55
75
|
/**
|
|
56
76
|
* Transient view chrome — a drifting popup, particle, or tween that lives for
|
|
57
77
|
* a moment and is never something the player reads for meaning. Layout lints
|
|
@@ -75,6 +95,28 @@ export interface CircleCommand extends Base {
|
|
|
75
95
|
cy: number;
|
|
76
96
|
radius: number;
|
|
77
97
|
}
|
|
98
|
+
export interface EllipseCommand extends Base {
|
|
99
|
+
kind: 'ellipse';
|
|
100
|
+
cx: number;
|
|
101
|
+
cy: number;
|
|
102
|
+
rx: number;
|
|
103
|
+
ry: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A circular arc (open stroke) or sector/pie (closed to the center).
|
|
107
|
+
* Angles are radians, clockwise from +x (screen convention, y-down);
|
|
108
|
+
* the arc runs from `start` to `end` in the clockwise direction.
|
|
109
|
+
*/
|
|
110
|
+
export interface ArcCommand extends Base {
|
|
111
|
+
kind: 'arc';
|
|
112
|
+
cx: number;
|
|
113
|
+
cy: number;
|
|
114
|
+
radius: number;
|
|
115
|
+
start: number;
|
|
116
|
+
end: number;
|
|
117
|
+
/** When true, close through the center (a pie slice) so `fill` reads as a sector. */
|
|
118
|
+
sector?: boolean;
|
|
119
|
+
}
|
|
78
120
|
export interface PolyCommand extends Base {
|
|
79
121
|
kind: 'poly';
|
|
80
122
|
/** Flat [x0,y0,x1,y1,…] in local space. */
|
|
@@ -105,7 +147,18 @@ export interface ImageCommand extends Base {
|
|
|
105
147
|
w: number;
|
|
106
148
|
h: number;
|
|
107
149
|
}
|
|
108
|
-
export type DrawCommand = RectCommand | CircleCommand | PolyCommand | PathCommand | TextCommand | ImageCommand;
|
|
109
|
-
/**
|
|
150
|
+
export type DrawCommand = RectCommand | CircleCommand | EllipseCommand | ArcCommand | PolyCommand | PathCommand | TextCommand | ImageCommand;
|
|
151
|
+
/**
|
|
152
|
+
* Render-layer conventions. Commands sort by layer FIRST (see `sortCommands`),
|
|
153
|
+
* so these are painter-order bands, not a fixed enum — a game may still use any
|
|
154
|
+
* fractional layer. `LAYER_LIGHT` sits between world and HUD so the lighting run
|
|
155
|
+
* multiplies over world content but never darkens the overlay pass.
|
|
156
|
+
*/
|
|
157
|
+
export declare const LAYER_WORLD = 0;
|
|
158
|
+
/** The lighting run: reserved band above the world, below the HUD. */
|
|
159
|
+
export declare const LAYER_LIGHT = 0.5;
|
|
160
|
+
/** Screen-space overlay pass (HUD, transitions) — set for `screenSpace` subtrees. */
|
|
161
|
+
export declare const LAYER_HUD = 1;
|
|
162
|
+
/** Stable painter's sort: by layer, then z, then original index (tree order) as tiebreak. */
|
|
110
163
|
export declare function sortCommands(cmds: DrawCommand[]): DrawCommand[];
|
|
111
164
|
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type CircleCommand, type DrawCommand, type PolyCommand, type RectCommand } from './commands';
|
|
2
|
+
/** The three painter bands the compositor needs: world, the light run, HUD/overlay. */
|
|
3
|
+
export interface LightRunSplit {
|
|
4
|
+
/** Everything below the light layer (the lit world). */
|
|
5
|
+
below: DrawCommand[];
|
|
6
|
+
/** The light run itself (layer === LAYER_LIGHT), in stream order. */
|
|
7
|
+
light: DrawCommand[];
|
|
8
|
+
/** Everything above the light layer (HUD/overlay — never darkened). */
|
|
9
|
+
above: DrawCommand[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Split a SORTED display list into below/light/above runs by layer. The caller
|
|
13
|
+
* passes commands already through `sortCommands`, so the light run is contiguous.
|
|
14
|
+
*/
|
|
15
|
+
export declare function splitByLightLayer(sorted: DrawCommand[]): LightRunSplit;
|
|
16
|
+
/** One parsed light: its pool circle plus the shadow polys that erase it. */
|
|
17
|
+
export interface ParsedLight {
|
|
18
|
+
circle: CircleCommand;
|
|
19
|
+
shadows: PolyCommand[];
|
|
20
|
+
}
|
|
21
|
+
/** A parsed light run: the ambient darkness base + the per-light pools/shadows. */
|
|
22
|
+
export interface ParsedLightRun {
|
|
23
|
+
ambient: RectCommand;
|
|
24
|
+
lights: ParsedLight[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse a light run (as produced by `splitByLightLayer().light`) into its
|
|
28
|
+
* structured form, or `null` if the stream does not match the expected order —
|
|
29
|
+
* in which case the backend must fall back to painting the run flat. An EMPTY
|
|
30
|
+
* run is unparseable (null): there is nothing to composite, so backends paint
|
|
31
|
+
* it flat (a no-op) and produce byte-identical output to a lit-layer-free frame.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseLightRun(run: DrawCommand[]): ParsedLightRun | null;
|
|
34
|
+
/** True when a command carries the light-layer tag (helper for tests/backends). */
|
|
35
|
+
export declare const isLightCommand: (c: DrawCommand) => boolean;
|
package/dist/render/paint.d.ts
CHANGED
|
@@ -25,6 +25,14 @@ export interface BBox {
|
|
|
25
25
|
}
|
|
26
26
|
/** Local-space bounding box of a shape command (null for paths/text/image). */
|
|
27
27
|
export declare function shapeBBox(c: DrawCommand): BBox | null;
|
|
28
|
+
/** Why a command's geometry is unpaintable, or null when it's fine. */
|
|
29
|
+
export declare function invalidCommandReason(c: DrawCommand): string | null;
|
|
30
|
+
/**
|
|
31
|
+
* console.warn once per (kind + reason) — a bad value animating every frame
|
|
32
|
+
* logs a single line, not sixty a second. `detail` (the offending command or
|
|
33
|
+
* error) is passed through so the console shows the actual value.
|
|
34
|
+
*/
|
|
35
|
+
export declare function warnCommandOnce(kind: string, reason: string, detail?: unknown): void;
|
|
28
36
|
/** Build a Canvas gradient mapped from object-bounding-box space into `bbox`. */
|
|
29
37
|
export declare function canvasGradient(ctx: {
|
|
30
38
|
createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient;
|
|
@@ -18,8 +18,24 @@ export interface Renderer {
|
|
|
18
18
|
* (values fall outside 0..width/height there).
|
|
19
19
|
*/
|
|
20
20
|
toDesign?(clientX: number, clientY: number): Vec2;
|
|
21
|
+
/**
|
|
22
|
+
* The drawn (letterboxed) design area within the mount, in mount-local px:
|
|
23
|
+
* `{ x, y, width, height, scale }`. Host-drawn UI (floating touch controls,
|
|
24
|
+
* DOM HUD) anchors to this rect instead of re-deriving the `object-fit:
|
|
25
|
+
* contain` math the backend already does for `toDesign`. Undefined for
|
|
26
|
+
* headless backends.
|
|
27
|
+
*/
|
|
28
|
+
viewport?(): Viewport;
|
|
21
29
|
dispose?(): void;
|
|
22
30
|
}
|
|
31
|
+
/** The drawn area within the mount: offset + size in host px, plus design→px scale. */
|
|
32
|
+
export interface Viewport {
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
scale: number;
|
|
38
|
+
}
|
|
23
39
|
export interface RendererConfig {
|
|
24
40
|
width: number;
|
|
25
41
|
height: number;
|
|
@@ -36,3 +52,12 @@ export declare function clientToDesign(rect: {
|
|
|
36
52
|
width: number;
|
|
37
53
|
height: number;
|
|
38
54
|
}, width: number, height: number, clientX: number, clientY: number): Vec2;
|
|
55
|
+
/**
|
|
56
|
+
* The centered uniform-fit rect for a design box inside an element of size
|
|
57
|
+
* `rect` — the inverse framing of `clientToDesign`, in element-local px. Shared
|
|
58
|
+
* by every DOM backend's `viewport()`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function fitViewport(rect: {
|
|
61
|
+
width: number;
|
|
62
|
+
height: number;
|
|
63
|
+
}, width: number, height: number): Viewport;
|
package/dist/render/svg.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DrawCommand } from './commands';
|
|
2
|
-
import { type Renderer, type RendererConfig } from './renderer';
|
|
2
|
+
import { type Renderer, type RendererConfig, type Viewport } from './renderer';
|
|
3
3
|
import type { Vec2 } from '../core/math';
|
|
4
4
|
export declare class SvgRenderer implements Renderer {
|
|
5
5
|
readonly width: number;
|
|
@@ -15,5 +15,6 @@ export declare class SvgRenderer implements Renderer {
|
|
|
15
15
|
get element(): SVGSVGElement;
|
|
16
16
|
/** Map a pointer event's clientX/Y into design space (undoes the letterbox). */
|
|
17
17
|
toDesign(clientX: number, clientY: number): Vec2;
|
|
18
|
+
viewport(): Viewport;
|
|
18
19
|
dispose(): void;
|
|
19
20
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { type DrawCommand } from './commands';
|
|
2
2
|
/**
|
|
3
3
|
* Inner SVG markup for a display list (no wrapping <svg>). `idPrefix` salts the
|
|
4
|
-
* ids of any gradient/shadow <defs> so several inner markups can share one
|
|
5
|
-
* document (e.g. a filmstrip) without their `url(#…)` references colliding.
|
|
4
|
+
* ids of any gradient/shadow/mask <defs> so several inner markups can share one
|
|
5
|
+
* SVG document (e.g. a filmstrip) without their `url(#…)` references colliding.
|
|
6
|
+
*
|
|
7
|
+
* A lighting run (commands at `LAYER_LIGHT`) is emitted as the nested-blend group
|
|
8
|
+
* between the world and HUD passes. An EMPTY or unparseable run falls through the
|
|
9
|
+
* normal per-command path (honouring each command's `blend` via mix-blend-mode),
|
|
10
|
+
* so a lit-layer-free frame is byte-identical to before this feature.
|
|
6
11
|
*/
|
|
7
|
-
export declare function commandsToSVGInner(commands: DrawCommand[], idPrefix?: string): string;
|
|
12
|
+
export declare function commandsToSVGInner(commands: DrawCommand[], idPrefix?: string, viewW?: number, viewH?: number): string;
|
|
8
13
|
/** A complete, standalone SVG document string for a frame — a headless screenshot. */
|
|
9
14
|
export declare function renderToSVGString(commands: DrawCommand[], width: number, height: number, background?: string): string;
|