@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/CHANGELOG.md +202 -0
- package/README.md +2 -4
- package/dist/BrowserHostClock.d.ts +1 -1
- package/dist/BrowserTickSource.d.ts +25 -0
- package/dist/BrowserTickSource.d.ts.map +1 -0
- package/dist/BrowserTickSource.js +45 -0
- package/dist/BrowserTickSource.js.map +1 -0
- package/dist/InputCapture.d.ts +4 -1
- package/dist/InputCapture.d.ts.map +1 -1
- package/dist/InputCapture.js.map +1 -1
- package/dist/Keyboard.d.ts +1 -1
- package/dist/Keyboard.d.ts.map +1 -1
- package/dist/Keyboard.js +5 -2
- package/dist/Keyboard.js.map +1 -1
- package/dist/Touch.d.ts.map +1 -1
- package/dist/Touch.js +10 -5
- package/dist/Touch.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/BrowserHostClock.ts +1 -1
- package/src/BrowserTickSource.ts +53 -0
- package/src/InputCapture.ts +4 -1
- package/src/Keyboard.ts +4 -2
- package/src/Touch.ts +10 -5
- package/src/index.ts +1 -1
- package/dist/FixedLoop.d.ts +0 -95
- package/dist/FixedLoop.d.ts.map +0 -1
- package/dist/FixedLoop.js +0 -127
- package/dist/FixedLoop.js.map +0 -1
- package/src/FixedLoop.ts +0 -236
package/src/FixedLoop.ts
DELETED
|
@@ -1,236 +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
|
-
|
|
14
|
-
/** One renderer or browser tick measured in milliseconds. */
|
|
15
|
-
export interface Tick {
|
|
16
|
-
readonly deltaMS: number
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** A stoppable host subscription. Calling `stop` more than once is safe. */
|
|
20
|
-
export interface Subscription {
|
|
21
|
-
stop(): void
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Minimal adapter implemented by Pixi tickers, RAF, and deterministic tests. */
|
|
25
|
-
export interface TickSource<TTick extends Tick = Tick> {
|
|
26
|
-
subscribe(onTick: (tick: TTick) => void): Subscription
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Validated fixed-step timing policy. */
|
|
30
|
-
export interface Options {
|
|
31
|
-
/**
|
|
32
|
-
* The fixed step every update advances (`Clock.hz(60)`): the same value
|
|
33
|
-
* the game's clock resource runs at, so the loop and the clock agree.
|
|
34
|
-
*/
|
|
35
|
-
readonly step: Clock.Step
|
|
36
|
-
/** Maximum host-frame duration admitted into the accumulator. */
|
|
37
|
-
readonly maxFrameSeconds: number
|
|
38
|
-
/** Maximum number of simulation updates performed for one host frame. */
|
|
39
|
-
readonly maxStepsPerFrame: number
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Configuration error returned before a loop subscribes to its host. */
|
|
43
|
-
export interface InvalidOptions {
|
|
44
|
-
readonly kind: "InvalidFixedLoopOptions"
|
|
45
|
-
readonly field: keyof Options
|
|
46
|
-
readonly reason: "mustBeFiniteAndPositive" | "mustBePositiveInteger"
|
|
47
|
-
readonly value: number
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Data passed to the renderer after all fixed updates for one host frame. */
|
|
51
|
-
export interface Frame {
|
|
52
|
-
/** Clamped time admitted for this host frame. */
|
|
53
|
-
readonly elapsedSeconds: number
|
|
54
|
-
/** Number of fixed updates completed for this host frame. */
|
|
55
|
-
readonly fixedSteps: number
|
|
56
|
-
/** Remaining accumulator fraction in the range `[0, 1)`. */
|
|
57
|
-
readonly alpha: number
|
|
58
|
-
/** Time discarded by frame clamping or the catch-up limit. */
|
|
59
|
-
readonly droppedSeconds: number
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Failure delivered by an active loop before it stops itself. */
|
|
63
|
-
export type Failure<UpdateError> =
|
|
64
|
-
| {
|
|
65
|
-
readonly kind: "InvalidTickDelta"
|
|
66
|
-
readonly deltaMS: number
|
|
67
|
-
}
|
|
68
|
-
| {
|
|
69
|
-
readonly kind: "UpdateFailure"
|
|
70
|
-
readonly error: UpdateError
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** A running fixed-step driver. */
|
|
74
|
-
export interface Driver extends Subscription {
|
|
75
|
-
/** Clears accumulated partial time without removing the host subscription. */
|
|
76
|
-
reset(): void
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Input for a fixed-step driver. */
|
|
80
|
-
export interface StartOptions<TTick extends Tick, UpdateError> extends Options {
|
|
81
|
-
readonly source: TickSource<TTick>
|
|
82
|
-
readonly update: (stepSeconds: number) => Result.Result<void, UpdateError>
|
|
83
|
-
readonly render: (frame: Frame) => void
|
|
84
|
-
readonly onFailure: (failure: Failure<UpdateError>) => void
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Wraps a host-specific subscription function in the canonical source type. */
|
|
88
|
-
export const source = <TTick extends Tick>(
|
|
89
|
-
subscribe: TickSource<TTick>["subscribe"]
|
|
90
|
-
): TickSource<TTick> => ({ subscribe })
|
|
91
|
-
|
|
92
|
-
const validate = (options: Options): Result.Result<void, InvalidOptions> => {
|
|
93
|
-
for (const field of ["maxFrameSeconds"] as const) {
|
|
94
|
-
const value = options[field]
|
|
95
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
96
|
-
return Result.failure({
|
|
97
|
-
kind: "InvalidFixedLoopOptions",
|
|
98
|
-
field,
|
|
99
|
-
reason: "mustBeFiniteAndPositive",
|
|
100
|
-
value
|
|
101
|
-
})
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (!Number.isInteger(options.maxStepsPerFrame) || options.maxStepsPerFrame <= 0) {
|
|
105
|
-
return Result.failure({
|
|
106
|
-
kind: "InvalidFixedLoopOptions",
|
|
107
|
-
field: "maxStepsPerFrame",
|
|
108
|
-
reason: "mustBePositiveInteger",
|
|
109
|
-
value: options.maxStepsPerFrame
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
return Result.success(undefined)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Starts a fixed-step loop after validating its timing policy.
|
|
117
|
-
*
|
|
118
|
-
* Update failures and malformed host ticks are delivered to `onFailure`, then
|
|
119
|
-
* the driver stops. Render defects are deliberately not caught: they are
|
|
120
|
-
* programmer defects, not expected game outcomes.
|
|
121
|
-
*/
|
|
122
|
-
export const start = <TTick extends Tick, UpdateError>(
|
|
123
|
-
options: StartOptions<TTick, UpdateError>
|
|
124
|
-
): Result.Result<Driver, InvalidOptions> => {
|
|
125
|
-
const validity = validate(options)
|
|
126
|
-
if (!validity.ok) {
|
|
127
|
-
return validity
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
let accumulator = 0
|
|
131
|
-
let stopped = false
|
|
132
|
-
let hostSubscription: Subscription | undefined
|
|
133
|
-
|
|
134
|
-
const stop = (): void => {
|
|
135
|
-
if (stopped) {
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
stopped = true
|
|
139
|
-
hostSubscription?.stop()
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const fail = (failure: Failure<UpdateError>): void => {
|
|
143
|
-
stop()
|
|
144
|
-
options.onFailure(failure)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const onTick = (tick: TTick): void => {
|
|
148
|
-
if (stopped) {
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
if (!Number.isFinite(tick.deltaMS) || tick.deltaMS < 0) {
|
|
152
|
-
fail({ kind: "InvalidTickDelta", deltaMS: tick.deltaMS })
|
|
153
|
-
return
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const rawSeconds = tick.deltaMS / 1000
|
|
157
|
-
const elapsedSeconds = Math.min(rawSeconds, options.maxFrameSeconds)
|
|
158
|
-
let droppedSeconds = rawSeconds - elapsedSeconds
|
|
159
|
-
accumulator += elapsedSeconds
|
|
160
|
-
|
|
161
|
-
let fixedSteps = 0
|
|
162
|
-
while (accumulator >= options.step.dt && fixedSteps < options.maxStepsPerFrame) {
|
|
163
|
-
const update = options.update(options.step.dt)
|
|
164
|
-
if (!update.ok) {
|
|
165
|
-
fail({ kind: "UpdateFailure", error: update.error })
|
|
166
|
-
return
|
|
167
|
-
}
|
|
168
|
-
accumulator -= options.step.dt
|
|
169
|
-
fixedSteps += 1
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (accumulator >= options.step.dt) {
|
|
173
|
-
const droppedWholeSteps = Math.floor(accumulator / options.step.dt) * options.step.dt
|
|
174
|
-
accumulator -= droppedWholeSteps
|
|
175
|
-
droppedSeconds += droppedWholeSteps
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
options.render({
|
|
179
|
-
elapsedSeconds,
|
|
180
|
-
fixedSteps,
|
|
181
|
-
alpha: accumulator / options.step.dt,
|
|
182
|
-
droppedSeconds
|
|
183
|
-
})
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
hostSubscription = options.source.subscribe(onTick)
|
|
187
|
-
if (stopped) {
|
|
188
|
-
hostSubscription.stop()
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return Result.success({
|
|
192
|
-
stop,
|
|
193
|
-
reset() {
|
|
194
|
-
accumulator = 0
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Host surface needed by the built-in requestAnimationFrame adapter. */
|
|
200
|
-
export interface AnimationFrameHost {
|
|
201
|
-
requestAnimationFrame(callback: (timestamp: number) => void): number
|
|
202
|
-
cancelAnimationFrame(handle: number): void
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Adapts requestAnimationFrame to a `TickSource` without reading global state.
|
|
207
|
-
* The first frame reports zero elapsed time because it establishes the clock.
|
|
208
|
-
*/
|
|
209
|
-
export const animationFrames = (host: AnimationFrameHost): TickSource => source((onTick) => {
|
|
210
|
-
let stopped = false
|
|
211
|
-
let handle = 0
|
|
212
|
-
let previous: number | undefined
|
|
213
|
-
|
|
214
|
-
const frame = (timestamp: number): void => {
|
|
215
|
-
if (stopped) {
|
|
216
|
-
return
|
|
217
|
-
}
|
|
218
|
-
const deltaMS = previous === undefined ? 0 : timestamp - previous
|
|
219
|
-
previous = timestamp
|
|
220
|
-
onTick({ deltaMS })
|
|
221
|
-
if (!stopped) {
|
|
222
|
-
handle = host.requestAnimationFrame(frame)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
handle = host.requestAnimationFrame(frame)
|
|
227
|
-
return {
|
|
228
|
-
stop() {
|
|
229
|
-
if (stopped) {
|
|
230
|
-
return
|
|
231
|
-
}
|
|
232
|
-
stopped = true
|
|
233
|
-
host.cancelAnimationFrame(handle)
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
})
|