@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 +21 -0
- package/README.md +280 -2
- package/dist/lib/index.d.ts +270 -0
- package/dist/lib/index.js +693 -0
- package/dist/lib/index.js.map +1 -0
- package/package.json +79 -4
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
|
-
|
|
1
|
+
<h1 align="center">Cassette Canvas</h1>
|
|
2
2
|
|
|
3
|
-
|
|
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 };
|