Skip to content

packages/core/src/features/textures/lib/lightning.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import { ElectricMemory, type ElectricConnection } from "./wasm/electric.js";
2 import { lazyKernel } from "./wasm/memory.js";
3 import { bytes } from "./wasm/lightning-bytes.js";
4 const wasmModule = /* @__PURE__ */ lazyKernel(bytes);
5 import { defineTexture } from "@pibbl/core";
6 import { validateSeed } from "./palette.js";
7 import {
8   electricOptions,
9   point,
10   range,
11   type ElectricControl,
12   type ElectricOptions,
13   type ElectricPoint,
14 } from "./electric-shared.js";
15 
16 /**
17  * Electrical appearance, electrode positions, topology frequency, and base-connection controls.
18  *
19  * @see {@link ElectricOptions}
20  * @see {@link ElectricPoint}
21  * @see {@link lightning}
22  * @see {@link LightningMessage}
23  */
24 export interface LightningOptions extends ElectricOptions {
25   /** Position of the positive electrode. See {@link ElectricPoint}. */
26   readonly positive?: ElectricPoint;
27   /** Position of the negative electrode. See {@link ElectricPoint}. */
28   readonly negative?: ElectricPoint;
29   /** Boundary edge used as the electrical connection's origin. See {@link LightningOptions}. */
30   readonly edge?: "top" | "right" | "bottom" | "left";
31   /** Enables the configured base connection; manual connections remain independent. */
32   readonly active?: boolean;
33   /** Topology refreshes per second, 1..30. Connections survive several refreshes. */
34   readonly frequency?: number;
35   /** Whether to draw the electrode contact core. See {@link LightningOptions}. */
36   readonly contactCore?: boolean;
37 }
38 /**
39  * Control, connection management, and live configuration commands for a lightning texture.
40  *
41  * @see {@link ElectricControl}
42  * @see {@link ElectricPoint}
43  * @see {@link LightningOptions}
44  */
45 export type LightningMessage =
46   | ElectricControl
47   | {
48       /** The literal "connect" identifying this variant. See {@link LightningMessage}. */
49       readonly type: "connect";
50       /** Stable identifier of this resource or connection. See {@link LightningMessage}. */
51       readonly id: string;
52       /** Position of the positive electrode. See {@link ElectricPoint}. */
53       readonly positive: ElectricPoint;
54       /** Position of the negative electrode. See {@link ElectricPoint}. */
55       readonly negative: ElectricPoint;
56     }
57   | {
58       /** The literal "disconnect" identifying this variant. See {@link LightningMessage}. */
59       readonly type: "disconnect";
60       /** Stable identifier of this resource or connection. See {@link LightningMessage}. */
61       readonly id: string;
62     }
63   | {
64       /** The literal "configure" identifying this variant. See {@link LightningMessage}. */
65       readonly type: "configure";
66       /** Position of the positive electrode. See {@link ElectricPoint}. */
67       readonly positive?: ElectricPoint;
68       /** Position of the negative electrode. See {@link ElectricPoint}. */
69       readonly negative?: ElectricPoint;
70       /** Boundary edge used as the electrical connection's origin. See {@link LightningOptions}. */
71       readonly edge?: LightningOptions["edge"];
72       /** Current active operation or its presentation configuration. See {@link LightningMessage}. */
73       readonly active?: boolean;
74       /** Amount of secondary electrical branching. See {@link LightningMessage}. */
75       readonly branching?: number;
76       /** Intensity of the electrical halo around the core. See {@link LightningMessage}. */
77       readonly glow?: number;
78       /** Rate at which the pattern or motion repeats. See {@link LightningMessage}. */
79       readonly frequency?: number;
80     };
81 function options(o: LightningOptions) {
82   if (
83     o.edge !== undefined &&
84     !["top", "right", "bottom", "left"].includes(o.edge)
85   )
86     throw new TypeError("Unknown lightning edge.");
87   return {
88     ...electricOptions(o),
89     positive: point(o.positive ?? { x: 0.2, y: 0.5 }),
90     negative: point(o.negative ?? { x: 0.8, y: 0.5 }),
91     edge: o.edge,
92     active: o.active ?? true,
93     frequency: range(o.frequency ?? 15, 1, 30, "Frequency"),
94     contactCore: o.contactCore ?? false,
95   };
96 }
97 type Connection = ElectricConnection;
98 interface State {
99   kernel: ElectricMemory;
100   config: ReturnType<typeof options>;
101   inputKey: string;
102   time: number;
103   paused: boolean;
104   connections: Map<string, Connection>;
105 }
106 function salt(id: string) {
107   let n = 0;
108   for (let i = 0; i < id.length; i++)
109     n = (Math.imul(n, 31) + id.charCodeAt(i)) | 0;
110   return n;
111 }
112 const recipe = /* @__PURE__ */ defineTexture<
113   LightningOptions,
114   State,
115   LightningMessage
116 >({
117   defaultPlacement: { mode: "stretch" },
118   create(context, input) {
119     if (input.active !== false) context.requestFrame();
120     const config = options(input);
121     return {
122       kernel: new ElectricMemory(wasmModule()),
123       config,
124       inputKey: JSON.stringify(input),
125       time: 0,
126       paused: false,
127       connections: new Map(),
128     };
129   },
130   update(s, input, context) {
131     const key = JSON.stringify(input);
132     if (key === s.inputKey) return;
133     const next = options(input);
134     if (next.seed !== s.config.seed) {
135       s.time = 0;
136       s.connections.clear();
137       s.kernel.clearConnections();
138     }
139     s.config = next;
140     s.inputKey = key;
141     context.invalidate();
142     if (!s.paused && (next.active || s.connections.size))
143       context.requestFrame();
144   },
145   receive(s, m, context) {
146     switch (m.type) {
147       case "pause":
148         s.paused = m.paused;
149         break;
150       case "reset":
151         s.config.seed =
152           m.seed === undefined ? s.config.seed : validateSeed(m.seed);
153         s.time = 0;
154         s.connections.clear();
155         s.kernel.clearConnections();
156         break;
157       case "configure":
158         s.config = options({ ...s.config, ...m });
159         break;
160       case "connect": {
161         if (typeof m.id !== "string" || m.id.length > 128)
162           throw new RangeError(
163             "Connection id must be a string of at most 128 characters.",
164           );
165         const positive = point(m.positive),
166           negative = point(m.negative),
167           old = s.connections.get(m.id);
168         if (old) {
169           old.positive = positive;
170           old.negative = negative;
171           old.active = true;
172           old.strength = 1;
173         } else if (s.connections.size < 24)
174           s.connections.set(
175             m.id,
176             Object.assign(s.kernel.connection(), {
177               positive,
178               negative,
179               strength: 1,
180               active: true,
181               salt: salt(m.id),
182             }),
183           );
184         break;
185       }
186       case "disconnect": {
187         const old = s.connections.get(m.id);
188         if (old) old.active = false;
189         break;
190       }
191       default:
192         throw new TypeError("Unknown lightning message.");
193     }
194     context.invalidate();
195     if (!s.paused && (s.config.active || s.connections.size))
196       context.requestFrame();
197   },
198   advance(s, frame, context) {
199     if (s.paused) return;
200     const dt = Math.min(50, frame.delta) / 1000;
201     s.time += dt;
202     s.kernel.advanceLightning(dt);
203     for (const [id, c] of s.connections) {
204       if (c.strength < 0.005) {
205         c.release();
206         s.connections.delete(id);
207       }
208     }
209     context.invalidate();
210     if (s.config.active || s.connections.size) context.requestFrame();
211   },
212   rasterize(s, context) {
213     s.kernel.lightning(s.config, s.time, s.connections.values());
214     s.kernel.draw(context, s.config.glow);
215   },
216   dispose(s) {
217     s.connections.clear();
218     s.kernel.dispose();
219   },
220 });
221 /**
222  * Persistent branching arcs between artistic positive/negative points.
223  *
224  * @param config - Lightning geometry, timing, and appearance settings. See
225  * {@link LightningOptions} .
226  * @returns A lightning texture recipe for useTexture.
227  *
228  * @see {@link LightningOptions}
229  */
230 export function lightning(config: LightningOptions = {}) {
231   return recipe(config);
232 }
233 

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.