git-hash-art 0.8.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.
- package/ALGORITHM.md +323 -270
- package/CHANGELOG.md +10 -0
- package/bin/cli.js +17 -14
- package/bin/generateVersionComparison.js +353 -0
- package/dist/browser.js +1246 -36
- package/dist/browser.js.map +1 -1
- package/dist/main.js +1246 -36
- package/dist/main.js.map +1 -1
- package/dist/module.js +1246 -36
- package/dist/module.js.map +1 -1
- package/package.json +2 -1
- package/src/lib/archetypes.ts +68 -0
- package/src/lib/canvas/draw.ts +318 -1
- package/src/lib/canvas/shapes/affinity.ts +146 -1
- package/src/lib/canvas/shapes/procedural.ts +395 -32
- package/src/lib/render.ts +259 -13
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-hash-art",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"author": "gfargo <ghfargo@gmail.com>",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"watch": "parcel watch",
|
|
7
7
|
"prebuild": "rm -rf .parcel-cache",
|
|
8
8
|
"build": "parcel build",
|
|
9
9
|
"build:examples": "node bin/generateExamples.js",
|
|
10
|
+
"build:versions": "node bin/generateVersionComparison.js",
|
|
10
11
|
"format": "prettier --write 'src/**/*.{ts,tsx,js,jsx,json,css,md}'",
|
|
11
12
|
"format:check": "prettier --check 'src/**/*.{ts,tsx,js,jsx,json,css,md}'",
|
|
12
13
|
"release": "release-it",
|
package/src/lib/archetypes.ts
CHANGED
|
@@ -287,6 +287,74 @@ const ARCHETYPES: Archetype[] = [
|
|
|
287
287
|
sizePower: 1.8,
|
|
288
288
|
invertForeground: false,
|
|
289
289
|
},
|
|
290
|
+
{
|
|
291
|
+
name: "shattered-glass",
|
|
292
|
+
gridSize: 8,
|
|
293
|
+
layers: 3,
|
|
294
|
+
baseOpacity: 0.85,
|
|
295
|
+
opacityReduction: 0.1,
|
|
296
|
+
minShapeSize: 15,
|
|
297
|
+
maxShapeSize: 250,
|
|
298
|
+
backgroundStyle: "solid-dark",
|
|
299
|
+
paletteMode: "high-contrast",
|
|
300
|
+
preferredStyles: ["fill-and-stroke", "stroke-only", "fill-only"],
|
|
301
|
+
flowLineMultiplier: 0,
|
|
302
|
+
heroShape: false,
|
|
303
|
+
glowMultiplier: 0.3,
|
|
304
|
+
sizePower: 1.0,
|
|
305
|
+
invertForeground: false,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "botanical",
|
|
309
|
+
gridSize: 4,
|
|
310
|
+
layers: 4,
|
|
311
|
+
baseOpacity: 0.5,
|
|
312
|
+
opacityReduction: 0.06,
|
|
313
|
+
minShapeSize: 30,
|
|
314
|
+
maxShapeSize: 400,
|
|
315
|
+
backgroundStyle: "radial-light",
|
|
316
|
+
paletteMode: "earth",
|
|
317
|
+
preferredStyles: ["watercolor", "fill-only", "incomplete"],
|
|
318
|
+
flowLineMultiplier: 3,
|
|
319
|
+
heroShape: true,
|
|
320
|
+
glowMultiplier: 0.2,
|
|
321
|
+
sizePower: 1.6,
|
|
322
|
+
invertForeground: false,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "stipple-portrait",
|
|
326
|
+
gridSize: 9,
|
|
327
|
+
layers: 2,
|
|
328
|
+
baseOpacity: 0.8,
|
|
329
|
+
opacityReduction: 0.05,
|
|
330
|
+
minShapeSize: 5,
|
|
331
|
+
maxShapeSize: 120,
|
|
332
|
+
backgroundStyle: "solid-light",
|
|
333
|
+
paletteMode: "monochrome",
|
|
334
|
+
preferredStyles: ["stipple", "fill-only", "hatched"],
|
|
335
|
+
flowLineMultiplier: 0,
|
|
336
|
+
heroShape: false,
|
|
337
|
+
glowMultiplier: 0,
|
|
338
|
+
sizePower: 2.8,
|
|
339
|
+
invertForeground: false,
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: "celestial",
|
|
343
|
+
gridSize: 7,
|
|
344
|
+
layers: 5,
|
|
345
|
+
baseOpacity: 0.45,
|
|
346
|
+
opacityReduction: 0.04,
|
|
347
|
+
minShapeSize: 8,
|
|
348
|
+
maxShapeSize: 450,
|
|
349
|
+
backgroundStyle: "radial-dark",
|
|
350
|
+
paletteMode: "neon",
|
|
351
|
+
preferredStyles: ["fill-only", "watercolor", "stroke-only", "incomplete"],
|
|
352
|
+
flowLineMultiplier: 2,
|
|
353
|
+
heroShape: true,
|
|
354
|
+
glowMultiplier: 2.5,
|
|
355
|
+
sizePower: 2.2,
|
|
356
|
+
invertForeground: false,
|
|
357
|
+
},
|
|
290
358
|
];
|
|
291
359
|
|
|
292
360
|
/**
|
package/src/lib/canvas/draw.ts
CHANGED
|
@@ -35,7 +35,13 @@ export type RenderStyle =
|
|
|
35
35
|
| "dashed" // dashed outline
|
|
36
36
|
| "watercolor" // multiple offset passes at low opacity
|
|
37
37
|
| "hatched" // cross-hatch texture fill
|
|
38
|
-
| "incomplete"
|
|
38
|
+
| "incomplete" // draw only 60-85% of the stroke path
|
|
39
|
+
| "stipple" // dot-fill texture
|
|
40
|
+
| "stencil" // negative-space cutout effect
|
|
41
|
+
| "noise-grain" // procedural noise grain texture clipped to shape
|
|
42
|
+
| "wood-grain" // parallel wavy lines simulating wood
|
|
43
|
+
| "marble-vein" // branching vein lines on a soft fill
|
|
44
|
+
| "fabric-weave"; // interlocking horizontal/vertical threads
|
|
39
45
|
|
|
40
46
|
const RENDER_STYLES: RenderStyle[] = [
|
|
41
47
|
"fill-and-stroke",
|
|
@@ -47,6 +53,12 @@ const RENDER_STYLES: RenderStyle[] = [
|
|
|
47
53
|
"watercolor",
|
|
48
54
|
"hatched",
|
|
49
55
|
"incomplete",
|
|
56
|
+
"stipple",
|
|
57
|
+
"stencil",
|
|
58
|
+
"noise-grain",
|
|
59
|
+
"wood-grain",
|
|
60
|
+
"marble-vein",
|
|
61
|
+
"fabric-weave",
|
|
50
62
|
];
|
|
51
63
|
|
|
52
64
|
export function pickRenderStyle(rng: () => number): RenderStyle {
|
|
@@ -274,6 +286,226 @@ function applyRenderStyle(
|
|
|
274
286
|
break;
|
|
275
287
|
}
|
|
276
288
|
|
|
289
|
+
case "stipple": {
|
|
290
|
+
// Dot-fill texture — clip to shape, then scatter dots
|
|
291
|
+
const savedAlphaS = ctx.globalAlpha;
|
|
292
|
+
ctx.globalAlpha = savedAlphaS * 0.15;
|
|
293
|
+
ctx.fill(); // ghost fill
|
|
294
|
+
ctx.globalAlpha = savedAlphaS;
|
|
295
|
+
|
|
296
|
+
ctx.save();
|
|
297
|
+
ctx.clip();
|
|
298
|
+
const dotSpacing = Math.max(2, size * 0.03);
|
|
299
|
+
const extent = size * 0.55;
|
|
300
|
+
ctx.globalAlpha = savedAlphaS * 0.7;
|
|
301
|
+
for (let dx = -extent; dx <= extent; dx += dotSpacing) {
|
|
302
|
+
for (let dy = -extent; dy <= extent; dy += dotSpacing) {
|
|
303
|
+
// Jitter each dot position for organic feel
|
|
304
|
+
const jx = rng ? (rng() - 0.5) * dotSpacing * 0.6 : 0;
|
|
305
|
+
const jy = rng ? (rng() - 0.5) * dotSpacing * 0.6 : 0;
|
|
306
|
+
const dotR = rng ? dotSpacing * (0.15 + rng() * 0.2) : dotSpacing * 0.2;
|
|
307
|
+
ctx.beginPath();
|
|
308
|
+
ctx.arc(dx + jx, dy + jy, dotR, 0, Math.PI * 2);
|
|
309
|
+
ctx.fill();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
ctx.restore();
|
|
313
|
+
ctx.globalAlpha = savedAlphaS;
|
|
314
|
+
// Outline
|
|
315
|
+
ctx.globalAlpha *= 0.4;
|
|
316
|
+
ctx.stroke();
|
|
317
|
+
ctx.globalAlpha /= 0.4;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
case "stencil": {
|
|
322
|
+
// Negative-space cutout — fill a rectangle, then erase the shape
|
|
323
|
+
const savedAlphaSt = ctx.globalAlpha;
|
|
324
|
+
// Fill a bounding area with the stroke color
|
|
325
|
+
ctx.globalAlpha = savedAlphaSt * 0.5;
|
|
326
|
+
ctx.fillStyle = strokeColor;
|
|
327
|
+
ctx.fillRect(-size * 0.6, -size * 0.6, size * 1.2, size * 1.2);
|
|
328
|
+
// Cut out the shape using destination-out
|
|
329
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
330
|
+
ctx.globalAlpha = 1;
|
|
331
|
+
ctx.fill();
|
|
332
|
+
ctx.globalCompositeOperation = "source-over";
|
|
333
|
+
ctx.globalAlpha = savedAlphaSt;
|
|
334
|
+
// Subtle outline of the cutout
|
|
335
|
+
ctx.globalAlpha *= 0.3;
|
|
336
|
+
ctx.stroke();
|
|
337
|
+
ctx.globalAlpha /= 0.3;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
case "noise-grain": {
|
|
342
|
+
// Procedural noise grain texture clipped to shape boundary
|
|
343
|
+
const savedAlphaN = ctx.globalAlpha;
|
|
344
|
+
ctx.globalAlpha = savedAlphaN * 0.25;
|
|
345
|
+
ctx.fill(); // base tint
|
|
346
|
+
ctx.globalAlpha = savedAlphaN;
|
|
347
|
+
|
|
348
|
+
ctx.save();
|
|
349
|
+
ctx.clip();
|
|
350
|
+
const grainSpacing = Math.max(1.5, size * 0.015);
|
|
351
|
+
const extentN = size * 0.55;
|
|
352
|
+
ctx.globalAlpha = savedAlphaN * 0.6;
|
|
353
|
+
for (let gx = -extentN; gx <= extentN; gx += grainSpacing) {
|
|
354
|
+
for (let gy = -extentN; gy <= extentN; gy += grainSpacing) {
|
|
355
|
+
if (!rng) break;
|
|
356
|
+
const jx = (rng() - 0.5) * grainSpacing * 1.2;
|
|
357
|
+
const jy = (rng() - 0.5) * grainSpacing * 1.2;
|
|
358
|
+
const brightness = rng() > 0.5 ? 255 : 0;
|
|
359
|
+
const dotAlpha = 0.15 + rng() * 0.35;
|
|
360
|
+
ctx.globalAlpha = savedAlphaN * dotAlpha;
|
|
361
|
+
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
362
|
+
const dotSize = grainSpacing * (0.3 + rng() * 0.5);
|
|
363
|
+
ctx.fillRect(gx + jx, gy + jy, dotSize, dotSize);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
ctx.restore();
|
|
367
|
+
ctx.fillStyle = fillColor;
|
|
368
|
+
ctx.globalAlpha = savedAlphaN;
|
|
369
|
+
ctx.globalAlpha *= 0.4;
|
|
370
|
+
ctx.stroke();
|
|
371
|
+
ctx.globalAlpha /= 0.4;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
case "wood-grain": {
|
|
376
|
+
// Parallel wavy lines simulating wood grain, clipped to shape
|
|
377
|
+
const savedAlphaW = ctx.globalAlpha;
|
|
378
|
+
ctx.globalAlpha = savedAlphaW * 0.2;
|
|
379
|
+
ctx.fill(); // base tint
|
|
380
|
+
ctx.globalAlpha = savedAlphaW;
|
|
381
|
+
|
|
382
|
+
ctx.save();
|
|
383
|
+
ctx.clip();
|
|
384
|
+
const grainLineSpacing = Math.max(2, size * 0.035);
|
|
385
|
+
const extentW = size * 0.55;
|
|
386
|
+
const waveFreq = rng ? 3 + rng() * 5 : 5;
|
|
387
|
+
const waveAmp = rng ? size * (0.01 + rng() * 0.03) : size * 0.02;
|
|
388
|
+
const grainAngle = rng ? rng() * Math.PI : Math.PI * 0.25;
|
|
389
|
+
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.3);
|
|
390
|
+
ctx.globalAlpha = savedAlphaW * 0.5;
|
|
391
|
+
|
|
392
|
+
const cosG = Math.cos(grainAngle);
|
|
393
|
+
const sinG = Math.sin(grainAngle);
|
|
394
|
+
for (let d = -extentW; d <= extentW; d += grainLineSpacing) {
|
|
395
|
+
ctx.beginPath();
|
|
396
|
+
for (let t = -extentW; t <= extentW; t += 2) {
|
|
397
|
+
const wave = Math.sin((t / extentW) * waveFreq * Math.PI) * waveAmp;
|
|
398
|
+
const px = t * cosG - (d + wave) * sinG;
|
|
399
|
+
const py = t * sinG + (d + wave) * cosG;
|
|
400
|
+
if (t === -extentW) ctx.moveTo(px, py);
|
|
401
|
+
else ctx.lineTo(px, py);
|
|
402
|
+
}
|
|
403
|
+
ctx.stroke();
|
|
404
|
+
}
|
|
405
|
+
ctx.restore();
|
|
406
|
+
ctx.globalAlpha = savedAlphaW;
|
|
407
|
+
ctx.globalAlpha *= 0.35;
|
|
408
|
+
ctx.stroke();
|
|
409
|
+
ctx.globalAlpha /= 0.35;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
case "marble-vein": {
|
|
414
|
+
// Branching vein lines on a soft fill, clipped to shape
|
|
415
|
+
const savedAlphaM = ctx.globalAlpha;
|
|
416
|
+
ctx.globalAlpha = savedAlphaM * 0.35;
|
|
417
|
+
ctx.fill(); // soft base
|
|
418
|
+
ctx.globalAlpha = savedAlphaM;
|
|
419
|
+
|
|
420
|
+
ctx.save();
|
|
421
|
+
ctx.clip();
|
|
422
|
+
const veinCount = rng ? 2 + Math.floor(rng() * 3) : 3;
|
|
423
|
+
const extentM = size * 0.45;
|
|
424
|
+
ctx.lineWidth = Math.max(0.5, strokeWidth * 0.5);
|
|
425
|
+
ctx.globalAlpha = savedAlphaM * 0.4;
|
|
426
|
+
|
|
427
|
+
for (let v = 0; v < veinCount; v++) {
|
|
428
|
+
const startX = rng ? (rng() - 0.5) * extentM * 2 : 0;
|
|
429
|
+
const startY = rng ? -extentM + rng() * extentM * 0.5 : -extentM;
|
|
430
|
+
let vx = startX;
|
|
431
|
+
let vy = startY;
|
|
432
|
+
const steps = 15 + (rng ? Math.floor(rng() * 15) : 10);
|
|
433
|
+
const stepLen = size * 0.04;
|
|
434
|
+
|
|
435
|
+
ctx.beginPath();
|
|
436
|
+
ctx.moveTo(vx, vy);
|
|
437
|
+
for (let s = 0; s < steps; s++) {
|
|
438
|
+
const drift = rng ? (rng() - 0.5) * stepLen * 1.5 : 0;
|
|
439
|
+
vx += drift;
|
|
440
|
+
vy += stepLen;
|
|
441
|
+
ctx.lineTo(vx, vy);
|
|
442
|
+
// Branch ~20% of the time
|
|
443
|
+
if (rng && rng() < 0.2 && s > 2 && s < steps - 3) {
|
|
444
|
+
const branchDir = rng() < 0.5 ? -1 : 1;
|
|
445
|
+
let bx = vx;
|
|
446
|
+
let by = vy;
|
|
447
|
+
const bSteps = 3 + Math.floor(rng() * 5);
|
|
448
|
+
ctx.moveTo(bx, by);
|
|
449
|
+
for (let bs = 0; bs < bSteps; bs++) {
|
|
450
|
+
bx += branchDir * stepLen * (0.5 + rng() * 0.5);
|
|
451
|
+
by += stepLen * 0.6;
|
|
452
|
+
ctx.lineTo(bx, by);
|
|
453
|
+
}
|
|
454
|
+
ctx.moveTo(vx, vy); // return to main vein
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
ctx.stroke();
|
|
458
|
+
}
|
|
459
|
+
ctx.restore();
|
|
460
|
+
ctx.globalAlpha = savedAlphaM;
|
|
461
|
+
ctx.globalAlpha *= 0.3;
|
|
462
|
+
ctx.stroke();
|
|
463
|
+
ctx.globalAlpha /= 0.3;
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
case "fabric-weave": {
|
|
468
|
+
// Interlocking horizontal/vertical threads clipped to shape
|
|
469
|
+
const savedAlphaF = ctx.globalAlpha;
|
|
470
|
+
ctx.globalAlpha = savedAlphaF * 0.15;
|
|
471
|
+
ctx.fill(); // ghost base
|
|
472
|
+
ctx.globalAlpha = savedAlphaF;
|
|
473
|
+
|
|
474
|
+
ctx.save();
|
|
475
|
+
ctx.clip();
|
|
476
|
+
const threadSpacing = Math.max(2, size * 0.04);
|
|
477
|
+
const extentF = size * 0.55;
|
|
478
|
+
ctx.lineWidth = Math.max(0.8, threadSpacing * 0.5);
|
|
479
|
+
ctx.globalAlpha = savedAlphaF * 0.55;
|
|
480
|
+
|
|
481
|
+
// Horizontal threads
|
|
482
|
+
for (let y = -extentF; y <= extentF; y += threadSpacing * 2) {
|
|
483
|
+
ctx.beginPath();
|
|
484
|
+
ctx.moveTo(-extentF, y);
|
|
485
|
+
ctx.lineTo(extentF, y);
|
|
486
|
+
ctx.stroke();
|
|
487
|
+
}
|
|
488
|
+
// Vertical threads (offset by half spacing for weave effect)
|
|
489
|
+
ctx.globalAlpha = savedAlphaF * 0.45;
|
|
490
|
+
ctx.strokeStyle = fillColor;
|
|
491
|
+
for (let x = -extentF; x <= extentF; x += threadSpacing * 2) {
|
|
492
|
+
ctx.beginPath();
|
|
493
|
+
for (let y = -extentF; y <= extentF; y += threadSpacing * 2) {
|
|
494
|
+
// Over-under: draw segment, skip segment
|
|
495
|
+
ctx.moveTo(x, y);
|
|
496
|
+
ctx.lineTo(x, y + threadSpacing);
|
|
497
|
+
}
|
|
498
|
+
ctx.stroke();
|
|
499
|
+
}
|
|
500
|
+
ctx.strokeStyle = strokeColor;
|
|
501
|
+
ctx.restore();
|
|
502
|
+
ctx.globalAlpha = savedAlphaF;
|
|
503
|
+
ctx.globalAlpha *= 0.3;
|
|
504
|
+
ctx.stroke();
|
|
505
|
+
ctx.globalAlpha /= 0.3;
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
|
|
277
509
|
case "fill-and-stroke":
|
|
278
510
|
default:
|
|
279
511
|
ctx.fill();
|
|
@@ -358,3 +590,88 @@ export function enhanceShapeGeneration(
|
|
|
358
590
|
|
|
359
591
|
ctx.restore();
|
|
360
592
|
}
|
|
593
|
+
|
|
594
|
+
// ── Shape mirroring effect ──────────────────────────────────────────
|
|
595
|
+
// Draws a shape and its mirror (reflected across an axis) for visual
|
|
596
|
+
// symmetry. Works especially well with basic shapes like triangles,
|
|
597
|
+
// crescents, and penrose tiles.
|
|
598
|
+
|
|
599
|
+
export type MirrorAxis = "horizontal" | "vertical" | "diagonal" | "radial-4";
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Draw a shape with a mirrored reflection.
|
|
603
|
+
* The mirror is drawn at reduced opacity with optional offset.
|
|
604
|
+
*/
|
|
605
|
+
export function drawMirroredShape(
|
|
606
|
+
ctx: CanvasRenderingContext2D,
|
|
607
|
+
shape: string,
|
|
608
|
+
x: number,
|
|
609
|
+
y: number,
|
|
610
|
+
config: EnhanceShapeConfig & { mirrorAxis?: MirrorAxis; mirrorGap?: number },
|
|
611
|
+
): void {
|
|
612
|
+
const { mirrorAxis = "horizontal", mirrorGap = 0 } = config;
|
|
613
|
+
|
|
614
|
+
// Draw the primary shape
|
|
615
|
+
enhanceShapeGeneration(ctx, shape, x, y, config);
|
|
616
|
+
|
|
617
|
+
// Draw the mirrored copy
|
|
618
|
+
ctx.save();
|
|
619
|
+
const savedAlpha = ctx.globalAlpha;
|
|
620
|
+
ctx.globalAlpha = savedAlpha * 0.7; // mirror is slightly softer
|
|
621
|
+
|
|
622
|
+
switch (mirrorAxis) {
|
|
623
|
+
case "horizontal":
|
|
624
|
+
// Reflect across vertical axis at shape position
|
|
625
|
+
enhanceShapeGeneration(ctx, shape, x, y + mirrorGap, {
|
|
626
|
+
...config,
|
|
627
|
+
rotation: -(config.rotation || 0),
|
|
628
|
+
size: config.size * 0.95,
|
|
629
|
+
});
|
|
630
|
+
break;
|
|
631
|
+
case "vertical":
|
|
632
|
+
enhanceShapeGeneration(ctx, shape, x + mirrorGap, y, {
|
|
633
|
+
...config,
|
|
634
|
+
rotation: 180 - (config.rotation || 0),
|
|
635
|
+
size: config.size * 0.95,
|
|
636
|
+
});
|
|
637
|
+
break;
|
|
638
|
+
case "diagonal":
|
|
639
|
+
// Reflect across 45° axis
|
|
640
|
+
enhanceShapeGeneration(ctx, shape, x + mirrorGap * 0.7, y + mirrorGap * 0.7, {
|
|
641
|
+
...config,
|
|
642
|
+
rotation: 90 - (config.rotation || 0),
|
|
643
|
+
size: config.size * 0.9,
|
|
644
|
+
});
|
|
645
|
+
break;
|
|
646
|
+
case "radial-4":
|
|
647
|
+
// Four-way radial mirror
|
|
648
|
+
for (let i = 1; i < 4; i++) {
|
|
649
|
+
const angle = (i / 4) * Math.PI * 2;
|
|
650
|
+
const mx = x + Math.cos(angle) * mirrorGap;
|
|
651
|
+
const my = y + Math.sin(angle) * mirrorGap;
|
|
652
|
+
ctx.globalAlpha = savedAlpha * (0.7 - i * 0.1);
|
|
653
|
+
enhanceShapeGeneration(ctx, shape, mx, my, {
|
|
654
|
+
...config,
|
|
655
|
+
rotation: (config.rotation || 0) + i * 90,
|
|
656
|
+
size: config.size * (0.95 - i * 0.05),
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
ctx.globalAlpha = savedAlpha;
|
|
663
|
+
ctx.restore();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Pick a mirror axis deterministically.
|
|
668
|
+
* Returns null ~60% of the time (no mirroring).
|
|
669
|
+
*/
|
|
670
|
+
export function pickMirrorAxis(rng: () => number): MirrorAxis | null {
|
|
671
|
+
const roll = rng();
|
|
672
|
+
if (roll < 0.60) return null;
|
|
673
|
+
if (roll < 0.75) return "horizontal";
|
|
674
|
+
if (roll < 0.87) return "vertical";
|
|
675
|
+
if (roll < 0.95) return "diagonal";
|
|
676
|
+
return "radial-4";
|
|
677
|
+
}
|
|
@@ -288,7 +288,7 @@ export const SHAPE_PROFILES: Record<string, ShapeProfile> = {
|
|
|
288
288
|
affinities: ["circle", "square", "blob", "hexagon"],
|
|
289
289
|
category: "procedural",
|
|
290
290
|
heroCandidate: false,
|
|
291
|
-
bestStyles: ["fill-only", "watercolor", "fill-and-stroke"],
|
|
291
|
+
bestStyles: ["fill-only", "watercolor", "fill-and-stroke", "wood-grain"],
|
|
292
292
|
},
|
|
293
293
|
spirograph: {
|
|
294
294
|
tier: 1,
|
|
@@ -317,6 +317,107 @@ export const SHAPE_PROFILES: Record<string, ShapeProfile> = {
|
|
|
317
317
|
heroCandidate: true,
|
|
318
318
|
bestStyles: ["stroke-only", "fill-only", "watercolor"],
|
|
319
319
|
},
|
|
320
|
+
|
|
321
|
+
// ── New procedural shapes ─────────────────────────────────────
|
|
322
|
+
shardField: {
|
|
323
|
+
tier: 2,
|
|
324
|
+
minSizeFraction: 0.1,
|
|
325
|
+
maxSizeFraction: 0.7,
|
|
326
|
+
affinities: ["voronoiCell", "diamond", "triangle", "penroseTile"],
|
|
327
|
+
category: "procedural",
|
|
328
|
+
heroCandidate: false,
|
|
329
|
+
bestStyles: ["fill-and-stroke", "stroke-only", "fill-only"],
|
|
330
|
+
},
|
|
331
|
+
voronoiCell: {
|
|
332
|
+
tier: 1,
|
|
333
|
+
minSizeFraction: 0.08,
|
|
334
|
+
maxSizeFraction: 0.9,
|
|
335
|
+
affinities: ["shardField", "ngon", "superellipse", "blob"],
|
|
336
|
+
category: "procedural",
|
|
337
|
+
heroCandidate: false,
|
|
338
|
+
bestStyles: ["fill-and-stroke", "fill-only", "watercolor", "marble-vein"],
|
|
339
|
+
},
|
|
340
|
+
crescent: {
|
|
341
|
+
tier: 1,
|
|
342
|
+
minSizeFraction: 0.1,
|
|
343
|
+
maxSizeFraction: 1.0,
|
|
344
|
+
affinities: ["circle", "blob", "cloudForm", "vesicaPiscis"],
|
|
345
|
+
category: "procedural",
|
|
346
|
+
heroCandidate: true,
|
|
347
|
+
bestStyles: ["fill-only", "watercolor", "fill-and-stroke"],
|
|
348
|
+
},
|
|
349
|
+
tendril: {
|
|
350
|
+
tier: 2,
|
|
351
|
+
minSizeFraction: 0.1,
|
|
352
|
+
maxSizeFraction: 0.8,
|
|
353
|
+
affinities: ["blob", "inkSplat", "lissajous", "fibonacciSpiral"],
|
|
354
|
+
category: "procedural",
|
|
355
|
+
heroCandidate: false,
|
|
356
|
+
bestStyles: ["fill-only", "watercolor", "fill-and-stroke"],
|
|
357
|
+
},
|
|
358
|
+
cloudForm: {
|
|
359
|
+
tier: 1,
|
|
360
|
+
minSizeFraction: 0.15,
|
|
361
|
+
maxSizeFraction: 1.0,
|
|
362
|
+
affinities: ["blob", "circle", "crescent", "superellipse"],
|
|
363
|
+
category: "procedural",
|
|
364
|
+
heroCandidate: false,
|
|
365
|
+
bestStyles: ["fill-only", "watercolor"],
|
|
366
|
+
},
|
|
367
|
+
inkSplat: {
|
|
368
|
+
tier: 2,
|
|
369
|
+
minSizeFraction: 0.1,
|
|
370
|
+
maxSizeFraction: 0.8,
|
|
371
|
+
affinities: ["blob", "tendril", "shardField", "star"],
|
|
372
|
+
category: "procedural",
|
|
373
|
+
heroCandidate: false,
|
|
374
|
+
bestStyles: ["fill-only", "watercolor", "fill-and-stroke"],
|
|
375
|
+
},
|
|
376
|
+
geodesicDome: {
|
|
377
|
+
tier: 2,
|
|
378
|
+
minSizeFraction: 0.2,
|
|
379
|
+
maxSizeFraction: 0.9,
|
|
380
|
+
affinities: ["metatronsCube", "platonicSolid", "hexagon", "triangle"],
|
|
381
|
+
category: "procedural",
|
|
382
|
+
heroCandidate: true,
|
|
383
|
+
bestStyles: ["stroke-only", "dashed", "double-stroke"],
|
|
384
|
+
},
|
|
385
|
+
penroseTile: {
|
|
386
|
+
tier: 2,
|
|
387
|
+
minSizeFraction: 0.06,
|
|
388
|
+
maxSizeFraction: 0.6,
|
|
389
|
+
affinities: ["diamond", "triangle", "shardField", "voronoiCell"],
|
|
390
|
+
category: "procedural",
|
|
391
|
+
heroCandidate: false,
|
|
392
|
+
bestStyles: ["fill-and-stroke", "fill-only", "double-stroke"],
|
|
393
|
+
},
|
|
394
|
+
reuleauxTriangle: {
|
|
395
|
+
tier: 1,
|
|
396
|
+
minSizeFraction: 0.08,
|
|
397
|
+
maxSizeFraction: 1.0,
|
|
398
|
+
affinities: ["triangle", "circle", "superellipse", "vesicaPiscis"],
|
|
399
|
+
category: "procedural",
|
|
400
|
+
heroCandidate: true,
|
|
401
|
+
bestStyles: ["fill-and-stroke", "fill-only", "watercolor"],
|
|
402
|
+
},
|
|
403
|
+
dotCluster: {
|
|
404
|
+
tier: 3,
|
|
405
|
+
minSizeFraction: 0.05,
|
|
406
|
+
maxSizeFraction: 0.5,
|
|
407
|
+
affinities: ["cloudForm", "inkSplat", "blob"],
|
|
408
|
+
category: "procedural",
|
|
409
|
+
heroCandidate: false,
|
|
410
|
+
bestStyles: ["fill-only", "stipple"],
|
|
411
|
+
},
|
|
412
|
+
crosshatchPatch: {
|
|
413
|
+
tier: 3,
|
|
414
|
+
minSizeFraction: 0.1,
|
|
415
|
+
maxSizeFraction: 0.6,
|
|
416
|
+
affinities: ["voronoiCell", "ngon", "superellipse"],
|
|
417
|
+
category: "procedural",
|
|
418
|
+
heroCandidate: false,
|
|
419
|
+
bestStyles: ["stroke-only", "hatched", "fabric-weave"],
|
|
420
|
+
},
|
|
320
421
|
};
|
|
321
422
|
|
|
322
423
|
// ── Shape palette: curated sets of shapes that work well together ────
|
|
@@ -418,6 +519,50 @@ export function buildShapePalette(
|
|
|
418
519
|
accents,
|
|
419
520
|
};
|
|
420
521
|
}
|
|
522
|
+
if (archetypeName === "shattered-glass") {
|
|
523
|
+
// Favor angular, fragmented shapes
|
|
524
|
+
const shardBoost = available.filter(
|
|
525
|
+
(s) => ["shardField", "voronoiCell", "penroseTile", "diamond", "triangle", "ngon"].includes(s) && !primary.includes(s),
|
|
526
|
+
);
|
|
527
|
+
return {
|
|
528
|
+
primary: [...primary.filter((s) => s !== "blob" && s !== "cloudForm"), ...shardBoost.slice(0, 3)],
|
|
529
|
+
supporting: supporting.filter((s) => s !== "blob" && s !== "cloudForm"),
|
|
530
|
+
accents,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (archetypeName === "botanical") {
|
|
534
|
+
// Favor organic, flowing shapes
|
|
535
|
+
const botanicalBoost = available.filter(
|
|
536
|
+
(s) => ["tendril", "cloudForm", "blob", "crescent", "rose", "inkSplat"].includes(s) && !primary.includes(s),
|
|
537
|
+
);
|
|
538
|
+
return {
|
|
539
|
+
primary: [...primary, ...botanicalBoost.slice(0, 3)],
|
|
540
|
+
supporting,
|
|
541
|
+
accents,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if (archetypeName === "stipple-portrait") {
|
|
545
|
+
// Favor small, dot-friendly shapes
|
|
546
|
+
const stippleBoost = available.filter(
|
|
547
|
+
(s) => ["dotCluster", "circle", "crosshatchPatch", "voronoiCell", "blob"].includes(s) && !primary.includes(s),
|
|
548
|
+
);
|
|
549
|
+
return {
|
|
550
|
+
primary: [...primary, ...stippleBoost.slice(0, 3)],
|
|
551
|
+
supporting,
|
|
552
|
+
accents,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
if (archetypeName === "celestial") {
|
|
556
|
+
// Favor sacred geometry and cosmic shapes
|
|
557
|
+
const celestialBoost = available.filter(
|
|
558
|
+
(s) => ["crescent", "geodesicDome", "mandala", "flowerOfLife", "spirograph", "fibonacciSpiral"].includes(s) && !primary.includes(s),
|
|
559
|
+
);
|
|
560
|
+
return {
|
|
561
|
+
primary: [...primary, ...celestialBoost.slice(0, 3)],
|
|
562
|
+
supporting,
|
|
563
|
+
accents,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
421
566
|
|
|
422
567
|
return { primary, supporting, accents };
|
|
423
568
|
}
|