zx-kit 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **A Speccy-flavoured fantasy toolkit for tiny TypeScript browser games.**
4
4
  > Inspired by the ZX Spectrum — not an emulator, not a hardware clone.
5
5
 
6
- Spectrum-palette canvas rendering. ROM bitmap font. AY-3-8912 three-channel audio. Beeper SFX. Tile maps. Free-roaming sprites. Collision detection. Saves. Camera. Scene manager. Particle pool. Dithered lighting. Offscreen layer cache. Zero dependencies. TypeScript-first.
6
+ Spectrum-palette canvas rendering. ROM bitmap font. AY-3-8912 three-channel audio. Beeper SFX. Tile maps. Free-roaming sprites. Collision detection. Saves. Camera. Scene manager. Particle pool. Dithered lighting. Offscreen layer cache. Authentic attribute clash. Monochrome playfield. Zero dependencies. TypeScript-first.
7
7
 
8
8
  [![npm](https://img.shields.io/npm/v/zx-kit)](https://www.npmjs.com/package/zx-kit)
9
9
  [![license](https://img.shields.io/npm/l/zx-kit)](LICENSE)
@@ -28,6 +28,8 @@ zx-kit captures that aesthetic in TypeScript. You get the Spectrum's palette, RO
28
28
  - **Canvas renderer** — pixel-perfect scaled rendering, sprite flipping, text drawing, CRT scanline overlay, animated border flashing
29
29
  - **Tile map engine** — scrollable maps, O(1) id-index, smart seasonal background swapping, solid-tile collision queries
30
30
  - **Offscreen layer cache** — render a static or rarely-changing layer (tile map, CRT overlay) once to an offscreen canvas and blit it each frame; `dirty`-flag invalidation turns thousands of per-pixel `fillRect`s into a single `drawImage`
31
+ - **Authentic attribute clash (opt-in)** — a 32×24 cell ink/paper screen that reproduces the real Spectrum colour bleed when a sprite and the background share an 8×8 cell; resolved to one `putImageData`/frame. Off by default, on when you want it
32
+ - **Monochrome playfield (opt-in)** — the classic anti-clash trick: render the action area in a single ink + paper at its own size, keep the colour in the HUD around it. Everything inside becomes a clean two-colour silhouette — no clash, ever
31
33
  - **Free-roaming sprites** — position, velocity, gravity, `flipX` caching, transparent or opaque background
32
34
  - **Three-tier collision** — AABB overlap tests, generic rect-vs-tile wall resolution (any sprite size), and pixel-precise mask overlap with O(pixels) sorted-merge intersection — no allocations per frame
33
35
  - **Keyboard and gamepad input** — configurable key-repeat, transparent gamepad polling, single-consume action flags, instant state reset on phase transitions
@@ -591,6 +593,8 @@ requestAnimationFrame(loop)
591
593
  | [`i18n.ts`](#i18nts--runtime-locale-selection) | Type-safe runtime locale selection for translated string packs |
592
594
  | [`lighting.ts`](#lightingts--dithered-cave-darkness) | Dithered cave darkness: pre-baked level tiles + dirty-cell buffer, one blit/frame (no per-frame putImageData) |
593
595
  | [`cache.ts`](#cachets--offscreen-layer-cache) | Offscreen layer cache: render a static layer once, blit each frame, `dirty`-flag invalidation |
596
+ | [`attrscreen.ts`](#attrscreents--attribute-clash-opt-in) | Opt-in authentic ZX colour clash: 1-bit pixels + 32×24 per-cell ink/paper, one `putImageData`/frame |
597
+ | [`monoscreen.ts`](#monoscreents--monochrome-playfield) | Opt-in monochrome playfield (own size): 1-bit mask + one ink/paper, blitted at an offset — clash-proof |
594
598
  | [`music.ts`](#musicts--note-name-ay-music) | Write AY music by note name (`A5`, `C#4`) and loop it for background tracks |
595
599
 
596
600
  ---
@@ -2792,6 +2796,123 @@ invalidateLayer(tiles)
2792
2796
 
2793
2797
  ---
2794
2798
 
2799
+ ## `attrscreen.ts` — Attribute Clash (opt-in)
2800
+
2801
+ The real Spectrum stored the screen as **two planes**: a 1-bit pixel bitmap and a
2802
+ 32×24 grid where each 8×8 cell holds exactly *one* ink + *one* paper. Drawing into a
2803
+ cell rewrote that single attribute, so the sprite and the background sharing the
2804
+ cell snapped to the same two colours — the famous **colour clash**.
2805
+
2806
+ zx-kit composites in full colour by default (no clash — see *Spectrum-inspired, not
2807
+ hardware-accurate*). `attrscreen.ts` is the opt-in way to get the authentic bleed,
2808
+ the same shape as `drawScanlines` / `renderDarkness`: route a frame through an
2809
+ `AttrScreen`, then `flushAttrScreen` once.
2810
+
2811
+ `stampMono` writes a monochrome bitmap's *shape* into the pixel plane and
2812
+ re-attributes every cell a lit pixel lands in — and it **never clears** other
2813
+ pixels, so whatever was already in a touched cell renders in the new colour. That
2814
+ is the clash. The flush resolves both planes into one reused `ImageData` and uploads
2815
+ it with a single `putImageData` + `drawImage` — never per-pixel `fillRect`.
2816
+
2817
+ > Not hardware-exact by choice: ink and paper may be any two of the 15 colours (the
2818
+ > real machine forced both to share the BRIGHT bit), and FLASH is not modelled (yet).
2819
+ > Assumes a little-endian platform (every browser). Headless-safe.
2820
+
2821
+ ### `createAttrScreen(cols = 32, rows = 24): AttrScreen`
2822
+
2823
+ Allocates a screen-space attribute plane (default 32×24 cells = 256×192). Create
2824
+ once; reuse across frames. Throws on a non-positive size.
2825
+
2826
+ ### `clearAttrScreen(scr, paper, ink?)`
2827
+
2828
+ Resets for a new frame: clears all pixels to paper and fills every cell's
2829
+ attributes (`ink` defaults to `paper`). Call before stamping.
2830
+
2831
+ ### `stampMono(scr, bitmap, x, y, ink, paper, policy?)`
2832
+
2833
+ Stamps a monochrome `Bitmap` at screen pixel `(x, y)` (rounded; may be sub-cell,
2834
+ negative, or off-screen — clipped). `policy` controls how touched cells recolour:
2835
+ `'both'` (default — ink **and** paper, the most authentic bleed), `'ink-only'`
2836
+ (keep the existing paper), `'paper-only'`.
2837
+
2838
+ ### `flushAttrScreen(ctx, scr)`
2839
+
2840
+ Resolves the two planes into RGBA and uploads with one `putImageData` + `drawImage`
2841
+ at `(0, 0)` under the current transform (so `setupCanvas`'s `×scale` fills the
2842
+ canvas). Headless: fills `scr.rgba` and skips the blit.
2843
+
2844
+ ```ts
2845
+ const scr = createAttrScreen() // once
2846
+ // each frame, in screen space:
2847
+ clearAttrScreen(scr, C.BLACK)
2848
+ stampMono(scr, caveBitmap, 0, 0, C.B_BLUE, C.BLACK) // background
2849
+ stampMono(scr, rabbitBitmap, rx, ry, C.B_WHITE, C.BLACK) // sprite → its cells clash to white
2850
+ flushAttrScreen(ctx, scr)
2851
+ ```
2852
+
2853
+ > A cell clashes only when a lit pixel lands in it (silhouette clash), so a sprite
2854
+ > recolours exactly the cells it visibly occupies.
2855
+
2856
+ ---
2857
+
2858
+ ## `monoscreen.ts` — Monochrome Playfield
2859
+
2860
+ The surest cure for attribute clash is to not have it: render the action area in a
2861
+ **single ink + paper** and keep the colour in a separate HUD/border. Light Force,
2862
+ Bobby Bearing, Highway Encounter and Head Over Heels all did exactly this.
2863
+
2864
+ A `MonoScreen` is a **1-bit foreground mask of its own size** (smaller than the
2865
+ canvas) plus two colours. Everything drawn into it collapses to ink (lit pixels)
2866
+ or paper — a white sprite, a green tile and a cyan hero all become the same ink, so
2867
+ they can never clash. Draw the colourful HUD normally *outside* the region;
2868
+ `flushMonoScreen` blits the playfield at an offset.
2869
+
2870
+ Versus `attrscreen` (authentic *per-cell* clash): mono is one ink/paper for the
2871
+ **whole** region — simpler, cheaper, and the right tool for a clean retro look
2872
+ rather than the colour-bleed artefact. Flush is one `putImageData` + `drawImage`.
2873
+ Little-endian; headless-safe.
2874
+
2875
+ ### `createMonoScreen(width, height, ink, paper): MonoScreen`
2876
+
2877
+ Creates a playfield of its own pixel size (not the canvas). `ink`/`paper` are
2878
+ mutable — recolour the whole playfield any time (e.g. per biome). Throws on a
2879
+ non-positive size.
2880
+
2881
+ ### `clearMonoScreen(scr)`
2882
+
2883
+ Resets the mask to all-paper. Call at the start of each frame.
2884
+
2885
+ ### `drawMonoBitmap(scr, bitmap, x, y)`
2886
+
2887
+ Draws a monochrome `Bitmap`'s lit pixels as foreground (clipped). Clear pixels are
2888
+ left untouched, so paper and earlier draws show through.
2889
+
2890
+ ### `fillMono(scr, x, y, w, h)`
2891
+
2892
+ Sets a filled foreground rectangle (clipped) — a 1px-wide rect is a thread, rail or
2893
+ laser; a block is a platform.
2894
+
2895
+ ### `flushMonoScreen(ctx, scr, dx?, dy?)`
2896
+
2897
+ Resolves the mask (ink where lit, paper elsewhere) and blits the region at canvas
2898
+ offset `(dx, dy)` under the current transform. Headless: fills `scr.rgba`, skips
2899
+ the blit.
2900
+
2901
+ ```ts
2902
+ const play = createMonoScreen(256, 160, C.BLACK, C.B_CYAN) // playfield, once
2903
+ // each frame, in playfield space:
2904
+ clearMonoScreen(play)
2905
+ drawMonoBitmap(play, tileBmp, tx, ty)
2906
+ drawMonoBitmap(play, heroBmp, hx, hy)
2907
+ fillMono(play, threadX, threadY, 1, len)
2908
+ flushMonoScreen(ctx, play, 0, 16) // playfield below a 16px colour HUD
2909
+ ```
2910
+
2911
+ > Tip: it carries its own `ink`/`paper`, so a one-line swap reskins the whole
2912
+ > playfield — a cheap "biome" tint or a damage flash.
2913
+
2914
+ ---
2915
+
2795
2916
  ## Architecture
2796
2917
 
2797
2918
  ### Module structure
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @module attrscreen
3
+ *
4
+ * **Authentic ZX attribute clash — opt-in.** The real Spectrum stored the screen
5
+ * as two separate planes: a 1-bit **pixel** bitmap (256×192) and a 32×24
6
+ * **attribute** grid where each 8×8 cell holds exactly *one* ink + *one* paper.
7
+ * Drawing a sprite into a cell rewrote that cell's attribute, so everything in
8
+ * the cell — sprite *and* background — snapped to the same two colours. That bleed
9
+ * is the famous *colour clash*.
10
+ *
11
+ * zx-kit normally composites in full colour (no clash — by design, see the README).
12
+ * This module is the opt-in way to get the real thing, the same shape as the other
13
+ * effects ({@link "renderer" | drawScanlines}, {@link "lighting" | renderDarkness}):
14
+ * a game that wants clash routes its drawing through an {@link AttrScreen} and
15
+ * calls {@link flushAttrScreen} once per frame.
16
+ *
17
+ * **The dual plane:** {@link stampMono} writes a monochrome bitmap's *shape* into
18
+ * the pixel plane and re-attributes every cell it touches (per {@link AttrPolicy}).
19
+ * Crucially it does **not** clear other pixels — so a leaf already drawn in a cell
20
+ * keeps its pixels but now renders in the *new* cell colour. That is the clash.
21
+ *
22
+ * **Fast flush:** the two planes are resolved into one RGBA buffer and uploaded
23
+ * with a single `putImageData` + `drawImage` — never per-pixel `fillRect`. Assumes
24
+ * a little-endian platform (every browser; common Node).
25
+ *
26
+ * Headless-safe: with no `document` the offscreen canvas is `null`, the resolve
27
+ * still runs (so the logic is testable) and the blit is skipped — nothing throws.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const scr = createAttrScreen() // 32×24 cells = 256×192, once
32
+ * // each frame, in screen space:
33
+ * clearAttrScreen(scr, C.BLACK) // blank paper
34
+ * stampMono(scr, caveBitmap, 0, 0, C.B_BLUE, C.BLACK) // background
35
+ * stampMono(scr, rabbitBitmap, rx, ry, C.B_WHITE, C.BLACK) // sprite → its cells clash to white
36
+ * flushAttrScreen(ctx, scr) // one putImageData + drawImage
37
+ * ```
38
+ */
39
+ import { type SpectrumColor } from './palette.js';
40
+ import type { Bitmap } from './renderer.js';
41
+ /**
42
+ * How a {@link stampMono} re-colours each cell it touches:
43
+ * - `'both'` — set ink **and** paper (last writer owns the whole cell — the most
44
+ * authentic clash: the background paper changes too).
45
+ * - `'ink-only'` — set ink, keep the existing paper (gentler bleed).
46
+ * - `'paper-only'` — set paper, keep the existing ink.
47
+ */
48
+ export type AttrPolicy = 'both' | 'ink-only' | 'paper-only';
49
+ /** A screen-space dual plane: 1-bit pixels + per-cell ink/paper attributes. */
50
+ export interface AttrScreen {
51
+ readonly cols: number;
52
+ readonly rows: number;
53
+ readonly width: number;
54
+ readonly height: number;
55
+ /** One byte per pixel — `1` = ink, `0` = paper. Row-major, `width*height`. */
56
+ readonly pixels: Uint8Array;
57
+ /** Packed RGBA (little-endian) ink colour per cell, `cols*rows`. */
58
+ readonly cellInk: Uint32Array;
59
+ /** Packed RGBA paper colour per cell, `cols*rows`. */
60
+ readonly cellPaper: Uint32Array;
61
+ /** Resolved RGBA frame buffer (`width*height*4`), filled by {@link flushAttrScreen}. */
62
+ readonly rgba: Uint8ClampedArray;
63
+ /** Reusable `ImageData` backing {@link rgba} (`null` when headless). */
64
+ readonly image: ImageData | null;
65
+ /** Offscreen canvas the resolved buffer is uploaded to (`null` when headless). */
66
+ readonly canvas: HTMLCanvasElement | null;
67
+ }
68
+ /**
69
+ * Creates a screen-space attribute plane of `cols`×`rows` 8×8 cells (default
70
+ * 32×24 = 256×192). Allocate once; reuse across frames. Throws on a non-positive
71
+ * size.
72
+ */
73
+ export declare function createAttrScreen(cols?: number, rows?: number): AttrScreen;
74
+ /**
75
+ * Resets the screen for a new frame: clears all pixels to paper and fills every
76
+ * cell's attributes. `ink` defaults to `paper`, so untouched cells are a flat
77
+ * paper colour. Call at the start of each frame, before stamping.
78
+ */
79
+ export declare function clearAttrScreen(scr: AttrScreen, paper: SpectrumColor, ink?: SpectrumColor): void;
80
+ /**
81
+ * Stamps a monochrome {@link Bitmap} at screen pixel `(x, y)` (rounded; may be
82
+ * sub-cell, negative, or off-screen — clipped). Sets the bitmap's lit pixels in
83
+ * the pixel plane and re-attributes every cell that receives a lit pixel, per
84
+ * `policy` (default `'both'`). Lit-pixel-only: existing pixels are never cleared,
85
+ * so other sprites/background in a touched cell bleed to the new colour.
86
+ */
87
+ export declare function stampMono(scr: AttrScreen, bitmap: Bitmap, x: number, y: number, ink: SpectrumColor, paper: SpectrumColor, policy?: AttrPolicy): void;
88
+ /**
89
+ * Resolves the two planes into RGBA (each pixel takes its cell's ink or paper)
90
+ * and uploads the result with one `putImageData` + `drawImage`. The image is
91
+ * drawn at `(0, 0)` under the current transform, so the usual `setupCanvas`
92
+ * `×scale` makes it fill the canvas. Headless: fills {@link AttrScreen.rgba} and
93
+ * skips the blit.
94
+ */
95
+ export declare function flushAttrScreen(ctx: CanvasRenderingContext2D, scr: AttrScreen): void;
96
+ //# sourceMappingURL=attrscreen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attrscreen.d.ts","sourceRoot":"","sources":["../src/attrscreen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAE3C;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,YAAY,CAAA;AAE3D,+EAA+E;AAC/E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAA;IAC/B,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;IAChC,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;IAChC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAA;CAC1C;AAUD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,SAAK,EAAE,IAAI,SAAK,GAAG,UAAU,CAiCjE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,GAAE,aAAqB,GAAG,IAAI,CAIvG;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,GAAG,EAAE,UAAU,EACf,MAAM,EAAE,MAAM,EACd,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,GAAG,EAAE,aAAa,EAClB,KAAK,EAAE,aAAa,EACpB,MAAM,GAAE,UAAmB,GAC1B,IAAI,CA+BN;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,wBAAwB,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAoBpF"}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @module attrscreen
3
+ *
4
+ * **Authentic ZX attribute clash — opt-in.** The real Spectrum stored the screen
5
+ * as two separate planes: a 1-bit **pixel** bitmap (256×192) and a 32×24
6
+ * **attribute** grid where each 8×8 cell holds exactly *one* ink + *one* paper.
7
+ * Drawing a sprite into a cell rewrote that cell's attribute, so everything in
8
+ * the cell — sprite *and* background — snapped to the same two colours. That bleed
9
+ * is the famous *colour clash*.
10
+ *
11
+ * zx-kit normally composites in full colour (no clash — by design, see the README).
12
+ * This module is the opt-in way to get the real thing, the same shape as the other
13
+ * effects ({@link "renderer" | drawScanlines}, {@link "lighting" | renderDarkness}):
14
+ * a game that wants clash routes its drawing through an {@link AttrScreen} and
15
+ * calls {@link flushAttrScreen} once per frame.
16
+ *
17
+ * **The dual plane:** {@link stampMono} writes a monochrome bitmap's *shape* into
18
+ * the pixel plane and re-attributes every cell it touches (per {@link AttrPolicy}).
19
+ * Crucially it does **not** clear other pixels — so a leaf already drawn in a cell
20
+ * keeps its pixels but now renders in the *new* cell colour. That is the clash.
21
+ *
22
+ * **Fast flush:** the two planes are resolved into one RGBA buffer and uploaded
23
+ * with a single `putImageData` + `drawImage` — never per-pixel `fillRect`. Assumes
24
+ * a little-endian platform (every browser; common Node).
25
+ *
26
+ * Headless-safe: with no `document` the offscreen canvas is `null`, the resolve
27
+ * still runs (so the logic is testable) and the blit is skipped — nothing throws.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const scr = createAttrScreen() // 32×24 cells = 256×192, once
32
+ * // each frame, in screen space:
33
+ * clearAttrScreen(scr, C.BLACK) // blank paper
34
+ * stampMono(scr, caveBitmap, 0, 0, C.B_BLUE, C.BLACK) // background
35
+ * stampMono(scr, rabbitBitmap, rx, ry, C.B_WHITE, C.BLACK) // sprite → its cells clash to white
36
+ * flushAttrScreen(ctx, scr) // one putImageData + drawImage
37
+ * ```
38
+ */
39
+ import { CELL } from './palette.js';
40
+ /** Packs a `#RRGGBB` Spectrum colour into a little-endian RGBA word (opaque). */
41
+ function hexToU32(hex) {
42
+ const r = parseInt(hex.slice(1, 3), 16);
43
+ const g = parseInt(hex.slice(3, 5), 16);
44
+ const b = parseInt(hex.slice(5, 7), 16);
45
+ return ((255 << 24) | (b << 16) | (g << 8) | r) >>> 0;
46
+ }
47
+ /**
48
+ * Creates a screen-space attribute plane of `cols`×`rows` 8×8 cells (default
49
+ * 32×24 = 256×192). Allocate once; reuse across frames. Throws on a non-positive
50
+ * size.
51
+ */
52
+ export function createAttrScreen(cols = 32, rows = 24) {
53
+ if (!Number.isInteger(cols) || cols <= 0 || !Number.isInteger(rows) || rows <= 0) {
54
+ throw new Error(`createAttrScreen: cols and rows must be positive integers, got ${cols}×${rows}`);
55
+ }
56
+ const width = cols * CELL;
57
+ const height = rows * CELL;
58
+ let canvas = null;
59
+ let image = null;
60
+ if (typeof document !== 'undefined') {
61
+ canvas = document.createElement('canvas');
62
+ canvas.width = width;
63
+ canvas.height = height;
64
+ const ctx = canvas.getContext('2d');
65
+ if (ctx) {
66
+ ctx.imageSmoothingEnabled = false;
67
+ image = ctx.createImageData(width, height);
68
+ }
69
+ }
70
+ // In the browser the resolved buffer IS the ImageData's data (no per-frame copy,
71
+ // no constructor); headless it's a standalone buffer so the resolve stays testable.
72
+ const rgba = image ? image.data : new Uint8ClampedArray(width * height * 4);
73
+ return {
74
+ cols,
75
+ rows,
76
+ width,
77
+ height,
78
+ pixels: new Uint8Array(width * height),
79
+ cellInk: new Uint32Array(cols * rows),
80
+ cellPaper: new Uint32Array(cols * rows),
81
+ rgba,
82
+ image,
83
+ canvas,
84
+ };
85
+ }
86
+ /**
87
+ * Resets the screen for a new frame: clears all pixels to paper and fills every
88
+ * cell's attributes. `ink` defaults to `paper`, so untouched cells are a flat
89
+ * paper colour. Call at the start of each frame, before stamping.
90
+ */
91
+ export function clearAttrScreen(scr, paper, ink = paper) {
92
+ scr.pixels.fill(0);
93
+ scr.cellPaper.fill(hexToU32(paper));
94
+ scr.cellInk.fill(hexToU32(ink));
95
+ }
96
+ /**
97
+ * Stamps a monochrome {@link Bitmap} at screen pixel `(x, y)` (rounded; may be
98
+ * sub-cell, negative, or off-screen — clipped). Sets the bitmap's lit pixels in
99
+ * the pixel plane and re-attributes every cell that receives a lit pixel, per
100
+ * `policy` (default `'both'`). Lit-pixel-only: existing pixels are never cleared,
101
+ * so other sprites/background in a touched cell bleed to the new colour.
102
+ */
103
+ export function stampMono(scr, bitmap, x, y, ink, paper, policy = 'both') {
104
+ const inkU32 = hexToU32(ink);
105
+ const paperU32 = hexToU32(paper);
106
+ const setInk = policy !== 'paper-only';
107
+ const setPaper = policy !== 'ink-only';
108
+ const { data, width: bw, height: bh } = bitmap;
109
+ const bytesPerRow = bw >> 3;
110
+ const ox = Math.round(x);
111
+ const oy = Math.round(y);
112
+ for (let row = 0; row < bh; row++) {
113
+ const py = oy + row;
114
+ if (py < 0 || py >= scr.height)
115
+ continue;
116
+ const rowByteBase = row * bytesPerRow;
117
+ const rowPixBase = py * scr.width;
118
+ const cellRowBase = (py >> 3) * scr.cols;
119
+ for (let byteIdx = 0; byteIdx < bytesPerRow; byteIdx++) {
120
+ const byte = data[rowByteBase + byteIdx];
121
+ if (!byte)
122
+ continue;
123
+ const colBase = byteIdx << 3;
124
+ for (let bit = 0; bit < 8; bit++) {
125
+ if (!(byte & (0x80 >> bit)))
126
+ continue;
127
+ const px = ox + colBase + bit;
128
+ if (px < 0 || px >= scr.width)
129
+ continue;
130
+ scr.pixels[rowPixBase + px] = 1;
131
+ const cell = cellRowBase + (px >> 3);
132
+ if (setInk)
133
+ scr.cellInk[cell] = inkU32;
134
+ if (setPaper)
135
+ scr.cellPaper[cell] = paperU32;
136
+ }
137
+ }
138
+ }
139
+ }
140
+ /**
141
+ * Resolves the two planes into RGBA (each pixel takes its cell's ink or paper)
142
+ * and uploads the result with one `putImageData` + `drawImage`. The image is
143
+ * drawn at `(0, 0)` under the current transform, so the usual `setupCanvas`
144
+ * `×scale` makes it fill the canvas. Headless: fills {@link AttrScreen.rgba} and
145
+ * skips the blit.
146
+ */
147
+ export function flushAttrScreen(ctx, scr) {
148
+ const { pixels, cellInk, cellPaper, width, height, cols } = scr;
149
+ const out = new Uint32Array(scr.rgba.buffer);
150
+ let p = 0;
151
+ for (let yy = 0; yy < height; yy++) {
152
+ const cellRow = (yy >> 3) * cols;
153
+ for (let xx = 0; xx < width; xx++) {
154
+ const cell = cellRow + (xx >> 3);
155
+ out[p] = pixels[p] ? cellInk[cell] : cellPaper[cell];
156
+ p++;
157
+ }
158
+ }
159
+ if (scr.canvas && scr.image) {
160
+ const offctx = scr.canvas.getContext('2d');
161
+ if (offctx) {
162
+ offctx.putImageData(scr.image, 0, 0);
163
+ ctx.drawImage(scr.canvas, 0, 0);
164
+ }
165
+ }
166
+ }
167
+ //# sourceMappingURL=attrscreen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attrscreen.js","sourceRoot":"","sources":["../src/attrscreen.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -18,5 +18,7 @@ export * from './save.js';
18
18
  export * from './i18n.js';
19
19
  export * from './lighting.js';
20
20
  export * from './cache.js';
21
+ export * from './attrscreen.js';
22
+ export * from './monoscreen.js';
21
23
  export * from './music.js';
22
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -18,5 +18,7 @@ export * from './save.js';
18
18
  export * from './i18n.js';
19
19
  export * from './lighting.js';
20
20
  export * from './cache.js';
21
+ export * from './attrscreen.js';
22
+ export * from './monoscreen.js';
21
23
  export * from './music.js';
22
24
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @module monoscreen
3
+ *
4
+ * **Monochrome playfield — the classic ZX anti-clash trick.** The surest way to
5
+ * avoid attribute clash is to have no clash at all: render the action area in a
6
+ * single ink + paper, and keep the colour in a separate HUD/border around it.
7
+ * Light Force, Bobby Bearing, Highway Encounter and Head Over Heels all did this.
8
+ *
9
+ * A {@link MonoScreen} is a **1-bit foreground mask of any size** (its own,
10
+ * smaller than the canvas) plus two colours. Everything drawn into it is reduced
11
+ * to ink (lit pixels) or paper (everything else) — a white sprite, a green tile
12
+ * and a cyan rabbit all become the same ink, so they can never clash. Draw your
13
+ * colourful HUD normally *outside* the region; {@link flushMonoScreen} blits the
14
+ * playfield at a given offset.
15
+ *
16
+ * Unlike {@link "attrscreen" | attrscreen} (authentic *per-cell* clash), this is
17
+ * one ink/paper for the **whole** region — simpler, cheaper, and the right tool
18
+ * when you want a clean retro look rather than the colour-bleed artefact.
19
+ *
20
+ * Fast flush: the mask is resolved into one reused `ImageData` and uploaded with
21
+ * a single `putImageData` + `drawImage` — never per-pixel `fillRect`. Little-endian
22
+ * (every browser). Headless-safe: the resolve runs, the blit is skipped.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const play = createMonoScreen(256, 160, C.BLACK, C.B_CYAN) // playfield, once
27
+ * // each frame, in playfield space:
28
+ * clearMonoScreen(play)
29
+ * drawMonoBitmap(play, tileBmp, tx, ty) // tiles → ink
30
+ * drawMonoBitmap(play, rabbitBmp, rx, ry) // sprite → ink (silhouette)
31
+ * fillMono(play, threadX, threadY, 1, len) // a thin line (e.g. a spider thread)
32
+ * flushMonoScreen(ctx, play, 0, 16) // blit the playfield below a 16px HUD
33
+ * // …draw the colourful HUD normally, outside the region…
34
+ * ```
35
+ */
36
+ import type { SpectrumColor } from './palette.js';
37
+ import type { Bitmap } from './renderer.js';
38
+ /** A monochrome region: a 1-bit foreground mask plus an ink/paper colour pair. */
39
+ export interface MonoScreen {
40
+ readonly width: number;
41
+ readonly height: number;
42
+ /** Foreground colour (lit pixels). Mutable — recolour the playfield any time. */
43
+ ink: SpectrumColor;
44
+ /** Background colour (everything else). Mutable. */
45
+ paper: SpectrumColor;
46
+ /** One byte per pixel — `1` = ink/foreground, `0` = paper. Row-major, `width*height`. */
47
+ readonly pixels: Uint8Array;
48
+ /** Resolved RGBA buffer (`width*height*4`), filled by {@link flushMonoScreen}. */
49
+ readonly rgba: Uint8ClampedArray;
50
+ /** Reusable `ImageData` backing {@link rgba} (`null` when headless). */
51
+ readonly image: ImageData | null;
52
+ /** Offscreen canvas the resolved buffer is uploaded to (`null` when headless). */
53
+ readonly canvas: HTMLCanvasElement | null;
54
+ }
55
+ /**
56
+ * Creates a monochrome playfield of the given pixel size — **its own size, not
57
+ * the canvas**. Pick a region smaller than the canvas and keep the HUD colourful
58
+ * around it. `ink`/`paper` are mutable. Create once; reuse across frames. Throws
59
+ * on a non-positive size.
60
+ */
61
+ export declare function createMonoScreen(width: number, height: number, ink: SpectrumColor, paper: SpectrumColor): MonoScreen;
62
+ /** Resets the playfield to all-paper for a new frame. Call before drawing. */
63
+ export declare function clearMonoScreen(scr: MonoScreen): void;
64
+ /**
65
+ * Draws a monochrome {@link Bitmap}'s lit pixels as foreground (ink) at `(x, y)`
66
+ * (rounded; may be off-screen — clipped). Clear pixels are left untouched, so the
67
+ * paper (and anything drawn earlier) shows through.
68
+ */
69
+ export declare function drawMonoBitmap(scr: MonoScreen, bitmap: Bitmap, x: number, y: number): void;
70
+ /**
71
+ * Sets a filled foreground rectangle (clipped) — handy for thin lines (a 1px-wide
72
+ * rect is a spider thread, a ladder rail, a laser) or solid blocks.
73
+ */
74
+ export declare function fillMono(scr: MonoScreen, x: number, y: number, w: number, h: number): void;
75
+ /**
76
+ * Resolves the mask into RGBA (ink where lit, paper elsewhere) and blits the
77
+ * region at canvas offset `(dx, dy)` with one `putImageData` + `drawImage` — under
78
+ * the current transform, so `setupCanvas`'s `×scale` is honoured. Headless: fills
79
+ * {@link MonoScreen.rgba} and skips the blit.
80
+ */
81
+ export declare function flushMonoScreen(ctx: CanvasRenderingContext2D, scr: MonoScreen, dx?: number, dy?: number): void;
82
+ //# sourceMappingURL=monoscreen.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monoscreen.d.ts","sourceRoot":"","sources":["../src/monoscreen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAE3C,kFAAkF;AAClF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,iFAAiF;IACjF,GAAG,EAAE,aAAa,CAAA;IAClB,oDAAoD;IACpD,KAAK,EAAE,aAAa,CAAA;IACpB,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;IAC3B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAA;IAChC,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;IAChC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAA;CAC1C;AAUD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,aAAa,EAClB,KAAK,EAAE,aAAa,GACnB,UAAU,CAkBZ;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAErD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAsB1F;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAS1F;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,wBAAwB,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,SAAI,EAAE,EAAE,SAAI,GAAG,IAAI,CAcpG"}
@@ -0,0 +1,105 @@
1
+ /** Packs a `#RRGGBB` colour into a little-endian RGBA word (opaque). */
2
+ function hexToU32(hex) {
3
+ const r = parseInt(hex.slice(1, 3), 16);
4
+ const g = parseInt(hex.slice(3, 5), 16);
5
+ const b = parseInt(hex.slice(5, 7), 16);
6
+ return ((255 << 24) | (b << 16) | (g << 8) | r) >>> 0;
7
+ }
8
+ /**
9
+ * Creates a monochrome playfield of the given pixel size — **its own size, not
10
+ * the canvas**. Pick a region smaller than the canvas and keep the HUD colourful
11
+ * around it. `ink`/`paper` are mutable. Create once; reuse across frames. Throws
12
+ * on a non-positive size.
13
+ */
14
+ export function createMonoScreen(width, height, ink, paper) {
15
+ if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
16
+ throw new Error(`createMonoScreen: width and height must be positive, got ${width}×${height}`);
17
+ }
18
+ let canvas = null;
19
+ let image = null;
20
+ if (typeof document !== 'undefined') {
21
+ canvas = document.createElement('canvas');
22
+ canvas.width = width;
23
+ canvas.height = height;
24
+ const ctx = canvas.getContext('2d');
25
+ if (ctx) {
26
+ ctx.imageSmoothingEnabled = false;
27
+ image = ctx.createImageData(width, height);
28
+ }
29
+ }
30
+ const rgba = image ? image.data : new Uint8ClampedArray(width * height * 4);
31
+ return { width, height, ink, paper, pixels: new Uint8Array(width * height), rgba, image, canvas };
32
+ }
33
+ /** Resets the playfield to all-paper for a new frame. Call before drawing. */
34
+ export function clearMonoScreen(scr) {
35
+ scr.pixels.fill(0);
36
+ }
37
+ /**
38
+ * Draws a monochrome {@link Bitmap}'s lit pixels as foreground (ink) at `(x, y)`
39
+ * (rounded; may be off-screen — clipped). Clear pixels are left untouched, so the
40
+ * paper (and anything drawn earlier) shows through.
41
+ */
42
+ export function drawMonoBitmap(scr, bitmap, x, y) {
43
+ const { data, width: bw, height: bh } = bitmap;
44
+ const bytesPerRow = bw >> 3;
45
+ const ox = Math.round(x);
46
+ const oy = Math.round(y);
47
+ for (let row = 0; row < bh; row++) {
48
+ const py = oy + row;
49
+ if (py < 0 || py >= scr.height)
50
+ continue;
51
+ const rowByteBase = row * bytesPerRow;
52
+ const rowPixBase = py * scr.width;
53
+ for (let byteIdx = 0; byteIdx < bytesPerRow; byteIdx++) {
54
+ const byte = data[rowByteBase + byteIdx];
55
+ if (!byte)
56
+ continue;
57
+ const colBase = byteIdx << 3;
58
+ for (let bit = 0; bit < 8; bit++) {
59
+ if (!(byte & (0x80 >> bit)))
60
+ continue;
61
+ const px = ox + colBase + bit;
62
+ if (px < 0 || px >= scr.width)
63
+ continue;
64
+ scr.pixels[rowPixBase + px] = 1;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ /**
70
+ * Sets a filled foreground rectangle (clipped) — handy for thin lines (a 1px-wide
71
+ * rect is a spider thread, a ladder rail, a laser) or solid blocks.
72
+ */
73
+ export function fillMono(scr, x, y, w, h) {
74
+ const x0 = Math.max(0, Math.round(x));
75
+ const y0 = Math.max(0, Math.round(y));
76
+ const x1 = Math.min(scr.width, Math.round(x) + w);
77
+ const y1 = Math.min(scr.height, Math.round(y) + h);
78
+ for (let yy = y0; yy < y1; yy++) {
79
+ const base = yy * scr.width;
80
+ for (let xx = x0; xx < x1; xx++)
81
+ scr.pixels[base + xx] = 1;
82
+ }
83
+ }
84
+ /**
85
+ * Resolves the mask into RGBA (ink where lit, paper elsewhere) and blits the
86
+ * region at canvas offset `(dx, dy)` with one `putImageData` + `drawImage` — under
87
+ * the current transform, so `setupCanvas`'s `×scale` is honoured. Headless: fills
88
+ * {@link MonoScreen.rgba} and skips the blit.
89
+ */
90
+ export function flushMonoScreen(ctx, scr, dx = 0, dy = 0) {
91
+ const inkU32 = hexToU32(scr.ink);
92
+ const paperU32 = hexToU32(scr.paper);
93
+ const out = new Uint32Array(scr.rgba.buffer);
94
+ const px = scr.pixels;
95
+ for (let i = 0; i < px.length; i++)
96
+ out[i] = px[i] ? inkU32 : paperU32;
97
+ if (scr.canvas && scr.image) {
98
+ const offctx = scr.canvas.getContext('2d');
99
+ if (offctx) {
100
+ offctx.putImageData(scr.image, 0, 0);
101
+ ctx.drawImage(scr.canvas, dx, dy);
102
+ }
103
+ }
104
+ }
105
+ //# sourceMappingURL=monoscreen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monoscreen.js","sourceRoot":"","sources":["../src/monoscreen.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zx-kit",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zrebec/zx-kit.git"