git-hash-art 0.8.0 → 0.10.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 +425 -274
- package/CHANGELOG.md +18 -0
- package/bin/cli.js +17 -14
- package/bin/generateVersionComparison.js +353 -0
- package/dist/browser.js +1563 -123
- package/dist/browser.js.map +1 -1
- package/dist/main.js +1563 -123
- package/dist/main.js.map +1 -1
- package/dist/module.js +1563 -123
- package/dist/module.js.map +1 -1
- package/package.json +2 -1
- package/src/lib/archetypes.ts +115 -3
- package/src/lib/canvas/colors.ts +25 -0
- package/src/lib/canvas/draw.ts +348 -1
- package/src/lib/canvas/shapes/affinity.ts +149 -4
- package/src/lib/canvas/shapes/procedural.ts +395 -32
- package/src/lib/render.ts +426 -19
package/src/lib/render.ts
CHANGED
|
@@ -31,13 +31,15 @@ import {
|
|
|
31
31
|
enforceContrast,
|
|
32
32
|
buildColorHierarchy,
|
|
33
33
|
pickHierarchyColor, pickColorGrade,
|
|
34
|
-
type ColorHierarchy
|
|
34
|
+
evolveHierarchy, type ColorHierarchy
|
|
35
35
|
} from "./canvas/colors";
|
|
36
36
|
import {
|
|
37
37
|
enhanceShapeGeneration,
|
|
38
|
+
drawMirroredShape,
|
|
39
|
+
pickMirrorAxis,
|
|
38
40
|
pickBlendMode,
|
|
39
41
|
pickRenderStyle,
|
|
40
|
-
type RenderStyle
|
|
42
|
+
type RenderStyle
|
|
41
43
|
} from "./canvas/draw";
|
|
42
44
|
import { shapes } from "./canvas/shapes";
|
|
43
45
|
import {
|
|
@@ -272,6 +274,92 @@ function drawBackground(
|
|
|
272
274
|
}
|
|
273
275
|
}
|
|
274
276
|
|
|
277
|
+
// ── Shape constellations — pre-composed groups of shapes ────────
|
|
278
|
+
|
|
279
|
+
interface ConstellationDef {
|
|
280
|
+
name: string;
|
|
281
|
+
/** Generate member positions/shapes relative to center */
|
|
282
|
+
build: (rng: () => number, baseSize: number) => Array<{
|
|
283
|
+
dx: number; dy: number; shape: string; size: number; rotation: number;
|
|
284
|
+
}>;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const CONSTELLATIONS: ConstellationDef[] = [
|
|
288
|
+
{
|
|
289
|
+
name: "flanked-triangle",
|
|
290
|
+
build: (rng, baseSize) => {
|
|
291
|
+
const gap = baseSize * (0.6 + rng() * 0.3);
|
|
292
|
+
return [
|
|
293
|
+
{ dx: 0, dy: 0, shape: "triangle", size: baseSize, rotation: rng() * 360 },
|
|
294
|
+
{ dx: -gap, dy: gap * 0.3, shape: "circle", size: baseSize * 0.35, rotation: 0 },
|
|
295
|
+
{ dx: gap, dy: gap * 0.3, shape: "circle", size: baseSize * 0.35, rotation: 0 },
|
|
296
|
+
];
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "hexagon-ring",
|
|
301
|
+
build: (rng, baseSize) => {
|
|
302
|
+
const members: Array<{ dx: number; dy: number; shape: string; size: number; rotation: number }> = [];
|
|
303
|
+
const count = 5 + Math.floor(rng() * 2);
|
|
304
|
+
const ringR = baseSize * 0.6;
|
|
305
|
+
for (let i = 0; i < count; i++) {
|
|
306
|
+
const angle = (i / count) * Math.PI * 2;
|
|
307
|
+
members.push({
|
|
308
|
+
dx: Math.cos(angle) * ringR,
|
|
309
|
+
dy: Math.sin(angle) * ringR,
|
|
310
|
+
shape: "hexagon",
|
|
311
|
+
size: baseSize * (0.25 + rng() * 0.1),
|
|
312
|
+
rotation: (angle * 180) / Math.PI,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return members;
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: "spiral-dots",
|
|
320
|
+
build: (rng, baseSize) => {
|
|
321
|
+
const members: Array<{ dx: number; dy: number; shape: string; size: number; rotation: number }> = [];
|
|
322
|
+
const count = 7 + Math.floor(rng() * 5);
|
|
323
|
+
const turns = 1.5 + rng();
|
|
324
|
+
for (let i = 0; i < count; i++) {
|
|
325
|
+
const t = i / count;
|
|
326
|
+
const angle = t * Math.PI * 2 * turns;
|
|
327
|
+
const r = t * baseSize * 0.7;
|
|
328
|
+
members.push({
|
|
329
|
+
dx: Math.cos(angle) * r,
|
|
330
|
+
dy: Math.sin(angle) * r,
|
|
331
|
+
shape: "circle",
|
|
332
|
+
size: baseSize * (0.08 + (1 - t) * 0.12),
|
|
333
|
+
rotation: 0,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return members;
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "diamond-cluster",
|
|
341
|
+
build: (rng, baseSize) => {
|
|
342
|
+
const gap = baseSize * 0.45;
|
|
343
|
+
return [
|
|
344
|
+
{ dx: 0, dy: -gap, shape: "diamond", size: baseSize * 0.4, rotation: 0 },
|
|
345
|
+
{ dx: gap, dy: 0, shape: "diamond", size: baseSize * 0.35, rotation: 15 },
|
|
346
|
+
{ dx: 0, dy: gap, shape: "diamond", size: baseSize * 0.3, rotation: 30 },
|
|
347
|
+
{ dx: -gap, dy: 0, shape: "diamond", size: baseSize * 0.35, rotation: -15 },
|
|
348
|
+
];
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "crescent-pair",
|
|
353
|
+
build: (rng, baseSize) => {
|
|
354
|
+
const gap = baseSize * 0.5;
|
|
355
|
+
return [
|
|
356
|
+
{ dx: -gap * 0.4, dy: 0, shape: "crescent", size: baseSize * 0.5, rotation: rng() * 30 },
|
|
357
|
+
{ dx: gap * 0.4, dy: 0, shape: "crescent", size: baseSize * 0.45, rotation: 180 + rng() * 30 },
|
|
358
|
+
];
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
];
|
|
362
|
+
|
|
275
363
|
// ── Main render function ────────────────────────────────────────────
|
|
276
364
|
|
|
277
365
|
export function renderHashArt(
|
|
@@ -321,6 +409,9 @@ export function renderHashArt(
|
|
|
321
409
|
// ── 0e. Light direction — consistent shadow angle ──────────────
|
|
322
410
|
const lightAngle = rng() * Math.PI * 2;
|
|
323
411
|
|
|
412
|
+
// ── 0f. Palette evolution — hue drift direction across layers ──
|
|
413
|
+
const paletteHueShift = (rng() - 0.5) * 40; // -20° to +20° total drift
|
|
414
|
+
|
|
324
415
|
const scaleFactor = Math.min(width, height) / 1024;
|
|
325
416
|
const adjustedMinSize = minShapeSize * scaleFactor;
|
|
326
417
|
const adjustedMaxSize = maxShapeSize * scaleFactor;
|
|
@@ -385,6 +476,69 @@ export function renderHashArt(
|
|
|
385
476
|
}
|
|
386
477
|
ctx.globalCompositeOperation = "source-over";
|
|
387
478
|
|
|
479
|
+
// ── 1c. Background pattern layer — subtle textured paper ───────
|
|
480
|
+
const bgPatternRoll = rng();
|
|
481
|
+
if (bgPatternRoll < 0.6) {
|
|
482
|
+
ctx.save();
|
|
483
|
+
ctx.globalCompositeOperation = "soft-light";
|
|
484
|
+
const patternOpacity = 0.02 + rng() * 0.04;
|
|
485
|
+
const patternColor = hexWithAlpha(colorHierarchy.dominant, 0.15);
|
|
486
|
+
|
|
487
|
+
if (bgPatternRoll < 0.2) {
|
|
488
|
+
// Dot grid
|
|
489
|
+
const dotSpacing = Math.max(8, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
490
|
+
const dotR = dotSpacing * 0.08;
|
|
491
|
+
ctx.globalAlpha = patternOpacity;
|
|
492
|
+
ctx.fillStyle = patternColor;
|
|
493
|
+
for (let px = 0; px < width; px += dotSpacing) {
|
|
494
|
+
for (let py = 0; py < height; py += dotSpacing) {
|
|
495
|
+
ctx.beginPath();
|
|
496
|
+
ctx.arc(px, py, dotR, 0, Math.PI * 2);
|
|
497
|
+
ctx.fill();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} else if (bgPatternRoll < 0.4) {
|
|
501
|
+
// Diagonal lines
|
|
502
|
+
const lineSpacing = Math.max(6, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
503
|
+
ctx.globalAlpha = patternOpacity;
|
|
504
|
+
ctx.strokeStyle = patternColor;
|
|
505
|
+
ctx.lineWidth = 0.5 * scaleFactor;
|
|
506
|
+
const diag = Math.hypot(width, height);
|
|
507
|
+
for (let d = -diag; d < diag; d += lineSpacing) {
|
|
508
|
+
ctx.beginPath();
|
|
509
|
+
ctx.moveTo(d, 0);
|
|
510
|
+
ctx.lineTo(d + height, height);
|
|
511
|
+
ctx.stroke();
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
// Tessellation — hexagonal grid of tiny shapes
|
|
515
|
+
const tessSize = Math.max(10, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
516
|
+
const tessH = tessSize * Math.sqrt(3);
|
|
517
|
+
ctx.globalAlpha = patternOpacity * 0.7;
|
|
518
|
+
ctx.strokeStyle = patternColor;
|
|
519
|
+
ctx.lineWidth = 0.4 * scaleFactor;
|
|
520
|
+
for (let row = 0; row * tessH < height + tessH; row++) {
|
|
521
|
+
const offsetX = (row % 2) * tessSize * 0.75;
|
|
522
|
+
for (let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++) {
|
|
523
|
+
const hx = col * tessSize * 1.5 + offsetX;
|
|
524
|
+
const hy = row * tessH;
|
|
525
|
+
ctx.beginPath();
|
|
526
|
+
for (let s = 0; s < 6; s++) {
|
|
527
|
+
const angle = (Math.PI / 3) * s - Math.PI / 6;
|
|
528
|
+
const vx = hx + Math.cos(angle) * tessSize * 0.5;
|
|
529
|
+
const vy = hy + Math.sin(angle) * tessSize * 0.5;
|
|
530
|
+
if (s === 0) ctx.moveTo(vx, vy);
|
|
531
|
+
else ctx.lineTo(vx, vy);
|
|
532
|
+
}
|
|
533
|
+
ctx.closePath();
|
|
534
|
+
ctx.stroke();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
ctx.restore();
|
|
539
|
+
}
|
|
540
|
+
ctx.globalCompositeOperation = "source-over";
|
|
541
|
+
|
|
388
542
|
// ── 2. Composition mode ────────────────────────────────────────
|
|
389
543
|
const compositionMode =
|
|
390
544
|
COMPOSITION_MODES[Math.floor(rng() * COMPOSITION_MODES.length)];
|
|
@@ -447,6 +601,48 @@ export function renderHashArt(
|
|
|
447
601
|
return [rx + (nearest.x - rx) * pull, ry + (nearest.y - ry) * pull];
|
|
448
602
|
}
|
|
449
603
|
|
|
604
|
+
// ── 3b. Void zone decoration — intentional negative space ────
|
|
605
|
+
for (const zone of voidZones) {
|
|
606
|
+
// Subtle halo ring around void zones
|
|
607
|
+
ctx.globalAlpha = 0.04 + rng() * 0.04;
|
|
608
|
+
ctx.strokeStyle = hexWithAlpha(colorHierarchy.accent, 0.2);
|
|
609
|
+
ctx.lineWidth = 1.5 * scaleFactor;
|
|
610
|
+
ctx.beginPath();
|
|
611
|
+
ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
|
|
612
|
+
ctx.stroke();
|
|
613
|
+
|
|
614
|
+
// ~50% chance: scatter tiny dots inside the void
|
|
615
|
+
if (rng() < 0.5) {
|
|
616
|
+
const dotCount = 3 + Math.floor(rng() * 6);
|
|
617
|
+
ctx.globalAlpha = 0.06 + rng() * 0.04;
|
|
618
|
+
ctx.fillStyle = hexWithAlpha(colorHierarchy.secondary, 0.15);
|
|
619
|
+
for (let d = 0; d < dotCount; d++) {
|
|
620
|
+
const angle = rng() * Math.PI * 2;
|
|
621
|
+
const dist = rng() * zone.radius * 0.7;
|
|
622
|
+
const dotR = (1 + rng() * 3) * scaleFactor;
|
|
623
|
+
ctx.beginPath();
|
|
624
|
+
ctx.arc(
|
|
625
|
+
zone.x + Math.cos(angle) * dist,
|
|
626
|
+
zone.y + Math.sin(angle) * dist,
|
|
627
|
+
dotR, 0, Math.PI * 2,
|
|
628
|
+
);
|
|
629
|
+
ctx.fill();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ~30% chance: thin concentric ring inside
|
|
634
|
+
if (rng() < 0.3) {
|
|
635
|
+
ctx.globalAlpha = 0.03 + rng() * 0.03;
|
|
636
|
+
ctx.strokeStyle = hexWithAlpha(colorHierarchy.dominant, 0.1);
|
|
637
|
+
ctx.lineWidth = 0.5 * scaleFactor;
|
|
638
|
+
const innerR = zone.radius * (0.4 + rng() * 0.3);
|
|
639
|
+
ctx.beginPath();
|
|
640
|
+
ctx.arc(zone.x, zone.y, innerR, 0, Math.PI * 2);
|
|
641
|
+
ctx.stroke();
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
ctx.globalAlpha = 1;
|
|
645
|
+
|
|
450
646
|
// ── 4. Flow field seed values ──────────────────────────────────
|
|
451
647
|
const fieldAngleBase = rng() * Math.PI * 2;
|
|
452
648
|
const fieldFreq = 0.5 + rng() * 2;
|
|
@@ -533,6 +729,26 @@ export function renderHashArt(
|
|
|
533
729
|
// Atmospheric desaturation for later layers
|
|
534
730
|
const atmosphericDesat = layerRatio * 0.3;
|
|
535
731
|
|
|
732
|
+
// Depth-of-field simulation — later layers are "further away"
|
|
733
|
+
// Reduce stroke widths and shift colors toward the background
|
|
734
|
+
const dofFactor = 1 - layerRatio * 0.5; // 1.0 for front layer, 0.5 for back
|
|
735
|
+
const dofStrokeScale = 0.4 + dofFactor * 0.6; // strokes thin out with depth
|
|
736
|
+
const dofContrastReduction = layerRatio * 0.2; // colors fade toward bg
|
|
737
|
+
|
|
738
|
+
// Color palette evolution — hue-rotate the hierarchy per layer
|
|
739
|
+
const layerHierarchy = evolveHierarchy(colorHierarchy, layerRatio, paletteHueShift);
|
|
740
|
+
|
|
741
|
+
// Focal depth: shapes near focal points get more detail
|
|
742
|
+
const focalDetailBoost = (px: number, py: number): number => {
|
|
743
|
+
let minFocalDist = Infinity;
|
|
744
|
+
for (const fp of focalPoints) {
|
|
745
|
+
const d = Math.hypot(px - fp.x, py - fp.y);
|
|
746
|
+
if (d < minFocalDist) minFocalDist = d;
|
|
747
|
+
}
|
|
748
|
+
const maxDist = Math.hypot(width, height) * 0.5;
|
|
749
|
+
return Math.max(0, 1 - minFocalDist / maxDist); // 1.0 at focal, 0.0 at edges
|
|
750
|
+
};
|
|
751
|
+
|
|
536
752
|
for (let i = 0; i < numShapes; i++) {
|
|
537
753
|
// Position from composition mode, then focal bias
|
|
538
754
|
const rawPos = getCompositionPosition(
|
|
@@ -584,9 +800,9 @@ export function renderHashArt(
|
|
|
584
800
|
}
|
|
585
801
|
}
|
|
586
802
|
|
|
587
|
-
// Positional color from hierarchy + jitter
|
|
588
|
-
let fillBase = getPositionalColor(x, y, width, height,
|
|
589
|
-
const strokeBase = pickHierarchyColor(
|
|
803
|
+
// Positional color from hierarchy + jitter (using evolved layer palette)
|
|
804
|
+
let fillBase = getPositionalColor(x, y, width, height, layerHierarchy, rng);
|
|
805
|
+
const strokeBase = pickHierarchyColor(layerHierarchy, rng);
|
|
590
806
|
|
|
591
807
|
// Desaturate colors on later layers for depth
|
|
592
808
|
if (atmosphericDesat > 0) {
|
|
@@ -605,9 +821,11 @@ export function renderHashArt(
|
|
|
605
821
|
const fillAlpha = 0.2 + rng() * 0.5;
|
|
606
822
|
const transparentFill = hexWithAlpha(fillColor, fillAlpha);
|
|
607
823
|
|
|
608
|
-
const strokeWidth = (0.5 + rng() * 2.0) * scaleFactor;
|
|
824
|
+
const strokeWidth = (0.5 + rng() * 2.0) * scaleFactor * dofStrokeScale;
|
|
609
825
|
|
|
610
|
-
|
|
826
|
+
// Depth-of-field: reduce opacity slightly for distant layers
|
|
827
|
+
const dofOpacityScale = 1 - dofContrastReduction;
|
|
828
|
+
ctx.globalAlpha = layerOpacity * (0.5 + rng() * 0.5) * dofOpacityScale;
|
|
611
829
|
|
|
612
830
|
// Glow on sacred shapes more often — scaled by archetype
|
|
613
831
|
const isSacred = SACRED_SHAPES.includes(shape);
|
|
@@ -634,13 +852,47 @@ export function renderHashArt(
|
|
|
634
852
|
const shadowOffX = shadowDist * Math.cos(lightAngle);
|
|
635
853
|
const shadowOffY = shadowDist * Math.sin(lightAngle);
|
|
636
854
|
|
|
637
|
-
|
|
855
|
+
// ── 5a. Tangent placement — nudge toward nearest shape edge ──
|
|
856
|
+
let finalX = x;
|
|
857
|
+
let finalY = y;
|
|
858
|
+
if (shapePositions.length > 0 && rng() < 0.25) {
|
|
859
|
+
// Find nearest placed shape
|
|
860
|
+
let nearestDist = Infinity;
|
|
861
|
+
let nearestPos: { x: number; y: number; size: number } | null = null;
|
|
862
|
+
for (const sp of shapePositions) {
|
|
863
|
+
const d = Math.hypot(x - sp.x, y - sp.y);
|
|
864
|
+
if (d < nearestDist && d > 0) {
|
|
865
|
+
nearestDist = d;
|
|
866
|
+
nearestPos = sp;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
if (nearestPos) {
|
|
870
|
+
// Target distance: edges kissing (sum of half-sizes)
|
|
871
|
+
const targetDist = (size + nearestPos.size) * 0.5;
|
|
872
|
+
if (nearestDist > targetDist * 0.5 && nearestDist < targetDist * 3) {
|
|
873
|
+
const angle = Math.atan2(y - nearestPos.y, x - nearestPos.x);
|
|
874
|
+
finalX = nearestPos.x + Math.cos(angle) * targetDist;
|
|
875
|
+
finalY = nearestPos.y + Math.sin(angle) * targetDist;
|
|
876
|
+
// Keep in bounds
|
|
877
|
+
finalX = Math.max(0, Math.min(width, finalX));
|
|
878
|
+
finalY = Math.max(0, Math.min(height, finalY));
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// ── 5b. Shape mirroring — basic shapes get reflected copies ──
|
|
884
|
+
const mirrorAxis = pickMirrorAxis(rng);
|
|
885
|
+
const isBasicShape = ["circle", "triangle", "square", "hexagon", "star",
|
|
886
|
+
"diamond", "crescent", "penroseTile", "reuleauxTriangle"].includes(shape);
|
|
887
|
+
const shouldMirror = mirrorAxis !== null && isBasicShape && size > adjustedMaxSize * 0.2;
|
|
888
|
+
|
|
889
|
+
const shapeConfig = {
|
|
638
890
|
fillColor: transparentFill,
|
|
639
891
|
strokeColor,
|
|
640
892
|
strokeWidth,
|
|
641
893
|
size,
|
|
642
894
|
rotation,
|
|
643
|
-
proportionType: "GOLDEN_RATIO",
|
|
895
|
+
proportionType: "GOLDEN_RATIO" as const,
|
|
644
896
|
glowRadius: glowRadius || (shadowDist > 0 ? shadowDist * 2 : 0),
|
|
645
897
|
glowColor: hasGlow
|
|
646
898
|
? hexWithAlpha(fillColor, 0.6)
|
|
@@ -648,19 +900,49 @@ export function renderHashArt(
|
|
|
648
900
|
gradientFillEnd: gradientEnd,
|
|
649
901
|
renderStyle: finalRenderStyle,
|
|
650
902
|
rng,
|
|
651
|
-
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
if (shouldMirror) {
|
|
906
|
+
drawMirroredShape(ctx, shape, finalX, finalY, {
|
|
907
|
+
...shapeConfig,
|
|
908
|
+
mirrorAxis: mirrorAxis!,
|
|
909
|
+
mirrorGap: size * (0.1 + rng() * 0.3),
|
|
910
|
+
});
|
|
911
|
+
} else {
|
|
912
|
+
enhanceShapeGeneration(ctx, shape, finalX, finalY, shapeConfig);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
|
|
916
|
+
if (rng() < 0.2 && size > adjustedMinSize * 2) {
|
|
917
|
+
const glazePasses = 2 + Math.floor(rng() * 2);
|
|
918
|
+
for (let g = 0; g < glazePasses; g++) {
|
|
919
|
+
const glazeScale = 1 - (g + 1) * 0.12; // progressively smaller
|
|
920
|
+
const glazeAlpha = 0.08 + g * 0.04; // progressively more opaque toward center
|
|
921
|
+
ctx.globalAlpha = glazeAlpha;
|
|
922
|
+
enhanceShapeGeneration(ctx, shape, finalX, finalY, {
|
|
923
|
+
fillColor: hexWithAlpha(fillColor, 0.15 + g * 0.1),
|
|
924
|
+
strokeColor: "rgba(0,0,0,0)",
|
|
925
|
+
strokeWidth: 0,
|
|
926
|
+
size: size * glazeScale,
|
|
927
|
+
rotation,
|
|
928
|
+
proportionType: "GOLDEN_RATIO",
|
|
929
|
+
renderStyle: "fill-only",
|
|
930
|
+
rng,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
652
934
|
|
|
653
|
-
shapePositions.push({ x, y, size, shape });
|
|
935
|
+
shapePositions.push({ x: finalX, y: finalY, size, shape });
|
|
654
936
|
|
|
655
|
-
// ──
|
|
937
|
+
// ── 5c. Size echo — large shapes spawn trailing smaller copies ──
|
|
656
938
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
657
939
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
658
940
|
const echoAngle = rng() * Math.PI * 2;
|
|
659
941
|
for (let e = 0; e < echoCount; e++) {
|
|
660
942
|
const echoScale = 0.3 - e * 0.08;
|
|
661
943
|
const echoDist = size * (0.6 + e * 0.4);
|
|
662
|
-
const echoX =
|
|
663
|
-
const echoY =
|
|
944
|
+
const echoX = finalX + Math.cos(echoAngle) * echoDist;
|
|
945
|
+
const echoY = finalY + Math.sin(echoAngle) * echoDist;
|
|
664
946
|
const echoSize = size * Math.max(0.1, echoScale);
|
|
665
947
|
|
|
666
948
|
if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
|
|
@@ -680,8 +962,11 @@ export function renderHashArt(
|
|
|
680
962
|
}
|
|
681
963
|
}
|
|
682
964
|
|
|
683
|
-
// ──
|
|
684
|
-
|
|
965
|
+
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
966
|
+
// Focal depth: shapes near focal points get more detail
|
|
967
|
+
const focalProximity = focalDetailBoost(finalX, finalY);
|
|
968
|
+
const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
|
|
969
|
+
if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
|
|
685
970
|
const innerCount = 1 + Math.floor(rng() * 3);
|
|
686
971
|
for (let n = 0; n < innerCount; n++) {
|
|
687
972
|
// Pick inner shape from palette affinities
|
|
@@ -700,8 +985,8 @@ export function renderHashArt(
|
|
|
700
985
|
enhanceShapeGeneration(
|
|
701
986
|
ctx,
|
|
702
987
|
innerShape,
|
|
703
|
-
|
|
704
|
-
|
|
988
|
+
finalX + innerOffX,
|
|
989
|
+
finalY + innerOffY,
|
|
705
990
|
{
|
|
706
991
|
fillColor: innerFill,
|
|
707
992
|
strokeColor: hexWithAlpha(strokeColor, 0.5),
|
|
@@ -715,6 +1000,50 @@ export function renderHashArt(
|
|
|
715
1000
|
);
|
|
716
1001
|
}
|
|
717
1002
|
}
|
|
1003
|
+
|
|
1004
|
+
// ── 5e. Shape constellations — pre-composed groups ─────────
|
|
1005
|
+
const constellationChance = 0.12 + focalProximity * 0.1; // 12-22% near focal
|
|
1006
|
+
if (size > adjustedMaxSize * 0.35 && rng() < constellationChance) {
|
|
1007
|
+
const constellation = CONSTELLATIONS[Math.floor(rng() * CONSTELLATIONS.length)];
|
|
1008
|
+
const members = constellation.build(rng, size);
|
|
1009
|
+
const groupRotation = rng() * Math.PI * 2;
|
|
1010
|
+
const cosR = Math.cos(groupRotation);
|
|
1011
|
+
const sinR = Math.sin(groupRotation);
|
|
1012
|
+
|
|
1013
|
+
for (const member of members) {
|
|
1014
|
+
// Rotate the group offset by the group rotation
|
|
1015
|
+
const mx = finalX + member.dx * cosR - member.dy * sinR;
|
|
1016
|
+
const my = finalY + member.dx * sinR + member.dy * cosR;
|
|
1017
|
+
|
|
1018
|
+
if (mx < 0 || mx > width || my < 0 || my > height) continue;
|
|
1019
|
+
|
|
1020
|
+
const memberFill = hexWithAlpha(
|
|
1021
|
+
jitterColorHSL(pickHierarchyColor(colorHierarchy, rng), rng, 8, 0.06),
|
|
1022
|
+
fillAlpha * 0.8,
|
|
1023
|
+
);
|
|
1024
|
+
const memberStroke = enforceContrast(
|
|
1025
|
+
jitterColorHSL(strokeBase, rng, 5, 0.04), bgLum,
|
|
1026
|
+
);
|
|
1027
|
+
|
|
1028
|
+
ctx.globalAlpha = layerOpacity * 0.6;
|
|
1029
|
+
// Use the member's shape if available, otherwise fall back to palette
|
|
1030
|
+
const memberShape = shapeNames.includes(member.shape)
|
|
1031
|
+
? member.shape
|
|
1032
|
+
: pickShapeFromPalette(shapePalette, rng, member.size / adjustedMaxSize);
|
|
1033
|
+
|
|
1034
|
+
enhanceShapeGeneration(ctx, memberShape, mx, my, {
|
|
1035
|
+
fillColor: memberFill,
|
|
1036
|
+
strokeColor: memberStroke,
|
|
1037
|
+
strokeWidth: strokeWidth * 0.7,
|
|
1038
|
+
size: member.size,
|
|
1039
|
+
rotation: member.rotation + (groupRotation * 180) / Math.PI,
|
|
1040
|
+
proportionType: "GOLDEN_RATIO",
|
|
1041
|
+
renderStyle: pickStyleForShape(memberShape, layerRenderStyle, rng) as RenderStyle,
|
|
1042
|
+
rng,
|
|
1043
|
+
});
|
|
1044
|
+
shapePositions.push({ x: mx, y: my, size: member.size, shape: memberShape });
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
718
1047
|
}
|
|
719
1048
|
}
|
|
720
1049
|
|
|
@@ -801,7 +1130,41 @@ export function renderHashArt(
|
|
|
801
1130
|
}
|
|
802
1131
|
}
|
|
803
1132
|
|
|
804
|
-
// ── 6b.
|
|
1133
|
+
// ── 6b. Motion/energy lines — short directional bursts ─────────
|
|
1134
|
+
const energyArchetypes = ["dense-chaotic", "cosmic", "neon-glow", "bold-graphic"];
|
|
1135
|
+
const hasEnergyLines = energyArchetypes.some(a => archetype.name.includes(a)) || rng() < 0.25;
|
|
1136
|
+
if (hasEnergyLines && shapePositions.length > 0) {
|
|
1137
|
+
const energyCount = 5 + Math.floor(rng() * 10);
|
|
1138
|
+
ctx.lineCap = "round";
|
|
1139
|
+
for (let e = 0; e < energyCount; e++) {
|
|
1140
|
+
// Pick a random shape to radiate from
|
|
1141
|
+
const source = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
1142
|
+
const burstCount = 2 + Math.floor(rng() * 4);
|
|
1143
|
+
const baseAngle = flowAngle(source.x, source.y);
|
|
1144
|
+
|
|
1145
|
+
for (let b = 0; b < burstCount; b++) {
|
|
1146
|
+
const angle = baseAngle + (rng() - 0.5) * 1.2;
|
|
1147
|
+
const lineLen = (source.size * 0.3 + rng() * source.size * 0.5) * scaleFactor * 0.3;
|
|
1148
|
+
const startDist = source.size * 0.5;
|
|
1149
|
+
const sx = source.x + Math.cos(angle) * startDist;
|
|
1150
|
+
const sy = source.y + Math.sin(angle) * startDist;
|
|
1151
|
+
const ex = sx + Math.cos(angle) * lineLen;
|
|
1152
|
+
const ey = sy + Math.sin(angle) * lineLen;
|
|
1153
|
+
|
|
1154
|
+
ctx.globalAlpha = 0.04 + rng() * 0.06;
|
|
1155
|
+
ctx.strokeStyle = hexWithAlpha(
|
|
1156
|
+
enforceContrast(pickHierarchyColor(colorHierarchy, rng), bgLum), 0.3,
|
|
1157
|
+
);
|
|
1158
|
+
ctx.lineWidth = (0.5 + rng() * 1.5) * scaleFactor;
|
|
1159
|
+
ctx.beginPath();
|
|
1160
|
+
ctx.moveTo(sx, sy);
|
|
1161
|
+
ctx.lineTo(ex, ey);
|
|
1162
|
+
ctx.stroke();
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// ── 6c. Apply symmetry mirroring ─────────────────────────────────
|
|
805
1168
|
if (symmetryMode !== "none") {
|
|
806
1169
|
const canvas = ctx.canvas;
|
|
807
1170
|
ctx.save();
|
|
@@ -924,6 +1287,50 @@ export function renderHashArt(
|
|
|
924
1287
|
ctx.globalCompositeOperation = "source-over";
|
|
925
1288
|
}
|
|
926
1289
|
|
|
1290
|
+
// ── 11. Signature mark — unique geometric chop from hash prefix ──
|
|
1291
|
+
{
|
|
1292
|
+
const sigRng = createRng(seedFromHash(gitHash, 42));
|
|
1293
|
+
const sigSize = Math.min(width, height) * 0.025;
|
|
1294
|
+
// Bottom-right corner with padding
|
|
1295
|
+
const sigX = width - sigSize * 2.5;
|
|
1296
|
+
const sigY = height - sigSize * 2.5;
|
|
1297
|
+
const sigSegments = 4 + Math.floor(sigRng() * 4); // 4-7 segments
|
|
1298
|
+
const sigColor = hexWithAlpha(colorHierarchy.accent, 0.15);
|
|
1299
|
+
|
|
1300
|
+
ctx.save();
|
|
1301
|
+
ctx.globalAlpha = 0.12 + sigRng() * 0.08;
|
|
1302
|
+
ctx.translate(sigX, sigY);
|
|
1303
|
+
ctx.strokeStyle = sigColor;
|
|
1304
|
+
ctx.fillStyle = hexWithAlpha(colorHierarchy.dominant, 0.06);
|
|
1305
|
+
ctx.lineWidth = Math.max(0.5, 0.8 * scaleFactor);
|
|
1306
|
+
|
|
1307
|
+
// Outer ring
|
|
1308
|
+
ctx.beginPath();
|
|
1309
|
+
ctx.arc(0, 0, sigSize, 0, Math.PI * 2);
|
|
1310
|
+
ctx.stroke();
|
|
1311
|
+
ctx.fill();
|
|
1312
|
+
|
|
1313
|
+
// Inner geometric pattern — unique per hash
|
|
1314
|
+
ctx.beginPath();
|
|
1315
|
+
for (let s = 0; s < sigSegments; s++) {
|
|
1316
|
+
const angle1 = sigRng() * Math.PI * 2;
|
|
1317
|
+
const angle2 = sigRng() * Math.PI * 2;
|
|
1318
|
+
const r1 = sigSize * (0.2 + sigRng() * 0.6);
|
|
1319
|
+
const r2 = sigSize * (0.2 + sigRng() * 0.6);
|
|
1320
|
+
ctx.moveTo(Math.cos(angle1) * r1, Math.sin(angle1) * r1);
|
|
1321
|
+
ctx.lineTo(Math.cos(angle2) * r2, Math.sin(angle2) * r2);
|
|
1322
|
+
}
|
|
1323
|
+
ctx.stroke();
|
|
1324
|
+
|
|
1325
|
+
// Center dot
|
|
1326
|
+
ctx.beginPath();
|
|
1327
|
+
ctx.arc(0, 0, sigSize * 0.12, 0, Math.PI * 2);
|
|
1328
|
+
ctx.fillStyle = sigColor;
|
|
1329
|
+
ctx.fill();
|
|
1330
|
+
|
|
1331
|
+
ctx.restore();
|
|
1332
|
+
}
|
|
1333
|
+
|
|
927
1334
|
ctx.globalAlpha = 1;
|
|
928
1335
|
|
|
929
1336
|
}
|