@chris.araneo/cassette-canvas 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Krzysztof Pająk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,281 @@
1
- # Temporary Holding Version
1
+ <h1 align="center">Cassette Canvas</h1>
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <p align="center">
4
+ <img src="https://raw.githubusercontent.com/ChrisAraneo/cassette-canvas/master/cassette.png" width="512px" height="256px"/>
5
+ <br>
6
+ <a href="https://github.com/ChrisAraneo/cassette-canvas/blob/master/package.json"><img src="https://img.shields.io/badge/version-v0.1.0-blue" alt="version"></a>
7
+ <a href="https://github.com/ChrisAraneo/cassette-canvas/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="@chris.araneo/cassette-canvas is released under the MIT license."></a>
8
+ <a href="https://github.com/ChrisAraneo/cassette-canvas/actions/workflows/ci.yml"><img alt="GitHub CI Status" src="https://img.shields.io/github/actions/workflow/status/ChrisAraneo/cassette-canvas/ci.yml?label=CI&logo=GitHub"></a>
9
+ <br>
10
+ <br>
11
+ <em>Retro rendering engine for the HTML canvas.</em>
12
+ <br>
13
+ <br>
14
+ </p>
15
+
16
+ - Sprites use 16 colors with 8 shades each (128 colors).
17
+ - Pixels are large, and every sprite has the same size.
18
+ - Ambient light and point lights use a scale of 32 shades per color. Sprites can cast shadows.
19
+ - Retro monitor effect is always on: a soft glow around bright pixels, scanlines, film grain and a surface texture.
20
+
21
+ ## Installation
22
+
23
+ ```sh
24
+ npm install @chris.araneo/cassette-canvas
25
+ ```
26
+
27
+ The package is an ES module with TypeScript types included. It runs in the
28
+ browser and needs WebGL.
29
+
30
+ ## Concepts
31
+
32
+ | Concept | Meaning |
33
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | **Base colors** | The 16 colors you provide, as `#rrggbb`. |
35
+ | **Shade** | Sprites can use 8 shades of each color (0 to 7). Shade 4 (`BASE_SHADE`) is the base color. Shades 0 to 3 are darker and 5 to 7 are lighter. |
36
+ | **Extended shade** | Lighting uses 32 shades of each color (0 to 31). 4 extended shades = 1 shade. The palette holds all 16 × 32 = 512 colors. |
37
+ | **Pixel** | A number with the color in the high hex digit and the shade in the low hex digit. `0x35` is color 3, shade 5. `TRANSPARENT` (`-1`) draws nothing. |
38
+ | **Pixel size** | The size of one logical pixel in canvas pixels, for example `4`. The screen size in logical pixels is the canvas size divided by the pixel size. |
39
+ | **Sprite** | A flat array of `spriteSize × spriteSize` pixels, row by row from the top left. |
40
+ | **Screen** | Immutable data: the background, sprites, lights and ambient light. Later sprites are drawn over earlier ones. |
41
+ | **Display** | The canvas, its WebGL context, the monitor shader and the palette. You create it once. |
42
+ | **Ambient light** | Shifts every pixel by a number of extended shades. `0` keeps the authored colors. `-16` (4 shades darker) looks like night. |
43
+ | **Light** | A point at `row`, `column`. It brightens pixels by up to `intensity` extended shades at its center and fades to zero at `radius`. A negative intensity darkens. |
44
+ | **Shadow** | A sprite with `castsShadow: true` blocks light. Pixels behind it get no light from that light. Its side that faces the light is still lit. |
45
+ | **Monitor effect** | A shader that adds a 2-pixel glow, scanlines, film grain and a surface texture. It uses full RGB color, not the palette. |
46
+
47
+ ## Usage
48
+
49
+ ```ts
50
+ import {
51
+ addLight,
52
+ addSprite,
53
+ createScreen,
54
+ drawScreen,
55
+ patchAmbient,
56
+ patchLight,
57
+ patchLightByOffset,
58
+ patchSprite,
59
+ patchSpriteByOffset,
60
+ toPixel,
61
+ toSprite,
62
+ tryCreateDisplay,
63
+ } from '@chris.araneo/cassette-canvas';
64
+ import { constant, noop, times } from 'lodash-es';
65
+ import { pipe } from 'remeda';
66
+ import { match, P } from 'ts-pattern';
67
+
68
+ const display = tryCreateDisplay(canvas, {
69
+ colors: [
70
+ '#7f7f7f',
71
+ '#d23c3c',
72
+ '#e07a2e',
73
+ '#e6c23a',
74
+ '#98c93c',
75
+ '#3d9e4a',
76
+ '#2e9c8f',
77
+ '#3bb4d6',
78
+ '#4a8fe0',
79
+ '#3c56c8',
80
+ '#5b42c2',
81
+ '#8a45c8',
82
+ '#c045b4',
83
+ '#e0608f',
84
+ '#8c5a36',
85
+ '#d4a373',
86
+ ],
87
+ pixelSize: 4,
88
+ spriteSize: 8,
89
+ });
90
+
91
+ const HEART = toSprite(
92
+ [
93
+ '........',
94
+ '.rr..rr.',
95
+ 'rRRrrrrr',
96
+ 'rRrrrrrr',
97
+ '.rrrrrr.',
98
+ '..rrrr..',
99
+ '...rr...',
100
+ '........',
101
+ ],
102
+ { r: toPixel(1, 4), R: toPixel(1, 6) },
103
+ );
104
+
105
+ const screen = addSprite(createScreen(toPixel(10, 0)), 'heart', HEART, 20, 30);
106
+ const moved = patchSpriteByOffset(screen, 'heart', 0, 5); // 5 pixels right
107
+ const placed = patchSprite(moved, 'heart', { row: 40, column: 60 });
108
+ ```
109
+
110
+ Draw the screen once per animation frame. Pass the time in milliseconds; it
111
+ animates the film grain. `display` can be `undefined`, so check it first:
112
+
113
+ ```ts
114
+ const { nonNullable } = P;
115
+
116
+ requestAnimationFrame((time) =>
117
+ match(display)
118
+ .with(nonNullable, (ready) => drawScreen(ready, placed, time))
119
+ .otherwise(noop),
120
+ );
121
+ ```
122
+
123
+ `tryCreateDisplay` returns `undefined` if:
124
+
125
+ - the canvas has no WebGL context,
126
+ - the monitor shader cannot be set up,
127
+ - `pixelSize` or `spriteSize` is not a positive whole number,
128
+ - the canvas is smaller than one logical pixel.
129
+
130
+ See [`apps/demo/src/main.ts`](https://github.com/ChrisAraneo/cassette-canvas/blob/master/apps/demo/src/main.ts)
131
+ for a full animation loop.
132
+
133
+ ### Lights and shadows
134
+
135
+ Sprites use 8 shades per color. Lighting uses 32 extended shades per color
136
+ (`EXTENDED_SHADE_COUNT`). Shade `s` is extended shade `4 × s + 1`. Extended
137
+ shade 0 is darker than shade 0, and extended shades 30 and 31 are lighter than
138
+ shade 7. Only light can reach them.
139
+
140
+ ```
141
+ extended 0 1 2 3 4 5 … 17 … 29 30 31
142
+ shade 0 1 4 7
143
+ ↑ darkest ↑ lightest a sprite can use
144
+ ```
145
+
146
+ Light never changes a pixel's color, only its shade. Each pixel moves by the
147
+ ambient light plus every light that reaches it. Both are measured in extended
148
+ shades (`EXTENDED_SHADES_PER_SHADE` = 4 per shade). The result is rounded to a
149
+ whole extended shade and kept between 0 and 31. With ambient 0 and no lights,
150
+ every pixel shows its authored color.
151
+
152
+ ```ts
153
+ const WALL = toSprite(times(8, constant('..oooo..')), { o: toPixel(0, 3) });
154
+
155
+ const night = pipe(
156
+ createScreen(toPixel(8, 5), -16), // light-blue sky, 4 shades darker
157
+ (screen) => addSprite(screen, 'wall', WALL, 40, 60),
158
+ (screen) => patchSprite(screen, 'wall', { castsShadow: true }),
159
+ (screen) =>
160
+ addLight(screen, 'torch', {
161
+ row: 44,
162
+ column: 30,
163
+ radius: 40,
164
+ intensity: 16,
165
+ }),
166
+ );
167
+
168
+ const torchMoved = patchLightByOffset(night, 'torch', 0, 2);
169
+ const flickered = patchLight(torchMoved, 'torch', { intensity: 13 });
170
+ const dawn = patchAmbient(flickered, -3);
171
+ ```
172
+
173
+ With ambient `-16` and a light with intensity `16`, pixels at the center of
174
+ the light show their authored colors. Further away, the light fades back to
175
+ night in 16 small steps instead of 4 large ones. Ambient and intensity can be
176
+ fractions; the total is rounded for each pixel.
177
+
178
+ A lit frame holds extended pixels:
179
+ `toExtendedPixel(color, extendedShade)` = `color × 32 + extendedShade`. The
180
+ palette is keyed by extended pixels. `toLitPixel(pixel, shadeOffset)` returns
181
+ the extended pixel that a sprite pixel shows under that much light.
182
+ `toLitPixel(pixel, 0)` returns its authored color.
183
+
184
+ ### API
185
+
186
+ | Function | What it does |
187
+ | ------------------------------------------------------------------- | --------------------------------------------------------------------- |
188
+ | `tryCreateDisplay(canvas, { colors, pixelSize, spriteSize })` | Creates a display, or returns `undefined` |
189
+ | `drawScreen(display, screen, time)` | Draws the screen with the monitor effect and returns the display |
190
+ | `createScreen(background?, ambient?)` | Creates an empty screen (transparent, ambient `0` by default) |
191
+ | `addSprite(screen, id, sprite, row, column)` | Adds a sprite on top, replacing any sprite with the same id |
192
+ | `patchSprite(screen, id, { sprite?, row?, column?, castsShadow? })` | Changes a sprite's image (animation), position or shadow |
193
+ | `patchSpriteByOffset(screen, id, rowOffset, columnOffset)` | Moves a sprite by an offset |
194
+ | `removeSprite(screen, id)` | Removes a sprite |
195
+ | `findSprite(screen, id)` | Returns a sprite, or `undefined` |
196
+ | `addLight(screen, id, { row, column, radius, intensity })` | Adds a light, replacing any light with the same id |
197
+ | `patchLight(screen, id, { row?, column?, radius?, intensity? })` | Changes a light |
198
+ | `patchLightByOffset(screen, id, rowOffset, columnOffset)` | Moves a light by an offset |
199
+ | `removeLight(screen, id)` | Removes a light |
200
+ | `findLight(screen, id)` | Returns a light, or `undefined` |
201
+ | `patchAmbient(screen, ambient)` | Sets the ambient light |
202
+ | `toSprite(rows, legend)` | Builds a sprite from text art |
203
+ | `toPixel(color, shade)` | Returns a pixel value |
204
+ | `isPixel(value)` | Checks whether a value is a pixel |
205
+ | `toExtendedPixel(color, extendedShade)` | Returns an extended pixel |
206
+ | `toLitPixel(pixel, shadeOffset)` | Returns the extended pixel a pixel shows under light |
207
+ | `createPalette(colors)` | Returns the 512 palette colors, keyed by extended pixel |
208
+ | `computeFrame(screen, spriteSize, width, height)` | Returns the lit frame as extended pixels, without drawing (for tests) |
209
+
210
+ Screen functions never change the screen you pass in. They return a new one.
211
+
212
+ ## How drawing works
213
+
214
+ `drawScreen` runs three steps:
215
+
216
+ ```
217
+ screen
218
+ → computeFrame draw the sprites, then apply light and shadows
219
+ → toFrameBytes turn extended pixels into RGBA bytes
220
+ → drawCrt upload the bytes as a texture and draw it with the shader
221
+ ```
222
+
223
+ **`computeFrame`** fills the frame with the background and draws each sprite
224
+ in order. It also records which pixels belong to sprites that cast shadows.
225
+ Then it computes a light level for every pixel and moves the pixel to the
226
+ matching extended shade.
227
+
228
+ **Shadows.** For each light, the engine visits pixels in rings, moving
229
+ outward from the light. A pixel gets light if the next pixel toward the light
230
+ gets light and does not cast a shadow. This costs one lookup per pixel
231
+ instead of tracing a full line back to the light.
232
+
233
+ **The shader** runs once for every canvas pixel. It:
234
+
235
+ 1. reads the logical pixel under it;
236
+ 2. adds glow where a pixel within 2 canvas pixels is brighter (flat areas do
237
+ not change);
238
+ 3. darkens the bottom of each pixel row (scanline);
239
+ 4. adds film grain, which changes 24 times per second, and a fixed surface
240
+ texture.
241
+
242
+ The strength of each effect is a constant in `libs/engine/src/crt/consts.ts`.
243
+
244
+ Everything before the WebGL calls is pure and unit-tested. The WebGL calls are
245
+ the only side effects. Each one runs through `runEffect` in `drawCrt`.
246
+
247
+ ## Project layout
248
+
249
+ ```
250
+ libs/
251
+ ├── common/src/ helpers for any lib (runEffect)
252
+ └── engine/src/
253
+ ├── index.ts the public API
254
+ ├── palette/ colors, shades and pixel values
255
+ ├── screen/ sprites, lights and ambient light
256
+ ├── lighting/ light levels and shadows
257
+ ├── frame/ turns a screen into lit pixels
258
+ ├── crt/ the WebGL monitor shader
259
+ └── display/ draws to the canvas
260
+ apps/
261
+ └── demo/ a Vite page: the palette, a day/night cycle, a lantern,
262
+ a glowing heart and pillars that cast shadows
263
+ ```
264
+
265
+ ## Performance
266
+
267
+ Measured on the demo scene at 1280×720 (320×180 logical pixels, 182 sprites,
268
+ 2 lights), in a Vite bundle run with Node 24:
269
+
270
+ | Step | Time per frame |
271
+ | -------------- | -------------- |
272
+ | `computeFrame` | ~1.3 ms |
273
+ | `toFrameBytes` | ~0.3 ms |
274
+
275
+ ## License
276
+
277
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
278
+
279
+ ## Author
280
+
281
+ Krzysztof Pająk (Chris Araneo) - chris.araneo@gmail.com
@@ -0,0 +1,270 @@
1
+ interface CrtUniforms {
2
+ readonly frame: WebGLUniformLocation;
3
+ readonly frameSize: WebGLUniformLocation;
4
+ readonly canvasSize: WebGLUniformLocation;
5
+ readonly pixelSize: WebGLUniformLocation;
6
+ readonly time: WebGLUniformLocation;
7
+ }
8
+ /**
9
+ * The WebGL objects the monitor effect needs: the shader program, the frame
10
+ * texture, the vertex buffer and the uniform locations.
11
+ */
12
+ interface Crt {
13
+ readonly program: WebGLProgram;
14
+ readonly texture: WebGLTexture;
15
+ readonly vertices: WebGLBuffer;
16
+ readonly uniforms: CrtUniforms;
17
+ }
18
+ /**
19
+ * A point light centered on the logical pixel at `row` and `column`. At its
20
+ * center it brightens pixels by `intensity` extended shades (4 extended
21
+ * shades = 1 sprite shade). The effect fades evenly to zero at `radius`
22
+ * logical pixels. A negative intensity darkens. Sprites that cast shadows
23
+ * block it.
24
+ */
25
+ interface Light {
26
+ readonly id: string;
27
+ readonly row: number;
28
+ readonly column: number;
29
+ readonly radius: number;
30
+ readonly intensity: number;
31
+ }
32
+ /**
33
+ * A sprite: `spriteSize * spriteSize` pixel values, row by row from the top
34
+ * left. Each value is a pixel such as `0x35` (color 3, shade 5) or
35
+ * `TRANSPARENT`.
36
+ */
37
+ type Sprite = readonly number[];
38
+ /**
39
+ * A sprite on the screen. `row` and `column` set its top-left pixel. If
40
+ * `castsShadow` is true, its non-transparent pixels block light.
41
+ */
42
+ interface SpritePlacement {
43
+ readonly id: string;
44
+ readonly sprite: Sprite;
45
+ readonly row: number;
46
+ readonly column: number;
47
+ readonly castsShadow: boolean;
48
+ }
49
+ /**
50
+ * Everything a display draws: a background pixel, sprites and lights.
51
+ * Sprites are drawn in order, so later sprites cover earlier ones. `ambient`
52
+ * shifts every pixel by that many extended shades before lights are added:
53
+ * positive is lighter, negative is darker, and 0 keeps the authored colors.
54
+ * 4 extended shades = 1 sprite shade.
55
+ */
56
+ interface Screen {
57
+ readonly background: number;
58
+ readonly ambient: number;
59
+ readonly sprites: readonly SpritePlacement[];
60
+ readonly lights: readonly Light[];
61
+ }
62
+ /** A color as red, green and blue values from 0 to 255. */
63
+ interface Rgb {
64
+ readonly red: number;
65
+ readonly green: number;
66
+ readonly blue: number;
67
+ }
68
+ /**
69
+ * The 512 colors a display can draw (16 colors x 32 extended shades), keyed
70
+ * by extended pixel.
71
+ */
72
+ type Palette = Readonly<Record<number, Rgb>>;
73
+ /**
74
+ * A canvas set up for drawing screens. `width` and `height` are in logical
75
+ * pixels: the canvas size divided by `pixelSize`.
76
+ */
77
+ interface Display {
78
+ readonly context: WebGLRenderingContext;
79
+ readonly crt: Crt;
80
+ readonly palette: Palette;
81
+ readonly pixelSize: number;
82
+ readonly spriteSize: number;
83
+ readonly width: number;
84
+ readonly height: number;
85
+ }
86
+ /**
87
+ * Draws the screen on the canvas. It composes and lights the frame, then
88
+ * draws it with the monitor effect. `time` is the current time in
89
+ * milliseconds, for example the `requestAnimationFrame` timestamp. It
90
+ * animates the film grain. Returns the same display.
91
+ */
92
+ export declare const drawScreen: (display: Display, screen: Screen, time: number) => Display;
93
+ /** A color in `#rrggbb` form, such as `#3c56c8`. */
94
+ type HexColor = `#${string}`;
95
+ /** Exactly 16 base colors. The first is color 0, the last is color 15. */
96
+ type PaletteColors = readonly [HexColor, HexColor, HexColor, HexColor, HexColor, HexColor, HexColor, HexColor, HexColor, HexColor, HexColor, HexColor, HexColor, HexColor, HexColor, HexColor];
97
+ /** The settings for creating a display. */
98
+ interface DisplaySettings {
99
+ /** The 16 base colors. Sprites can use 8 shades of each. */
100
+ readonly colors: PaletteColors;
101
+ /** The width and height of one logical pixel, in canvas pixels. */
102
+ readonly pixelSize: number;
103
+ /** The width and height of every sprite, in logical pixels. */
104
+ readonly spriteSize: number;
105
+ }
106
+ /**
107
+ * Creates a display on the canvas.
108
+ *
109
+ * - `colors`: the 16 base colors.
110
+ * - `pixelSize`: the width and height of one logical pixel, in canvas pixels.
111
+ * - `spriteSize`: the width and height of every sprite, in logical pixels.
112
+ *
113
+ * The screen size in logical pixels is the canvas size divided by
114
+ * `pixelSize`. The display draws with WebGL, and the monitor effect is
115
+ * always on.
116
+ *
117
+ * Returns `undefined` if the canvas has no WebGL context, the monitor effect
118
+ * cannot be set up, a size is not a positive whole number, or the canvas is
119
+ * smaller than one logical pixel.
120
+ */
121
+ export declare const tryCreateDisplay: (canvas: HTMLCanvasElement, { colors, pixelSize, spriteSize }: DisplaySettings) => Display | undefined;
122
+ /**
123
+ * A composed and lit screen: one extended pixel per logical pixel, row by row
124
+ * from the top left. `TRANSPARENT` marks places where nothing is drawn.
125
+ */
126
+ type Frame = readonly number[];
127
+ /**
128
+ * Turns a screen into a frame of `width * height` extended pixels:
129
+ *
130
+ * 1. Fills the frame with the background.
131
+ * 2. Draws the sprites in order. Transparent pixels are skipped, and parts
132
+ * outside the frame are cut off.
133
+ * 3. Lights every pixel: moves it along its 32-shade scale by the ambient
134
+ * light plus each light that reaches it. Sprites that cast shadows block
135
+ * light.
136
+ *
137
+ * With ambient 0 and no lights, every pixel keeps its authored color.
138
+ */
139
+ export declare const computeFrame: (screen: Screen, spriteSize: number, width: number, height: number) => Frame;
140
+ /** The number of base colors in a palette. */
141
+ export declare const COLOR_COUNT = 16;
142
+ /** The number of shades of each color that sprites and backgrounds use. */
143
+ export declare const SHADE_COUNT = 8;
144
+ /**
145
+ * The shade that shows a base color exactly as given. Lower shades are
146
+ * darker, higher shades are lighter.
147
+ */
148
+ export declare const BASE_SHADE = 4;
149
+ /**
150
+ * The number of shades of each color that lighting uses. The finer scale
151
+ * lets light fade in smaller steps than sprite shades.
152
+ */
153
+ export declare const EXTENDED_SHADE_COUNT = 32;
154
+ /** The number of extended shades in one sprite shade. */
155
+ export declare const EXTENDED_SHADES_PER_SHADE: number;
156
+ /** The pixel value that draws nothing. Whatever is below shows through. */
157
+ export declare const TRANSPARENT = -1;
158
+ /**
159
+ * Expands the 16 base colors into 512 colors: 32 extended shades per color,
160
+ * keyed by extended pixel.
161
+ *
162
+ * Sprite shade `s` is extended shade `4 * s + 1`. Shade `BASE_SHADE` is the
163
+ * base color itself. Darker shades are mixed with black, lighter shades with
164
+ * white. Only light reaches the extended shades between and beyond the
165
+ * sprite shades. A color that is not in `#rrggbb` form is read as black.
166
+ */
167
+ export declare const createPalette: (colors: PaletteColors) => Palette;
168
+ /**
169
+ * Returns `true` if the number is a pixel the palette can draw: a color from
170
+ * 0 to 15 in the high hex digit and a shade from 0 to 7 in the low hex digit.
171
+ * Returns `false` for `TRANSPARENT` and all other values.
172
+ */
173
+ export declare const isPixel: (pixel: number) => boolean;
174
+ /**
175
+ * Returns the extended pixel for a color (0 to 15) and an extended shade
176
+ * (0 to 31): `color * 32 + extendedShade`. Lit frames hold extended pixels,
177
+ * and the palette is keyed by them.
178
+ *
179
+ * Sprite shade `s` is extended shade `4 * s + 1`. Extended shades 0, 30 and
180
+ * 31 are outside the sprite shades; only light reaches them. Returns
181
+ * `TRANSPARENT` for out-of-range input.
182
+ */
183
+ export declare const toExtendedPixel: (color: number, extendedShade: number) => number;
184
+ /**
185
+ * Returns the extended pixel that a pixel shows under light. The color stays
186
+ * the same. The shade moves `shadeOffset` extended shades up or down
187
+ * (4 extended shades = 1 sprite shade). The result is rounded and stops at
188
+ * the darkest and lightest extended shade. `toLitPixel(pixel, 0)` returns
189
+ * the authored color. Returns `TRANSPARENT` for `TRANSPARENT` and for any
190
+ * value that is not a pixel.
191
+ */
192
+ export declare const toLitPixel: (pixel: number, shadeOffset: number) => number;
193
+ /**
194
+ * Returns the pixel value for a color (0 to 15) and a shade (0 to 7):
195
+ * `toPixel(3, 5)` is `0x35`. Returns `TRANSPARENT` for out-of-range input.
196
+ */
197
+ export declare const toPixel: (color: number, shade: number) => number;
198
+ /** All the fields of a light except its id. */
199
+ type LightSettings = Omit<Light, 'id'>;
200
+ /**
201
+ * Adds a light to the screen. If a light with the same id exists, it is
202
+ * replaced. Returns a new screen.
203
+ */
204
+ export declare const addLight: (screen: Screen, id: string, { row, column, radius, intensity }: LightSettings) => Screen;
205
+ /**
206
+ * Adds a sprite on top of the other sprites. `row` and `column` set its
207
+ * top-left pixel. If a sprite with the same id exists, it is removed first.
208
+ * The new sprite does not cast a shadow; to change that, call
209
+ * `patchSprite(screen, id, { castsShadow: true })`. Returns a new screen.
210
+ */
211
+ export declare const addSprite: (screen: Screen, id: string, sprite: Sprite, row: number, column: number) => Screen;
212
+ /**
213
+ * Creates an empty screen, with no sprites and no lights, filled with
214
+ * `background`. `ambient` shifts every pixel by that many extended shades
215
+ * (4 extended shades = 1 sprite shade). For example, a night scene could
216
+ * use -16.
217
+ */
218
+ export declare const createScreen: (background?: number, ambient?: number) => Screen;
219
+ /** Returns the light with the id, or `undefined` if there is none. */
220
+ export declare const findLight: (screen: Screen, id: string) => Light | undefined;
221
+ /** Returns the sprite with the id, or `undefined` if there is none. */
222
+ export declare const findSprite: (screen: Screen, id: string) => SpritePlacement | undefined;
223
+ /**
224
+ * Sets the ambient light: how many extended shades every pixel moves up
225
+ * (positive) or down (negative) before lights are added. Change it over time
226
+ * to fade between day and night. Returns a new screen.
227
+ */
228
+ export declare const patchAmbient: (screen: Screen, ambient: number) => Screen;
229
+ /** The light fields to change. Fields you leave out keep their values. */
230
+ type LightChanges = Partial<LightSettings>;
231
+ /**
232
+ * Changes a light's `row`, `column`, `radius` or `intensity`. Fields you
233
+ * leave out keep their values. Returns a new screen. If no light has the id,
234
+ * nothing changes.
235
+ */
236
+ export declare const patchLight: (screen: Screen, id: string, changes: LightChanges) => Screen;
237
+ /**
238
+ * Moves a light `rowOffset` pixels down and `columnOffset` pixels right.
239
+ * Negative values move it up and left. Returns a new screen. If no light has
240
+ * the id, nothing changes.
241
+ */
242
+ export declare const patchLightByOffset: (screen: Screen, id: string, rowOffset: number, columnOffset: number) => Screen;
243
+ /** The sprite fields to change. Fields you leave out keep their values. */
244
+ type SpriteChanges = Partial<Pick<SpritePlacement, 'sprite' | 'row' | 'column' | 'castsShadow'>>;
245
+ /**
246
+ * Changes a sprite's `sprite` (for animation), `row`, `column` or
247
+ * `castsShadow`. Fields you leave out keep their values. The sprite keeps its
248
+ * place in the drawing order. Returns a new screen. If no sprite has the id,
249
+ * nothing changes.
250
+ */
251
+ export declare const patchSprite: (screen: Screen, id: string, changes: SpriteChanges) => Screen;
252
+ /**
253
+ * Moves a sprite `rowOffset` pixels down and `columnOffset` pixels right.
254
+ * Negative values move it up and left. Returns a new screen. If no sprite
255
+ * has the id, nothing changes.
256
+ */
257
+ export declare const patchSpriteByOffset: (screen: Screen, id: string, rowOffset: number, columnOffset: number) => Screen;
258
+ /** Removes the light with the id. Returns a new screen. */
259
+ export declare const removeLight: (screen: Screen, id: string) => Screen;
260
+ /** Removes the sprite with the id. Returns a new screen. */
261
+ export declare const removeSprite: (screen: Screen, id: string) => Screen;
262
+ /** Maps each character of sprite text art to a pixel value. */
263
+ type SpriteLegend = Readonly<Record<string, number>>;
264
+ /**
265
+ * Builds a sprite from text art: one string per row, one character per
266
+ * pixel. Each character is looked up in `legend`. Characters that are not in
267
+ * the legend become transparent.
268
+ */
269
+ export declare const toSprite: (rows: readonly string[], legend: SpriteLegend) => Sprite;
270
+ export type { Crt, Display, DisplaySettings, Frame, HexColor, Light, LightChanges, LightSettings, Palette, PaletteColors, Rgb, Screen, Sprite, SpriteChanges, SpriteLegend, SpritePlacement };