zx-kit 0.1.1 → 0.2.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 +197 -256
- package/dist/audio.d.ts +77 -0
- package/dist/audio.d.ts.map +1 -1
- package/dist/audio.js +83 -1
- package/dist/audio.js.map +1 -1
- package/dist/font.d.ts +22 -0
- package/dist/font.d.ts.map +1 -1
- package/dist/font.js +23 -0
- package/dist/font.js.map +1 -1
- package/dist/input.d.ts +42 -0
- package/dist/input.d.ts.map +1 -1
- package/dist/input.js +41 -0
- package/dist/input.js.map +1 -1
- package/dist/palette.d.ts +19 -0
- package/dist/palette.d.ts.map +1 -1
- package/dist/palette.js +11 -1
- package/dist/palette.js.map +1 -1
- package/dist/renderer.d.ts +99 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +117 -3
- package/dist/renderer.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,356 +8,296 @@ Extracted from [Minefield](https://github.com/zrebec/minefield) — a ZX Spectru
|
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```json
|
|
14
|
-
"dependencies": {
|
|
15
|
-
"zx-kit": "file:../zx-kit"
|
|
16
|
-
}
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Then add a Vite alias so bundler resolution works (Vite doesn't transform `node_modules` by default):
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
// vite.config.ts
|
|
23
|
-
import { resolve } from 'path'
|
|
24
|
-
|
|
25
|
-
export default defineConfig({
|
|
26
|
-
resolve: {
|
|
27
|
-
alias: { 'zx-kit': resolve(__dirname, '../zx-kit/src/index.ts') },
|
|
28
|
-
},
|
|
29
|
-
})
|
|
11
|
+
```bash
|
|
12
|
+
npm install zx-kit
|
|
30
13
|
```
|
|
31
14
|
|
|
32
15
|
Import from the barrel:
|
|
33
16
|
|
|
34
17
|
```ts
|
|
35
|
-
import { C, CELL,
|
|
18
|
+
import { C, CELL, setupCanvas, initAudio, playPattern, initInput, tickMovement } from 'zx-kit'
|
|
36
19
|
```
|
|
37
20
|
|
|
21
|
+
No Vite alias or path mapping required — the package ships compiled JavaScript (`dist/`).
|
|
22
|
+
|
|
38
23
|
---
|
|
39
24
|
|
|
40
|
-
##
|
|
25
|
+
## Quick start
|
|
41
26
|
|
|
42
|
-
|
|
27
|
+
```ts
|
|
28
|
+
import { setupCanvas, C, CELL, drawText, initAudio, playPattern, initInput, tickMovement } from 'zx-kit'
|
|
43
29
|
|
|
44
|
-
|
|
30
|
+
// Canvas — one call replaces the manual boilerplate
|
|
31
|
+
const canvas = document.getElementById('game') as HTMLCanvasElement
|
|
32
|
+
const ctx = setupCanvas(canvas, 256, 192) // sets size + imageSmoothingEnabled = false
|
|
45
33
|
|
|
46
|
-
|
|
34
|
+
// Input
|
|
35
|
+
initInput()
|
|
47
36
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
| `SCALE` | `number` | `4` | CSS pixel scale factor (1 game pixel = 4 CSS pixels) |
|
|
51
|
-
| `CELL` | `number` | `8` | Sprite / character grid size in game pixels |
|
|
52
|
-
| `C` | `object` | see below | All 15 Spectrum colors as `#RRGGBB` hex strings |
|
|
53
|
-
| `SpectrumColor` | `type` | union of all `C` values | TypeScript type for palette-safe color values |
|
|
37
|
+
// Audio (must be inside a user gesture)
|
|
38
|
+
window.addEventListener('keydown', () => initAudio(), { once: true })
|
|
54
39
|
|
|
55
|
-
|
|
40
|
+
// Game loop
|
|
41
|
+
function loop(dt: number) {
|
|
42
|
+
const dir = tickMovement(dt)
|
|
43
|
+
if (dir) movePlayer(dir)
|
|
56
44
|
|
|
57
|
-
|
|
45
|
+
drawText(ctx, 'SCORE:00000', 0, 0, C.B_WHITE, C.BLACK)
|
|
46
|
+
requestAnimationFrame(t => loop(t - lastT))
|
|
47
|
+
}
|
|
48
|
+
```
|
|
58
49
|
|
|
59
|
-
|
|
60
|
-
|-----|-----|-----------|
|
|
61
|
-
| `C.BLACK` | `#000000` | Black |
|
|
62
|
-
| `C.BLUE` | `#0000CD` | Dark blue |
|
|
63
|
-
| `C.RED` | `#CD0000` | Dark red |
|
|
64
|
-
| `C.MAGENTA` | `#CD00CD` | Dark magenta |
|
|
65
|
-
| `C.GREEN` | `#00CD00` | Dark green |
|
|
66
|
-
| `C.CYAN` | `#00CDCD` | Dark cyan |
|
|
67
|
-
| `C.YELLOW` | `#CDCD00` | Dark yellow |
|
|
68
|
-
| `C.WHITE` | `#CDCDCD` | Light grey |
|
|
50
|
+
---
|
|
69
51
|
|
|
70
|
-
|
|
52
|
+
## Modules
|
|
71
53
|
|
|
72
|
-
|
|
73
|
-
|-----|-----|-----------|
|
|
74
|
-
| `C.B_BLACK` | `#000000` | Same as `BLACK` |
|
|
75
|
-
| `C.B_BLUE` | `#0000FF` | Bright blue |
|
|
76
|
-
| `C.B_RED` | `#FF0000` | Bright red |
|
|
77
|
-
| `C.B_MAGENTA` | `#FF00FF` | Bright magenta |
|
|
78
|
-
| `C.B_GREEN` | `#00FF00` | Bright green |
|
|
79
|
-
| `C.B_CYAN` | `#00FFFF` | Bright cyan |
|
|
80
|
-
| `C.B_YELLOW` | `#FFFF00` | Bright yellow |
|
|
81
|
-
| `C.B_WHITE` | `#FFFFFF` | Pure white |
|
|
54
|
+
### `palette.ts` — ZX Spectrum color constants
|
|
82
55
|
|
|
83
|
-
####
|
|
56
|
+
#### Exports
|
|
84
57
|
|
|
85
|
-
|
|
86
|
-
|
|
58
|
+
| Export | Type | Description |
|
|
59
|
+
|--------|------|-------------|
|
|
60
|
+
| `SCALE` | `number` | CSS pixel scale factor (1 game pixel = 4 CSS pixels) |
|
|
61
|
+
| `CELL` | `number` | Sprite / character grid size: `8` game pixels |
|
|
62
|
+
| `C` | `object` | All 15 Spectrum colors as `#RRGGBB` hex strings |
|
|
63
|
+
| `SpectrumColor` | `type` | Union of every value in `C` — use for compile-time palette enforcement |
|
|
64
|
+
|
|
65
|
+
#### Color table
|
|
66
|
+
|
|
67
|
+
Normal brightness:
|
|
68
|
+
|
|
69
|
+
| Key | Hex |
|
|
70
|
+
|-----|-----|
|
|
71
|
+
| `C.BLACK` | `#000000` |
|
|
72
|
+
| `C.BLUE` | `#0000CD` |
|
|
73
|
+
| `C.RED` | `#CD0000` |
|
|
74
|
+
| `C.MAGENTA` | `#CD00CD` |
|
|
75
|
+
| `C.GREEN` | `#00CD00` |
|
|
76
|
+
| `C.CYAN` | `#00CDCD` |
|
|
77
|
+
| `C.YELLOW` | `#CDCD00` |
|
|
78
|
+
| `C.WHITE` | `#CDCDCD` |
|
|
87
79
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
Bright variants (`B_` prefix):
|
|
81
|
+
|
|
82
|
+
| Key | Hex |
|
|
83
|
+
|-----|-----|
|
|
84
|
+
| `C.B_BLACK` | `#000000` |
|
|
85
|
+
| `C.B_BLUE` | `#0000FF` |
|
|
86
|
+
| `C.B_RED` | `#FF0000` |
|
|
87
|
+
| `C.B_MAGENTA` | `#FF00FF` |
|
|
88
|
+
| `C.B_GREEN` | `#00FF00` |
|
|
89
|
+
| `C.B_CYAN` | `#00FFFF` |
|
|
90
|
+
| `C.B_YELLOW` | `#FFFF00` |
|
|
91
|
+
| `C.B_WHITE` | `#FFFFFF` |
|
|
91
92
|
|
|
92
93
|
---
|
|
93
94
|
|
|
94
95
|
### `font.ts` — ZX Spectrum ROM bitmap font
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
The font data is sourced from the original Spectrum ROM and must not be altered — any change breaks Spectrum authenticity.
|
|
97
|
+
96 printable characters (ASCII 32–127), each 8×8 pixels. Character 127 is a solid block █.
|
|
99
98
|
|
|
100
99
|
#### Exports
|
|
101
100
|
|
|
102
|
-
| Export |
|
|
103
|
-
|
|
104
|
-
| `FONT` | `Uint8Array` | Flat array: 96 chars × 8 bytes
|
|
105
|
-
| `getCharRow
|
|
101
|
+
| Export | Signature | Description |
|
|
102
|
+
|--------|-----------|-------------|
|
|
103
|
+
| `FONT` | `Uint8Array` | Flat array: 96 chars × 8 bytes. `FONT[(code-32)*8 + row]` = one row bitmap. |
|
|
104
|
+
| `getCharRow` | `(charCode, row) => number` | Bitmap byte for one row of a character (bit 7 = leftmost pixel). |
|
|
106
105
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
import { getCharRow } from 'zx-kit'
|
|
111
|
-
|
|
112
|
-
// Draw character 'A' (65) row by row
|
|
113
|
-
for (let row = 0; row < 8; row++) {
|
|
114
|
-
const byte = getCharRow(65, row)
|
|
115
|
-
for (let bit = 0; bit < 8; bit++) {
|
|
116
|
-
if (byte & (0x80 >> bit)) {
|
|
117
|
-
ctx.fillRect(x + bit, y + row, 1, 1)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
In practice you won't need this directly — use `drawChar` / `drawText` from `renderer.ts` instead.
|
|
106
|
+
In practice use `drawChar` / `drawText` from `renderer.ts` — you rarely need `getCharRow` directly.
|
|
124
107
|
|
|
125
108
|
---
|
|
126
109
|
|
|
127
110
|
### `renderer.ts` — Canvas drawing primitives
|
|
128
111
|
|
|
129
|
-
All
|
|
112
|
+
All functions work in **game pixels**. At `SCALE=4` each game pixel maps to a 4×4 CSS pixel block. Call `setupCanvas` once at startup to configure the canvas correctly.
|
|
130
113
|
|
|
131
|
-
Every function follows the ZX Spectrum **ink / paper** model: each 8×8 cell has
|
|
132
|
-
|
|
133
|
-
#### Exports
|
|
114
|
+
Every draw function follows the ZX Spectrum **ink / paper** model: each 8×8 cell has one foreground (ink) and one background (paper) color.
|
|
134
115
|
|
|
135
|
-
|
|
116
|
+
#### `setupCanvas(canvas, width, height): CanvasRenderingContext2D`
|
|
136
117
|
|
|
137
|
-
|
|
118
|
+
Initialises a canvas element for pixel-perfect rendering. Sets dimensions, disables image smoothing, and returns the 2D context. **Replaces the manual canvas setup boilerplate.**
|
|
138
119
|
|
|
139
120
|
```ts
|
|
140
|
-
|
|
121
|
+
const canvas = document.getElementById('game') as HTMLCanvasElement
|
|
122
|
+
const ctx = setupCanvas(canvas, 256, 192)
|
|
123
|
+
// ctx.imageSmoothingEnabled is already false — no need to set it manually
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### `mirrorSprite(src): Uint8Array`
|
|
141
127
|
|
|
142
|
-
|
|
128
|
+
Flips an 8×8 sprite horizontally. Returns a new `Uint8Array`.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
export const PLAYER_RIGHT = new Uint8Array([...])
|
|
143
132
|
export const PLAYER_LEFT = mirrorSprite(PLAYER_RIGHT)
|
|
144
133
|
```
|
|
145
134
|
|
|
146
|
-
|
|
135
|
+
#### `drawSprite(ctx, sprite, x, y, ink, paper): void`
|
|
147
136
|
|
|
148
|
-
Draws an 8×8 sprite at game coordinates
|
|
149
|
-
|
|
150
|
-
| Parameter | Type | Description |
|
|
151
|
-
|-----------|------|-------------|
|
|
152
|
-
| `ctx` | `CanvasRenderingContext2D` | Target canvas context |
|
|
153
|
-
| `sprite` | `Uint8Array` | 8-byte sprite (one byte per row) |
|
|
154
|
-
| `x` | `number` | Left edge in game pixels |
|
|
155
|
-
| `y` | `number` | Top edge in game pixels |
|
|
156
|
-
| `ink` | `string` | Foreground color (Spectrum palette hex) |
|
|
157
|
-
| `paper` | `string` | Background color (Spectrum palette hex) |
|
|
137
|
+
Draws an 8×8 sprite at game coordinates. Always fills the background first.
|
|
158
138
|
|
|
159
139
|
```ts
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
drawSprite(ctx, MINE_SPRITE, col * 8, row * 8, C.B_RED, C.BLACK)
|
|
140
|
+
drawSprite(ctx, MINE_SPRITE, col * CELL, row * CELL, C.B_RED, C.BLACK)
|
|
163
141
|
```
|
|
164
142
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
Draws a single ASCII character at game coordinates using the ROM font. If `paper` is omitted, the background is not cleared (transparent background).
|
|
143
|
+
#### `drawChar(ctx, code, x, y, ink, paper?): void`
|
|
168
144
|
|
|
169
|
-
|
|
170
|
-
|-----------|------|-------------|
|
|
171
|
-
| `ctx` | `CanvasRenderingContext2D` | Target canvas context |
|
|
172
|
-
| `code` | `number` | ASCII character code (32–127) |
|
|
173
|
-
| `x` | `number` | Left edge in game pixels |
|
|
174
|
-
| `y` | `number` | Top edge in game pixels |
|
|
175
|
-
| `ink` | `string` | Foreground color |
|
|
176
|
-
| `paper?` | `string` | Optional background color |
|
|
145
|
+
Draws one ASCII character using the ROM font. Omit `paper` for transparent background.
|
|
177
146
|
|
|
178
147
|
```ts
|
|
179
|
-
import { drawChar, C } from 'zx-kit'
|
|
180
|
-
|
|
181
148
|
drawChar(ctx, 127, x, y, C.B_GREEN, C.BLACK) // solid block █
|
|
149
|
+
drawChar(ctx, 'A'.charCodeAt(0), x, y, C.B_WHITE)
|
|
182
150
|
```
|
|
183
151
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
Draws a string left-to-right starting at `(x, y)`, one character per `CELL`-wide slot.
|
|
152
|
+
#### `drawText(ctx, text, x, y, ink, paper?): void`
|
|
187
153
|
|
|
188
|
-
|
|
189
|
-
|-----------|------|-------------|
|
|
190
|
-
| `ctx` | `CanvasRenderingContext2D` | Target canvas context |
|
|
191
|
-
| `text` | `string` | ASCII string to render |
|
|
192
|
-
| `x` | `number` | Left edge in game pixels |
|
|
193
|
-
| `y` | `number` | Top edge in game pixels |
|
|
194
|
-
| `ink` | `string` | Foreground color |
|
|
195
|
-
| `paper?` | `string` | Optional background color |
|
|
154
|
+
Draws a string left-to-right, one character per `CELL`-wide slot.
|
|
196
155
|
|
|
197
156
|
```ts
|
|
198
|
-
import { drawText, C } from 'zx-kit'
|
|
199
|
-
|
|
200
157
|
drawText(ctx, 'SCORE:00000', 0, statusY, C.B_WHITE, C.BLACK)
|
|
201
158
|
```
|
|
202
159
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
Draws a string horizontally centered within a canvas of `cols` character columns. The `cols` parameter must match your game's grid width (e.g. 32 for a standard Spectrum layout).
|
|
160
|
+
#### `drawTextCentered(ctx, text, y, cols, ink, paper?): void`
|
|
206
161
|
|
|
207
|
-
|
|
208
|
-
|-----------|------|-------------|
|
|
209
|
-
| `ctx` | `CanvasRenderingContext2D` | Target canvas context |
|
|
210
|
-
| `text` | `string` | ASCII string to render |
|
|
211
|
-
| `y` | `number` | Top edge in game pixels |
|
|
212
|
-
| `cols` | `number` | Total character columns (canvas width ÷ CELL) |
|
|
213
|
-
| `ink` | `string` | Foreground color |
|
|
214
|
-
| `paper?` | `string` | Optional background color |
|
|
162
|
+
Centers a string within `cols` character columns. Bind `cols` once in a helper to avoid repetition.
|
|
215
163
|
|
|
216
164
|
```ts
|
|
217
|
-
|
|
165
|
+
// Bind once:
|
|
166
|
+
const centered = (ctx: CanvasRenderingContext2D, text: string, y: number, ink: string) =>
|
|
167
|
+
drawTextCentered(ctx, text, y, 32, ink)
|
|
218
168
|
|
|
219
|
-
|
|
169
|
+
centered(ctx, 'GAME OVER', y, C.B_RED)
|
|
220
170
|
```
|
|
221
171
|
|
|
222
|
-
|
|
172
|
+
#### `flashBorder(color, times, intervalMs, resetColor?): void`
|
|
173
|
+
|
|
174
|
+
Flashes `document.body.style.backgroundColor` between `color` and `resetColor`. Fire-and-forget — does not block. One flash = one `color → resetColor` cycle. Always resets to `resetColor` on completion (default `C.BLACK`).
|
|
223
175
|
|
|
224
176
|
```ts
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
177
|
+
flashBorder(C.B_RED, 3, 150) // explosion — 3 red flashes → black
|
|
178
|
+
flashBorder(C.B_GREEN, 2, 200) // level complete
|
|
179
|
+
flashBorder(C.B_CYAN, 2, 120, C.BLUE) // flash → reset to blue border
|
|
228
180
|
```
|
|
229
181
|
|
|
230
182
|
---
|
|
231
183
|
|
|
232
184
|
### `audio.ts` — Web Audio engine (ZX Spectrum style)
|
|
233
185
|
|
|
234
|
-
Wraps the Web Audio API
|
|
186
|
+
Wraps the Web Audio API for authentic 1-bit square-wave sound. All audio goes through a shared `AudioContext` and a single master `GainNode`.
|
|
235
187
|
|
|
236
|
-
**
|
|
188
|
+
**Browser autoplay policy:** `AudioContext` must be created inside a user gesture (click or keydown). Call `initAudio()` from an event handler.
|
|
237
189
|
|
|
238
|
-
####
|
|
190
|
+
#### `initAudio(volume?): void`
|
|
239
191
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
Creates the `AudioContext` and master `GainNode`. Idempotent — safe to call multiple times (subsequent calls are no-ops). Default volume: `0.3`.
|
|
192
|
+
Creates the `AudioContext` and master gain. Idempotent — safe to call multiple times.
|
|
243
193
|
|
|
244
194
|
```ts
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
document.addEventListener('keydown', () => initAudio(), { once: true })
|
|
195
|
+
window.addEventListener('keydown', () => initAudio(), { once: true })
|
|
196
|
+
window.addEventListener('click', () => initAudio(), { once: true })
|
|
248
197
|
```
|
|
249
198
|
|
|
250
|
-
|
|
199
|
+
#### `resumeAudio(): void`
|
|
251
200
|
|
|
252
|
-
Resumes a suspended `AudioContext`.
|
|
201
|
+
Resumes a suspended `AudioContext`. Call before scheduling audio in the game loop.
|
|
253
202
|
|
|
254
|
-
|
|
255
|
-
import { resumeAudio, beep, getAudioContext } from 'zx-kit'
|
|
203
|
+
#### `getAudioContext(): AudioContext | null`
|
|
256
204
|
|
|
257
|
-
|
|
258
|
-
if (ctx) {
|
|
259
|
-
resumeAudio()
|
|
260
|
-
beep(440, 80, ctx.currentTime)
|
|
261
|
-
}
|
|
262
|
-
```
|
|
205
|
+
Returns the current context, or `null` before `initAudio()`.
|
|
263
206
|
|
|
264
|
-
|
|
207
|
+
#### `getMasterGain(): GainNode | null`
|
|
265
208
|
|
|
266
|
-
Returns the
|
|
209
|
+
Returns the master gain node. Connect custom oscillators here to respect global volume.
|
|
267
210
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
Returns the master `GainNode`. Connect your own oscillators/gains to this node to respect the global volume level. Returns `null` before `initAudio()`.
|
|
211
|
+
#### `Note` interface
|
|
271
212
|
|
|
272
213
|
```ts
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
214
|
+
interface Note {
|
|
215
|
+
freq: number // Hz — use 0 for a rest (silence)
|
|
216
|
+
dur: number // ms — duration of note or rest
|
|
217
|
+
}
|
|
277
218
|
```
|
|
278
219
|
|
|
279
|
-
|
|
220
|
+
#### `playPattern(notes, startDelay?): void`
|
|
221
|
+
|
|
222
|
+
Schedules a sequence of notes. `freq: 0` = rest (advances time, no sound). `startDelay` delays the whole pattern in milliseconds.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// Rising arpeggio
|
|
226
|
+
playPattern([
|
|
227
|
+
{ freq: 262, dur: 100 }, // C4
|
|
228
|
+
{ freq: 330, dur: 100 }, // E4
|
|
229
|
+
{ freq: 392, dur: 100 }, // G4
|
|
230
|
+
{ freq: 523, dur: 200 }, // C5
|
|
231
|
+
])
|
|
232
|
+
|
|
233
|
+
// With rests and a 200ms startup delay
|
|
234
|
+
playPattern([
|
|
235
|
+
{ freq: 523, dur: 120 }, // C5
|
|
236
|
+
{ freq: 0, dur: 40 }, // rest
|
|
237
|
+
{ freq: 784, dur: 200 }, // G5
|
|
238
|
+
], 200)
|
|
239
|
+
```
|
|
280
240
|
|
|
281
|
-
|
|
241
|
+
#### `beep(freq, durationMs, startTime): void`
|
|
282
242
|
|
|
283
|
-
|
|
284
|
-
|-----------|------|-------------|
|
|
285
|
-
| `freq` | `number` | Frequency in Hz |
|
|
286
|
-
| `durationMs` | `number` | Duration in milliseconds |
|
|
287
|
-
| `startTime` | `number` | `AudioContext.currentTime` offset to start at |
|
|
243
|
+
Schedules a single square-wave beep at an absolute `AudioContext.currentTime`. Use `playPattern` for sequences; use `beep` directly when you need algorithmic timing control.
|
|
288
244
|
|
|
289
245
|
```ts
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
resumeAudio()
|
|
295
|
-
const now = ctx.currentTime
|
|
296
|
-
// Two-pip warning: 880Hz beep twice with 60ms gap
|
|
297
|
-
beep(880, 80, now)
|
|
298
|
-
beep(880, 80, now + 0.14)
|
|
299
|
-
}
|
|
246
|
+
const audio = getAudioContext()!
|
|
247
|
+
resumeAudio()
|
|
248
|
+
beep(880, 80, audio.currentTime)
|
|
249
|
+
beep(880, 80, audio.currentTime + 0.14) // 140ms later
|
|
300
250
|
```
|
|
301
251
|
|
|
302
252
|
---
|
|
303
253
|
|
|
304
254
|
### `input.ts` — Keyboard input with key-repeat
|
|
305
255
|
|
|
306
|
-
Handles arrow
|
|
307
|
-
|
|
308
|
-
**Important:** Call `initInput()` once at startup (after DOM is ready) to attach `keydown`/`keyup` listeners. Then call `tickMovement(dt)` every frame.
|
|
309
|
-
|
|
310
|
-
#### Exports
|
|
256
|
+
Handles arrow-key movement with configurable key-repeat (immediate on first press, auto-repeat on hold) plus single-consume flags for action keys.
|
|
311
257
|
|
|
312
|
-
|
|
258
|
+
**Call `initInput()` once at startup.** Then call `tickMovement(dt)` every frame.
|
|
313
259
|
|
|
314
|
-
|
|
315
|
-
- `repeatDelay`: `150` ms — time before auto-repeat kicks in after initial press
|
|
316
|
-
- `repeatInterval`: `80` ms — time between repeats while key is held
|
|
260
|
+
#### `Direction` type
|
|
317
261
|
|
|
318
262
|
```ts
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
initInput(150, 80)
|
|
263
|
+
type Direction = 'up' | 'down' | 'left' | 'right'
|
|
322
264
|
```
|
|
323
265
|
|
|
324
|
-
|
|
266
|
+
#### `initInput(repeatDelay?, repeatInterval?): void`
|
|
325
267
|
|
|
326
|
-
|
|
268
|
+
Attaches `keydown`/`keyup` listeners. Keys: arrows = movement, `F` = flag, `P` = pause, `Ctrl+Shift+B` = debug.
|
|
327
269
|
|
|
328
270
|
```ts
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
function gameLoop(dtMs: number) {
|
|
332
|
-
const dir = tickMovement(dtMs)
|
|
333
|
-
if (dir) movePlayer(dir)
|
|
334
|
-
}
|
|
271
|
+
initInput() // 150ms delay, 80ms repeat
|
|
272
|
+
initInput(200, 60) // custom timing
|
|
335
273
|
```
|
|
336
274
|
|
|
337
|
-
|
|
275
|
+
#### `tickMovement(dtMs): Direction | null`
|
|
338
276
|
|
|
339
|
-
Returns
|
|
277
|
+
Returns the movement direction for this frame, or `null`. Call once per frame.
|
|
340
278
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
##### `consumePause(): boolean`
|
|
346
|
-
|
|
347
|
-
Returns `true` once if `P` key was pressed.
|
|
279
|
+
```ts
|
|
280
|
+
const dir = tickMovement(dt)
|
|
281
|
+
if (dir) movePlayer(dir)
|
|
282
|
+
```
|
|
348
283
|
|
|
349
|
-
|
|
284
|
+
#### Consume flags
|
|
350
285
|
|
|
351
|
-
|
|
286
|
+
| Function | Trigger | Use case |
|
|
287
|
+
|----------|---------|----------|
|
|
288
|
+
| `consumeFlag()` | `F` key | Flag / unflag a cell |
|
|
289
|
+
| `consumeDebug()` | `Ctrl+Shift+B` | Toggle debug mode |
|
|
290
|
+
| `consumePause()` | `P` key | Pause / unpause |
|
|
291
|
+
| `consumeAnyKey()` | Any key | Dismiss overlays, start game |
|
|
352
292
|
|
|
353
|
-
|
|
293
|
+
Each returns `true` once per press, then resets.
|
|
354
294
|
|
|
355
|
-
|
|
295
|
+
#### `isHeld(key): boolean`
|
|
356
296
|
|
|
357
|
-
|
|
297
|
+
Returns whether a key is currently held. Argument is `KeyboardEvent.key`.
|
|
358
298
|
|
|
359
299
|
```ts
|
|
360
|
-
|
|
300
|
+
if (isHeld('ArrowUp')) { ... }
|
|
361
301
|
```
|
|
362
302
|
|
|
363
303
|
---
|
|
@@ -366,46 +306,47 @@ export type Direction = 'up' | 'down' | 'left' | 'right'
|
|
|
366
306
|
|
|
367
307
|
```
|
|
368
308
|
zx-kit/
|
|
369
|
-
├── package.json #
|
|
370
|
-
├── tsconfig.json # strict,
|
|
309
|
+
├── package.json # exports: { ".": "./dist/index.js" }
|
|
310
|
+
├── tsconfig.json # strict, emits to dist/
|
|
371
311
|
├── README.md
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
312
|
+
├── src/ # TypeScript source
|
|
313
|
+
│ ├── index.ts # barrel — re-exports everything
|
|
314
|
+
│ ├── palette.ts # SCALE, CELL, C, SpectrumColor
|
|
315
|
+
│ ├── font.ts # FONT, getCharRow
|
|
316
|
+
│ ├── renderer.ts # setupCanvas, mirrorSprite, drawSprite, drawChar, drawText,
|
|
317
|
+
│ │ # drawTextCentered, flashBorder
|
|
318
|
+
│ ├── audio.ts # initAudio, resumeAudio, beep, playPattern, Note,
|
|
319
|
+
│ │ # getAudioContext, getMasterGain
|
|
320
|
+
│ └── input.ts # initInput, tickMovement, consumeFlag/Debug/Pause/AnyKey,
|
|
321
|
+
│ # isHeld, Direction
|
|
322
|
+
└── dist/ # compiled output (generated by npm run build)
|
|
323
|
+
├── index.js / .d.ts
|
|
324
|
+
└── ...
|
|
379
325
|
```
|
|
380
326
|
|
|
381
327
|
---
|
|
382
328
|
|
|
383
329
|
## Design principles
|
|
384
330
|
|
|
385
|
-
- **
|
|
331
|
+
- **Compiled distribution** — ships compiled JS + `.d.ts` in `dist/`. No bundler configuration needed in the consuming project.
|
|
386
332
|
- **No runtime dependencies** — only Web platform APIs (`CanvasRenderingContext2D`, `AudioContext`, `KeyboardEvent`).
|
|
387
333
|
- **Strict TypeScript** — `strict: true`, `noUnusedLocals`, `noUnusedParameters`. No `any`.
|
|
388
|
-
- **Singleton state** — `audio.ts` and `input.ts` hold module-level state. Suitable for single-game use; not suitable for
|
|
389
|
-
- **ZX Spectrum
|
|
334
|
+
- **Singleton state** — `audio.ts` and `input.ts` hold module-level state. Suitable for single-game use; not suitable for multiple independent game instances on the same page.
|
|
335
|
+
- **ZX Spectrum authenticity** — palette values, cell size, and font bytes are constants, not configuration. The library is deliberately opinionated.
|
|
390
336
|
|
|
391
337
|
---
|
|
392
338
|
|
|
393
|
-
##
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
| `FONT`, `getCharRow` | `font.ts` | Re-export from game's `font.ts` |
|
|
405
|
-
| `AudioContext` + `GainNode` init | `audio.ts` | Replace with `initAudio()` / `getAudioContext()` / `getMasterGain()` |
|
|
406
|
-
| `beep()` helper | `audio.ts` | Import directly; remove inline version |
|
|
407
|
-
| Arrow-key repeat logic | `input.ts` | Replace with `initInput()` + `tickMovement(dt)` |
|
|
408
|
-
| Flag / debug / pause key flags | `input.ts` | Use `consumeFlag()`, `consumeDebug()`, `consumePause()` |
|
|
339
|
+
## Local development
|
|
340
|
+
|
|
341
|
+
To work against a local checkout instead of the npm version:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
# In your game project
|
|
345
|
+
npm install ../zx-kit --prefer-online
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
> The `--prefer-online` flag ensures npm resolves from the local path without caching issues.
|
|
349
|
+
> After publishing a new version to npm, switch back with `npm install zx-kit@latest`.
|
|
409
350
|
|
|
410
351
|
---
|
|
411
352
|
|