@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,32 @@
1
+ import * as z from "zod";
2
+ /** Portable editor descriptors; native input bindings belong to each renderer. */
3
+ export declare const controlSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
4
+ type: z.ZodLiteral<"number">;
5
+ label: z.ZodString;
6
+ default: z.ZodNumber;
7
+ min: z.ZodNumber;
8
+ max: z.ZodNumber;
9
+ step: z.ZodOptional<z.ZodNumber>;
10
+ }, z.core.$strict>, z.ZodObject<{
11
+ type: z.ZodLiteral<"choice">;
12
+ label: z.ZodString;
13
+ default: z.ZodString;
14
+ options: z.ZodArray<z.ZodObject<{
15
+ value: z.ZodString;
16
+ label: z.ZodString;
17
+ }, z.core.$strict>>;
18
+ }, z.core.$strict>, z.ZodObject<{
19
+ type: z.ZodLiteral<"color">;
20
+ label: z.ZodString;
21
+ default: z.ZodString;
22
+ }, z.core.$strict>, z.ZodObject<{
23
+ type: z.ZodLiteral<"boolean">;
24
+ label: z.ZodString;
25
+ default: z.ZodBoolean;
26
+ }, z.core.$strict>], "type">;
27
+ /** A validated, character-owned customization control. */
28
+ export type Control = z.infer<typeof controlSchema>;
29
+ /** Values saved by the shell; no renderer objects or transient activity. */
30
+ export type Appearance = Readonly<Record<string, string | number | boolean>>;
31
+ /** Validate the complete appearance before applying any native updates. */
32
+ export declare function resolveAppearance(controls: Readonly<Record<string, Control>>, overrides?: Appearance): Appearance;
@@ -0,0 +1,64 @@
1
+ import * as z from "zod";
2
+ const label = z.string().trim().min(1);
3
+ const color = z.string().regex(/^#[0-9a-f]{6}$/i);
4
+ const numberControl = z
5
+ .strictObject({
6
+ type: z.literal("number"),
7
+ label,
8
+ default: z.number(),
9
+ min: z.number(),
10
+ max: z.number(),
11
+ step: z.number().positive().optional(),
12
+ })
13
+ .refine((control) => control.min <= control.default && control.default <= control.max, {
14
+ message: "Number control default must be within its bounds",
15
+ });
16
+ const choiceControl = z
17
+ .strictObject({
18
+ type: z.literal("choice"),
19
+ label,
20
+ default: z.string(),
21
+ options: z.array(z.strictObject({ value: z.string(), label })).min(1),
22
+ })
23
+ .refine((control) => control.options.some((option) => option.value === control.default) &&
24
+ new Set(control.options.map((option) => option.value)).size ===
25
+ control.options.length, {
26
+ message: "Choice control needs unique options and a declared default",
27
+ });
28
+ /** Portable editor descriptors; native input bindings belong to each renderer. */
29
+ export const controlSchema = z.discriminatedUnion("type", [
30
+ numberControl,
31
+ choiceControl,
32
+ z.strictObject({ type: z.literal("color"), label, default: color }),
33
+ z.strictObject({ type: z.literal("boolean"), label, default: z.boolean() }),
34
+ ]);
35
+ /** Validate the complete appearance before applying any native updates. */
36
+ export function resolveAppearance(controls, overrides = {}) {
37
+ for (const key of Object.keys(overrides)) {
38
+ if (!Object.hasOwn(controls, key))
39
+ throw new Error(`Unknown appearance control: ${key}`);
40
+ }
41
+ return Object.fromEntries(Object.entries(controls).map(([key, control]) => {
42
+ const value = Object.hasOwn(overrides, key)
43
+ ? overrides[key]
44
+ : control.default;
45
+ switch (control.type) {
46
+ case "number":
47
+ return [
48
+ key,
49
+ z.number().min(control.min).max(control.max).parse(value),
50
+ ];
51
+ case "color":
52
+ return [key, color.parse(value)];
53
+ case "boolean":
54
+ return [key, z.boolean().parse(value)];
55
+ case "choice": {
56
+ const selected = z.string().parse(value);
57
+ if (!control.options.some((option) => option.value === selected)) {
58
+ throw new Error(`Unknown choice for ${key}: ${selected}`);
59
+ }
60
+ return [key, selected];
61
+ }
62
+ }
63
+ }));
64
+ }
@@ -0,0 +1,21 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * What a person saves about their Sprite. Hosts parse stored values with this
4
+ * schema at their boundary; character controls still validate `appearance`
5
+ * against the selected document when it is presented.
6
+ */
7
+ export declare const customizationSchema: z.ZodObject<{
8
+ appearance: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
9
+ renderStyle: z.ZodDefault<z.ZodEnum<{
10
+ plain: "plain";
11
+ pencil: "pencil";
12
+ manga: "manga";
13
+ riso: "riso";
14
+ halftone: "halftone";
15
+ blueprint: "blueprint";
16
+ crt: "crt";
17
+ watercolor: "watercolor";
18
+ }>>;
19
+ }, z.core.$strict>;
20
+ /** A parsed, saved Sprite customization. */
21
+ export type Customization = z.infer<typeof customizationSchema>;
@@ -0,0 +1,13 @@
1
+ import * as z from "zod";
2
+ import { renderStyleSchema } from "./render-style.js";
3
+ /**
4
+ * What a person saves about their Sprite. Hosts parse stored values with this
5
+ * schema at their boundary; character controls still validate `appearance`
6
+ * against the selected document when it is presented.
7
+ */
8
+ export const customizationSchema = z.strictObject({
9
+ appearance: z
10
+ .record(z.string().min(1), z.union([z.string(), z.number(), z.boolean()]))
11
+ .optional(),
12
+ renderStyle: renderStyleSchema.default("plain"),
13
+ });
@@ -0,0 +1,43 @@
1
+ import * as z from "zod";
2
+ /** Shared envelope only. The selected adapter must decode data before use. */
3
+ export declare const characterSchema: z.ZodObject<{
4
+ version: z.ZodLiteral<1>;
5
+ name: z.ZodString;
6
+ renderer: z.ZodString;
7
+ controls: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
8
+ type: z.ZodLiteral<"number">;
9
+ label: z.ZodString;
10
+ default: z.ZodNumber;
11
+ min: z.ZodNumber;
12
+ max: z.ZodNumber;
13
+ step: z.ZodOptional<z.ZodNumber>;
14
+ }, z.core.$strict>, z.ZodObject<{
15
+ type: z.ZodLiteral<"choice">;
16
+ label: z.ZodString;
17
+ default: z.ZodString;
18
+ options: z.ZodArray<z.ZodObject<{
19
+ value: z.ZodString;
20
+ label: z.ZodString;
21
+ }, z.core.$strict>>;
22
+ }, z.core.$strict>, z.ZodObject<{
23
+ type: z.ZodLiteral<"color">;
24
+ label: z.ZodString;
25
+ default: z.ZodString;
26
+ }, z.core.$strict>, z.ZodObject<{
27
+ type: z.ZodLiteral<"boolean">;
28
+ label: z.ZodString;
29
+ default: z.ZodBoolean;
30
+ }, z.core.$strict>], "type">>>;
31
+ poster: z.ZodOptional<z.ZodString>;
32
+ data: z.ZodJSONSchema;
33
+ }, z.core.$strict>;
34
+ /** A portable character, with renderer data still awaiting its native decoder. */
35
+ export type CharacterDocument = z.infer<typeof characterSchema>;
36
+ /** Domain key for a character installed by the host. */
37
+ export declare const localCharacterSourceSchema: z.ZodObject<{
38
+ local: z.ZodString;
39
+ }, z.core.$strict>;
40
+ /** A served document URL or a host-installed character key. */
41
+ export type CharacterSource = string | {
42
+ readonly local: string;
43
+ };
@@ -0,0 +1,15 @@
1
+ import * as z from "zod";
2
+ import { controlSchema } from "./controls.js";
3
+ /** Shared envelope only. The selected adapter must decode data before use. */
4
+ export const characterSchema = z.strictObject({
5
+ version: z.literal(1),
6
+ name: z.string().trim().min(1),
7
+ renderer: z.string().trim().min(1),
8
+ controls: z.record(z.string().min(1), controlSchema).default({}),
9
+ poster: z.string().min(1).optional(),
10
+ data: z.json(),
11
+ });
12
+ /** Domain key for a character installed by the host. */
13
+ export const localCharacterSourceSchema = z.strictObject({
14
+ local: z.string().trim().min(1),
15
+ });
@@ -0,0 +1,11 @@
1
+ export { createSprite } from "./player.js";
2
+ export type { AvatarPlayer, AvatarPresentation, EffectivePresentation, LocalCharacter, NativePlayer, NativePresentation, PlayerState, Renderer, RendererContext, SpriteOptions, SpriteService, } from "./player.js";
3
+ export { characterSchema } from "./document.js";
4
+ export type { CharacterDocument, CharacterSource } from "./document.js";
5
+ export { controlSchema, resolveAppearance } from "./controls.js";
6
+ export type { Appearance, Control } from "./controls.js";
7
+ export type { Asset, AssetPolicy } from "./assets.js";
8
+ export { createRenderStyleLayer, renderStyleLabels, renderStyles, renderStyleSchema, } from "./render-style.js";
9
+ export type { RenderStyle, RenderStyleLayer } from "./render-style.js";
10
+ export { customizationSchema } from "./customization.js";
11
+ export type { Customization } from "./customization.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { createSprite } from "./player.js";
2
+ export { characterSchema } from "./document.js";
3
+ export { controlSchema, resolveAppearance } from "./controls.js";
4
+ export { createRenderStyleLayer, renderStyleLabels, renderStyles, renderStyleSchema, } from "./render-style.js";
5
+ export { customizationSchema } from "./customization.js";
package/dist/ink.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Ink drawings: traced manga strokes drawn on the page, stroke by stroke, with
3
+ * a little hand wobble. Screentone dots sprinkle in after the linework and
4
+ * coral goes down last. Plain Canvas 2D, so there is nothing else to load.
5
+ */
6
+ /**
7
+ * Traced art: flat [x, y, width, ...] strokes, solid fills as solids (an outer
8
+ * loop then its holes, each a flat [x, y, ...] list, filled even-odd), and a
9
+ * flat [x, y, r, ...] screentone dot field.
10
+ */
11
+ export type InkArt = Readonly<{
12
+ width: number;
13
+ height: number;
14
+ ink: readonly (readonly number[])[];
15
+ coral: readonly (readonly number[])[];
16
+ fills?: Readonly<{
17
+ ink: readonly InkSolid[];
18
+ coral: readonly InkSolid[];
19
+ }>;
20
+ dots: readonly number[];
21
+ }>;
22
+ /** One filled solid: its outer loop first, then any holes. */
23
+ export type InkSolid = readonly (readonly number[])[];
24
+ /** Colours as CSS colour strings; the caller reads them from its theme. */
25
+ export type InkPalette = Readonly<{
26
+ ink: string;
27
+ coral: string;
28
+ }>;
29
+ /** Options accepted by mountInkDrawing. */
30
+ export type InkDrawingOptions = Readonly<{
31
+ art: InkArt;
32
+ palette: () => InkPalette;
33
+ /** Seconds for the whole drawing. Defaults to 2.5. */
34
+ duration?: number;
35
+ /** Wobble amplitude in art pixels. Defaults to 0.9. */
36
+ wobble?: number;
37
+ /** Seed for a repeatable hand. */
38
+ seed?: number;
39
+ }>;
40
+ /** A mounted drawing. `redraw` repaints (after a theme change); `destroy` removes it. */
41
+ export type InkDrawing = Readonly<{
42
+ redraw: () => void;
43
+ destroy: () => void;
44
+ }>;
45
+ /**
46
+ * Draw `art` into a canvas that fills `host`'s width at the art's aspect ratio.
47
+ * Drawing starts when the host scrolls into view and pauses off screen.
48
+ * Reduced motion paints the finished drawing at once.
49
+ */
50
+ export declare function mountInkDrawing(host: HTMLElement, options: InkDrawingOptions): InkDrawing;
package/dist/ink.js ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Ink drawings: traced manga strokes drawn on the page, stroke by stroke, with
3
+ * a little hand wobble. Screentone dots sprinkle in after the linework and
4
+ * coral goes down last. Plain Canvas 2D, so there is nothing else to load.
5
+ */
6
+ function random(seed) {
7
+ let state = seed >>> 0;
8
+ return () => {
9
+ state = (state + 0x6d2b79f5) >>> 0;
10
+ let t = state;
11
+ t = Math.imul(t ^ (t >>> 15), t | 1);
12
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
13
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
14
+ };
15
+ }
16
+ /** Every stroke broken into wobbled segments, in drawing order. */
17
+ function segmentsOf(art, wobble, seed) {
18
+ const rand = random(seed);
19
+ const out = [];
20
+ const add = (stroke, coral) => {
21
+ // One slow drift per stroke plus a little jitter per point reads as a hand.
22
+ const phase = rand() * Math.PI * 2;
23
+ const drift = wobble * (0.5 + rand());
24
+ const points = [];
25
+ for (let index = 0; index + 2 < stroke.length; index += 3) {
26
+ const step = index / 3;
27
+ const bend = Math.sin(phase + step * 0.35) * drift;
28
+ points.push([
29
+ (stroke[index] ?? 0) + bend + (rand() - 0.5) * wobble * 0.4,
30
+ (stroke[index + 1] ?? 0) + bend * 0.6 + (rand() - 0.5) * wobble * 0.4,
31
+ (stroke[index + 2] ?? 1) * (0.85 + rand() * 0.3),
32
+ ]);
33
+ }
34
+ for (let index = 1; index < points.length; index += 1) {
35
+ const a = points[index - 1];
36
+ const b = points[index];
37
+ if (!a || !b)
38
+ continue;
39
+ out.push({
40
+ x0: a[0],
41
+ y0: a[1],
42
+ x1: b[0],
43
+ y1: b[1],
44
+ width: (a[2] + b[2]) / 2,
45
+ coral,
46
+ });
47
+ }
48
+ };
49
+ for (const stroke of art.ink)
50
+ add(stroke, false);
51
+ const inkCount = out.length;
52
+ for (const stroke of art.coral)
53
+ add(stroke, true);
54
+ // Ink first, then coral: the order the brief asks for.
55
+ return [...out.slice(0, inkCount), ...out.slice(inkCount)];
56
+ }
57
+ function prefersStill(view) {
58
+ return view.matchMedia("(prefers-reduced-motion: reduce)").matches;
59
+ }
60
+ /**
61
+ * Draw `art` into a canvas that fills `host`'s width at the art's aspect ratio.
62
+ * Drawing starts when the host scrolls into view and pauses off screen.
63
+ * Reduced motion paints the finished drawing at once.
64
+ */
65
+ export function mountInkDrawing(host, options) {
66
+ const { art, palette, duration = 2.5, wobble = 0.9, seed = 11 } = options;
67
+ const view = host.ownerDocument.defaultView;
68
+ if (!view)
69
+ throw new Error("Ink drawings need a browser window");
70
+ const canvas = host.ownerDocument.createElement("canvas");
71
+ canvas.setAttribute("aria-hidden", "true");
72
+ canvas.style.display = "block";
73
+ canvas.style.width = "100%";
74
+ canvas.style.height = "auto";
75
+ canvas.style.aspectRatio = `${String(art.width)} / ${String(art.height)}`;
76
+ host.append(canvas);
77
+ const context = canvas.getContext("2d");
78
+ if (!context)
79
+ throw new Error("Canvas 2D is unavailable");
80
+ const segments = segmentsOf(art, wobble, seed);
81
+ const inkFills = art.fills?.ink ?? [];
82
+ const coralFills = art.fills?.coral ?? [];
83
+ const dotCount = art.dots.length / 3;
84
+ // Share of the timeline for each pass: linework, solid ink, screentone, coral.
85
+ const phases = { lines: 0.5, fills: 0.15, dots: 0.15 };
86
+ let colours = palette();
87
+ let drawnSegments = 0;
88
+ let drawnInkSolids = 0;
89
+ let drawnCoralSolids = 0;
90
+ let drawnDots = 0;
91
+ let elapsed = 0;
92
+ let frame = 0;
93
+ let last = 0;
94
+ let visible = false;
95
+ const size = () => {
96
+ const scale = Math.min(view.devicePixelRatio || 1, 2);
97
+ const cssWidth = host.clientWidth || art.width;
98
+ canvas.width = Math.round(cssWidth * scale);
99
+ canvas.height = Math.round((cssWidth * scale * art.height) / art.width);
100
+ context.setTransform(canvas.width / art.width, 0, 0, canvas.height / art.height, 0, 0);
101
+ context.lineCap = "round";
102
+ context.lineJoin = "round";
103
+ };
104
+ const drawSegments = (until) => {
105
+ for (; drawnSegments < until; drawnSegments += 1) {
106
+ const segment = segments[drawnSegments];
107
+ if (!segment)
108
+ break;
109
+ context.strokeStyle = segment.coral ? colours.coral : colours.ink;
110
+ context.lineWidth = segment.width;
111
+ context.beginPath();
112
+ context.moveTo(segment.x0, segment.y0);
113
+ context.lineTo(segment.x1, segment.y1);
114
+ context.stroke();
115
+ }
116
+ };
117
+ const drawDots = (until) => {
118
+ context.fillStyle = colours.ink;
119
+ for (; drawnDots < until; drawnDots += 1) {
120
+ const x = art.dots[drawnDots * 3] ?? 0;
121
+ const y = art.dots[drawnDots * 3 + 1] ?? 0;
122
+ const r = art.dots[drawnDots * 3 + 2] ?? 1;
123
+ context.beginPath();
124
+ context.arc(x, y, Math.max(0.6, r), 0, Math.PI * 2);
125
+ context.fill();
126
+ }
127
+ };
128
+ // Solid areas go down one closed solid at a time, holes cut even-odd.
129
+ const drawSolids = (solids, from, until, colour) => {
130
+ context.fillStyle = colour;
131
+ for (let index = from; index < until; index += 1) {
132
+ const solid = solids[index];
133
+ if (!solid)
134
+ continue;
135
+ context.beginPath();
136
+ for (const loop of solid) {
137
+ context.moveTo(loop[0] ?? 0, loop[1] ?? 0);
138
+ for (let point = 2; point + 1 < loop.length; point += 2)
139
+ context.lineTo(loop[point] ?? 0, loop[point + 1] ?? 0);
140
+ context.closePath();
141
+ }
142
+ context.fill("evenodd");
143
+ }
144
+ return Math.max(from, until);
145
+ };
146
+ const inkSegments = segments.findIndex((segment) => segment.coral);
147
+ const inkEnd = inkSegments === -1 ? segments.length : inkSegments;
148
+ const share = (progress, start, length) => Math.min(1, Math.max(0, (progress - start) / length));
149
+ const paint = (progress) => {
150
+ drawSegments(Math.floor(inkEnd * share(progress, 0, phases.lines)));
151
+ drawnInkSolids = drawSolids(inkFills, drawnInkSolids, Math.floor(inkFills.length * share(progress, phases.lines, phases.fills)), colours.ink);
152
+ const dotsStart = phases.lines + phases.fills;
153
+ drawDots(Math.floor(dotCount * share(progress, dotsStart, phases.dots)));
154
+ const coralStart = dotsStart + phases.dots;
155
+ const coral = share(progress, coralStart, 1 - coralStart);
156
+ if (coral > 0) {
157
+ drawSegments(inkEnd + Math.floor((segments.length - inkEnd) * coral));
158
+ drawnCoralSolids = drawSolids(coralFills, drawnCoralSolids, Math.floor(coralFills.length * coral), colours.coral);
159
+ }
160
+ };
161
+ const finish = () => {
162
+ elapsed = duration;
163
+ paint(1);
164
+ };
165
+ const reset = () => {
166
+ size();
167
+ context.clearRect(0, 0, art.width, art.height);
168
+ drawnSegments = 0;
169
+ drawnInkSolids = 0;
170
+ drawnCoralSolids = 0;
171
+ drawnDots = 0;
172
+ };
173
+ const tick = (now) => {
174
+ frame = 0;
175
+ if (!visible)
176
+ return;
177
+ elapsed += Math.min(0.1, (now - (last || now)) / 1000);
178
+ last = now;
179
+ paint(Math.min(1, elapsed / duration));
180
+ if (elapsed < duration)
181
+ frame = view.requestAnimationFrame(tick);
182
+ };
183
+ const play = () => {
184
+ if (frame || elapsed >= duration)
185
+ return;
186
+ last = 0;
187
+ frame = view.requestAnimationFrame(tick);
188
+ };
189
+ reset();
190
+ if (prefersStill(view))
191
+ finish();
192
+ const observer = new view.IntersectionObserver((entries) => {
193
+ visible = entries.some((entry) => entry.isIntersecting);
194
+ if (visible)
195
+ play();
196
+ else if (frame) {
197
+ view.cancelAnimationFrame(frame);
198
+ frame = 0;
199
+ }
200
+ }, { threshold: 0.2 });
201
+ observer.observe(host);
202
+ const redraw = () => {
203
+ colours = palette();
204
+ reset();
205
+ paint(Math.min(1, elapsed / duration));
206
+ };
207
+ let lastWidth = host.clientWidth;
208
+ const resized = new view.ResizeObserver(() => {
209
+ if (host.clientWidth === lastWidth)
210
+ return;
211
+ lastWidth = host.clientWidth;
212
+ redraw();
213
+ });
214
+ resized.observe(host);
215
+ return {
216
+ redraw,
217
+ destroy: () => {
218
+ observer.disconnect();
219
+ resized.disconnect();
220
+ if (frame)
221
+ view.cancelAnimationFrame(frame);
222
+ canvas.remove();
223
+ },
224
+ };
225
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * A living mycelium field: hyphae grow slowly across a host element and coral
3
+ * nodes pulse when something finishes. Rendered with Pixi 8, which the host
4
+ * loads lazily and passes in, so Sprite itself never bundles Pixi.
5
+ */
6
+ /** The slice of Pixi 8's Graphics this field draws with. */
7
+ export type PixiGraphics = {
8
+ moveTo(x: number, y: number): PixiGraphics;
9
+ lineTo(x: number, y: number): PixiGraphics;
10
+ stroke(style: {
11
+ width: number;
12
+ color: string;
13
+ alpha: number;
14
+ cap: "round";
15
+ }): PixiGraphics;
16
+ circle(x: number, y: number, radius: number): PixiGraphics;
17
+ fill(style: {
18
+ color: string;
19
+ }): PixiGraphics;
20
+ clear(): PixiGraphics;
21
+ position: {
22
+ set(x: number, y: number): void;
23
+ };
24
+ scale: {
25
+ set(value: number): void;
26
+ };
27
+ alpha: number;
28
+ destroy(): void;
29
+ };
30
+ /** A Pixi render texture this field bakes threads into. */
31
+ export type PixiTexture = {
32
+ destroy(destroySource?: boolean): void;
33
+ };
34
+ /** The slice of Pixi 8's Application this field drives. */
35
+ export type PixiApplication<Texture extends PixiTexture = PixiTexture> = {
36
+ init(options: {
37
+ resizeTo: HTMLElement;
38
+ backgroundAlpha: number;
39
+ antialias: boolean;
40
+ autoDensity: boolean;
41
+ resolution: number;
42
+ preference: "webgl";
43
+ }): Promise<void>;
44
+ canvas: HTMLCanvasElement;
45
+ screen: {
46
+ width: number;
47
+ height: number;
48
+ };
49
+ renderer: {
50
+ resolution: number;
51
+ render(options: {
52
+ container: PixiGraphics;
53
+ target: Texture;
54
+ clear: boolean;
55
+ }): void;
56
+ };
57
+ stage: {
58
+ addChild(...children: unknown[]): void;
59
+ };
60
+ ticker: {
61
+ deltaMS: number;
62
+ add(listener: () => void): void;
63
+ start(): void;
64
+ stop(): void;
65
+ };
66
+ render(): void;
67
+ destroy(rendererOptions: {
68
+ removeView: boolean;
69
+ }, options: {
70
+ children: boolean;
71
+ }): void;
72
+ };
73
+ /** The Pixi 8 module surface this field needs; pass `await import("pixi.js")`. */
74
+ export type PixiModule<Texture extends PixiTexture = PixiTexture> = {
75
+ Application: new () => PixiApplication<Texture>;
76
+ Container: new () => {
77
+ addChild(...children: unknown[]): void;
78
+ };
79
+ Graphics: new () => PixiGraphics;
80
+ Sprite: new (texture: Texture) => {
81
+ texture: Texture;
82
+ };
83
+ RenderTexture: {
84
+ create(options: {
85
+ width: number;
86
+ height: number;
87
+ resolution: number;
88
+ }): Texture;
89
+ };
90
+ };
91
+ /** Colors as CSS color strings; the caller reads them from its theme. */
92
+ export type MyceliumPalette = Readonly<{
93
+ thread: string;
94
+ node: string;
95
+ }>;
96
+ /** Options accepted by mountMycelium. */
97
+ export type MyceliumOptions<Texture extends PixiTexture = PixiTexture> = Readonly<{
98
+ /** The Pixi 8 module, loaded by the host. */
99
+ pixi: PixiModule<Texture>;
100
+ palette: MyceliumPalette;
101
+ /** Seed for a repeatable network. */
102
+ seed?: number;
103
+ /** Segment budget before growth rests. Defaults to 2600. */
104
+ segments?: number;
105
+ /** Thread opacity, 0 to 1. Defaults to 0.22 so text above stays readable. */
106
+ threadAlpha?: number;
107
+ /** Mean seconds between automatic node pulses. Defaults to 2.4. */
108
+ pulseEvery?: number;
109
+ /** Starting thread width in CSS pixels. Defaults to 1.6. */
110
+ threadWidth?: number;
111
+ /**
112
+ * Rectangles, in host coordinates, that threads never enter (for example
113
+ * a headline's line boxes). Read again on every restart.
114
+ */
115
+ avoid?: () => readonly MyceliumRect[];
116
+ /**
117
+ * A focal rectangle, in host coordinates. Growth starts along its edges so
118
+ * the network is densest around it. Defaults to the host's bottom and sides.
119
+ */
120
+ focus?: () => MyceliumRect | null;
121
+ }>;
122
+ /** A rectangle in the host's coordinate space. */
123
+ export type MyceliumRect = Readonly<{
124
+ left: number;
125
+ top: number;
126
+ right: number;
127
+ bottom: number;
128
+ }>;
129
+ /** A mounted field. `pulse` lights one node; `destroy` removes everything. */
130
+ export type MyceliumField = Readonly<{
131
+ pulse: () => void;
132
+ destroy: () => void;
133
+ }>;
134
+ /**
135
+ * Mount a mycelium field that fills `host`. Reduced motion renders one
136
+ * composed, fully grown frame and never ticks. The ticker pauses while the host
137
+ * is off screen or the document is hidden.
138
+ */
139
+ export declare function mountMycelium<Texture extends PixiTexture>(host: HTMLElement, options: MyceliumOptions<Texture>): Promise<MyceliumField>;