@typeonce/ritmo-browser 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Browser host integrations kept outside the renderer-agnostic ECS core. */
2
2
  export * as BrowserHostClock from "./BrowserHostClock.ts";
3
- export * as FixedLoop from "./FixedLoop.ts";
3
+ export * as BrowserTickSource from "./BrowserTickSource.ts";
4
4
  export * as InputCapture from "./InputCapture.ts";
5
5
  export * as Keyboard from "./Keyboard.ts";
6
6
  export * as Pointer from "./Pointer.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,OAAO,KAAK,gBAAgB,MAAM,uBAAuB,CAAA;AACzD,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAA;AACjD,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAEnC,eAAO,MAAM,UAAU,EAAG,SAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,OAAO,KAAK,gBAAgB,MAAM,uBAAuB,CAAA;AACzD,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAA;AACjD,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAEnC,eAAO,MAAM,UAAU,EAAG,SAAkB,CAAA"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Browser host integrations kept outside the renderer-agnostic ECS core. */
2
2
  export * as BrowserHostClock from "./BrowserHostClock.js";
3
- export * as FixedLoop from "./FixedLoop.js";
3
+ export * as BrowserTickSource from "./BrowserTickSource.js";
4
4
  export * as InputCapture from "./InputCapture.js";
5
5
  export * as Keyboard from "./Keyboard.js";
6
6
  export * as Pointer from "./Pointer.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,OAAO,KAAK,gBAAgB,MAAM,uBAAuB,CAAA;AACzD,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAA;AACjD,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAEnC,MAAM,CAAC,MAAM,UAAU,GAAG,SAAkB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,OAAO,KAAK,gBAAgB,MAAM,uBAAuB,CAAA;AACzD,OAAO,KAAK,iBAAiB,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAA;AACjD,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAEnC,MAAM,CAAC,MAAM,UAAU,GAAG,SAAkB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeonce/ritmo-browser",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Browser fixed-step loop, keyboard actions, and input capture for Ritmo",
5
5
  "author": "Sandro Maglione",
6
6
  "license": "MIT",
@@ -27,6 +27,7 @@
27
27
  "src/**/*.ts",
28
28
  "dist",
29
29
  "README.md",
30
+ "CHANGELOG.md",
30
31
  "LICENSE"
31
32
  ],
32
33
  "exports": {
@@ -47,7 +48,7 @@
47
48
  "provenance": true
48
49
  },
49
50
  "dependencies": {
50
- "@typeonce/ritmo": "^0.8.0"
51
+ "@typeonce/ritmo": "^0.10.0"
51
52
  },
52
53
  "engines": {
53
54
  "node": ">=20"
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @example
8
8
  * ```ts
9
- * const runtime = Game.Runtime.make({ services, debug: { hostClock: BrowserHostClock.make(window) } })
9
+ * const runtime = Game.Runtime.make({ services, debug: { hostClock: BrowserHostClock.make(window) } }).value
10
10
  * ```
11
11
  *
12
12
  * @module BrowserHostClock
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The browser's animation frames as a core `FixedLoop.TickSource`.
3
+ *
4
+ * Each frame reports the milliseconds since the previous one; the first frame
5
+ * reports zero because it establishes the clock. The window is passed in, so
6
+ * nothing reads a global. To drive the loop from a renderer's own ticker
7
+ * instead (such as Pixi's), wrap it with `FixedLoop.source`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const loop = FixedLoop.start({ source: BrowserTickSource.make(window), step: Clock.hz(60), ... })
12
+ * ```
13
+ *
14
+ * @module BrowserTickSource
15
+ * @docGroup browser
16
+ */
17
+ import * as FixedLoop from "@typeonce/ritmo/FixedLoop"
18
+
19
+ /** What the source needs from the window. */
20
+ export interface Host {
21
+ requestAnimationFrame(callback: (timestamp: number) => void): number
22
+ cancelAnimationFrame(handle: number): void
23
+ }
24
+
25
+ /** A tick source that schedules one `requestAnimationFrame` per frame until stopped. */
26
+ export const make = (host: Host): FixedLoop.TickSource => FixedLoop.source((onTick) => {
27
+ let stopped = false
28
+ let handle = 0
29
+ let previous: number | undefined
30
+
31
+ const frame = (timestamp: number): void => {
32
+ if (stopped) {
33
+ return
34
+ }
35
+ const deltaMS = previous === undefined ? 0 : timestamp - previous
36
+ previous = timestamp
37
+ onTick({ deltaMS })
38
+ if (!stopped) {
39
+ handle = host.requestAnimationFrame(frame)
40
+ }
41
+ }
42
+
43
+ handle = host.requestAnimationFrame(frame)
44
+ return {
45
+ stop() {
46
+ if (stopped) {
47
+ return
48
+ }
49
+ stopped = true
50
+ host.cancelAnimationFrame(handle)
51
+ }
52
+ }
53
+ })
@@ -12,6 +12,9 @@
12
12
  * directly, or a game-specific adapter that merges several devices. The
13
13
  * resource's value type must be exactly the snapshot type.
14
14
  *
15
+ * Its spec is erased, so it does not count as a writer of the resource for
16
+ * `Game.System.thisTick(...)` order checks: place it first yourself.
17
+ *
15
18
  * @module InputCapture
16
19
  * @docGroup browser
17
20
  *
@@ -28,7 +31,7 @@
28
31
  * const runtime = Game.Runtime.make({
29
32
  * services: Game.Runtime.services(Game.Runtime.service(KeyboardInput, Keyboard.actions(window, bindings))),
30
33
  * resources: { Input: Keyboard.idle(bindings) }
31
- * })
34
+ * }).value
32
35
  * ```
33
36
  */
34
37
  import type { Descriptor } from "@typeonce/ritmo/Descriptor"
package/src/Keyboard.ts CHANGED
@@ -304,7 +304,7 @@ export interface Scripted<B extends Bindings> extends Actions<B> {
304
304
  /**
305
305
  * Plays `timeline`: before the snapshot at index `frame`, every entry for
306
306
  * that frame is applied. Actions stay held from their press until their
307
- * release, like keys.
307
+ * release, like keys; pressing an action already held is not a new press.
308
308
  */
309
309
  export const scripted = <const B extends Bindings>(bindings: B, timeline: Timeline<B>): Scripted<B> => {
310
310
  const names = Object.keys(bindings) as Array<keyof B & string>
@@ -322,8 +322,10 @@ export const scripted = <const B extends Bindings>(bindings: B, timeline: Timeli
322
322
  const released = new Set<keyof B>()
323
323
  for (const entry of byFrame.get(frame) ?? []) {
324
324
  for (const name of entry.press ?? []) {
325
+ // A press of a held action is not a new press, as with a live
326
+ // keyboard's auto-repeat.
327
+ if (!held.has(name)) pressed.add(name)
325
328
  held.add(name)
326
- pressed.add(name)
327
329
  }
328
330
  for (const name of entry.release ?? []) {
329
331
  if (held.delete(name)) released.add(name)
package/src/Touch.ts CHANGED
@@ -196,7 +196,7 @@ export const idle = <const B extends string>(buttons: ReadonlyArray<B>): Snapsho
196
196
 
197
197
  /** `offset / radius`, clamped to length 1, with the inner `deadZone` fraction mapped to zero. */
198
198
  const tilt = (offset: Position, radius: number, deadZone: number): Vector => {
199
- const length = Math.hypot(offset.x, offset.y)
199
+ const length = Math.sqrt(offset.x * offset.x + offset.y * offset.y)
200
200
  if (radius <= 0 || length <= deadZone * radius) return zero
201
201
  const strength = Math.min(1, (length / radius - deadZone) / (1 - deadZone))
202
202
  return { x: (offset.x / length) * strength, y: (offset.y / length) * strength }
@@ -206,7 +206,7 @@ const tilt = (offset: Position, radius: number, deadZone: number): Vector => {
206
206
  const aimOf = (zone: ButtonZone, at: Position): Vector | null => {
207
207
  if (zone.aimRadius === undefined) return null
208
208
  const offset = { x: at.x - zone.center.x, y: at.y - zone.center.y }
209
- const length = Math.hypot(offset.x, offset.y)
209
+ const length = Math.sqrt(offset.x * offset.x + offset.y * offset.y)
210
210
  if (length <= (zone.aimDeadZone ?? 0.2) * zone.aimRadius) return null
211
211
  const scale = Math.min(1, length / zone.aimRadius) / length
212
212
  return { x: offset.x * scale, y: offset.y * scale }
@@ -295,13 +295,18 @@ export const track = <const B extends string>(
295
295
  const onDown = (event: TouchEventLike): void => {
296
296
  if (!tracked(event)) return
297
297
  event.preventDefault()
298
+ // A pointer id that comes down again without an up in between (the up
299
+ // was lost) releases what it held first, as a cancel.
300
+ end(event, true)
298
301
  touched = true
299
302
  current = layout(sizeOf())
300
303
  const at = locate(event)
301
304
  for (const name of names) {
302
305
  const zone = current.buttons[name]
303
306
  const button = buttons.get(name)!
304
- if (button.held || Math.hypot(at.x - zone.center.x, at.y - zone.center.y) > zone.radius) continue
307
+ const dx = at.x - zone.center.x
308
+ const dy = at.y - zone.center.y
309
+ if (button.held || dx * dx + dy * dy > zone.radius * zone.radius) continue
305
310
  press(button, aimOf(zone, at))
306
311
  fingers.set(event.pointerId, name)
307
312
  target.setPointerCapture?.(event.pointerId)
@@ -395,7 +400,7 @@ export const track = <const B extends string>(
395
400
  ? null
396
401
  : (() => {
397
402
  const offset = { x: stick.knob.x - stick.origin.x, y: stick.knob.y - stick.origin.y }
398
- const length = Math.hypot(offset.x, offset.y)
403
+ const length = Math.sqrt(offset.x * offset.x + offset.y * offset.y)
399
404
  const scale = length > radius ? radius / length : 1
400
405
  return { origin: stick.origin, knob: { x: stick.origin.x + offset.x * scale, y: stick.origin.y + offset.y * scale } }
401
406
  })(),
@@ -458,7 +463,7 @@ export const scripted = <const B extends string>(buttons: ReadonlyArray<B>, time
458
463
  else byFrame.set(entry.frame, [entry])
459
464
  }
460
465
  const clamp = (vector: Vector): Vector => {
461
- const length = Math.hypot(vector.x, vector.y)
466
+ const length = Math.sqrt(vector.x * vector.x + vector.y * vector.y)
462
467
  return length <= 1 ? vector : { x: vector.x / length, y: vector.y / length }
463
468
  }
464
469
  const tracks = makeButtonTracks(buttons)
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Browser host integrations kept outside the renderer-agnostic ECS core. */
2
2
  export * as BrowserHostClock from "./BrowserHostClock.ts"
3
- export * as FixedLoop from "./FixedLoop.ts"
3
+ export * as BrowserTickSource from "./BrowserTickSource.ts"
4
4
  export * as InputCapture from "./InputCapture.ts"
5
5
  export * as Keyboard from "./Keyboard.ts"
6
6
  export * as Pointer from "./Pointer.ts"
@@ -1,95 +0,0 @@
1
- /**
2
- * Deterministic fixed-step timing for browser and renderer hosts.
3
- *
4
- * The loop owns the timing policy that is easy to get subtly wrong in every
5
- * game: frame clamping, catch-up limits, interpolation, cancellation, and
6
- * typed update failures. It does not know about an ECS runtime or renderer.
7
- *
8
- * @module FixedLoop
9
- * @docGroup browser
10
- */
11
- import type * as Clock from "@typeonce/ritmo/Clock";
12
- import * as Result from "@typeonce/ritmo/Result";
13
- /** One renderer or browser tick measured in milliseconds. */
14
- export interface Tick {
15
- readonly deltaMS: number;
16
- }
17
- /** A stoppable host subscription. Calling `stop` more than once is safe. */
18
- export interface Subscription {
19
- stop(): void;
20
- }
21
- /** Minimal adapter implemented by Pixi tickers, RAF, and deterministic tests. */
22
- export interface TickSource<TTick extends Tick = Tick> {
23
- subscribe(onTick: (tick: TTick) => void): Subscription;
24
- }
25
- /** Validated fixed-step timing policy. */
26
- export interface Options {
27
- /**
28
- * The fixed step every update advances (`Clock.hz(60)`): the same value
29
- * the game's clock resource runs at, so the loop and the clock agree.
30
- */
31
- readonly step: Clock.Step;
32
- /** Maximum host-frame duration admitted into the accumulator. */
33
- readonly maxFrameSeconds: number;
34
- /** Maximum number of simulation updates performed for one host frame. */
35
- readonly maxStepsPerFrame: number;
36
- }
37
- /** Configuration error returned before a loop subscribes to its host. */
38
- export interface InvalidOptions {
39
- readonly kind: "InvalidFixedLoopOptions";
40
- readonly field: keyof Options;
41
- readonly reason: "mustBeFiniteAndPositive" | "mustBePositiveInteger";
42
- readonly value: number;
43
- }
44
- /** Data passed to the renderer after all fixed updates for one host frame. */
45
- export interface Frame {
46
- /** Clamped time admitted for this host frame. */
47
- readonly elapsedSeconds: number;
48
- /** Number of fixed updates completed for this host frame. */
49
- readonly fixedSteps: number;
50
- /** Remaining accumulator fraction in the range `[0, 1)`. */
51
- readonly alpha: number;
52
- /** Time discarded by frame clamping or the catch-up limit. */
53
- readonly droppedSeconds: number;
54
- }
55
- /** Failure delivered by an active loop before it stops itself. */
56
- export type Failure<UpdateError> = {
57
- readonly kind: "InvalidTickDelta";
58
- readonly deltaMS: number;
59
- } | {
60
- readonly kind: "UpdateFailure";
61
- readonly error: UpdateError;
62
- };
63
- /** A running fixed-step driver. */
64
- export interface Driver extends Subscription {
65
- /** Clears accumulated partial time without removing the host subscription. */
66
- reset(): void;
67
- }
68
- /** Input for a fixed-step driver. */
69
- export interface StartOptions<TTick extends Tick, UpdateError> extends Options {
70
- readonly source: TickSource<TTick>;
71
- readonly update: (stepSeconds: number) => Result.Result<void, UpdateError>;
72
- readonly render: (frame: Frame) => void;
73
- readonly onFailure: (failure: Failure<UpdateError>) => void;
74
- }
75
- /** Wraps a host-specific subscription function in the canonical source type. */
76
- export declare const source: <TTick extends Tick>(subscribe: TickSource<TTick>["subscribe"]) => TickSource<TTick>;
77
- /**
78
- * Starts a fixed-step loop after validating its timing policy.
79
- *
80
- * Update failures and malformed host ticks are delivered to `onFailure`, then
81
- * the driver stops. Render defects are deliberately not caught: they are
82
- * programmer defects, not expected game outcomes.
83
- */
84
- export declare const start: <TTick extends Tick, UpdateError>(options: StartOptions<TTick, UpdateError>) => Result.Result<Driver, InvalidOptions>;
85
- /** Host surface needed by the built-in requestAnimationFrame adapter. */
86
- export interface AnimationFrameHost {
87
- requestAnimationFrame(callback: (timestamp: number) => void): number;
88
- cancelAnimationFrame(handle: number): void;
89
- }
90
- /**
91
- * Adapts requestAnimationFrame to a `TickSource` without reading global state.
92
- * The first frame reports zero elapsed time because it establishes the clock.
93
- */
94
- export declare const animationFrames: (host: AnimationFrameHost) => TickSource;
95
- //# sourceMappingURL=FixedLoop.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"FixedLoop.d.ts","sourceRoot":"","sources":["../src/FixedLoop.ts"],"names":[],"mappings":"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"}
package/dist/FixedLoop.js DELETED
@@ -1,127 +0,0 @@
1
- import * as Result from "@typeonce/ritmo/Result";
2
- /** Wraps a host-specific subscription function in the canonical source type. */
3
- export const source = (subscribe) => ({ subscribe });
4
- const validate = (options) => {
5
- for (const field of ["maxFrameSeconds"]) {
6
- const value = options[field];
7
- if (!Number.isFinite(value) || value <= 0) {
8
- return Result.failure({
9
- kind: "InvalidFixedLoopOptions",
10
- field,
11
- reason: "mustBeFiniteAndPositive",
12
- value
13
- });
14
- }
15
- }
16
- if (!Number.isInteger(options.maxStepsPerFrame) || options.maxStepsPerFrame <= 0) {
17
- return Result.failure({
18
- kind: "InvalidFixedLoopOptions",
19
- field: "maxStepsPerFrame",
20
- reason: "mustBePositiveInteger",
21
- value: options.maxStepsPerFrame
22
- });
23
- }
24
- return Result.success(undefined);
25
- };
26
- /**
27
- * Starts a fixed-step loop after validating its timing policy.
28
- *
29
- * Update failures and malformed host ticks are delivered to `onFailure`, then
30
- * the driver stops. Render defects are deliberately not caught: they are
31
- * programmer defects, not expected game outcomes.
32
- */
33
- export const start = (options) => {
34
- const validity = validate(options);
35
- if (!validity.ok) {
36
- return validity;
37
- }
38
- let accumulator = 0;
39
- let stopped = false;
40
- let hostSubscription;
41
- const stop = () => {
42
- if (stopped) {
43
- return;
44
- }
45
- stopped = true;
46
- hostSubscription?.stop();
47
- };
48
- const fail = (failure) => {
49
- stop();
50
- options.onFailure(failure);
51
- };
52
- const onTick = (tick) => {
53
- if (stopped) {
54
- return;
55
- }
56
- if (!Number.isFinite(tick.deltaMS) || tick.deltaMS < 0) {
57
- fail({ kind: "InvalidTickDelta", deltaMS: tick.deltaMS });
58
- return;
59
- }
60
- const rawSeconds = tick.deltaMS / 1000;
61
- const elapsedSeconds = Math.min(rawSeconds, options.maxFrameSeconds);
62
- let droppedSeconds = rawSeconds - elapsedSeconds;
63
- accumulator += elapsedSeconds;
64
- let fixedSteps = 0;
65
- while (accumulator >= options.step.dt && fixedSteps < options.maxStepsPerFrame) {
66
- const update = options.update(options.step.dt);
67
- if (!update.ok) {
68
- fail({ kind: "UpdateFailure", error: update.error });
69
- return;
70
- }
71
- accumulator -= options.step.dt;
72
- fixedSteps += 1;
73
- }
74
- if (accumulator >= options.step.dt) {
75
- const droppedWholeSteps = Math.floor(accumulator / options.step.dt) * options.step.dt;
76
- accumulator -= droppedWholeSteps;
77
- droppedSeconds += droppedWholeSteps;
78
- }
79
- options.render({
80
- elapsedSeconds,
81
- fixedSteps,
82
- alpha: accumulator / options.step.dt,
83
- droppedSeconds
84
- });
85
- };
86
- hostSubscription = options.source.subscribe(onTick);
87
- if (stopped) {
88
- hostSubscription.stop();
89
- }
90
- return Result.success({
91
- stop,
92
- reset() {
93
- accumulator = 0;
94
- }
95
- });
96
- };
97
- /**
98
- * Adapts requestAnimationFrame to a `TickSource` without reading global state.
99
- * The first frame reports zero elapsed time because it establishes the clock.
100
- */
101
- export const animationFrames = (host) => source((onTick) => {
102
- let stopped = false;
103
- let handle = 0;
104
- let previous;
105
- const frame = (timestamp) => {
106
- if (stopped) {
107
- return;
108
- }
109
- const deltaMS = previous === undefined ? 0 : timestamp - previous;
110
- previous = timestamp;
111
- onTick({ deltaMS });
112
- if (!stopped) {
113
- handle = host.requestAnimationFrame(frame);
114
- }
115
- };
116
- handle = host.requestAnimationFrame(frame);
117
- return {
118
- stop() {
119
- if (stopped) {
120
- return;
121
- }
122
- stopped = true;
123
- host.cancelAnimationFrame(handle);
124
- }
125
- };
126
- });
127
- //# sourceMappingURL=FixedLoop.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"FixedLoop.js","sourceRoot":"","sources":["../src/FixedLoop.ts"],"names":[],"mappings":"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"}