Skip to content

packages/core/src/features/textures/lib/definitions.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 type { TextureRecipe } from '../../../lib/textures/texture.js';
2 import { rasterizeField, releaseField } from "./wasm/memory.js";
3 import { defineTexture, type TextureContext } from "@pibbl/core";
4 import {
5   marbleField,
6   paperField,
7   cellularField,
8   type MaterialGenerator,
9 } from "./material-field.js";
10 import { InkSimulation } from "./simulation.js";
11 import { paletteLut, texturePalettes, validateSeed } from "./palette.js";
12 export { useTexture } from "@pibbl/core";
13 /**
14  * Seed, feature scale, and detail controls shared by static material recipes.
15  *
16  * @see {@link marble}
17  * @see {@link paper}
18  * @see {@link cellular}
19  */
20 export interface MaterialOptions {
21   /**
22    * Seed used for deterministic sampling or simulation initialization. See {@link MaterialOptions}
23    * .
24    */
25   readonly seed?: number;
26   /** Feature frequency across the generated material. See {@link MaterialOptions}. */
27   readonly scale?: number;
28   /** Material-specific distortion, grain strength, or cellular jitter. See {@link MaterialOptions}. */
29   readonly detail?: number;
30 }
31 /**
32  * Seed and pattern preset for a reaction-diffusion recipe.
33  *
34  * @see {@link reactionDiffusion}
35  */
36 export interface ReactionDiffusionOptions {
37   /**
38    * Seed used for deterministic sampling or simulation initialization. See
39    * {@link ReactionDiffusionOptions}.
40    */
41   readonly seed?: number;
42   /**
43    * Named pattern configuration used to initialize the simulation. See
44    * {@link ReactionDiffusionOptions}.
45    */
46   readonly preset?: "spots" | "stripes";
47 }
48 /**
49  * Paint, stir, pause, or reset commands accepted by a reaction-diffusion texture.
50  *
51  * @see {@link reactionDiffusion}
52  */
53 export type InkMessage =
54   | {
55       /** The literal "paint" identifying this variant. See {@link InkMessage}. */
56       readonly type: "paint";
57       /** Normalized horizontal texture coordinate. See {@link InkMessage}. */
58       readonly u: number;
59       /** Normalized vertical texture coordinate. See {@link InkMessage}. */
60       readonly v: number;
61       /** Radius in the coordinate system of this geometry or effect. See {@link InkMessage}. */
62       readonly radius?: number;
63       /** Magnitude of the displacement or brush effect. See {@link InkMessage}. */
64       readonly strength?: number;
65     }
66   | {
67       /** The literal "stir" identifying this variant. See {@link InkMessage}. */
68       readonly type: "stir";
69       /** Normalized horizontal texture coordinate. See {@link InkMessage}. */
70       readonly u: number;
71       /** Normalized vertical texture coordinate. See {@link InkMessage}. */
72       readonly v: number;
73       /** Normalized displacement of the existing field, not a paint dose. */
74       readonly du: number;
75       /** Normalized vertical displacement of the texture field. See {@link InkMessage}. */
76       readonly dv: number;
77       /** Influence radius in texture cells. */
78       readonly radius?: number;
79     }
80   | {
81       /** The literal "pause" identifying this variant. See {@link InkMessage}. */
82       readonly type: "pause";
83       /** Whether simulation or playback advancement is suspended. See {@link InkMessage}. */
84       readonly paused: boolean;
85     }
86   | {
87       /** The literal "reset" identifying this variant. See {@link InkMessage}. */
88       readonly type: "reset";
89     };
90 function colorize(field: Float32Array, context: TextureContext, gain = 1) {
91   const ctx = context.canvas.getContext("2d")!;
92   const lut = paletteLut(context.palette ?? texturePalettes.porcelain);
93   const raster = rasterizeField(field, lut, gain);
94   if (!raster) throw new Error("Texture field has no WASM owner.");
95   ctx.putImageData(raster.image, 0, 0);
96 }
97 function materialConfig(config: MaterialOptions) {
98   const seed = validateSeed(config.seed),
99     scale = config.scale ?? 6,
100     detail = config.detail ?? 0.65;
101   if (
102     !Number.isFinite(scale) ||
103     scale < 1 ||
104     scale > 32 ||
105     !Number.isFinite(detail) ||
106     detail < 0 ||
107     detail > 1
108   )
109     throw new RangeError("Material scale must be 1..32 and detail 0..1.");
110   return { seed, scale, detail };
111 }
112 function material(generate: MaterialGenerator) {
113   const recipe = defineTexture<
114     MaterialOptions,
115     { config: ReturnType<typeof materialConfig>; field: Float32Array }
116   >({
117     create(context, input) {
118       const config = materialConfig(input);
119       return {
120         config,
121         field: generate(
122           context.canvas.width,
123           context.canvas.height,
124           config.seed,
125           config.scale,
126           config.detail,
127         ),
128       };
129     },
130     update(state, input, context) {
131       const config = materialConfig(input);
132       if (JSON.stringify(config) === JSON.stringify(state.config)) return;
133       state.field = generate(
134         context.canvas.width,
135         context.canvas.height,
136         config.seed,
137         config.scale,
138         config.detail,
139         state.field,
140       );
141       state.config = config;
142       context.invalidate();
143     },
144     rasterize(state, context) {
145       colorize(state.field, context);
146     },
147     dispose(state) {
148       releaseField(state.field);
149       state.field = new Float32Array(0);
150     },
151   });
152   return (options: MaterialOptions = {}) => recipe(options);
153 }
154 /**
155  * Creates a seeded marble texture recipe with configurable feature scale and distortion.
156  *
157  * @param options - Seed, feature scale, and detail settings. See {@link MaterialOptions}.
158  * @returns A procedural material recipe for useTexture. See {@link TextureRecipe}.
159  *
160  * @see {@link MaterialOptions}
161  * @see {@link TextureRecipe}
162  */
163 export const marble = /* @__PURE__ */ material(marbleField);
164 /**
165  * Creates a seeded paper texture recipe with configurable feature scale and grain.
166  *
167  * @param options - Seed, feature scale, and detail settings. See {@link MaterialOptions}.
168  * @returns A procedural material recipe for useTexture. See {@link TextureRecipe}.
169  *
170  * @see {@link MaterialOptions}
171  * @see {@link TextureRecipe}
172  */
173 export const paper = /* @__PURE__ */ material(paperField);
174 /**
175  * Creates a seeded cellular texture recipe with configurable feature scale and jitter.
176  *
177  * @param options - Seed, feature scale, and detail settings. See {@link MaterialOptions}.
178  * @returns A procedural material recipe for useTexture. See {@link TextureRecipe}.
179  *
180  * @see {@link MaterialOptions}
181  * @see {@link TextureRecipe}
182  */
183 export const cellular = /* @__PURE__ */ material(cellularField);
184 function inkConfig(input: ReactionDiffusionOptions) {
185   const seed = validateSeed(input.seed),
186     preset = input.preset ?? "spots";
187   if (preset !== "spots" && preset !== "stripes")
188     throw new TypeError("Unknown ink preset.");
189   return { seed, preset };
190 }
191 interface InkState {
192   simulation: InkSimulation;
193   config: ReturnType<typeof inkConfig>;
194   paused: boolean;
195   elapsed: number;
196   fresh: boolean;
197 }
198 function reset(state: InkState, context: TextureContext) {
199   const next = new InkSimulation(
200     context.canvas.width,
201     context.canvas.height,
202     state.config.seed,
203     state.config.preset,
204   );
205   state.simulation.dispose();
206   state.simulation = next;
207   state.elapsed = 0;
208   state.fresh = true;
209   context.invalidate();
210 }
211 const inkRecipe = /* @__PURE__ */ defineTexture<
212   ReactionDiffusionOptions,
213   InkState,
214   InkMessage
215 >({
216   defaultPlacement: { mode: "stretch" },
217   create(context, input) {
218     const config = inkConfig(input);
219     context.requestFrame();
220     return {
221       simulation: new InkSimulation(
222         context.canvas.width,
223         context.canvas.height,
224         config.seed,
225         config.preset,
226       ),
227       config,
228       paused: false,
229       elapsed: 0,
230       fresh: true,
231     };
232   },
233   update(state, input, context) {
234     const config = inkConfig(input);
235     if (
236       config.seed === state.config.seed &&
237       config.preset === state.config.preset
238     )
239       return;
240     state.config = config;
241     reset(state, context);
242   },
243   receive(state, message, context) {
244     if (message.type === "pause") {
245       state.paused = message.paused;
246       state.elapsed = 0;
247       state.fresh = true;
248       if (!state.paused) context.requestFrame();
249     } else if (message.type === "reset") reset(state, context);
250     else if (message.type === "stir") {
251       const radius = message.radius ?? 16;
252       if (
253         ![message.u, message.v, message.du, message.dv, radius].every(
254           Number.isFinite,
255         ) ||
256         radius <= 0 ||
257         radius > 128 ||
258         Math.abs(message.du) > 1 ||
259         Math.abs(message.dv) > 1
260       )
261         throw new RangeError("Invalid ink stir message.");
262       if (message.u < 0 || message.u > 1 || message.v < 0 || message.v > 1)
263         return;
264       state.simulation.stir(
265         message.u * context.canvas.width,
266         message.v * context.canvas.height,
267         message.du * context.canvas.width,
268         message.dv * context.canvas.height,
269         radius,
270       );
271       context.invalidate();
272     } else if (message.type === "paint") {
273       const radius = message.radius ?? 6,
274         strength = message.strength ?? 0.8;
275       if (
276         ![message.u, message.v, radius, strength].every(Number.isFinite) ||
277         radius <= 0 ||
278         radius > 128 ||
279         strength < 0 ||
280         strength > 1
281       )
282         throw new RangeError("Invalid ink paint message.");
283       if (message.u < 0 || message.v < 0 || message.u > 1 || message.v > 1)
284         return;
285       state.simulation.paint(
286         message.u * context.canvas.width,
287         message.v * context.canvas.height,
288         radius,
289         strength,
290       );
291       context.invalidate();
292     } else throw new TypeError("Unknown ink message.");
293   },
294   advance(state, frame, context) {
295     if (state.paused) return;
296     if (!state.fresh) {
297       state.elapsed += Math.min(50, frame.delta);
298       const steps = Math.min(24, Math.floor(state.elapsed * 0.48 + 1e-9));
299       state.elapsed -= steps / 0.48;
300       if (steps) {
301         state.simulation.advance(steps);
302         context.invalidate();
303       }
304     }
305     state.fresh = false;
306     context.requestFrame();
307   },
308   rasterize(state, context) {
309     colorize(state.simulation.b, context, 650 / 255);
310   },
311   dispose(state) {
312     state.simulation.dispose();
313   },
314 });
315 /**
316  * Creates a stateful reaction-diffusion texture recipe supporting paint, stirring, pause, and
317  * reset
318  * messages.
319  *
320  * @param options - Reaction and diffusion parameters, initialization, and rendering options. See
321  * {@link ReactionDiffusionOptions} .
322  * @returns A reaction-diffusion texture recipe for useTexture.
323  *
324  * @see {@link ReactionDiffusionOptions}
325  */
326 export function reactionDiffusion(options: ReactionDiffusionOptions = {}) {
327   return inkRecipe(options);
328 }
329 

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