@chris.araneo/cassette-canvas 0.0.0-stage → 0.2.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.
@@ -0,0 +1,816 @@
1
+ import { P, match } from "ts-pattern";
2
+ import { pipe } from "remeda";
3
+ import { clamp, filter, find, flatMap, floor, fromPairs, inRange, isInteger, isUndefined, map, mapValues, parseInt, range, round, split, words } from "lodash-es";
4
+ // libs/common/src/effects/run-effect.ts
5
+ /**
6
+ * Calls `effect` with `value`, then returns `value`. Use it to run a side
7
+ * effect as a step in a `pipe`: `(scene) => runEffect(scene, draw)`.
8
+ */
9
+ const runEffect = (value, effect) => match(effect(value)).otherwise(() => value);
10
+ // libs/engine/src/crt/consts.ts
11
+ const GLOW_STRENGTH = .6;
12
+ const GLOW_THRESHOLD = .45;
13
+ const SCANLINE_DEPTH = .09;
14
+ const GRAIN_AMOUNT = .015;
15
+ const SURFACE_AMOUNT = .0125;
16
+ const POSITION_ATTRIBUTE = "a_position";
17
+ const FULL_SCREEN_TRIANGLE = Object.freeze([
18
+ -1,
19
+ -1,
20
+ 3,
21
+ -1,
22
+ -1,
23
+ 3
24
+ ]);
25
+ const CRT_UNIFORM_NAMES = Object.freeze({
26
+ frame: "u_frame",
27
+ frameSize: "u_frameSize",
28
+ canvasSize: "u_canvasSize",
29
+ pixelSize: "u_pixelSize",
30
+ time: "u_time"
31
+ });
32
+ const CRT_CONTEXT_ATTRIBUTES = Object.freeze({
33
+ alpha: true,
34
+ antialias: false,
35
+ depth: false,
36
+ stencil: false,
37
+ premultipliedAlpha: true,
38
+ preserveDrawingBuffer: false
39
+ });
40
+ // libs/engine/src/crt/internal/draw-crt-triangle.ts
41
+ const VALUES_PER_CORNER = 2;
42
+ const CORNER_COUNT = 3;
43
+ const drawCrtTriangle = (context, { vertices }) => pipe(context, (gl) => runEffect(gl, () => gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)), (gl) => runEffect(gl, () => gl.bindBuffer(gl.ARRAY_BUFFER, vertices)), (gl) => runEffect(gl, () => gl.enableVertexAttribArray(0)), (gl) => runEffect(gl, () => gl.vertexAttribPointer(0, VALUES_PER_CORNER, gl.FLOAT, false, 0, 0)), (gl) => gl.drawArrays(gl.TRIANGLES, 0, CORNER_COUNT));
44
+ // libs/engine/src/crt/internal/patch-crt-texture.ts
45
+ const patchCrtTexture = (context, { texture }, { bytes, width, height }) => pipe(context, (gl) => runEffect(gl, () => gl.bindTexture(gl.TEXTURE_2D, texture)), (gl) => runEffect(gl, () => gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength))));
46
+ // libs/engine/src/crt/internal/patch-crt-uniforms.ts
47
+ const FRAME_TEXTURE_UNIT = 0;
48
+ const patchCrtUniforms = (context, { program, uniforms }, { width, height, pixelSize }, time) => pipe(context, (gl) => runEffect(gl, () => gl.useProgram(program)), (gl) => runEffect(gl, () => gl.uniform1i(uniforms.frame, FRAME_TEXTURE_UNIT)), (gl) => runEffect(gl, () => gl.uniform2f(uniforms.frameSize, width, height)), (gl) => runEffect(gl, () => gl.uniform2f(uniforms.canvasSize, gl.drawingBufferWidth, gl.drawingBufferHeight)), (gl) => runEffect(gl, () => gl.uniform1f(uniforms.pixelSize, pixelSize)), (gl) => runEffect(gl, () => gl.uniform1f(uniforms.time, time)));
49
+ // libs/engine/src/crt/draw-crt.ts
50
+ /**
51
+ * Draws a frame on the canvas with the monitor effect:
52
+ *
53
+ * - each logical pixel becomes a sharp `pixelSize` square;
54
+ * - bright pixels glow up to 2 canvas pixels outward;
55
+ * - each pixel row has a darker scanline at its bottom;
56
+ * - film grain (animated by `time`, in milliseconds) and a fixed surface
57
+ * texture are added on top.
58
+ *
59
+ * The effect uses full RGB color, not the palette.
60
+ */
61
+ const drawCrt = (context, crt, picture, time) => pipe(context, (gl) => runEffect(gl, () => patchCrtTexture(gl, crt, picture)), (gl) => runEffect(gl, () => patchCrtUniforms(gl, crt, picture, time)), (gl) => drawCrtTriangle(gl, crt));
62
+ // libs/engine/src/palette/consts.ts
63
+ /** The number of base colors in a palette. */
64
+ const COLOR_COUNT = 16;
65
+ /** The number of shades of each color that sprites and backgrounds use. */
66
+ const SHADE_COUNT = 8;
67
+ /**
68
+ * The shade that shows a base color exactly as given. Lower shades are
69
+ * darker, higher shades are lighter.
70
+ */
71
+ const BASE_SHADE = 4;
72
+ /**
73
+ * The number of shades of each color that lighting uses. The finer scale
74
+ * lets light fade in smaller steps than sprite shades.
75
+ */
76
+ const EXTENDED_SHADE_COUNT = 32;
77
+ /** The number of extended shades in one sprite shade. */
78
+ const EXTENDED_SHADES_PER_SHADE = 4;
79
+ /** The pixel value that draws nothing. Whatever is below shows through. */
80
+ const TRANSPARENT = -1;
81
+ const BLACK = Object.freeze({
82
+ red: 0,
83
+ green: 0,
84
+ blue: 0
85
+ });
86
+ const WHITE = Object.freeze({
87
+ red: 255,
88
+ green: 255,
89
+ blue: 255
90
+ });
91
+ // libs/engine/src/lighting/consts.ts
92
+ /**
93
+ * How many extended shades one doubling of light moves a pixel: one sprite
94
+ * shade per doubling. Lights add up as light and are read off this scale
95
+ * afterwards, so every doubling costs the same number of shades, the way the
96
+ * eye sees brightness.
97
+ */
98
+ const EXTENDED_SHADES_PER_STOP = 4;
99
+ /**
100
+ * The most light can move a pixel, in extended shades. A whole ramp in
101
+ * either direction is more than any pixel needs: every pixel stops at the
102
+ * darkest or the lightest extended shade well before this.
103
+ */
104
+ const MAX_SHADE_OFFSET = 32;
105
+ const MIN_EXPOSURE = 2 ** (-32 / EXTENDED_SHADES_PER_STOP);
106
+ const MAX_EXPOSURE = 2 ** (MAX_SHADE_OFFSET / EXTENDED_SHADES_PER_STOP);
107
+ /**
108
+ * How much of the light at a pixel comes from each side of the pixel ahead
109
+ * of it, toward the light, rather than all of it from that one pixel. It
110
+ * gives every shadow a penumbra: its edge ramps from full light to full
111
+ * shadow over two pixels instead of stepping down in one, the way an edge
112
+ * looks under a light that is not a single point.
113
+ *
114
+ * Only the edge softens. What blocks a light still blocks it, so the heart
115
+ * of a shadow stays as dark as ever, and a shadow never fades out with
116
+ * distance. 0 gives the hard edges of a point source, and 0.25 spreads the
117
+ * ramp evenly, which is as soft as three pixels go.
118
+ */
119
+ const SHADOW_SOFTNESS = .25;
120
+ // libs/engine/src/lighting/internal/compute-falloff.ts
121
+ const computeWindow = (squaredReach) => 1 - squaredReach * squaredReach;
122
+ /**
123
+ * Returns the share of a light that reaches a point: 1 at its center, 0 at
124
+ * its radius. `squaredReach` is the distance to the light as a share of the
125
+ * radius, squared, from 0 at the center to 1 at the radius. Squared, because
126
+ * then the caller needs no square root, and this runs once per lit pixel.
127
+ *
128
+ * The share thins out with distance the way light spreads out in the world,
129
+ * and a window closes it off smoothly at the radius, so a light leaves a
130
+ * bright core and a long dim halo instead of a flat disc with a visible rim.
131
+ * Callers keep to the center and the radius; a share past 1 means nothing.
132
+ */
133
+ const computeFalloff = (squaredReach) => computeWindow(squaredReach) ** 2 / (1 + 8 * squaredReach);
134
+ // libs/engine/src/lighting/internal/to-exposure.ts
135
+ /**
136
+ * Returns the amount of light that shows a pixel `shades` extended shades
137
+ * from its authored color. `toExposure(0)` is `REFERENCE_EXPOSURE`, and every
138
+ * `EXTENDED_SHADES_PER_STOP` shades up double it.
139
+ *
140
+ * Ambient light and light intensities are written in extended shades because
141
+ * that is what you see on screen. This turns one of them back into the amount
142
+ * of light behind it, which is what adds up.
143
+ */
144
+ const toExposure = (shades) => 2 ** (shades / EXTENDED_SHADES_PER_STOP);
145
+ // libs/engine/src/lighting/internal/compute-light-exposure.ts
146
+ const SHARE_AHEAD = 1 - 2 * SHADOW_SOFTNESS;
147
+ /**
148
+ * Returns how much light one light adds to every pixel. The values are
149
+ * amounts of light, not extended shades, because that is what adds up:
150
+ * `computeShadeOffsets` sums the amounts and reads the shades off them.
151
+ *
152
+ * At its center the light adds as much light as its intensity stands for,
153
+ * which thins out to nothing at its radius. Sprites that cast shadows block
154
+ * it, and the edge of a shadow ramps down over two pixels rather than
155
+ * stepping down in one. A negative intensity takes light away instead.
156
+ */
157
+ const computeLightExposure = ({ width, height, occluders }, light) => {
158
+ const exposures = new Float32Array(width * height);
159
+ const visible = new Uint8Array(width * height);
160
+ const lightRow = Math.floor(light.row);
161
+ const lightColumn = Math.floor(light.column);
162
+ const reach = Math.ceil(light.radius);
163
+ const squaredRadius = light.radius * light.radius;
164
+ const emission = toExposure(light.intensity) - 1;
165
+ const firstRing = Math.max(0, -lightRow, lightRow - (height - 1), -lightColumn, lightColumn - (width - 1));
166
+ for (let ring = firstRing; ring <= reach; ring += 1) {
167
+ const top = Math.max(-ring, -lightRow);
168
+ const bottom = Math.min(ring, height - 1 - lightRow);
169
+ const sideReach = ring - 1;
170
+ for (let rowOffset = top; rowOffset <= bottom; rowOffset += 1) {
171
+ const isEdgeRow = rowOffset === -ring || rowOffset === ring;
172
+ const left = isEdgeRow ? Math.max(-ring, -lightColumn) : -ring;
173
+ const right = isEdgeRow ? Math.min(ring, width - 1 - lightColumn) : ring;
174
+ const step = isEdgeRow ? 1 : 2 * ring;
175
+ for (let columnOffset = left; columnOffset <= right; columnOffset += step) {
176
+ const row = lightRow + rowOffset;
177
+ const column = lightColumn + columnOffset;
178
+ if (column < 0 || column >= width) continue;
179
+ const parentRowOffset = Math.round(rowOffset * sideReach / ring);
180
+ const parentColumnOffset = Math.round(columnOffset * sideReach / ring);
181
+ const parentRow = lightRow + parentRowOffset;
182
+ const parentColumn = lightColumn + parentColumnOffset;
183
+ const parentIndex = parentRow * width + parentColumn;
184
+ const hasParent = ring >= 2 && parentRow >= 0 && parentRow < height && parentColumn >= 0 && parentColumn < width;
185
+ const litAhead = !hasParent || visible[parentIndex] === 1 && occluders[parentIndex] !== 1 ? 1 : 0;
186
+ const runsAlongRow = Math.abs(parentRowOffset) >= Math.abs(parentColumnOffset);
187
+ const sideStep = runsAlongRow ? 1 : width;
188
+ const sideOffset = runsAlongRow ? parentColumnOffset : parentRowOffset;
189
+ const sidePlace = runsAlongRow ? parentColumn : parentRow;
190
+ const sideLimit = runsAlongRow ? width : height;
191
+ const lowIndex = parentIndex - sideStep;
192
+ const highIndex = parentIndex + sideStep;
193
+ const litLow = hasParent && sideOffset > -sideReach && sidePlace > 0 ? visible[lowIndex] === 1 && occluders[lowIndex] !== 1 ? 1 : 0 : litAhead;
194
+ const litHigh = hasParent && sideOffset < sideReach && sidePlace < sideLimit - 1 ? visible[highIndex] === 1 && occluders[highIndex] !== 1 ? 1 : 0 : litAhead;
195
+ const share = SHARE_AHEAD * litAhead + SHADOW_SOFTNESS * (litLow + litHigh);
196
+ const index = row * width + column;
197
+ const squaredDistance = rowOffset * rowOffset + columnOffset * columnOffset;
198
+ visible[index] = litAhead;
199
+ if (share > 0 && squaredDistance < squaredRadius) exposures[index] = emission * share * computeFalloff(squaredDistance / squaredRadius);
200
+ }
201
+ }
202
+ }
203
+ return exposures;
204
+ };
205
+ // libs/engine/src/lighting/internal/to-shade-offset.ts
206
+ /**
207
+ * Returns how many extended shades an amount of light moves a pixel from its
208
+ * authored color. This undoes `toExposure`: `REFERENCE_EXPOSURE` gives 0,
209
+ * twice as much light gives `EXTENDED_SHADES_PER_STOP` more shades, and half
210
+ * as much gives that many fewer. Light beyond what a pixel can show stops at
211
+ * the ends of the scale.
212
+ *
213
+ * Because every doubling costs the same number of shades, a scene that is
214
+ * already bright barely shows one more lamp, while the same lamp in the dark
215
+ * shows in full. That is how the eye reads brightness, and it is why lights
216
+ * no longer stack up into white.
217
+ *
218
+ * It clamps with `Math.min` and `Math.max` rather than `clamp`, because it
219
+ * runs once per lit pixel.
220
+ */
221
+ const toShadeOffset = (exposure) => EXTENDED_SHADES_PER_STOP * Math.log2(Math.min(MAX_EXPOSURE, Math.max(MIN_EXPOSURE, exposure)));
222
+ // libs/engine/src/lighting/internal/compute-shade-offsets.ts
223
+ /**
224
+ * Adds up the ambient light and every light that reaches a pixel, then reads
225
+ * off how many extended shades that much light moves the pixel.
226
+ *
227
+ * The amounts of light are added, not the shades. Two lamps on one spot are
228
+ * twice the light, which is one shade step more than one lamp, not twice as
229
+ * many shade steps, and a lamp in daylight hardly tells at all. That is why
230
+ * a lit scene keeps its shading instead of washing out to white.
231
+ */
232
+ const computeShadeOffsets = ({ ambient, width, height }, exposures) => {
233
+ const offsets = new Float64Array(Math.max(0, width * height));
234
+ const ambientExposure = toExposure(ambient);
235
+ for (const exposure of exposures) for (let index = 0; index < offsets.length; index += 1) offsets[index] = (offsets[index] ?? 0) + (exposure[index] ?? 0);
236
+ for (let index = 0; index < offsets.length; index += 1) {
237
+ const light = offsets[index] ?? 0;
238
+ offsets[index] = light === 0 ? ambient : toShadeOffset(ambientExposure + light);
239
+ }
240
+ return offsets;
241
+ };
242
+ // libs/engine/src/lighting/compute-light-levels.ts
243
+ /**
244
+ * Returns how many extended shades the light moves every pixel, row by row
245
+ * from the top left.
246
+ *
247
+ * Lights are combined the way light behaves, not the way numbers add. Each
248
+ * light and the ambient light are turned into amounts of light, those amounts
249
+ * are added together, and the shade is read off the total on a scale where
250
+ * every doubling of light is worth `EXTENDED_SHADES_PER_STOP` shades. So one
251
+ * light on an unlit scene moves a pixel by exactly its intensity, while a
252
+ * second light of the same intensity adds only one more step, and a lamp in
253
+ * bright daylight barely shows. Nothing stacks up into flat white.
254
+ */
255
+ const computeLightLevels = (lighting) => computeShadeOffsets(lighting, map(lighting.lights, (light) => computeLightExposure(lighting, light)));
256
+ // libs/engine/src/frame/internal/compute-frame-light.ts
257
+ const computeFrameLight = ({ pixels, lighting }) => ({
258
+ pixels,
259
+ levels: computeLightLevels(lighting)
260
+ });
261
+ // libs/engine/src/palette/internal/is-index.ts
262
+ const isIndex = (index, size) => isInteger(index) && inRange(index, size);
263
+ // libs/engine/src/palette/to-extended-pixel.ts
264
+ /**
265
+ * Returns the extended pixel for a color (0 to 15) and an extended shade
266
+ * (0 to 31): `color * 32 + extendedShade`. Lit frames hold extended pixels,
267
+ * and the palette is keyed by them.
268
+ *
269
+ * Sprite shade `s` is extended shade `4 * s + 1`. Extended shades 0, 30 and
270
+ * 31 are outside the sprite shades; only light reaches them. Returns
271
+ * `TRANSPARENT` for out-of-range input.
272
+ */
273
+ const toExtendedPixel = (color, extendedShade) => match(isIndex(color, 16) && isIndex(extendedShade, 32)).with(true, () => color * 32 + extendedShade).otherwise(() => -1);
274
+ // libs/engine/src/palette/to-pixel.ts
275
+ /**
276
+ * Returns the pixel value for a color (0 to 15) and a shade (0 to 7):
277
+ * `toPixel(3, 5)` is `0x35`. Returns `TRANSPARENT` for out-of-range input.
278
+ */
279
+ const toPixel = (color, shade) => match(isIndex(color, 16) && isIndex(shade, 8)).with(true, () => color * 16 + shade).otherwise(() => -1);
280
+ // libs/engine/src/palette/internal/to-extended-shade.ts
281
+ const toExtendedShade = (shade) => 1 + shade * 4;
282
+ // libs/engine/src/palette/internal/create-shade-ramps.ts
283
+ const createColorEntries = (color, pixels) => map(range(8), (shade) => [toPixel(color, shade), {
284
+ pixels,
285
+ extendedShade: toExtendedShade(shade)
286
+ }]);
287
+ const createShadeRamps = () => fromPairs(flatMap(range(16), (color) => createColorEntries(color, map(range(32), (extendedShade) => toExtendedPixel(color, extendedShade)))));
288
+ // libs/engine/src/palette/internal/shade-ramps.ts
289
+ const SHADE_RAMPS = Object.freeze(createShadeRamps());
290
+ // libs/engine/src/palette/is-pixel.ts
291
+ /**
292
+ * Returns `true` if the number is a pixel the palette can draw: a color from
293
+ * 0 to 15 in the high hex digit and a shade from 0 to 7 in the low hex digit.
294
+ * Returns `false` for `TRANSPARENT` and all other values.
295
+ */
296
+ const isPixel = (pixel) => !isUndefined(SHADE_RAMPS[pixel]);
297
+ // libs/engine/src/frame/internal/compute-frame-pixels.ts
298
+ const SHADOW_CASTER = 1;
299
+ const computeFramePixels = ({ screen, spriteSize, width, height }) => {
300
+ const size = Math.max(0, width * height);
301
+ const pixels = new Int16Array(size).fill(isPixel(screen.background) ? screen.background : -1);
302
+ const occluders = new Uint8Array(size);
303
+ const spritePixelCount = spriteSize * spriteSize;
304
+ for (const { sprite, row, column, castsShadow } of screen.sprites) {
305
+ const top = Math.floor(row);
306
+ const left = Math.floor(column);
307
+ const count = Math.min(sprite.length, spritePixelCount);
308
+ for (let index = 0; index < count; index += 1) {
309
+ const pixel = sprite[index] ?? -1;
310
+ const pixelRow = top + Math.floor(index / spriteSize);
311
+ const pixelColumn = left + index % spriteSize;
312
+ if (pixelRow >= 0 && pixelRow < height && pixelColumn >= 0 && pixelColumn < width && isPixel(pixel)) {
313
+ const place = pixelRow * width + pixelColumn;
314
+ pixels[place] = pixel;
315
+ if (castsShadow) occluders[place] = SHADOW_CASTER;
316
+ }
317
+ }
318
+ }
319
+ return {
320
+ pixels,
321
+ lighting: {
322
+ ambient: screen.ambient,
323
+ lights: screen.lights,
324
+ width,
325
+ height,
326
+ occluders
327
+ }
328
+ };
329
+ };
330
+ // libs/engine/src/palette/to-lit-pixels.ts
331
+ const toLitPixels = (pixels, shadeOffsets) => {
332
+ const litPixels = new Array(pixels.length);
333
+ for (let index = 0; index < pixels.length; index += 1) {
334
+ const ramp = SHADE_RAMPS[pixels[index] ?? -1];
335
+ const extendedShade = Math.round((ramp?.extendedShade ?? 0) + (shadeOffsets[index] ?? 0));
336
+ litPixels[index] = ramp?.pixels[Math.min(31, Math.max(0, extendedShade))] ?? -1;
337
+ }
338
+ return litPixels;
339
+ };
340
+ // libs/engine/src/frame/internal/patch-frame-shades.ts
341
+ const patchFrameShades = ({ pixels, levels }) => toLitPixels(pixels, levels);
342
+ // libs/engine/src/frame/compute-frame.ts
343
+ /**
344
+ * Turns a screen into a frame of `width * height` extended pixels:
345
+ *
346
+ * 1. Fills the frame with the background.
347
+ * 2. Draws the sprites in order. Transparent pixels are skipped, and parts
348
+ * outside the frame are cut off.
349
+ * 3. Lights every pixel: moves it along its 32-shade scale by the ambient
350
+ * light plus each light that reaches it. Sprites that cast shadows block
351
+ * light.
352
+ *
353
+ * With ambient 0 and no lights, every pixel keeps its authored color.
354
+ */
355
+ const computeFrame = (screen, spriteSize, width, height) => pipe({
356
+ screen,
357
+ spriteSize,
358
+ width,
359
+ height
360
+ }, computeFramePixels, computeFrameLight, patchFrameShades);
361
+ // libs/engine/src/display/internal/to-frame-bytes.ts
362
+ const BYTES_PER_PIXEL = 4;
363
+ const RED_OFFSET = 0;
364
+ const GREEN_OFFSET = 1;
365
+ const BLUE_OFFSET = 2;
366
+ const ALPHA_OFFSET = 3;
367
+ const toFrameBytes = (frame, palette) => {
368
+ const bytes = new Uint8ClampedArray(frame.length * BYTES_PER_PIXEL);
369
+ for (let index = 0; index < frame.length; index += 1) {
370
+ const rgb = palette[frame[index] ?? -1];
371
+ if (rgb !== void 0) {
372
+ const offset = index * BYTES_PER_PIXEL;
373
+ bytes[offset + RED_OFFSET] = rgb.red;
374
+ bytes[offset + GREEN_OFFSET] = rgb.green;
375
+ bytes[offset + BLUE_OFFSET] = rgb.blue;
376
+ bytes[offset + ALPHA_OFFSET] = 255;
377
+ }
378
+ }
379
+ return bytes;
380
+ };
381
+ // libs/engine/src/display/draw-screen.ts
382
+ /**
383
+ * Draws the screen on the canvas. It composes and lights the frame, then
384
+ * draws it with the monitor effect. `time` is the current time in
385
+ * milliseconds, for example the `requestAnimationFrame` timestamp. It
386
+ * animates the film grain. Returns the same display.
387
+ */
388
+ const drawScreen = (display, screen, time) => pipe(computeFrame(screen, display.spriteSize, display.width, display.height), (frame) => toFrameBytes(frame, display.palette), (bytes) => runEffect(display, ({ context, crt, width, height, pixelSize }) => drawCrt(context, crt, {
389
+ bytes,
390
+ width,
391
+ height,
392
+ pixelSize
393
+ }, time)));
394
+ // libs/engine/src/crt/internal/find-crt-uniforms.ts
395
+ const { nonNullable: nonNullable$9 } = P;
396
+ const findCrtUniforms = (context, program) => match(mapValues(CRT_UNIFORM_NAMES, (name) => context.getUniformLocation(program, name) ?? void 0)).with({
397
+ frame: nonNullable$9,
398
+ frameSize: nonNullable$9,
399
+ canvasSize: nonNullable$9,
400
+ pixelSize: nonNullable$9,
401
+ time: nonNullable$9
402
+ }, (uniforms) => uniforms).otherwise(() => void 0);
403
+ // libs/engine/src/crt/internal/format-glsl-float.ts
404
+ const GLSL_FLOAT_DIGITS = 4;
405
+ const formatGlslFloat = (value) => value.toFixed(GLSL_FLOAT_DIGITS);
406
+ // libs/engine/src/crt/internal/fragment-shader-source.ts
407
+ const FRAGMENT_SHADER_SOURCE = `
408
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
409
+ precision highp float;
410
+ #else
411
+ precision mediump float;
412
+ #endif
413
+
414
+ uniform sampler2D ${CRT_UNIFORM_NAMES.frame};
415
+ uniform vec2 ${CRT_UNIFORM_NAMES.frameSize};
416
+ uniform vec2 ${CRT_UNIFORM_NAMES.canvasSize};
417
+ uniform float ${CRT_UNIFORM_NAMES.pixelSize};
418
+ uniform float ${CRT_UNIFORM_NAMES.time};
419
+
420
+ const float GLOW_STRENGTH = ${formatGlslFloat(GLOW_STRENGTH)};
421
+ const float GLOW_THRESHOLD = ${formatGlslFloat(GLOW_THRESHOLD)};
422
+ const int GLOW_REACH = 2;
423
+ const float SCANLINE_DEPTH = ${formatGlslFloat(SCANLINE_DEPTH)};
424
+ const float GRAIN_AMOUNT = ${formatGlslFloat(GRAIN_AMOUNT)};
425
+ const float GRAIN_RATE = ${formatGlslFloat(24)};
426
+ const float SURFACE_AMOUNT = ${formatGlslFloat(SURFACE_AMOUNT)};
427
+ const float SURFACE_SCALE = ${formatGlslFloat(3)};
428
+ const vec3 LUMINANCE = vec3(0.299, 0.587, 0.114);
429
+
430
+ vec4 sampleFrame(vec2 position) {
431
+ vec2 logical = floor(position / ${CRT_UNIFORM_NAMES.pixelSize});
432
+
433
+ if (logical.x < 0.0 || logical.y < 0.0
434
+ || logical.x >= ${CRT_UNIFORM_NAMES.frameSize}.x
435
+ || logical.y >= ${CRT_UNIFORM_NAMES.frameSize}.y) {
436
+ return vec4(0.0);
437
+ }
438
+
439
+ return texture2D(
440
+ ${CRT_UNIFORM_NAMES.frame},
441
+ (logical + 0.5) / ${CRT_UNIFORM_NAMES.frameSize}
442
+ );
443
+ }
444
+
445
+ float hash(vec2 point) {
446
+ vec3 mixed = fract(vec3(point.xyx) * 0.1031);
447
+ mixed += dot(mixed, mixed.yzx + 33.33);
448
+ return fract((mixed.x + mixed.y) * mixed.z);
449
+ }
450
+
451
+ float valueNoise(vec2 point) {
452
+ vec2 cell = floor(point);
453
+ vec2 blend = fract(point);
454
+ blend = blend * blend * (3.0 - 2.0 * blend);
455
+
456
+ return mix(
457
+ mix(hash(cell), hash(cell + vec2(1.0, 0.0)), blend.x),
458
+ mix(hash(cell + vec2(0.0, 1.0)), hash(cell + vec2(1.0, 1.0)), blend.x),
459
+ blend.y
460
+ );
461
+ }
462
+
463
+ vec3 computeBright(vec3 color) {
464
+ return color * smoothstep(GLOW_THRESHOLD, 1.0, dot(color, LUMINANCE));
465
+ }
466
+
467
+ vec3 computeGlow(vec2 position, vec3 base) {
468
+ vec3 glow = vec3(0.0);
469
+ float total = 0.0;
470
+
471
+ for (int row = -GLOW_REACH; row <= GLOW_REACH; row++) {
472
+ for (int column = -GLOW_REACH; column <= GLOW_REACH; column++) {
473
+ vec2 offset = vec2(float(column), float(row));
474
+ float reach = length(offset);
475
+
476
+ if (reach > float(GLOW_REACH)) {
477
+ continue;
478
+ }
479
+
480
+ float weight = 1.0 - reach / (float(GLOW_REACH) + 1.0);
481
+
482
+ glow += computeBright(sampleFrame(position + offset).rgb) * weight;
483
+ total += weight;
484
+ }
485
+ }
486
+
487
+ return max(glow / total - computeBright(base), 0.0);
488
+ }
489
+
490
+ float computeScanline(vec2 position) {
491
+ float period = max(${CRT_UNIFORM_NAMES.pixelSize}, 2.0);
492
+ float phase = (mod(floor(position.y), period) + 0.5) / period;
493
+
494
+ return 1.0 - SCANLINE_DEPTH * smoothstep(0.4, 1.0, phase);
495
+ }
496
+
497
+ float computeGrain(vec2 position) {
498
+ float grainFrame = floor(${CRT_UNIFORM_NAMES.time} * GRAIN_RATE / 1000.0);
499
+ vec2 shift = fract(vec2(grainFrame * 0.6180339, grainFrame * 0.3819660))
500
+ * 1024.0;
501
+
502
+ return (hash(position + shift) - 0.5) * GRAIN_AMOUNT;
503
+ }
504
+
505
+ float computeSurface(vec2 position) {
506
+ return (valueNoise(position / SURFACE_SCALE) - 0.5) * SURFACE_AMOUNT;
507
+ }
508
+
509
+ void main() {
510
+ vec2 position = vec2(
511
+ gl_FragCoord.x,
512
+ ${CRT_UNIFORM_NAMES.canvasSize}.y - gl_FragCoord.y
513
+ );
514
+ vec4 base = sampleFrame(position);
515
+ vec3 color = (base.rgb + computeGlow(position, base.rgb) * GLOW_STRENGTH)
516
+ * computeScanline(position)
517
+ * (1.0 + computeSurface(position))
518
+ + computeGrain(position) * base.a;
519
+
520
+ color = clamp(color, 0.0, 1.0);
521
+
522
+ gl_FragColor = vec4(color, max(base.a, max(color.r, max(color.g, color.b))));
523
+ }
524
+ `;
525
+ // libs/engine/src/crt/internal/try-create-shader.ts
526
+ const { nonNullable: nonNullable$8 } = P;
527
+ const tryCompileShader = (context, shader, source) => pipe(shader, (created) => runEffect(created, (target) => context.shaderSource(target, source)), (sourced) => runEffect(sourced, (target) => context.compileShader(target)), (compiled) => match(context.getShaderParameter(compiled, context.COMPILE_STATUS)).with(true, () => compiled).otherwise(() => void 0));
528
+ const tryCreateShader = (context, type, source) => match(context.createShader(type)).with(nonNullable$8, (shader) => tryCompileShader(context, shader, source)).otherwise(() => void 0);
529
+ // libs/engine/src/crt/internal/try-link-program.ts
530
+ const tryLinkProgram = (context, program, vertexShader, fragmentShader) => pipe(program, (created) => runEffect(created, (target) => context.attachShader(target, vertexShader)), (attached) => runEffect(attached, (target) => context.attachShader(target, fragmentShader)), (attached) => runEffect(attached, (target) => context.bindAttribLocation(target, 0, POSITION_ATTRIBUTE)), (bound) => runEffect(bound, (target) => context.linkProgram(target)), (linked) => match(context.getProgramParameter(linked, context.LINK_STATUS)).with(true, () => linked).otherwise(() => void 0));
531
+ // libs/engine/src/crt/internal/vertex-shader-source.ts
532
+ const VERTEX_SHADER_SOURCE = `
533
+ attribute vec2 ${POSITION_ATTRIBUTE};
534
+
535
+ void main() {
536
+ gl_Position = vec4(${POSITION_ATTRIBUTE}, 0.0, 1.0);
537
+ }
538
+ `;
539
+ // libs/engine/src/crt/internal/try-create-program.ts
540
+ const { nonNullable: nonNullable$7 } = P;
541
+ const tryCreateProgram = (context) => match({
542
+ program: context.createProgram(),
543
+ vertexShader: tryCreateShader(context, context.VERTEX_SHADER, VERTEX_SHADER_SOURCE),
544
+ fragmentShader: tryCreateShader(context, context.FRAGMENT_SHADER, FRAGMENT_SHADER_SOURCE)
545
+ }).with({
546
+ program: nonNullable$7,
547
+ vertexShader: nonNullable$7,
548
+ fragmentShader: nonNullable$7
549
+ }, ({ program, vertexShader, fragmentShader }) => tryLinkProgram(context, program, vertexShader, fragmentShader)).otherwise(() => void 0);
550
+ // libs/engine/src/crt/internal/try-create-texture.ts
551
+ const { nonNullable: nonNullable$6 } = P;
552
+ const patchTextureSampling = (context, texture) => pipe(texture, (created) => runEffect(created, (target) => context.bindTexture(context.TEXTURE_2D, target)), (bound) => runEffect(bound, () => context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MIN_FILTER, context.NEAREST)), (bound) => runEffect(bound, () => context.texParameteri(context.TEXTURE_2D, context.TEXTURE_MAG_FILTER, context.NEAREST)), (bound) => runEffect(bound, () => context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_S, context.CLAMP_TO_EDGE)), (bound) => runEffect(bound, () => context.texParameteri(context.TEXTURE_2D, context.TEXTURE_WRAP_T, context.CLAMP_TO_EDGE)));
553
+ const tryCreateTexture = (context) => match(context.createTexture()).with(nonNullable$6, (texture) => patchTextureSampling(context, texture)).otherwise(() => void 0);
554
+ // libs/engine/src/crt/internal/try-create-vertices.ts
555
+ const { nonNullable: nonNullable$5 } = P;
556
+ const patchVertices = (context, vertices) => pipe(vertices, (created) => runEffect(created, (target) => context.bindBuffer(context.ARRAY_BUFFER, target)), (bound) => runEffect(bound, () => context.bufferData(context.ARRAY_BUFFER, new Float32Array(FULL_SCREEN_TRIANGLE), context.STATIC_DRAW)));
557
+ const tryCreateVertices = (context) => match(context.createBuffer()).with(nonNullable$5, (vertices) => patchVertices(context, vertices)).otherwise(() => void 0);
558
+ // libs/engine/src/crt/try-create-crt.ts
559
+ const { nonNullable: nonNullable$4 } = P;
560
+ const createCrtDraft = (context, program) => ({
561
+ program,
562
+ texture: tryCreateTexture(context),
563
+ vertices: tryCreateVertices(context),
564
+ uniforms: match(program).with(nonNullable$4, (linked) => findCrtUniforms(context, linked)).otherwise(() => void 0)
565
+ });
566
+ const tryToCrt = (draft) => match(draft).with({
567
+ program: nonNullable$4,
568
+ texture: nonNullable$4,
569
+ vertices: nonNullable$4,
570
+ uniforms: nonNullable$4
571
+ }, (crt) => crt).otherwise(() => void 0);
572
+ /**
573
+ * Sets up the monitor effect on a WebGL context: the shader program, the
574
+ * texture that holds the frame, and a triangle that covers the canvas.
575
+ * Returns `undefined` if there is no context or any step fails.
576
+ */
577
+ const tryCreateCrt = (context) => match(context).with(nonNullable$4, (ready) => tryToCrt(createCrtDraft(ready, tryCreateProgram(ready)))).otherwise(() => void 0);
578
+ // libs/engine/src/palette/internal/compute-rgb-mix.ts
579
+ const computeChannelMix = (channel, target, amount) => round(channel + (target - channel) * amount);
580
+ const computeRgbMix = (color, target, amount) => ({
581
+ red: computeChannelMix(color.red, target.red, amount),
582
+ green: computeChannelMix(color.green, target.green, amount),
583
+ blue: computeChannelMix(color.blue, target.blue, amount)
584
+ });
585
+ // libs/engine/src/palette/internal/compute-extended-shade.ts
586
+ const { number: number$1 } = P;
587
+ const EXTENDED_BASE_SHADE = toExtendedShade(4);
588
+ const DARKEN_STEP = 1 / 20;
589
+ const LIGHTEN_STEP = 1 / 16;
590
+ const computeExtendedShade = (base, extendedShade) => match(extendedShade).with(number$1.lt(EXTENDED_BASE_SHADE), (darker) => computeRgbMix(base, BLACK, (EXTENDED_BASE_SHADE - darker) * DARKEN_STEP)).with(number$1.gt(EXTENDED_BASE_SHADE), (lighter) => computeRgbMix(base, WHITE, (lighter - EXTENDED_BASE_SHADE) * LIGHTEN_STEP)).otherwise(() => base);
591
+ // libs/engine/src/palette/internal/create-palette-entries.ts
592
+ const createPaletteEntries = (base, color) => map(range(32), (extendedShade) => [toExtendedPixel(color, extendedShade), computeExtendedShade(base, extendedShade)]);
593
+ // libs/engine/src/palette/internal/split-hex-rgb.ts
594
+ const HEX_CHANNEL_PATTERN = /[\da-f]{2}/gi;
595
+ const HEX_RADIX = 16;
596
+ const splitHexRgb = (color) => pipe(words(color, HEX_CHANNEL_PATTERN), (channels) => map(channels, (channel) => parseInt(channel, HEX_RADIX)), ([red = 0, green = 0, blue = 0]) => ({
597
+ red,
598
+ green,
599
+ blue
600
+ }));
601
+ // libs/engine/src/palette/internal/to-rgb.ts
602
+ const { string } = P;
603
+ const HEX_COLOR_PATTERN = /^#[\da-f]{6}$/i;
604
+ const toRgb = (color) => match(color).with(string.regex(HEX_COLOR_PATTERN), splitHexRgb).otherwise(() => BLACK);
605
+ // libs/engine/src/palette/create-palette.ts
606
+ /**
607
+ * Expands the 16 base colors into 512 colors: 32 extended shades per color,
608
+ * keyed by extended pixel.
609
+ *
610
+ * Sprite shade `s` is extended shade `4 * s + 1`. Shade `BASE_SHADE` is the
611
+ * base color itself. Darker shades are mixed with black, lighter shades with
612
+ * white. Only light reaches the extended shades between and beyond the
613
+ * sprite shades. A color that is not in `#rrggbb` form is read as black.
614
+ */
615
+ const createPalette = (colors) => pipe(map(colors, toRgb), (bases) => flatMap(bases, createPaletteEntries), (entries) => fromPairs(entries));
616
+ // libs/engine/src/display/internal/compute-display-size.ts
617
+ const computeDisplaySize = (canvas, pixelSize) => ({
618
+ width: floor(canvas.width / pixelSize),
619
+ height: floor(canvas.height / pixelSize)
620
+ });
621
+ // libs/engine/src/display/internal/try-to-display.ts
622
+ const { nonNullable: nonNullable$3, number } = P;
623
+ const POSITIVE_INTEGER = number.int().positive();
624
+ const tryToDisplay = (draft) => match(draft).with({
625
+ context: nonNullable$3,
626
+ crt: nonNullable$3,
627
+ pixelSize: POSITIVE_INTEGER,
628
+ spriteSize: POSITIVE_INTEGER,
629
+ width: POSITIVE_INTEGER,
630
+ height: POSITIVE_INTEGER
631
+ }, (display) => display).otherwise(() => void 0);
632
+ // libs/engine/src/display/try-create-display.ts
633
+ /**
634
+ * Creates a display on the canvas.
635
+ *
636
+ * - `colors`: the 16 base colors.
637
+ * - `pixelSize`: the width and height of one logical pixel, in canvas pixels.
638
+ * - `spriteSize`: the width and height of every sprite, in logical pixels.
639
+ *
640
+ * The screen size in logical pixels is the canvas size divided by
641
+ * `pixelSize`. The display draws with WebGL, and the monitor effect is
642
+ * always on.
643
+ *
644
+ * Returns `undefined` if the canvas has no WebGL context, the monitor effect
645
+ * cannot be set up, a size is not a positive whole number, or the canvas is
646
+ * smaller than one logical pixel.
647
+ */
648
+ const tryCreateDisplay = (canvas, { colors, pixelSize, spriteSize }) => pipe({
649
+ context: canvas.getContext("webgl", CRT_CONTEXT_ATTRIBUTES) ?? void 0,
650
+ size: computeDisplaySize(canvas, pixelSize)
651
+ }, ({ context, size }) => ({
652
+ context,
653
+ crt: tryCreateCrt(context),
654
+ palette: createPalette(colors),
655
+ pixelSize,
656
+ spriteSize,
657
+ width: size.width,
658
+ height: size.height
659
+ }), tryToDisplay);
660
+ // libs/engine/src/palette/to-lit-pixel.ts
661
+ const { nonNullable: nonNullable$2 } = P;
662
+ /**
663
+ * Returns the extended pixel that a pixel shows under light. The color stays
664
+ * the same. The shade moves `shadeOffset` extended shades up or down
665
+ * (4 extended shades = 1 sprite shade). The result is rounded and stops at
666
+ * the darkest and lightest extended shade. `toLitPixel(pixel, 0)` returns
667
+ * the authored color. Returns `TRANSPARENT` for `TRANSPARENT` and for any
668
+ * value that is not a pixel.
669
+ */
670
+ const toLitPixel = (pixel, shadeOffset) => match(SHADE_RAMPS[pixel]).with(nonNullable$2, ({ pixels, extendedShade }) => pixels[clamp(round(extendedShade + shadeOffset), 0, 31)] ?? -1).otherwise(() => -1);
671
+ // libs/engine/src/screen/internal/filter-other-lights.ts
672
+ const filterOtherLights = (lights, id) => filter(lights, (light) => light.id !== id);
673
+ // libs/engine/src/screen/add-light.ts
674
+ /**
675
+ * Adds a light to the screen. If a light with the same id exists, it is
676
+ * replaced. Returns a new screen.
677
+ */
678
+ const addLight = (screen, id, { row, column, radius, intensity }) => ({
679
+ ...screen,
680
+ lights: [...filterOtherLights(screen.lights, id), {
681
+ id,
682
+ row,
683
+ column,
684
+ radius,
685
+ intensity
686
+ }]
687
+ });
688
+ // libs/engine/src/screen/internal/filter-other-sprites.ts
689
+ const filterOtherSprites = (sprites, id) => filter(sprites, (placement) => placement.id !== id);
690
+ // libs/engine/src/screen/add-sprite.ts
691
+ /**
692
+ * Adds a sprite on top of the other sprites. `row` and `column` set its
693
+ * top-left pixel. If a sprite with the same id exists, it is removed first.
694
+ * The new sprite does not cast a shadow; to change that, call
695
+ * `patchSprite(screen, id, { castsShadow: true })`. Returns a new screen.
696
+ */
697
+ const addSprite = (screen, id, sprite, row, column) => ({
698
+ ...screen,
699
+ sprites: [...filterOtherSprites(screen.sprites, id), {
700
+ id,
701
+ sprite,
702
+ row,
703
+ column,
704
+ castsShadow: false
705
+ }]
706
+ });
707
+ // libs/engine/src/screen/create-screen.ts
708
+ /**
709
+ * Creates an empty screen, with no sprites and no lights, filled with
710
+ * `background`. `ambient` shifts every pixel by that many extended shades
711
+ * (4 extended shades = 1 sprite shade). For example, a night scene could
712
+ * use -16.
713
+ */
714
+ const createScreen = (background = -1, ambient = 0) => ({
715
+ background,
716
+ ambient,
717
+ sprites: [],
718
+ lights: []
719
+ });
720
+ // libs/engine/src/screen/find-light.ts
721
+ /** Returns the light with the id, or `undefined` if there is none. */
722
+ const findLight = (screen, id) => find(screen.lights, (light) => light.id === id);
723
+ // libs/engine/src/screen/find-sprite.ts
724
+ /** Returns the sprite with the id, or `undefined` if there is none. */
725
+ const findSprite = (screen, id) => find(screen.sprites, (placement) => placement.id === id);
726
+ // libs/engine/src/screen/patch-ambient.ts
727
+ /**
728
+ * Sets the ambient light: how many extended shades every pixel moves up
729
+ * (positive) or down (negative) before lights are added. Change it over time
730
+ * to fade between day and night. Returns a new screen.
731
+ */
732
+ const patchAmbient = (screen, ambient) => ({
733
+ ...screen,
734
+ ambient
735
+ });
736
+ // libs/engine/src/screen/internal/patch-matching-light.ts
737
+ const patchMatchingLight = (light, id, changes) => match(light.id === id).with(true, () => ({
738
+ id,
739
+ row: changes.row ?? light.row,
740
+ column: changes.column ?? light.column,
741
+ radius: changes.radius ?? light.radius,
742
+ intensity: changes.intensity ?? light.intensity
743
+ })).otherwise(() => light);
744
+ // libs/engine/src/screen/patch-light.ts
745
+ /**
746
+ * Changes a light's `row`, `column`, `radius` or `intensity`. Fields you
747
+ * leave out keep their values. Returns a new screen. If no light has the id,
748
+ * nothing changes.
749
+ */
750
+ const patchLight = (screen, id, changes) => ({
751
+ ...screen,
752
+ lights: map(screen.lights, (light) => patchMatchingLight(light, id, changes))
753
+ });
754
+ // libs/engine/src/screen/patch-light-by-offset.ts
755
+ const { nonNullable: nonNullable$1 } = P;
756
+ /**
757
+ * Moves a light `rowOffset` pixels down and `columnOffset` pixels right.
758
+ * Negative values move it up and left. Returns a new screen. If no light has
759
+ * the id, nothing changes.
760
+ */
761
+ const patchLightByOffset = (screen, id, rowOffset, columnOffset) => match(findLight(screen, id)).with(nonNullable$1, ({ row, column }) => patchLight(screen, id, {
762
+ row: row + rowOffset,
763
+ column: column + columnOffset
764
+ })).otherwise(() => screen);
765
+ // libs/engine/src/screen/internal/patch-sprite-placement.ts
766
+ const patchSpritePlacement = (placement, id, changes) => match(placement.id === id).with(true, () => ({
767
+ id,
768
+ sprite: changes.sprite ?? placement.sprite,
769
+ row: changes.row ?? placement.row,
770
+ column: changes.column ?? placement.column,
771
+ castsShadow: changes.castsShadow ?? placement.castsShadow
772
+ })).otherwise(() => placement);
773
+ // libs/engine/src/screen/patch-sprite.ts
774
+ /**
775
+ * Changes a sprite's `sprite` (for animation), `row`, `column` or
776
+ * `castsShadow`. Fields you leave out keep their values. The sprite keeps its
777
+ * place in the drawing order. Returns a new screen. If no sprite has the id,
778
+ * nothing changes.
779
+ */
780
+ const patchSprite = (screen, id, changes) => ({
781
+ ...screen,
782
+ sprites: map(screen.sprites, (placement) => patchSpritePlacement(placement, id, changes))
783
+ });
784
+ // libs/engine/src/screen/patch-sprite-by-offset.ts
785
+ const { nonNullable } = P;
786
+ /**
787
+ * Moves a sprite `rowOffset` pixels down and `columnOffset` pixels right.
788
+ * Negative values move it up and left. Returns a new screen. If no sprite
789
+ * has the id, nothing changes.
790
+ */
791
+ const patchSpriteByOffset = (screen, id, rowOffset, columnOffset) => match(findSprite(screen, id)).with(nonNullable, ({ row, column }) => patchSprite(screen, id, {
792
+ row: row + rowOffset,
793
+ column: column + columnOffset
794
+ })).otherwise(() => screen);
795
+ // libs/engine/src/screen/remove-light.ts
796
+ /** Removes the light with the id. Returns a new screen. */
797
+ const removeLight = (screen, id) => ({
798
+ ...screen,
799
+ lights: filterOtherLights(screen.lights, id)
800
+ });
801
+ // libs/engine/src/screen/remove-sprite.ts
802
+ /** Removes the sprite with the id. Returns a new screen. */
803
+ const removeSprite = (screen, id) => ({
804
+ ...screen,
805
+ sprites: filterOtherSprites(screen.sprites, id)
806
+ });
807
+ // libs/engine/src/screen/to-sprite.ts
808
+ /**
809
+ * Builds a sprite from text art: one string per row, one character per
810
+ * pixel. Each character is looked up in `legend`. Characters that are not in
811
+ * the legend become transparent.
812
+ */
813
+ const toSprite = (rows, legend) => flatMap(rows, (row) => map(split(row, ""), (symbol) => legend[symbol] ?? -1));
814
+ export { BASE_SHADE, COLOR_COUNT, EXTENDED_SHADES_PER_SHADE, EXTENDED_SHADE_COUNT, SHADE_COUNT, TRANSPARENT, addLight, addSprite, computeFrame, createPalette, createScreen, drawScreen, findLight, findSprite, isPixel, patchAmbient, patchLight, patchLightByOffset, patchSprite, patchSpriteByOffset, removeLight, removeSprite, toExtendedPixel, toLitPixel, toPixel, toSprite, tryCreateDisplay };
815
+
816
+ //# sourceMappingURL=index.js.map