packages/core/src/features/textures/lib/lightning.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.