trid3d 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,257 @@
1
+ import { localToWorld, rotateXYZ } from "../math/transform.js";
2
+ import { transformPoint, normalMatrix, transformNormal, determinantMat4 } from "../math/matrix.js";
3
+ import { forEachPlaced, worldMatrix } from "../sceneGraph.js";
4
+
5
+ /** Deep copy of one mesh field: arrays and plain objects are copied, the rest kept. */
6
+ const copy = (x) => (Array.isArray(x) ? x.map(copy) : x && typeof x === "object" ? { ...x } : x);
7
+
8
+ /** A transform with every part filled in. */
9
+ const fullTransform = ({ pos, rot, scale } = {}) => ({
10
+ pos: { x: 0, y: 0, z: 0, ...pos },
11
+ rot: { x: 0, y: 0, z: 0, ...rot },
12
+ scale: { x: 1, y: 1, z: 1, ...scale },
13
+ });
14
+
15
+ /** A copy of a mesh with a world matrix baked in (normals turned, faces flipped for mirrors). */
16
+ function bake(mesh, world) {
17
+ const out = cloneMesh(mesh);
18
+ for (const v of out.vertices) Object.assign(v, transformPoint(world, v));
19
+ if (out.normals) {
20
+ const nm = normalMatrix(world);
21
+ out.normals = out.normals.map((n) => transformNormal(nm, n));
22
+ }
23
+ if (determinantMat4(world) < 0 && out.faces) {
24
+ const flip = (list) => list?.map((f) => (f ? [...f].reverse() : f));
25
+ out.faces = flip(out.faces);
26
+ if (out.faceUVs) out.faceUVs = flip(out.faceUVs);
27
+ if (out.faceNormals) out.faceNormals = flip(out.faceNormals);
28
+ }
29
+ return out;
30
+ }
31
+
32
+ /**
33
+ * Deep copy of a mesh: vertices, edges, faces, texture coordinates, normals
34
+ * and materials. Change the copy without touching the original (or the other
35
+ * objects that share it).
36
+ *
37
+ * @template M
38
+ * @param {M} mesh
39
+ * @returns {M} A new mesh
40
+ */
41
+ export function cloneMesh(mesh) {
42
+ const out = {};
43
+ for (const k of Object.keys(mesh)) if (k !== "dirty") out[k] = copy(mesh[k]);
44
+ return out;
45
+ }
46
+
47
+ /**
48
+ * Bakes a transform into a mesh's vertices, in place: scale, then rotate
49
+ * (X, then Y, then Z), then move, the same order objects use. Its own normals
50
+ * turn with it, and a mirroring scale (an odd number of negative factors)
51
+ * reverses the faces so they still face outward.
52
+ *
53
+ * Pass a scene object to bake its `pos`, `rot` and `scale` (then reset them
54
+ * on the object, or it's transformed twice).
55
+ *
56
+ * @example
57
+ * // A handle standing on the side of a mug
58
+ * transformMesh(handle, { pos: { x: 0.3, y: 0, z: 0 }, rot: { x: 0, y: 0, z: Math.PI / 2 } });
59
+ *
60
+ * @template M
61
+ * @param {M} mesh - Mesh to change
62
+ * @param {{ pos?:{x:number,y:number,z:number}, rot?:{x:number,y:number,z:number}, scale?:{x:number,y:number,z:number} }} transform
63
+ * Parts left out don't change anything
64
+ * @returns {M} The same mesh
65
+ */
66
+ export function transformMesh(mesh, transform) {
67
+ const t = fullTransform(transform);
68
+ for (const v of mesh.vertices) Object.assign(v, localToWorld(v, t));
69
+
70
+ // Normals: divide by the scale (keeps them perpendicular to stretched faces), turn, then normalize
71
+ if (mesh.normals) {
72
+ const s = t.scale;
73
+ mesh.normals = mesh.normals.map((n) => {
74
+ const r = rotateXYZ({ x: n.x / (s.x || 1), y: n.y / (s.y || 1), z: n.z / (s.z || 1) }, t.rot);
75
+ const len = Math.hypot(r.x, r.y, r.z) || 1;
76
+ return { x: r.x / len, y: r.y / len, z: r.z / len };
77
+ });
78
+ }
79
+
80
+ // A mirror turns clockwise faces counter-clockwise: reverse them (and their corner lists)
81
+ if (t.scale.x * t.scale.y * t.scale.z < 0 && mesh.faces) {
82
+ const flip = (list) => list?.map((f) => (f ? [...f].reverse() : f));
83
+ mesh.faces = flip(mesh.faces);
84
+ if (mesh.faceUVs) mesh.faceUVs = flip(mesh.faceUVs);
85
+ if (mesh.faceNormals) mesh.faceNormals = flip(mesh.faceNormals);
86
+ }
87
+
88
+ mesh.dirty = true;
89
+ return mesh;
90
+ }
91
+
92
+ /**
93
+ * Moves every vertex to `fn(vertex, index)`, in place: noise on a rock,
94
+ * waves on a plane, a bend or a twist.
95
+ *
96
+ * The mesh's own normals (from an OBJ file) no longer fit the new shape, so
97
+ * they're removed and the renderers compute new ones.
98
+ *
99
+ * @example
100
+ * // A lumpy rock
101
+ * displaceMesh(makeShape("sphere"), (v) => {
102
+ * const k = 1 + 0.3 * (fbm3(v.x * 3, v.y * 3, v.z * 3) - 0.5);
103
+ * return { x: v.x * k, y: v.y * k, z: v.z * k };
104
+ * });
105
+ *
106
+ * @template M
107
+ * @param {M} mesh - Mesh to change
108
+ * @param {(v:{x:number,y:number,z:number}, index:number) => {x:number,y:number,z:number}} fn - New position of a vertex
109
+ * @returns {M} The same mesh
110
+ */
111
+ export function displaceMesh(mesh, fn) {
112
+ mesh.vertices.forEach((v, i) => {
113
+ const p = fn({ x: v.x, y: v.y, z: v.z }, i);
114
+ v.x = p.x;
115
+ v.y = p.y;
116
+ v.z = p.z;
117
+ });
118
+ delete mesh.normals;
119
+ delete mesh.faceNormals;
120
+ mesh.dirty = true;
121
+ return mesh;
122
+ }
123
+
124
+ /**
125
+ * Joins meshes into one: one object to draw instead of many, or one mesh to
126
+ * export. Faces, edges, texture coordinates, normals and materials are kept,
127
+ * with their indices shifted.
128
+ *
129
+ * An object (`{ mesh, pos, rot, scale }`, added to a scene or not) in the
130
+ * list is merged where it stands, with its transform baked in (its mesh
131
+ * isn't changed). A group is merged with all its children, placed by the
132
+ * group's own transform (not its parents').
133
+ *
134
+ * With `materialPerMesh`, the faces of the i-th mesh use material i, so an
135
+ * object with `material: [blade, guard, grip]` paints each part its own way.
136
+ * Otherwise each mesh's `faceMaterials` are kept; material names (from OBJ
137
+ * files) are joined into one list when every mesh with materials has them.
138
+ *
139
+ * The meshes are copied, never changed.
140
+ *
141
+ * @example
142
+ * const sword = mergeMeshes([blade, guard, grip], { materialPerMesh: true });
143
+ * scene.add({ mesh: sword, material: [{ fill: "#ccd" }, { fill: "#c90" }, { fill: "#642" }] });
144
+ *
145
+ * @param {Object[]} items - Meshes, or scene objects with a `mesh`
146
+ * @param {Object} [opts]
147
+ * @param {boolean} [opts.materialPerMesh=false] - Faces of the i-th mesh use material i
148
+ * @returns {import("../index.d.ts").Mesh} A new mesh
149
+ */
150
+ export function mergeMeshes(items, { materialPerMesh = false } = {}) {
151
+ // Meshes as they are; objects and groups baked where they stand, one entry per item
152
+ const meshes = items.map((it) => {
153
+ if (it.vertices) return it;
154
+ const parts = [];
155
+ forEachPlaced([it], () => null, (obj, world) => {
156
+ if (obj.mesh) parts.push(bake(obj.mesh, world));
157
+ });
158
+ return parts.length === 1 ? parts[0] : mergeMeshes(parts);
159
+ });
160
+ const some = (k) => meshes.some((m) => m[k]);
161
+
162
+ const out = { vertices: [], faces: [] };
163
+ const hasEdges = some("edges");
164
+ const hasUVs = some("faceUVs") && some("uvs");
165
+ const hasNormals = some("faceNormals") && some("normals");
166
+ const hasMats = materialPerMesh || some("faceMaterials");
167
+ const withMats = meshes.filter((m) => m.faceMaterials);
168
+ const names = !materialPerMesh && withMats.length > 0 && withMats.every((m) => m.materialNames) ? [] : null;
169
+ if (hasEdges) out.edges = [];
170
+ if (hasUVs) Object.assign(out, { uvs: [], faceUVs: [] });
171
+ if (hasNormals) Object.assign(out, { normals: [], faceNormals: [] });
172
+ if (hasMats) out.faceMaterials = [];
173
+
174
+ meshes.forEach((m, mi) => {
175
+ const vo = out.vertices.length, uo = out.uvs?.length ?? 0, no = out.normals?.length ?? 0;
176
+ const shift = (list, o) => list.map((i) => i + o);
177
+ out.vertices.push(...m.vertices.map((v) => ({ x: v.x, y: v.y, z: v.z })));
178
+ // A mesh without edges keeps showing its face outlines next to meshes that have some
179
+ if (hasEdges) out.edges.push(...(m.edges ?? m.faces ?? []).map((e) => shift(e, vo)));
180
+ if (hasUVs) out.uvs.push(...(m.uvs ?? []).map((t) => ({ u: t.u, v: t.v })));
181
+ if (hasNormals) out.normals.push(...(m.normals ?? []).map((n) => ({ x: n.x, y: n.y, z: n.z })));
182
+
183
+ // Material names: this mesh's index k → the index of its name in the joined list
184
+ const remap = names && m.materialNames
185
+ ? m.materialNames.map((n) => (names.includes(n) ? names.indexOf(n) : names.push(n) - 1))
186
+ : null;
187
+
188
+ (m.faces ?? []).forEach((f, fi) => {
189
+ out.faces.push(shift(f, vo));
190
+ if (hasUVs) {
191
+ const fu = m.uvs && m.faceUVs?.[fi];
192
+ out.faceUVs.push(fu ? shift(fu, uo) : null);
193
+ }
194
+ if (hasNormals) {
195
+ const fn = m.normals && m.faceNormals?.[fi];
196
+ out.faceNormals.push(fn ? shift(fn, no) : null);
197
+ }
198
+ if (hasMats) {
199
+ const k = m.faceMaterials?.[fi] ?? 0;
200
+ out.faceMaterials.push(materialPerMesh ? mi : remap ? (remap[k] ?? 0) : k);
201
+ }
202
+ });
203
+ });
204
+
205
+ if (names) out.materialNames = names;
206
+ return out;
207
+ }
208
+
209
+ /**
210
+ * The box around one or more meshes: `min` and `max` corners, plus its
211
+ * `center` and `size`. Scene objects (and a whole Scene) are measured where
212
+ * they stand, with their transform.
213
+ *
214
+ * @example
215
+ * const { center, size } = meshBounds(scene);
216
+ * orbit.target = center;
217
+ * orbit.distance = Math.max(size.x, size.y, size.z) * 2;
218
+ *
219
+ * @param {Object|Object[]} input - A mesh, a scene object, a list of them, or a Scene
220
+ * @returns {{ min:{x:number,y:number,z:number}, max:{x:number,y:number,z:number},
221
+ * center:{x:number,y:number,z:number}, size:{x:number,y:number,z:number} } | null}
222
+ * null when there are no vertices
223
+ */
224
+ export function meshBounds(input) {
225
+ const items = Array.isArray(input) ? input : Array.isArray(input?.objects) ? input.objects : [input];
226
+ const min = { x: Infinity, y: Infinity, z: Infinity };
227
+ const max = { x: -Infinity, y: -Infinity, z: -Infinity };
228
+ let any = false;
229
+ // Plain meshes as they are; objects (and their children) where they're drawn
230
+ const placed = [];
231
+ const objects = [];
232
+ for (const it of items) {
233
+ if (it?.vertices) placed.push({ mesh: it, world: null });
234
+ else if (it) objects.push(it);
235
+ }
236
+ forEachPlaced(objects, (root) => (root.parent ? worldMatrix(root.parent) : null), (obj, world) => {
237
+ if (obj.mesh?.vertices) placed.push({ mesh: obj.mesh, world });
238
+ });
239
+ for (const { mesh, world } of placed) {
240
+ for (const v of mesh.vertices) {
241
+ const p = world ? transformPoint(world, v) : v;
242
+ any = true;
243
+ if (p.x < min.x) min.x = p.x;
244
+ if (p.y < min.y) min.y = p.y;
245
+ if (p.z < min.z) min.z = p.z;
246
+ if (p.x > max.x) max.x = p.x;
247
+ if (p.y > max.y) max.y = p.y;
248
+ if (p.z > max.z) max.z = p.z;
249
+ }
250
+ }
251
+ if (!any) return null;
252
+ return {
253
+ min, max,
254
+ center: { x: (min.x + max.x) / 2, y: (min.y + max.y) / 2, z: (min.z + max.z) / 2 },
255
+ size: { x: max.x - min.x, y: max.y - min.y, z: max.z - min.z },
256
+ };
257
+ }
@@ -0,0 +1,211 @@
1
+ import { fbm2, noise2 } from "../math/noise.js";
2
+ import { updateTexture } from "../loaders/texture.js";
3
+
4
+ /**
5
+ * Procedural textures: patterns painted in code, from a style, two colors
6
+ * and a seed. Every style tiles with no seam (the noise repeats and every
7
+ * count is a whole number), so they wrap around spheres, lathes and
8
+ * repeated textures cleanly. Like a canvas painted once, they're uploaded
9
+ * once.
10
+ *
11
+ * scene.add({ mesh: makeShape("cube"), material: { texture: makeTexture("wood") } });
12
+ */
13
+
14
+ /** The built-in styles of `makeTexture`. */
15
+ export const TEXTURE_STYLES = ["noise", "marble", "wood", "bricks", "checker", "bark", "spots", "stripes", "solid"];
16
+
17
+ /** Default colors for each style: [first, second]. */
18
+ const DEFAULT_COLORS = {
19
+ noise: ["#3a3f4b", "#c9ced6"],
20
+ marble: ["#e9e6df", "#5d5a55"],
21
+ wood: ["#8a5a33", "#5a3820"],
22
+ bricks: ["#9c4a35", "#cfc8bb"],
23
+ checker: ["#f5f0e6", "#E9806E"],
24
+ bark: ["#4a3426", "#2a1d15"],
25
+ spots: ["#e8c88f", "#5a3d22"],
26
+ stripes: ["#f2efe8", "#4a6fa5"],
27
+ solid: ["#c0c0c0", "#c0c0c0"],
28
+ };
29
+
30
+ const clamp01 = (t) => Math.min(1, Math.max(0, t));
31
+ const smooth = (a, b, t) => {
32
+ const x = clamp01((t - a) / (b - a));
33
+ return x * x * (3 - 2 * x);
34
+ };
35
+ const even = (n) => Math.max(2, Math.round(n / 2) * 2);
36
+ const frac = (x) => x - Math.floor(x);
37
+
38
+ /**
39
+ * For a style, a function of (u, v) in [0, 1) giving `[t, light]`: how much
40
+ * of the second color to use (0..1), and a brightness to multiply by.
41
+ */
42
+ function styleFn(style, s, seed) {
43
+ // Noise with `period` p repeats every p units: sampled at u * (a multiple of p), it tiles
44
+ const f = (x, y, octaves, period, k = 0) => fbm2(x, y, { octaves, period, seed: seed + k });
45
+ switch (style) {
46
+ case "solid":
47
+ return () => [0, 1];
48
+ case "noise":
49
+ return (u, v) => [smooth(0.25, 0.75, f(u * s, v * s, 5, s)), 1];
50
+ case "marble":
51
+ return (u, v) => {
52
+ const turb = f(u * s, v * s, 6, s);
53
+ const t = 0.5 + 0.5 * Math.sin(((u + v) * s + turb * 2.5) * Math.PI * 2);
54
+ return [Math.pow(t, 6), 1];
55
+ };
56
+ case "wood":
57
+ return (u, v) => {
58
+ const warp = f(u * s, v * s, 4, s) * 3;
59
+ const ring = frac(u * s * 2 + warp);
60
+ // Fine grain along the boards (64 and 4 are both multiples of the period 4)
61
+ return [smooth(0.55, 0.95, ring), 0.9 + 0.1 * f(u * 64, v * 4, 2, 4, 7)];
62
+ };
63
+ case "bricks": {
64
+ const rows = even(s * 2), cols = s;
65
+ return (u, v) => {
66
+ const row = Math.floor(v * rows);
67
+ const x = u * cols + (row % 2) * 0.5;
68
+ const fx = frac(x), fy = v * rows - row;
69
+ if (Math.min(fx, 1 - fx) < 0.04 || Math.min(fy, 1 - fy) < 0.08) return [1, 1]; // mortar
70
+ // Each brick its own shade (bricks cut by the edge are the same brick on the other side)
71
+ const brick = ((Math.floor(x) % cols) + cols) % cols;
72
+ const tint = 0.85 + 0.3 * noise2(brick, row, { seed });
73
+ return [0.15 * f(u * 32, v * 32, 3, 32), tint];
74
+ };
75
+ }
76
+ case "checker": {
77
+ const c = even(s);
78
+ return (u, v) => [(Math.floor(u * c) + Math.floor(v * c)) % 2, 1];
79
+ }
80
+ case "bark":
81
+ return (u, v) => {
82
+ const ridges = Math.abs(f(u * s * 3, v * s, 5, s) * 2 - 1);
83
+ return [smooth(0.05, 0.4, ridges), 0.8 + 0.4 * ridges];
84
+ };
85
+ case "spots":
86
+ return (u, v) => [smooth(0.55, 0.62, f(u * s, v * s, 4, s)), 1];
87
+ case "stripes":
88
+ return (u, v) => [smooth(0.45, 0.55, 0.5 + 0.5 * Math.sin((v * s + f(u * 4, v * 4, 3, 4) * 0.4) * Math.PI * 2)), 1];
89
+ default:
90
+ throw new Error(`makeTexture: unknown style "${style}" (one of ${TEXTURE_STYLES.join(", ")}, or a function)`);
91
+ }
92
+ }
93
+
94
+ /** [r, g, b, a] (0..255, a 0..1) from "#rgb", "#rrggbb", "#rrggbbaa", "rgb(...)" / "rgba(...)" or an array. */
95
+ export function colorBytes(color) {
96
+ if (Array.isArray(color)) return [color[0] ?? 0, color[1] ?? 0, color[2] ?? 0, color[3] ?? 1];
97
+ const s = String(color).trim();
98
+ if (s[0] === "#") {
99
+ let h = s.slice(1);
100
+ if (h.length === 3 || h.length === 4) h = h.replace(/./g, "$&$&");
101
+ const v = (i) => parseInt(h.slice(i, i + 2), 16);
102
+ return [v(0), v(2), v(4), h.length === 8 ? v(6) / 255 : 1];
103
+ }
104
+ const nums = s.match(/-?[\d.]+%?/g);
105
+ if (/^rgba?\(/i.test(s) && nums && nums.length >= 3) {
106
+ const ch = (x) => (x.endsWith("%") ? (parseFloat(x) * 255) / 100 : +x);
107
+ const a = nums[3] === undefined ? 1 : nums[3].endsWith("%") ? parseFloat(nums[3]) / 100 : +nums[3];
108
+ return [ch(nums[0]), ch(nums[1]), ch(nums[2]), a];
109
+ }
110
+ // Named CSS colors need the browser to read them
111
+ if (typeof document !== "undefined") {
112
+ const ctx = document.createElement("canvas").getContext("2d");
113
+ if (ctx) {
114
+ ctx.fillStyle = "#000";
115
+ ctx.fillStyle = s;
116
+ if (ctx.fillStyle !== s) return colorBytes(ctx.fillStyle);
117
+ }
118
+ }
119
+ throw new Error(`Can't read the color "${color}"`);
120
+ }
121
+
122
+ /**
123
+ * The pixels of a procedural texture, without a canvas (for workers, or to
124
+ * process them further). Same options as `makeTexture`.
125
+ *
126
+ * @param {string|((u:number, v:number) => number)} style - A style name, or `(u, v) => t`
127
+ * @param {Object} [opts] - See `makeTexture`
128
+ * @returns {{ width:number, height:number, data:Uint8ClampedArray }} RGBA rows, top row first
129
+ */
130
+ export function texturePixels(style = "noise", opts = {}) {
131
+ const {
132
+ size = 256,
133
+ colors = typeof style === "string" ? DEFAULT_COLORS[style] ?? DEFAULT_COLORS.noise : ["#000000", "#ffffff"],
134
+ scale = 4,
135
+ seed = 0,
136
+ grain = 0,
137
+ } = opts;
138
+ const width = Math.max(1, Math.floor(size)), height = width;
139
+ const s = Math.max(1, Math.round(scale));
140
+ const a = colorBytes(colors[0]), b = colorBytes(colors[1] ?? colors[0]);
141
+ const fn = typeof style === "function" ? (u, v) => [clamp01(style(u, v)), 1] : styleFn(style, s, seed);
142
+ const data = new Uint8ClampedArray(width * height * 4);
143
+
144
+ for (let y = 0; y < height; y++) {
145
+ // Image rows go down, texture v goes up
146
+ const v = 1 - (y + 0.5) / height;
147
+ for (let x = 0; x < width; x++) {
148
+ const u = (x + 0.5) / width;
149
+ const [t, light] = fn(u, v);
150
+ const g = grain > 0 ? 1 + (fbm2(u * 128, v * 128, { octaves: 2, period: 128, seed: seed + 99 }) - 0.5) * grain : 1;
151
+ const k = (y * width + x) * 4;
152
+ for (let c = 0; c < 3; c++) data[k + c] = (a[c] + (b[c] - a[c]) * t) * light * g;
153
+ data[k + 3] = (a[3] + (b[3] - a[3]) * t) * 255;
154
+ }
155
+ }
156
+ return { width, height, data };
157
+ }
158
+
159
+ /**
160
+ * Paints a procedural texture on a canvas: wood, marble, bricks... Every
161
+ * style tiles with no seam. Pass the result as a material's `texture`.
162
+ *
163
+ * | Style | Looks like | `scale` is |
164
+ * | --- | --- | --- |
165
+ * | `"noise"` | Soft clouds | Blobs across |
166
+ * | `"marble"` | Veined stone | Veins across |
167
+ * | `"wood"` | Boards with rings and grain (rings run along v) | Rings across, ×2 |
168
+ * | `"bricks"` | A brick wall, each brick its own shade | Bricks across (rows: 2× as many) |
169
+ * | `"checker"` | A checkerboard | Squares across (made even) |
170
+ * | `"bark"` | Rough ridges running along v | Ridges across, ×3 |
171
+ * | `"spots"` | Blotches, like a giraffe | Spots across |
172
+ * | `"stripes"` | Wavy stripes across v | Stripes |
173
+ * | `"solid"` | Just the first color (with `grain`: a rough surface) | — |
174
+ *
175
+ * `style` can also be your own function `(u, v) => t`: 0 gives the first
176
+ * color, 1 the second. It should repeat for u and v in [0, 1) to tile.
177
+ *
178
+ * Repainting into the same canvas (`opts.canvas`) calls `updateTexture` for you.
179
+ *
180
+ * @example
181
+ * const floor = makeTexture("wood", { colors: ["#a87447", "#6b4426"], scale: 3, grain: 0.15 });
182
+ * scene.add({ mesh: makeShape("plane"), material: { texture: floor, textureRepeat: 4 } });
183
+ *
184
+ * @param {string|((u:number, v:number) => number)} [style="noise"] - One of `TEXTURE_STYLES`, or a function
185
+ * @param {Object} [opts]
186
+ * @param {number} [opts.size=256] - Width and height in pixels (a power of two is best for WebGL 1)
187
+ * @param {(string|number[])[]} [opts.colors] - [first, second]: hex, rgb() / rgba(), CSS names or [r, g, b, a?]
188
+ * (default: a pair that suits the style)
189
+ * @param {number} [opts.scale=4] - How many times the pattern repeats across (a whole number)
190
+ * @param {number} [opts.seed=0] - Another whole number gives another texture of the same style
191
+ * @param {number} [opts.grain=0] - Fine speckles on top, 0 to 1
192
+ * @param {HTMLCanvasElement|OffscreenCanvas} [opts.canvas] - Paint into this canvas instead of a new one
193
+ * @returns {HTMLCanvasElement|OffscreenCanvas} The canvas
194
+ */
195
+ export function makeTexture(style = "noise", opts = {}) {
196
+ const { width, height, data } = texturePixels(style, opts);
197
+ let canvas = opts.canvas;
198
+ if (!canvas) {
199
+ if (typeof document !== "undefined") canvas = document.createElement("canvas");
200
+ else if (typeof OffscreenCanvas !== "undefined") canvas = new OffscreenCanvas(width, height);
201
+ else throw new Error("makeTexture needs a browser (or a worker with OffscreenCanvas); use texturePixels instead");
202
+ }
203
+ canvas.width = width;
204
+ canvas.height = height;
205
+ const ctx = canvas.getContext("2d");
206
+ const img = ctx.createImageData(width, height);
207
+ img.data.set(data);
208
+ ctx.putImageData(img, 0, 0);
209
+ if (opts.canvas) updateTexture(canvas);
210
+ return canvas;
211
+ }