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