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