Skip to content

packages/core/src/features/textures/lib/procedural-texture.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 { releaseField } from "./wasm/memory.js";
2 import { defineRenderLayer } from "@pibbl/core";
3 import {
4   paletteLut,
5   texturePalettes,
6   validateResolution,
7   validateSeed,
8   type TexturePalette,
9   type TextureResolution,
10 } from "./palette.js";
11 import { TextureRaster, type TextureRenderer } from "./presentation.js";
12 import { materialField, type TextureKind } from "./material-field.js";
13 
14 /**
15  * Authored inputs for ProceduralTexture, including the declared data and presentation options.
16  *
17  * @see {@link TextureKind}
18  * @see {@link TextureResolution}
19  * @see {@link TexturePalette}
20  * @see {@link TextureRenderer}
21  * @see {@link ProceduralTexture}
22  */
23 export interface ProceduralTextureProps {
24   /** Selects the kind of geometry, material, or result. See {@link TextureKind}. */
25   readonly kind: TextureKind;
26   /**
27    * Seed used for deterministic sampling or simulation initialization. See
28    * {@link ProceduralTextureProps}.
29    */
30   readonly seed?: number;
31   /** Dimensions of the simulation or raster grid. See {@link TextureResolution}. */
32   readonly resolution?: TextureResolution;
33   /** Feature frequency across the texture, from 1 to 32. */
34   readonly scale?: number;
35   /** Distortion, grain strength, or cellular jitter, from 0 to 1. */
36   readonly detail?: number;
37   /** Ordered colors used to present texture intensity. See {@link TexturePalette}. */
38   readonly palette?: TexturePalette;
39   /**
40    * Renders the current data or resources using the supplied context. See {@link TextureRenderer}
41    * .
42    */
43   readonly render?: TextureRenderer;
44 }
45 function normalize(p: ProceduralTextureProps) {
46   if (!["marble", "paper", "cellular"].includes(p.kind))
47     throw new TypeError("Unknown procedural texture kind.");
48   const scale = p.scale ?? 6,
49     detail = p.detail ?? 0.65;
50   if (
51     !Number.isFinite(scale) ||
52     scale < 1 ||
53     scale > 32 ||
54     !Number.isFinite(detail) ||
55     detail < 0 ||
56     detail > 1
57   )
58     throw new RangeError(
59       "Texture scale must be in [1,32] and detail in [0,1].",
60     );
61   return {
62     kind: p.kind,
63     seed: validateSeed(p.seed),
64     resolution: validateResolution(p.resolution),
65     scale,
66     detail,
67     palette: [...(p.palette ?? texturePalettes.porcelain)],
68     render: p.render,
69   };
70 }
71 type Options = ReturnType<typeof normalize>;
72 interface Resources {
73   options: Options;
74   field: Float32Array;
75   raster: TextureRaster;
76   lut: Uint8ClampedArray;
77   output: OffscreenCanvasRenderingContext2D;
78   dirty: boolean;
79 }
80 function generate(o: Options, previous?: Float32Array) {
81   return materialField(
82     o.resolution.width,
83     o.resolution.height,
84     o.kind,
85     o.seed,
86     o.scale,
87     o.detail,
88     previous,
89   );
90 }
91 
92 /**
93  * A seeded static material. It requests no continuing animation frames.
94  *
95  * @param props - Authored component inputs, supplied through JSX or createElement. See the linked
96  * props and style types.
97  * @throws When called directly; Pibbl mounts this component through JSX or createElement.
98  *
99  * @see {@link ProceduralTextureProps}
100  */
101 export const ProceduralTexture = /* @__PURE__ */ defineRenderLayer<
102   ProceduralTextureProps,
103   Resources
104 >({
105   create(context, props) {
106     const options = normalize(props),
107       lut = paletteLut(options.palette);
108     const output = context.canvas.getContext("2d");
109     if (!output) throw new Error("ProceduralTexture requires Canvas 2D.");
110     const raster = new TextureRaster(
111       options.resolution.width,
112       options.resolution.height,
113     );
114     try {
115       return {
116         options,
117         lut,
118         output,
119         raster,
120         field: generate(options),
121         dirty: true,
122       };
123     } catch (error) {
124       raster.dispose();
125       throw error;
126     }
127   },
128   update(r, props) {
129     const next = normalize(props),
130       old = r.options;
131     if (next.palette.join() !== old.palette.join()) {
132       r.lut = paletteLut(next.palette);
133       r.dirty = true;
134     }
135     const resized =
136       next.resolution.width !== old.resolution.width ||
137       next.resolution.height !== old.resolution.height;
138     if (
139       resized ||
140       next.kind !== old.kind ||
141       next.seed !== old.seed ||
142       next.scale !== old.scale ||
143       next.detail !== old.detail
144     ) {
145       const field = generate(next, r.field);
146       if (resized) {
147         const raster = new TextureRaster(
148           next.resolution.width,
149           next.resolution.height,
150         );
151         r.raster.dispose();
152         r.raster = raster;
153       }
154       if (field !== r.field) releaseField(r.field);
155       r.field = field;
156       r.dirty = true;
157     }
158     r.options = next;
159   },
160   render(r, _frame, context) {
161     if (r.dirty) {
162       r.raster.update(r.field, r.lut);
163       r.dirty = false;
164     }
165     r.raster.draw(
166       r.output,
167       context.size.width,
168       context.size.height,
169       context.size.backingWidth,
170       context.size.backingHeight,
171       r.options.render,
172     );
173   },
174   dispose(r) {
175     r.raster.dispose();
176     releaseField(r.field);
177     r.field = new Float32Array(0);
178     r.options.render = undefined;
179   },
180 });
181 

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