git-hash-art 0.7.0 → 0.9.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.
@@ -14,18 +14,16 @@ type DrawFunction = (
14
14
  ) => void;
15
15
 
16
16
  // ── Blob: organic closed curve via cubic bezier ─────────────────────
17
- // Generates 5-9 control points around a circle with hash-derived
18
- // radius jitter, then connects them with smooth cubic beziers.
19
17
 
20
18
  export const drawBlob: DrawFunction = (ctx, size, config) => {
21
19
  const rng: () => number = config?.rng ?? Math.random;
22
20
  const r = size / 2;
23
- const numPoints = 5 + Math.floor(rng() * 5); // 5-9 lobes
21
+ const numPoints = 5 + Math.floor(rng() * 5);
24
22
  const points: Array<{ x: number; y: number }> = [];
25
23
 
26
24
  for (let i = 0; i < numPoints; i++) {
27
25
  const angle = (i / numPoints) * Math.PI * 2;
28
- const jitter = 0.5 + rng() * 0.5; // radius varies 50-100%
26
+ const jitter = 0.5 + rng() * 0.5;
29
27
  points.push({
30
28
  x: Math.cos(angle) * r * jitter,
31
29
  y: Math.sin(angle) * r * jitter,
@@ -33,7 +31,6 @@ export const drawBlob: DrawFunction = (ctx, size, config) => {
33
31
  }
34
32
 
35
33
  ctx.beginPath();
36
- // Start at midpoint between last and first point
37
34
  const last = points[points.length - 1];
38
35
  const first = points[0];
39
36
  ctx.moveTo((last.x + first.x) / 2, (last.y + first.y) / 2);
@@ -41,22 +38,18 @@ export const drawBlob: DrawFunction = (ctx, size, config) => {
41
38
  for (let i = 0; i < numPoints; i++) {
42
39
  const curr = points[i];
43
40
  const next = points[(i + 1) % numPoints];
44
- const midX = (curr.x + next.x) / 2;
45
- const midY = (curr.y + next.y) / 2;
46
- ctx.quadraticCurveTo(curr.x, curr.y, midX, midY);
41
+ ctx.quadraticCurveTo(curr.x, curr.y, (curr.x + next.x) / 2, (curr.y + next.y) / 2);
47
42
  }
48
43
  ctx.closePath();
49
44
  };
50
45
 
51
46
  // ── Ngon: irregular polygon with hash-controlled vertices ───────────
52
- // Vertex count 3-12, each vertex has independent radius jitter
53
- // producing irregular, organic polygons.
54
47
 
55
48
  export const drawNgon: DrawFunction = (ctx, size, config) => {
56
49
  const rng: () => number = config?.rng ?? Math.random;
57
50
  const r = size / 2;
58
- const sides = 3 + Math.floor(rng() * 10); // 3-12 sides
59
- const jitterAmount = 0.1 + rng() * 0.4; // 10-50% vertex displacement
51
+ const sides = 3 + Math.floor(rng() * 10);
52
+ const jitterAmount = 0.1 + rng() * 0.4;
60
53
 
61
54
  ctx.beginPath();
62
55
  for (let i = 0; i < sides; i++) {
@@ -71,15 +64,13 @@ export const drawNgon: DrawFunction = (ctx, size, config) => {
71
64
  };
72
65
 
73
66
  // ── Lissajous: parametric curves with hash-derived frequency ratios ─
74
- // Produces figure-8s, knots, and complex looping curves.
75
67
 
76
68
  export const drawLissajous: DrawFunction = (ctx, size, config) => {
77
69
  const rng: () => number = config?.rng ?? Math.random;
78
70
  const r = size / 2;
79
- // Frequency ratios small integers produce recognizable patterns
80
- const freqA = 1 + Math.floor(rng() * 5); // 1-5
81
- const freqB = 1 + Math.floor(rng() * 5); // 1-5
82
- const phase = rng() * Math.PI; // phase offset
71
+ const freqA = 1 + Math.floor(rng() * 5);
72
+ const freqB = 1 + Math.floor(rng() * 5);
73
+ const phase = rng() * Math.PI;
83
74
  const steps = 120;
84
75
 
85
76
  ctx.beginPath();
@@ -94,12 +85,10 @@ export const drawLissajous: DrawFunction = (ctx, size, config) => {
94
85
  };
95
86
 
96
87
  // ── Superellipse: |x|^n + |y|^n = 1 with hash-derived exponent ─────
97
- // n=2 is circle, n>2 is squircle, n<1 is astroid/star-like.
98
88
 
99
89
  export const drawSuperellipse: DrawFunction = (ctx, size, config) => {
100
90
  const rng: () => number = config?.rng ?? Math.random;
101
91
  const r = size / 2;
102
- // Exponent range: 0.3 (spiky astroid) to 5 (rounded rectangle)
103
92
  const n = 0.3 + rng() * 4.7;
104
93
  const steps = 120;
105
94
 
@@ -108,7 +97,6 @@ export const drawSuperellipse: DrawFunction = (ctx, size, config) => {
108
97
  const t = (i / steps) * Math.PI * 2;
109
98
  const cosT = Math.cos(t);
110
99
  const sinT = Math.sin(t);
111
- // Superellipse parametric form
112
100
  const x = Math.sign(cosT) * Math.pow(Math.abs(cosT), 2 / n) * r;
113
101
  const y = Math.sign(sinT) * Math.pow(Math.abs(sinT), 2 / n) * r;
114
102
  if (i === 0) ctx.moveTo(x, y);
@@ -118,16 +106,13 @@ export const drawSuperellipse: DrawFunction = (ctx, size, config) => {
118
106
  };
119
107
 
120
108
  // ── Spirograph: hypotrochoid curves ─────────────────────────────────
121
- // Inner/outer radius ratios from hash produce unique looping patterns.
122
109
 
123
110
  export const drawSpirograph: DrawFunction = (ctx, size, config) => {
124
111
  const rng: () => number = config?.rng ?? Math.random;
125
112
  const scale = size / 2;
126
- // R = outer radius, r = inner radius, d = pen distance from inner center
127
113
  const R = 1;
128
- const r = 0.2 + rng() * 0.6; // 0.2-0.8
129
- const d = 0.3 + rng() * 0.7; // 0.3-1.0
130
- // Number of full rotations needed to close the curve
114
+ const r = 0.2 + rng() * 0.6;
115
+ const d = 0.3 + rng() * 0.7;
131
116
  const gcd = (a: number, b: number): number => {
132
117
  const ai = Math.round(a * 1000);
133
118
  const bi = Math.round(b * 1000);
@@ -135,7 +120,7 @@ export const drawSpirograph: DrawFunction = (ctx, size, config) => {
135
120
  return g(ai, bi) / 1000;
136
121
  };
137
122
  const period = r / gcd(R, r);
138
- const maxT = Math.min(period, 10) * Math.PI * 2; // cap at 10 rotations
123
+ const maxT = Math.min(period, 10) * Math.PI * 2;
139
124
  const steps = Math.min(600, Math.floor(maxT * 20));
140
125
 
141
126
  ctx.beginPath();
@@ -150,14 +135,13 @@ export const drawSpirograph: DrawFunction = (ctx, size, config) => {
150
135
  };
151
136
 
152
137
  // ── Wave ring: concentric ring with sinusoidal displacement ─────────
153
- // Hash controls frequency, amplitude, and number of rings.
154
138
 
155
139
  export const drawWaveRing: DrawFunction = (ctx, size, config) => {
156
140
  const rng: () => number = config?.rng ?? Math.random;
157
141
  const r = size / 2;
158
- const rings = 2 + Math.floor(rng() * 4); // 2-5 rings
159
- const freq = 3 + Math.floor(rng() * 12); // 3-14 waves per ring
160
- const amp = 0.05 + rng() * 0.15; // 5-20% of radius
142
+ const rings = 2 + Math.floor(rng() * 4);
143
+ const freq = 3 + Math.floor(rng() * 12);
144
+ const amp = 0.05 + rng() * 0.15;
161
145
 
162
146
  ctx.beginPath();
163
147
  for (let ring = 0; ring < rings; ring++) {
@@ -175,12 +159,11 @@ export const drawWaveRing: DrawFunction = (ctx, size, config) => {
175
159
  };
176
160
 
177
161
  // ── Rose curve: polar rose r = cos(k*theta) ────────────────────────
178
- // k determines petal count. Integer k = k petals (odd) or 2k petals (even).
179
162
 
180
163
  export const drawRose: DrawFunction = (ctx, size, config) => {
181
164
  const rng: () => number = config?.rng ?? Math.random;
182
165
  const r = size / 2;
183
- const k = 2 + Math.floor(rng() * 6); // 2-7 petal parameter
166
+ const k = 2 + Math.floor(rng() * 6);
184
167
  const steps = 200;
185
168
 
186
169
  ctx.beginPath();
@@ -195,6 +178,375 @@ export const drawRose: DrawFunction = (ctx, size, config) => {
195
178
  ctx.closePath();
196
179
  };
197
180
 
181
+ // ═══════════════════════════════════════════════════════════════════
182
+ // NEW PROCEDURAL SHAPES
183
+ // ═══════════════════════════════════════════════════════════════════
184
+
185
+ // ── ShardField: cluster of angular shards (broken glass / crystals) ─
186
+ // Generates 4-8 convex polygonal shards radiating from center.
187
+
188
+ export const drawShardField: DrawFunction = (ctx, size, config) => {
189
+ const rng: () => number = config?.rng ?? Math.random;
190
+ const r = size / 2;
191
+ const shardCount = 4 + Math.floor(rng() * 5); // 4-8 shards
192
+
193
+ ctx.beginPath();
194
+ for (let s = 0; s < shardCount; s++) {
195
+ const baseAngle = (s / shardCount) * Math.PI * 2 + (rng() - 0.5) * 0.3;
196
+ const dist = r * (0.15 + rng() * 0.35);
197
+ const cx = Math.cos(baseAngle) * dist;
198
+ const cy = Math.sin(baseAngle) * dist;
199
+ const shardSize = r * (0.2 + rng() * 0.4);
200
+ const verts = 3 + Math.floor(rng() * 3); // 3-5 vertices per shard
201
+ const shardAngleOffset = rng() * Math.PI * 2;
202
+
203
+ for (let v = 0; v < verts; v++) {
204
+ const angle = shardAngleOffset + (v / verts) * Math.PI * 2;
205
+ // Elongate shards along their radial direction
206
+ const stretch = v % 2 === 0 ? 1.0 : 0.3 + rng() * 0.4;
207
+ const px = cx + Math.cos(angle) * shardSize * stretch;
208
+ const py = cy + Math.sin(angle) * shardSize * stretch;
209
+ if (v === 0) ctx.moveTo(px, py);
210
+ else ctx.lineTo(px, py);
211
+ }
212
+ ctx.closePath();
213
+ }
214
+ };
215
+
216
+ // ── VoronoiCell: single organic cell with straight edges ────────────
217
+ // Simulates a Voronoi cell by generating a convex hull around
218
+ // a jittered set of midpoints between center and random neighbors.
219
+
220
+ export const drawVoronoiCell: DrawFunction = (ctx, size, config) => {
221
+ const rng: () => number = config?.rng ?? Math.random;
222
+ const r = size / 2;
223
+ const edgeCount = 5 + Math.floor(rng() * 4); // 5-8 edges
224
+ const points: Array<{ angle: number; x: number; y: number }> = [];
225
+
226
+ // Generate edge midpoints at varying distances
227
+ for (let i = 0; i < edgeCount; i++) {
228
+ const angle = (i / edgeCount) * Math.PI * 2 + (rng() - 0.5) * 0.4;
229
+ const dist = r * (0.6 + rng() * 0.4);
230
+ points.push({
231
+ angle,
232
+ x: Math.cos(angle) * dist,
233
+ y: Math.sin(angle) * dist,
234
+ });
235
+ }
236
+ // Sort by angle for proper winding
237
+ points.sort((a, b) => a.angle - b.angle);
238
+
239
+ ctx.beginPath();
240
+ ctx.moveTo(points[0].x, points[0].y);
241
+ for (let i = 1; i < points.length; i++) {
242
+ ctx.lineTo(points[i].x, points[i].y);
243
+ }
244
+ ctx.closePath();
245
+ };
246
+
247
+ // ── Crescent: two overlapping circles subtracted ────────────────────
248
+ // Hash controls bite size and angle of the crescent.
249
+
250
+ export const drawCrescent: DrawFunction = (ctx, size, config) => {
251
+ const rng: () => number = config?.rng ?? Math.random;
252
+ const r = size / 2;
253
+ const biteSize = 0.6 + rng() * 0.3; // 60-90% of radius
254
+ const biteOffset = r * (0.3 + rng() * 0.4);
255
+ const biteAngle = rng() * Math.PI * 2;
256
+
257
+ // Outer circle
258
+ ctx.beginPath();
259
+ ctx.arc(0, 0, r, 0, Math.PI * 2);
260
+
261
+ // Subtract inner circle using even-odd rule
262
+ const bx = Math.cos(biteAngle) * biteOffset;
263
+ const by = Math.sin(biteAngle) * biteOffset;
264
+ // Draw inner circle counter-clockwise for subtraction
265
+ ctx.moveTo(bx + r * biteSize, by);
266
+ ctx.arc(bx, by, r * biteSize, 0, Math.PI * 2, true);
267
+ };
268
+
269
+ // ── Tendril: tapered curving stroke that branches ───────────────────
270
+ // Like a vine or neural dendrite. Draws as a filled tapered path.
271
+
272
+ export const drawTendril: DrawFunction = (ctx, size, config) => {
273
+ const rng: () => number = config?.rng ?? Math.random;
274
+ const r = size / 2;
275
+ const segments = 12 + Math.floor(rng() * 8);
276
+ const startAngle = rng() * Math.PI * 2;
277
+ const curvature = (rng() - 0.5) * 0.4;
278
+
279
+ // Build spine points
280
+ const spine: Array<{ x: number; y: number }> = [];
281
+ let angle = startAngle;
282
+ let px = 0, py = 0;
283
+ for (let i = 0; i <= segments; i++) {
284
+ spine.push({ x: px, y: py });
285
+ const stepLen = (r / segments) * (1.5 + rng() * 0.5);
286
+ angle += curvature + (rng() - 0.5) * 0.6;
287
+ px += Math.cos(angle) * stepLen;
288
+ py += Math.sin(angle) * stepLen;
289
+ }
290
+
291
+ // Build tapered outline by offsetting perpendicular to spine
292
+ ctx.beginPath();
293
+ const leftSide: Array<{ x: number; y: number }> = [];
294
+ const rightSide: Array<{ x: number; y: number }> = [];
295
+
296
+ for (let i = 0; i < spine.length; i++) {
297
+ const t = i / (spine.length - 1);
298
+ const width = r * 0.12 * (1 - t * 0.9); // taper from thick to thin
299
+ const next = spine[Math.min(i + 1, spine.length - 1)];
300
+ const dx = next.x - spine[i].x;
301
+ const dy = next.y - spine[i].y;
302
+ const len = Math.hypot(dx, dy) || 1;
303
+ const nx = -dy / len;
304
+ const ny = dx / len;
305
+ leftSide.push({ x: spine[i].x + nx * width, y: spine[i].y + ny * width });
306
+ rightSide.push({ x: spine[i].x - nx * width, y: spine[i].y - ny * width });
307
+ }
308
+
309
+ ctx.moveTo(leftSide[0].x, leftSide[0].y);
310
+ for (let i = 1; i < leftSide.length; i++) {
311
+ ctx.lineTo(leftSide[i].x, leftSide[i].y);
312
+ }
313
+ for (let i = rightSide.length - 1; i >= 0; i--) {
314
+ ctx.lineTo(rightSide[i].x, rightSide[i].y);
315
+ }
316
+ ctx.closePath();
317
+ };
318
+
319
+ // ── CloudForm: overlapping circles along a curved spine ─────────────
320
+ // Like cumulus clouds — soft, billowy, organic.
321
+
322
+ export const drawCloudForm: DrawFunction = (ctx, size, config) => {
323
+ const rng: () => number = config?.rng ?? Math.random;
324
+ const r = size / 2;
325
+ const lobeCount = 4 + Math.floor(rng() * 4); // 4-7 lobes
326
+ const spineAngle = rng() * Math.PI * 2;
327
+ const spineLen = r * 0.6;
328
+
329
+ ctx.beginPath();
330
+ for (let i = 0; i < lobeCount; i++) {
331
+ const t = (i / (lobeCount - 1)) - 0.5; // -0.5 to 0.5
332
+ const sx = Math.cos(spineAngle) * spineLen * t;
333
+ const sy = Math.sin(spineAngle) * spineLen * t;
334
+ // Offset perpendicular for cloud shape
335
+ const perpAngle = spineAngle + Math.PI / 2;
336
+ const perpOff = (rng() - 0.3) * r * 0.3;
337
+ const cx = sx + Math.cos(perpAngle) * perpOff;
338
+ const cy = sy + Math.sin(perpAngle) * perpOff;
339
+ const lobeR = r * (0.25 + rng() * 0.2);
340
+ ctx.moveTo(cx + lobeR, cy);
341
+ ctx.arc(cx, cy, lobeR, 0, Math.PI * 2);
342
+ }
343
+ };
344
+
345
+ // ── InkSplat: radial spikes with bezier curves between them ─────────
346
+ // Like an ink drop hitting paper — organic, explosive.
347
+
348
+ export const drawInkSplat: DrawFunction = (ctx, size, config) => {
349
+ const rng: () => number = config?.rng ?? Math.random;
350
+ const r = size / 2;
351
+ const spikeCount = 8 + Math.floor(rng() * 8); // 8-15 spikes
352
+ const points: Array<{ x: number; y: number }> = [];
353
+
354
+ for (let i = 0; i < spikeCount; i++) {
355
+ const angle = (i / spikeCount) * Math.PI * 2;
356
+ const isSpike = i % 2 === 0;
357
+ const dist = isSpike
358
+ ? r * (0.5 + rng() * 0.5) // spikes reach 50-100% of radius
359
+ : r * (0.15 + rng() * 0.2); // valleys at 15-35%
360
+ points.push({
361
+ x: Math.cos(angle) * dist,
362
+ y: Math.sin(angle) * dist,
363
+ });
364
+ }
365
+
366
+ ctx.beginPath();
367
+ ctx.moveTo(points[0].x, points[0].y);
368
+ for (let i = 0; i < points.length; i++) {
369
+ const curr = points[i];
370
+ const next = points[(i + 1) % points.length];
371
+ const cpx = (curr.x + next.x) / 2 + (rng() - 0.5) * r * 0.15;
372
+ const cpy = (curr.y + next.y) / 2 + (rng() - 0.5) * r * 0.15;
373
+ ctx.quadraticCurveTo(cpx, cpy, next.x, next.y);
374
+ }
375
+ ctx.closePath();
376
+ };
377
+
378
+ // ── GeodesicDome: subdivided icosahedron projection ─────────────────
379
+ // Hash controls subdivision level (1-3). Projected to 2D.
380
+
381
+ export const drawGeodesicDome: DrawFunction = (ctx, size, config) => {
382
+ const rng: () => number = config?.rng ?? Math.random;
383
+ const r = size / 2;
384
+ const subdivisions = 1 + Math.floor(rng() * 3); // 1-3
385
+
386
+ // Start with icosahedron vertices projected to 2D
387
+ const baseVerts = 6 + subdivisions * 4;
388
+ const points: Array<{ x: number; y: number }> = [];
389
+ for (let i = 0; i < baseVerts; i++) {
390
+ const angle = (i / baseVerts) * Math.PI * 2;
391
+ const ring = i % 2 === 0 ? 1.0 : 0.5 + rng() * 0.3;
392
+ points.push({
393
+ x: Math.cos(angle) * r * ring,
394
+ y: Math.sin(angle) * r * ring,
395
+ });
396
+ }
397
+
398
+ ctx.beginPath();
399
+ // Draw triangulated mesh — connect each point to neighbors and center
400
+ for (let i = 0; i < points.length; i++) {
401
+ const next = points[(i + 1) % points.length];
402
+ ctx.moveTo(points[i].x, points[i].y);
403
+ ctx.lineTo(next.x, next.y);
404
+ // Connect to center
405
+ ctx.moveTo(points[i].x, points[i].y);
406
+ ctx.lineTo(0, 0);
407
+ // Cross-connect to create triangulation
408
+ if (i % 2 === 0 && i + 2 < points.length) {
409
+ ctx.moveTo(points[i].x, points[i].y);
410
+ ctx.lineTo(points[i + 2].x, points[i + 2].y);
411
+ }
412
+ }
413
+ // Outer ring
414
+ ctx.moveTo(points[0].x, points[0].y);
415
+ for (let i = 1; i < points.length; i++) {
416
+ ctx.lineTo(points[i].x, points[i].y);
417
+ }
418
+ ctx.closePath();
419
+ };
420
+
421
+ // ── PenroseTile: kite or dart shape from Penrose tiling ─────────────
422
+ // Hash selects kite vs dart and rotation.
423
+
424
+ export const drawPenroseTile: DrawFunction = (ctx, size, config) => {
425
+ const rng: () => number = config?.rng ?? Math.random;
426
+ const r = size / 2;
427
+ const phi = (1 + Math.sqrt(5)) / 2; // golden ratio
428
+ const isKite = rng() < 0.5;
429
+
430
+ ctx.beginPath();
431
+ if (isKite) {
432
+ // Kite: two golden triangles joined at base
433
+ const topY = -r;
434
+ const bottomY = r * (1 / phi);
435
+ const midY = r * (1 / phi - 1) * 0.3;
436
+ const wingX = r * 0.6;
437
+ ctx.moveTo(0, topY);
438
+ ctx.lineTo(wingX, midY);
439
+ ctx.lineTo(0, bottomY);
440
+ ctx.lineTo(-wingX, midY);
441
+ } else {
442
+ // Dart: concave quadrilateral
443
+ const topY = -r;
444
+ const bottomY = r * 0.3;
445
+ const midY = -r * 0.1;
446
+ const wingX = r * 0.5;
447
+ ctx.moveTo(0, topY);
448
+ ctx.lineTo(wingX, midY);
449
+ ctx.lineTo(0, bottomY);
450
+ ctx.lineTo(-wingX, midY);
451
+ }
452
+ ctx.closePath();
453
+ };
454
+
455
+ // ── ReuleauxTriangle: constant-width curve ──────────────────────────
456
+ // Three circular arcs connecting the vertices of an equilateral triangle.
457
+
458
+ export const drawReuleauxTriangle: DrawFunction = (ctx, size, config) => {
459
+ const r = size / 2;
460
+ // Vertices of equilateral triangle
461
+ const verts = [];
462
+ for (let i = 0; i < 3; i++) {
463
+ const angle = (i / 3) * Math.PI * 2 - Math.PI / 2;
464
+ verts.push({ x: Math.cos(angle) * r * 0.7, y: Math.sin(angle) * r * 0.7 });
465
+ }
466
+ // Side length = distance between vertices
467
+ const sideLen = Math.hypot(verts[1].x - verts[0].x, verts[1].y - verts[0].y);
468
+
469
+ ctx.beginPath();
470
+ for (let i = 0; i < 3; i++) {
471
+ const from = verts[(i + 1) % 3];
472
+ const to = verts[(i + 2) % 3];
473
+ const center = verts[i];
474
+ const startAngle = Math.atan2(from.y - center.y, from.x - center.x);
475
+ const endAngle = Math.atan2(to.y - center.y, to.x - center.x);
476
+ if (i === 0) ctx.moveTo(from.x, from.y);
477
+ ctx.arc(center.x, center.y, sideLen, startAngle, endAngle);
478
+ }
479
+ ctx.closePath();
480
+ };
481
+
482
+ // ── DotCluster: cloud of dots in a bounded region ───────────────────
483
+ // Hash controls density, spread, and clustering.
484
+
485
+ export const drawDotCluster: DrawFunction = (ctx, size, config) => {
486
+ const rng: () => number = config?.rng ?? Math.random;
487
+ const r = size / 2;
488
+ const dotCount = 15 + Math.floor(rng() * 25); // 15-39 dots
489
+ const clusterTightness = 0.3 + rng() * 0.5;
490
+
491
+ ctx.beginPath();
492
+ for (let i = 0; i < dotCount; i++) {
493
+ // Gaussian-ish distribution via Box-Muller approximation
494
+ const u1 = Math.max(0.001, rng());
495
+ const u2 = rng();
496
+ const mag = Math.sqrt(-2 * Math.log(u1)) * clusterTightness;
497
+ const angle = u2 * Math.PI * 2;
498
+ const dx = Math.cos(angle) * mag * r;
499
+ const dy = Math.sin(angle) * mag * r;
500
+ const dotR = r * (0.02 + rng() * 0.04);
501
+ ctx.moveTo(dx + dotR, dy);
502
+ ctx.arc(dx, dy, dotR, 0, Math.PI * 2);
503
+ }
504
+ };
505
+
506
+ // ── CrosshatchPatch: bounded region filled with directional lines ───
507
+ // Like an engraving or etching mark.
508
+
509
+ export const drawCrosshatchPatch: DrawFunction = (ctx, size, config) => {
510
+ const rng: () => number = config?.rng ?? Math.random;
511
+ const r = size / 2;
512
+ const angle1 = rng() * Math.PI;
513
+ const angle2 = angle1 + Math.PI / 2 + (rng() - 0.5) * 0.3;
514
+ const spacing = r * (0.08 + rng() * 0.08);
515
+ const hasCross = rng() < 0.6;
516
+
517
+ // Draw bounding shape (ellipse)
518
+ const rx = r * (0.7 + rng() * 0.3);
519
+ const ry = r * (0.5 + rng() * 0.3);
520
+
521
+ // Outer boundary
522
+ ctx.beginPath();
523
+ ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);
524
+
525
+ // Hatch lines clipped to the ellipse
526
+ const cos1 = Math.cos(angle1);
527
+ const sin1 = Math.sin(angle1);
528
+ for (let d = -r; d <= r; d += spacing) {
529
+ const lx1 = d * cos1 - r * sin1;
530
+ const ly1 = d * sin1 + r * cos1;
531
+ const lx2 = d * cos1 + r * sin1;
532
+ const ly2 = d * sin1 - r * cos1;
533
+ ctx.moveTo(lx1, ly1);
534
+ ctx.lineTo(lx2, ly2);
535
+ }
536
+
537
+ if (hasCross) {
538
+ const cos2 = Math.cos(angle2);
539
+ const sin2 = Math.sin(angle2);
540
+ for (let d = -r; d <= r; d += spacing * 1.3) {
541
+ const lx1 = d * cos2 - r * sin2;
542
+ const ly1 = d * sin2 + r * cos2;
543
+ const lx2 = d * cos2 + r * sin2;
544
+ const ly2 = d * sin2 - r * cos2;
545
+ ctx.moveTo(lx1, ly1);
546
+ ctx.lineTo(lx2, ly2);
547
+ }
548
+ }
549
+ };
198
550
 
199
551
  // ── Shape registry ──────────────────────────────────────────────────
200
552
 
@@ -206,4 +558,15 @@ export const proceduralShapes: Record<string, DrawFunction> = {
206
558
  spirograph: drawSpirograph,
207
559
  waveRing: drawWaveRing,
208
560
  rose: drawRose,
561
+ shardField: drawShardField,
562
+ voronoiCell: drawVoronoiCell,
563
+ crescent: drawCrescent,
564
+ tendril: drawTendril,
565
+ cloudForm: drawCloudForm,
566
+ inkSplat: drawInkSplat,
567
+ geodesicDome: drawGeodesicDome,
568
+ penroseTile: drawPenroseTile,
569
+ reuleauxTriangle: drawReuleauxTriangle,
570
+ dotCluster: drawDotCluster,
571
+ crosshatchPatch: drawCrosshatchPatch,
209
572
  };