@fungi.computer/sprite 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,639 @@
1
+ import { correlationKey } from "@fungi.computer/cairn";
2
+ import * as z from "zod";
3
+ /**
4
+ * Render styles: an SVG filter drawn over a mounted Sprite. This module owns
5
+ * the style vocabulary, every filter definition, and the DOM layer that mounts
6
+ * one filter per player with ids unique to that player.
7
+ */
8
+ export const renderStyles = [
9
+ "plain",
10
+ "pencil",
11
+ "manga",
12
+ "riso",
13
+ "halftone",
14
+ "blueprint",
15
+ "crt",
16
+ "watercolor",
17
+ ];
18
+ /** A saved render style; unknown names fail at the boundary. */
19
+ export const renderStyleSchema = z.enum(renderStyles);
20
+ /** Picker labels for each style, in display order. */
21
+ export const renderStyleLabels = {
22
+ plain: "Plain",
23
+ pencil: "Pencil",
24
+ manga: "Manga",
25
+ riso: "Riso",
26
+ halftone: "Halftone",
27
+ blueprint: "Blueprint",
28
+ crt: "CRT",
29
+ watercolor: "Watercolor",
30
+ };
31
+ // Theme tokens. A host may pin any of these with the --sprite-* property.
32
+ const tone = {
33
+ ink: "var(--sprite-ink, var(--color-base-content, #1c1c1a))",
34
+ paper: "var(--sprite-paper, var(--color-base-100, #fbfaf6))",
35
+ accent: "var(--sprite-accent, var(--color-accent, #d20f39))",
36
+ risoFirst: "var(--sprite-riso-first, var(--color-info, #1e66f5))",
37
+ risoSecond: "var(--sprite-riso-second, var(--color-accent, #ea76cb))",
38
+ blueprint: "var(--sprite-blueprint, color-mix(in oklab, var(--color-info, #1e66f5) 55%, #0b2350))",
39
+ blueprintLine: "var(--sprite-blueprint-line, color-mix(in oklab, var(--color-info-content, #ffffff) 25%, #ffffff))",
40
+ screen: "var(--sprite-screen, color-mix(in oklab, var(--color-base-content, #111111) 12%, #050805))",
41
+ phosphor: "var(--sprite-phosphor, var(--color-success, #40a02b))",
42
+ };
43
+ const p = (tag, attributes = {}, ...children) => ({ tag, attributes, children });
44
+ const flood = (color, result, opacity = 1) => p("feFlood", {
45
+ style: `flood-color: ${color}`,
46
+ "flood-opacity": opacity,
47
+ result,
48
+ });
49
+ const tile = (svg, size, result) => [
50
+ p("feImage", {
51
+ href: `data:image/svg+xml,${encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' width='${String(size)}' height='${String(size)}'>${svg}</svg>`)}`,
52
+ width: size,
53
+ height: size,
54
+ result: `${result}Cell`,
55
+ }),
56
+ p("feTile", { in: `${result}Cell`, result }),
57
+ ];
58
+ const alphaMatrix = (values, input, result) => p("feColorMatrix", {
59
+ in: input,
60
+ type: "matrix",
61
+ values: `0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ${values}`,
62
+ result,
63
+ });
64
+ /** Darkness as alpha: paper-white is clear, black is opaque. */
65
+ const darkness = (input, result) => alphaMatrix("-0.333 -0.333 -0.333 1 0", input, result);
66
+ /** Laplacian contour of the value masses. */
67
+ const contour = (input, result, weight, outline = 1.1) => [
68
+ p("feConvolveMatrix", {
69
+ in: input,
70
+ order: 3,
71
+ kernelMatrix: "0 1 0 1 -4 1 0 1 0",
72
+ result: `${result}In`,
73
+ }),
74
+ p("feConvolveMatrix", {
75
+ in: input,
76
+ order: 3,
77
+ kernelMatrix: "0 -1 0 -1 4 -1 0 -1 0",
78
+ result: `${result}Out`,
79
+ }),
80
+ p("feComposite", {
81
+ in: `${result}In`,
82
+ in2: `${result}Out`,
83
+ operator: "arithmetic",
84
+ k2: 1,
85
+ k3: 1,
86
+ result: `${result}Edges`,
87
+ }),
88
+ alphaMatrix(`${String(weight)} ${String(weight)} ${String(weight)} ${String(outline)} 0`, `${result}Edges`, result),
89
+ ];
90
+ /**
91
+ * Line art for the non-pencil styles: interior value edges (the convolution
92
+ * keeps opacity, so edges inside the figure survive premultiplication) plus
93
+ * an outline ring around the silhouette.
94
+ */
95
+ const lineArt = (input, result, weight, outline) => [
96
+ p("feConvolveMatrix", {
97
+ in: input,
98
+ order: 3,
99
+ kernelMatrix: "0 1 0 1 -4 1 0 1 0",
100
+ preserveAlpha: "true",
101
+ result: `${result}In`,
102
+ }),
103
+ p("feConvolveMatrix", {
104
+ in: input,
105
+ order: 3,
106
+ kernelMatrix: "0 -1 0 -1 4 -1 0 -1 0",
107
+ preserveAlpha: "true",
108
+ result: `${result}Out`,
109
+ }),
110
+ p("feComposite", {
111
+ in: `${result}In`,
112
+ in2: `${result}Out`,
113
+ operator: "arithmetic",
114
+ k2: 1,
115
+ k3: 1,
116
+ result: `${result}Edges`,
117
+ }),
118
+ alphaMatrix(`${String(weight)} ${String(weight)} ${String(weight)} 0 -0.15`, `${result}Edges`, `${result}Inner`),
119
+ p("feMorphology", {
120
+ in: input,
121
+ operator: "dilate",
122
+ radius: outline,
123
+ result: `${result}Grown`,
124
+ }),
125
+ p("feComposite", {
126
+ in: `${result}Grown`,
127
+ in2: input,
128
+ operator: "out",
129
+ result: `${result}Ring`,
130
+ }),
131
+ alphaMatrix("0 0 0 1 0", `${result}Ring`, `${result}Outline`),
132
+ merge(result, `${result}Inner`, `${result}Outline`),
133
+ ];
134
+ const discrete = (input, table, result) => p("feComponentTransfer", { in: input, result }, p("feFuncA", { type: "discrete", tableValues: table }));
135
+ const merge = (result, ...inputs) => p("feMerge", { result }, ...inputs.map((input) => p("feMergeNode", { in: input })));
136
+ const paint = (color, mask, result, opacity = 1) => [
137
+ flood(color, `${result}Flood`, opacity),
138
+ p("feComposite", { in: `${result}Flood`, in2: mask, operator: "in", result }),
139
+ ];
140
+ const fullBox = { x: "0%", y: "0%", width: "100%", height: "100%" };
141
+ const bleedBox = { x: "-8%", y: "-8%", width: "116%", height: "116%" };
142
+ /** The one pencil pressure: firm lines and dense hatching, reads at 48px. */
143
+ const firmPressure = {
144
+ value: 1.6,
145
+ line: 6,
146
+ outline: 2.4,
147
+ midHatch: [3.2, -0.05],
148
+ darkHatch: [3.4, -0.4],
149
+ tooth: "0 0 0 -0.7 1.45",
150
+ opacity: 1,
151
+ };
152
+ /** Graphite over hatching; the light pressure is the shiit.app port. */
153
+ const pencil = (pressure) => ({
154
+ region: bleedBox,
155
+ primitives: [
156
+ p("feComponentTransfer", { in: "SourceGraphic", result: "valued" }, ...["feFuncR", "feFuncG", "feFuncB"].map((channel) => p(channel, {
157
+ type: "gamma",
158
+ amplitude: 1,
159
+ exponent: pressure.value,
160
+ offset: 0,
161
+ }))),
162
+ // Wobble: the boil. Its seed changes a few times a second.
163
+ p("feTurbulence", {
164
+ type: "fractalNoise",
165
+ baseFrequency: 0.05,
166
+ numOctaves: 2,
167
+ seed: 1,
168
+ result: "wobble",
169
+ "data-sprite-boil": "wobble",
170
+ }),
171
+ p("feDisplacementMap", {
172
+ in: "valued",
173
+ in2: "wobble",
174
+ scale: 2.6,
175
+ xChannelSelector: "R",
176
+ yChannelSelector: "G",
177
+ result: "drawn",
178
+ }),
179
+ // Contour: edges of the value masses, traced as pencil lines.
180
+ ...contour("drawn", "line", pressure.line, pressure.outline),
181
+ // Value: how much graphite each area takes.
182
+ darkness("drawn", "dark"),
183
+ // Two hatch directions: mid tones take one, dark tones cross it.
184
+ ...tile("<path d='M-1 1l2-2M0 6l6-6M5 7l2-2' stroke='black' stroke-width='1.1'/>", 6, "hatch45"),
185
+ p("feOffset", {
186
+ in: "hatch45",
187
+ dx: 0,
188
+ dy: 0,
189
+ result: "hatchOffset",
190
+ "data-sprite-boil": "hatch",
191
+ }),
192
+ // Hand-laid strokes bend a little; a slow noise bends the tile.
193
+ p("feTurbulence", {
194
+ type: "fractalNoise",
195
+ baseFrequency: 0.07,
196
+ numOctaves: 2,
197
+ seed: 5,
198
+ result: "strokeBend",
199
+ }),
200
+ p("feDisplacementMap", {
201
+ in: "hatchOffset",
202
+ in2: "strokeBend",
203
+ scale: 4,
204
+ xChannelSelector: "R",
205
+ yChannelSelector: "B",
206
+ result: "hatchA",
207
+ }),
208
+ ...tile("<path d='M-1 4l2 2M0 0l5 5M4 -1l2 2' stroke='black' stroke-width='0.9'/>", 5, "hatch135"),
209
+ p("feDisplacementMap", {
210
+ in: "hatch135",
211
+ in2: "strokeBend",
212
+ scale: 3,
213
+ xChannelSelector: "G",
214
+ yChannelSelector: "R",
215
+ result: "hatchB",
216
+ }),
217
+ p("feComposite", {
218
+ in: "dark",
219
+ in2: "hatchA",
220
+ operator: "arithmetic",
221
+ k1: pressure.midHatch[0],
222
+ k4: pressure.midHatch[1],
223
+ result: "shadeA",
224
+ }),
225
+ p("feComposite", {
226
+ in: "dark",
227
+ in2: "hatchB",
228
+ operator: "arithmetic",
229
+ k1: pressure.darkHatch[0],
230
+ k4: pressure.darkHatch[1],
231
+ result: "shadeB",
232
+ }),
233
+ // Grain: graphite skips over the paper's tooth.
234
+ p("feTurbulence", {
235
+ type: "fractalNoise",
236
+ baseFrequency: 0.9,
237
+ numOctaves: 1,
238
+ seed: 3,
239
+ result: "tooth",
240
+ }),
241
+ alphaMatrix(pressure.tooth, "tooth", "toothMask"),
242
+ // The darkest marks, eyes and mouth, are pressed in solid.
243
+ alphaMatrix("0 0 0 6 -4.6", "dark", "solid"),
244
+ merge("graphiteRaw", "solid", "shadeA", "shadeB", "line"),
245
+ p("feComposite", {
246
+ in: "graphiteRaw",
247
+ in2: "toothMask",
248
+ operator: "in",
249
+ result: "graphite",
250
+ }),
251
+ ...paint(tone.ink, "graphite", "pencil", pressure.opacity),
252
+ ],
253
+ boil: {
254
+ intervalMs: 160,
255
+ tracks: [
256
+ { name: "wobble", attribute: "seed", frames: [1, 2, 3] },
257
+ { name: "hatch", attribute: "dx", frames: [0, 1, 2] },
258
+ ],
259
+ },
260
+ });
261
+ /** Brush contours, screentone dots on mid tones, and one accent colour. */
262
+ const manga = {
263
+ region: bleedBox,
264
+ primitives: [
265
+ p("feTurbulence", {
266
+ type: "fractalNoise",
267
+ baseFrequency: 0.04,
268
+ numOctaves: 2,
269
+ seed: 2,
270
+ result: "brush",
271
+ "data-sprite-boil": "brush",
272
+ }),
273
+ p("feDisplacementMap", {
274
+ in: "SourceGraphic",
275
+ in2: "brush",
276
+ scale: 2,
277
+ xChannelSelector: "R",
278
+ yChannelSelector: "G",
279
+ result: "drawn",
280
+ }),
281
+ ...paint(tone.paper, "SourceAlpha", "paperFill"),
282
+ darkness("drawn", "dark"),
283
+ // Screentone: a dot sheet laid over the mid tones only.
284
+ ...tile("<circle cx='2.5' cy='2.5' r='1.15' fill='black'/>", 5, "dots"),
285
+ discrete("dark", "0 1 1 0", "midTone"),
286
+ p("feComposite", {
287
+ in: "dots",
288
+ in2: "midTone",
289
+ operator: "in",
290
+ result: "tone",
291
+ }),
292
+ discrete("dark", "0 0 0 1", "solid"),
293
+ // Accent: the most saturated areas take the one spot colour.
294
+ alphaMatrix("3 -3 0 0 -0.35", "drawn", "satRG"),
295
+ alphaMatrix("-3 3 0 0 -0.35", "drawn", "satGR"),
296
+ alphaMatrix("0 3 -3 0 -0.35", "drawn", "satGB"),
297
+ alphaMatrix("0 -3 3 0 -0.35", "drawn", "satBG"),
298
+ merge("saturation", "satRG", "satGR", "satGB", "satBG"),
299
+ discrete("saturation", "0 1", "accentMask"),
300
+ ...paint(tone.accent, "accentMask", "accentFill"),
301
+ // Brush line: a thick outline and swelling interior strokes.
302
+ ...lineArt("drawn", "edge", 2, 2.2),
303
+ p("feMorphology", {
304
+ in: "edge",
305
+ operator: "dilate",
306
+ radius: 0.25,
307
+ result: "stroke",
308
+ }),
309
+ merge("inkRaw", "tone", "solid", "stroke"),
310
+ ...paint(tone.ink, "inkRaw", "ink"),
311
+ merge("manga", "paperFill", "accentFill", "ink"),
312
+ ],
313
+ boil: {
314
+ intervalMs: 180,
315
+ tracks: [{ name: "brush", attribute: "seed", frames: [2, 4, 6] }],
316
+ },
317
+ };
318
+ /** Two-ink duotone printed slightly out of register. */
319
+ const riso = {
320
+ region: bleedBox,
321
+ primitives: [
322
+ alphaMatrix("-0.55 -0.55 -0.55 1.65 0", "SourceGraphic", "dark"),
323
+ p("feTurbulence", {
324
+ type: "fractalNoise",
325
+ baseFrequency: 0.85,
326
+ numOctaves: 1,
327
+ seed: 7,
328
+ result: "grain",
329
+ }),
330
+ alphaMatrix("0 0 0 -0.9 1.35", "grain", "grainMask"),
331
+ // First drum: the darks and mids, speckled by the drum's grain.
332
+ p("feComposite", {
333
+ in: "dark",
334
+ in2: "grainMask",
335
+ operator: "in",
336
+ result: "firstMask",
337
+ }),
338
+ ...paint(tone.risoFirst, "firstMask", "firstInk", 0.92),
339
+ // Second drum: warm and dark areas, everything short of paper-light.
340
+ alphaMatrix("0 -1.9 0 1.9 0", "SourceGraphic", "secondMask"),
341
+ ...paint(tone.risoSecond, "secondMask", "secondInk", 0.9),
342
+ p("feOffset", {
343
+ in: "firstInk",
344
+ dx: -1.5,
345
+ dy: 1,
346
+ result: "firstPlate",
347
+ "data-sprite-boil": "register",
348
+ }),
349
+ p("feOffset", {
350
+ in: "secondInk",
351
+ dx: 1.5,
352
+ dy: -0.5,
353
+ result: "secondPlate",
354
+ }),
355
+ p("feBlend", { in: "firstPlate", in2: "secondPlate", mode: "multiply" }),
356
+ ],
357
+ boil: {
358
+ intervalMs: 240,
359
+ tracks: [{ name: "register", attribute: "dx", frames: [-1.5, -1, -2] }],
360
+ },
361
+ };
362
+ /** Round dots whose size follows the value underneath. */
363
+ const halftone = {
364
+ region: bleedBox,
365
+ primitives: [
366
+ alphaMatrix("-0.5 -0.5 -0.5 1.65 0", "SourceGraphic", "value"),
367
+ // Even paper-light areas keep a fine dot so the figure never vanishes.
368
+ alphaMatrix("0 0 0 0.16 0", "SourceGraphic", "floor"),
369
+ merge("dark", "floor", "value"),
370
+ ...tile("<defs><radialGradient id='d'><stop offset='0' stop-opacity='1'/><stop offset='1' stop-opacity='0'/></radialGradient></defs><rect width='6' height='6' fill='url(#d)'/>", 6, "screen"),
371
+ p("feComposite", {
372
+ in: "dark",
373
+ in2: "screen",
374
+ operator: "arithmetic",
375
+ k2: 1,
376
+ k3: 1,
377
+ k4: -0.92,
378
+ result: "dotField",
379
+ }),
380
+ p("feComponentTransfer", { in: "dotField", result: "dotsRaw" }, p("feFuncA", { type: "linear", slope: 12, intercept: 0 })),
381
+ p("feComposite", {
382
+ in: "dotsRaw",
383
+ in2: "SourceAlpha",
384
+ operator: "in",
385
+ result: "dots",
386
+ }),
387
+ ...paint(tone.ink, "dots", "halftone"),
388
+ ],
389
+ };
390
+ /** White drafting lines on blueprint blue. */
391
+ const blueprint = {
392
+ region: fullBox,
393
+ primitives: [
394
+ flood(tone.blueprint, "ground"),
395
+ p("feTurbulence", {
396
+ type: "fractalNoise",
397
+ baseFrequency: 0.05,
398
+ numOctaves: 1,
399
+ seed: 4,
400
+ result: "hand",
401
+ "data-sprite-boil": "hand",
402
+ }),
403
+ p("feDisplacementMap", {
404
+ in: "SourceGraphic",
405
+ in2: "hand",
406
+ scale: 1.6,
407
+ xChannelSelector: "R",
408
+ yChannelSelector: "G",
409
+ result: "drawn",
410
+ }),
411
+ ...tile("<path d='M0 0.5h16M0.5 0v16' stroke='black' stroke-width='1' stroke-opacity='0.22'/>", 16, "grid"),
412
+ ...paint(tone.blueprintLine, "grid", "gridLines"),
413
+ ...paint(tone.blueprintLine, "SourceAlpha", "wash", 0.12),
414
+ ...lineArt("drawn", "edge", 3, 1.2),
415
+ p("feMorphology", {
416
+ in: "edge",
417
+ operator: "dilate",
418
+ radius: 0.3,
419
+ result: "stroke",
420
+ }),
421
+ ...paint(tone.blueprintLine, "stroke", "lines"),
422
+ merge("blueprint", "ground", "gridLines", "wash", "lines"),
423
+ ],
424
+ boil: {
425
+ intervalMs: 200,
426
+ tracks: [{ name: "hand", attribute: "seed", frames: [4, 5, 6] }],
427
+ },
428
+ };
429
+ /** A phosphor tube: glowing monochrome, bloom, and rolling scanlines. */
430
+ const crt = {
431
+ region: fullBox,
432
+ primitives: [
433
+ flood(tone.screen, "ground"),
434
+ p("feColorMatrix", {
435
+ in: "SourceGraphic",
436
+ type: "luminanceToAlpha",
437
+ result: "lumRaw",
438
+ }),
439
+ p("feComposite", {
440
+ in: "lumRaw",
441
+ in2: "SourceAlpha",
442
+ operator: "in",
443
+ result: "lum",
444
+ }),
445
+ p("feComponentTransfer", { in: "lum", result: "beam" }, p("feFuncA", { type: "linear", slope: 1.5, intercept: -0.35 })),
446
+ p("feComposite", {
447
+ in: "beam",
448
+ in2: "SourceAlpha",
449
+ operator: "in",
450
+ result: "trace",
451
+ }),
452
+ ...paint(tone.phosphor, "trace", "glow"),
453
+ p("feGaussianBlur", { in: "glow", stdDeviation: 2.5, result: "bloom" }),
454
+ ...tile("<rect y='2' width='3' height='1' fill='black'/>", 3, "scan"),
455
+ p("feOffset", {
456
+ in: "scan",
457
+ dx: 0,
458
+ dy: 0,
459
+ result: "scanRoll",
460
+ "data-sprite-boil": "roll",
461
+ }),
462
+ ...paint(tone.screen, "scanRoll", "scanlines", 0.6),
463
+ merge("crt", "ground", "bloom", "glow", "scanlines"),
464
+ ],
465
+ boil: {
466
+ intervalMs: 120,
467
+ tracks: [{ name: "roll", attribute: "dy", frames: [0, 1, 2] }],
468
+ },
469
+ };
470
+ /** Soft pigment bleeding into grained paper, pooling at its edges. */
471
+ const watercolor = {
472
+ region: bleedBox,
473
+ primitives: [
474
+ p("feTurbulence", {
475
+ type: "fractalNoise",
476
+ baseFrequency: 0.03,
477
+ numOctaves: 3,
478
+ seed: 9,
479
+ result: "flow",
480
+ }),
481
+ p("feDisplacementMap", {
482
+ in: "SourceGraphic",
483
+ in2: "flow",
484
+ scale: 7,
485
+ xChannelSelector: "R",
486
+ yChannelSelector: "G",
487
+ result: "bled",
488
+ }),
489
+ p("feGaussianBlur", { in: "bled", stdDeviation: 1.4, result: "wash" }),
490
+ // Pigment thins toward paper white inside the wash.
491
+ p("feComponentTransfer", { in: "wash", result: "thin" }, p("feFuncR", { type: "linear", slope: 0.78, intercept: 0.2 }), p("feFuncG", { type: "linear", slope: 0.78, intercept: 0.2 }), p("feFuncB", { type: "linear", slope: 0.78, intercept: 0.2 }), p("feFuncA", { type: "linear", slope: 0.9, intercept: 0 })),
492
+ // Edge pooling: the rim where the wash dries darker.
493
+ p("feGaussianBlur", { in: "wash", stdDeviation: 3, result: "spread" }),
494
+ p("feComposite", {
495
+ in: "wash",
496
+ in2: "spread",
497
+ operator: "arithmetic",
498
+ k2: 1.6,
499
+ k3: -1.4,
500
+ result: "rimMask",
501
+ }),
502
+ p("feComponentTransfer", { in: "wash", result: "deep" }, p("feFuncR", { type: "linear", slope: 0.6, intercept: 0 }), p("feFuncG", { type: "linear", slope: 0.6, intercept: 0 }), p("feFuncB", { type: "linear", slope: 0.6, intercept: 0 })),
503
+ p("feComposite", {
504
+ in: "deep",
505
+ in2: "rimMask",
506
+ operator: "in",
507
+ result: "rim",
508
+ }),
509
+ // Paper grain lifts pigment off the tooth.
510
+ p("feTurbulence", {
511
+ type: "fractalNoise",
512
+ baseFrequency: 0.7,
513
+ numOctaves: 2,
514
+ seed: 11,
515
+ result: "tooth",
516
+ }),
517
+ alphaMatrix("0 0 0 -0.8 1.5", "tooth", "toothMask"),
518
+ // Eyes and mouth stay as crisp darks.
519
+ darkness("SourceGraphic", "dark"),
520
+ alphaMatrix("0 0 0 5 -3.8", "dark", "keyMask"),
521
+ ...paint(tone.ink, "keyMask", "key", 0.9),
522
+ merge("paint", "thin", "rim", "key"),
523
+ p("feComposite", { in: "paint", in2: "toothMask", operator: "in" }),
524
+ ],
525
+ };
526
+ const pencilFirm = pencil(firmPressure);
527
+ const definitions = {
528
+ manga,
529
+ riso,
530
+ halftone,
531
+ blueprint,
532
+ crt,
533
+ watercolor,
534
+ };
535
+ const definitionFor = (style) => (style === "pencil" ? pencilFirm : definitions[style]);
536
+ const svgNamespace = "http://www.w3.org/2000/svg";
537
+ const build = (document, primitive) => {
538
+ const element = document.createElementNS(svgNamespace, primitive.tag);
539
+ for (const [name, value] of Object.entries(primitive.attributes))
540
+ element.setAttribute(name, String(value));
541
+ for (const child of primitive.children)
542
+ element.append(build(document, child));
543
+ return element;
544
+ };
545
+ /**
546
+ * Mount a render-style layer in `host`. The filter applies only to the stage
547
+ * box, and its boil runs only while the host is on screen and not still.
548
+ */
549
+ export function createRenderStyleLayer(host) {
550
+ const fragmentPrefix = `sprite-style-${correlationKey()}`;
551
+ const document = host.ownerDocument;
552
+ const view = document.defaultView;
553
+ let style = "plain";
554
+ let stage;
555
+ let defs;
556
+ let still = false;
557
+ let visible = true;
558
+ let timer;
559
+ let frame = 0;
560
+ const stop = () => {
561
+ if (timer !== undefined)
562
+ clearInterval(timer);
563
+ timer = undefined;
564
+ };
565
+ const run = () => {
566
+ stop();
567
+ if (style === "plain" || !defs || still || !visible || document.hidden)
568
+ return;
569
+ const boil = definitionFor(style).boil;
570
+ if (!boil)
571
+ return;
572
+ const filter = defs;
573
+ timer = setInterval(() => {
574
+ if (document.hidden)
575
+ return;
576
+ frame += 1;
577
+ for (const track of boil.tracks) {
578
+ const value = track.frames[frame % track.frames.length];
579
+ filter
580
+ .querySelector(`[data-sprite-boil="${track.name}"]`)
581
+ ?.setAttribute(track.attribute, String(value));
582
+ }
583
+ }, boil.intervalMs);
584
+ };
585
+ const Observer = view?.IntersectionObserver;
586
+ const observer = Observer
587
+ ? new Observer((entries) => {
588
+ visible = entries.some((entry) => entry.isIntersecting);
589
+ run();
590
+ })
591
+ : undefined;
592
+ observer?.observe(host);
593
+ const clear = () => {
594
+ stop();
595
+ defs?.remove();
596
+ defs = undefined;
597
+ if (stage)
598
+ stage.style.filter = "";
599
+ };
600
+ return {
601
+ set(next, nextStage) {
602
+ if (next === style && nextStage === stage)
603
+ return;
604
+ clear();
605
+ style = next;
606
+ stage = nextStage;
607
+ if (next === "plain")
608
+ return;
609
+ const definition = definitionFor(next);
610
+ const fragment = `${fragmentPrefix}-${next}`;
611
+ const svg = document.createElementNS(svgNamespace, "svg");
612
+ svg.setAttribute("aria-hidden", "true");
613
+ svg.setAttribute("focusable", "false");
614
+ svg.setAttribute("width", "0");
615
+ svg.setAttribute("height", "0");
616
+ svg.style.cssText = "position:absolute;width:0;height:0;overflow:hidden";
617
+ svg.append(build(document, p("filter", {
618
+ id: fragment,
619
+ ...definition.region,
620
+ "color-interpolation-filters": "sRGB",
621
+ }, ...definition.primitives)));
622
+ host.append(svg);
623
+ defs = svg;
624
+ nextStage.style.filter = `url(#${fragment})`;
625
+ run();
626
+ },
627
+ still(value) {
628
+ if (value === still)
629
+ return;
630
+ still = value;
631
+ run();
632
+ },
633
+ dispose() {
634
+ clear();
635
+ stage = undefined;
636
+ observer?.disconnect();
637
+ },
638
+ };
639
+ }
@@ -0,0 +1,4 @@
1
+ import type { Renderer } from "./player.js";
2
+ /** One character, with browser-native MP4 clips selected by semantic activity. */
3
+ declare const renderVideo: Renderer;
4
+ export default renderVideo;