git-hash-art 0.12.0 → 0.13.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/CHANGELOG.md +8 -0
- package/dist/browser.js +85 -85
- package/dist/browser.js.map +1 -1
- package/dist/main.js +85 -85
- package/dist/main.js.map +1 -1
- package/dist/module.js +85 -85
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/phase-breakdown.test.ts +44 -0
- package/src/lib/render.ts +96 -89
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-phase timing breakdown using _debugTiming instrumentation.
|
|
3
|
+
* This gives exact cost of each pipeline section inside renderHashArt.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { createCanvas } from "@napi-rs/canvas";
|
|
7
|
+
import { renderHashArt } from "../lib/render";
|
|
8
|
+
import type { GenerationConfig } from "../types";
|
|
9
|
+
|
|
10
|
+
const HASHES = [
|
|
11
|
+
{ label: "46192e59", hash: "46192e59d42f741c761cbea79462a8b3815dd905" },
|
|
12
|
+
{ label: "deadbeef", hash: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" },
|
|
13
|
+
{ label: "ff00ff00", hash: "ff00ff00ff00ff00ff00ff00ff00ff00ff00ff0" },
|
|
14
|
+
{ label: "77777777", hash: "7777777777777777777777777777777777777777" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function fmt(ms: number): string {
|
|
18
|
+
return ms < 1 ? `${(ms * 1000).toFixed(0)}µs` : `${ms.toFixed(1)}ms`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("Phase breakdown via _debugTiming", () => {
|
|
22
|
+
for (const { label, hash } of HASHES) {
|
|
23
|
+
it(`${label} at 1024×1024`, () => {
|
|
24
|
+
const canvas = createCanvas(1024, 1024);
|
|
25
|
+
const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
|
|
26
|
+
const timing: GenerationConfig["_debugTiming"] = { phases: {}, shapeCount: 0, extraCount: 0 };
|
|
27
|
+
const config: Partial<GenerationConfig> = { width: 1024, height: 1024, _debugTiming: timing };
|
|
28
|
+
|
|
29
|
+
const start = performance.now();
|
|
30
|
+
renderHashArt(ctx, hash, config);
|
|
31
|
+
const total = performance.now() - start;
|
|
32
|
+
|
|
33
|
+
console.log(`\n ═══ ${label} (${total.toFixed(0)}ms total, ${timing!.shapeCount} shapes, ${timing!.extraCount} extras) ═══`);
|
|
34
|
+
const phases = timing!.phases;
|
|
35
|
+
// Sort by cost descending
|
|
36
|
+
const sorted = Object.entries(phases).sort((a, b) => b[1] - a[1]);
|
|
37
|
+
for (const [name, ms] of sorted) {
|
|
38
|
+
const pct = ((ms / total) * 100).toFixed(1);
|
|
39
|
+
console.log(` ${name.padEnd(25)} ${fmt(ms).padStart(10)} (${pct}%)`);
|
|
40
|
+
}
|
|
41
|
+
expect(total).toBeLessThan(5_000); // was 30s, now targeting <5s
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
});
|
package/src/lib/render.ts
CHANGED
|
@@ -479,6 +479,15 @@ export function renderHashArt(
|
|
|
479
479
|
config: Partial<GenerationConfig> = {},
|
|
480
480
|
): void {
|
|
481
481
|
const finalConfig: GenerationConfig = { ...DEFAULT_CONFIG, ...config };
|
|
482
|
+
const _dt = finalConfig._debugTiming;
|
|
483
|
+
const _t = _dt ? () => performance.now() : undefined;
|
|
484
|
+
let _p = _t ? _t() : 0;
|
|
485
|
+
function _mark(name: string) {
|
|
486
|
+
if (!_dt || !_t) return;
|
|
487
|
+
const now = _t();
|
|
488
|
+
_dt.phases[name] = (now - _p);
|
|
489
|
+
_p = now;
|
|
490
|
+
}
|
|
482
491
|
|
|
483
492
|
const rng = createRng(seedFromHash(gitHash));
|
|
484
493
|
|
|
@@ -530,13 +539,16 @@ export function renderHashArt(
|
|
|
530
539
|
const cx = width / 2;
|
|
531
540
|
const cy = height / 2;
|
|
532
541
|
|
|
542
|
+
_mark("0_setup");
|
|
543
|
+
|
|
533
544
|
// ── 1. Background ──────────────────────────────────────────────
|
|
534
545
|
const bgRadius = Math.hypot(cx, cy);
|
|
535
546
|
drawBackground(ctx, archetype.backgroundStyle, bgStart, bgEnd, width, height, cx, cy, bgRadius, rng, colors);
|
|
536
547
|
|
|
537
548
|
// Gradient mesh overlay — 3-4 color control points for richer backgrounds
|
|
549
|
+
// Use source-over instead of soft-light for cheaper compositing
|
|
538
550
|
const meshPoints = 3 + Math.floor(rng() * 2);
|
|
539
|
-
ctx.
|
|
551
|
+
ctx.globalAlpha = 1;
|
|
540
552
|
for (let i = 0; i < meshPoints; i++) {
|
|
541
553
|
const mx = rng() * width;
|
|
542
554
|
const my = rng() * height;
|
|
@@ -545,104 +557,114 @@ export function renderHashArt(
|
|
|
545
557
|
const grad = ctx.createRadialGradient(mx, my, 0, mx, my, mRadius);
|
|
546
558
|
grad.addColorStop(0, hexWithAlpha(mColor, 0.08 + rng() * 0.06));
|
|
547
559
|
grad.addColorStop(1, "rgba(0,0,0,0)");
|
|
548
|
-
ctx.globalAlpha = 1;
|
|
549
560
|
ctx.fillStyle = grad;
|
|
550
|
-
|
|
561
|
+
// Clip to gradient bounding box — avoids blending transparent pixels
|
|
562
|
+
const gx = Math.max(0, mx - mRadius);
|
|
563
|
+
const gy = Math.max(0, my - mRadius);
|
|
564
|
+
const gw = Math.min(width, mx + mRadius) - gx;
|
|
565
|
+
const gh = Math.min(height, my + mRadius) - gy;
|
|
566
|
+
ctx.fillRect(gx, gy, gw, gh);
|
|
551
567
|
}
|
|
552
|
-
ctx.globalCompositeOperation = "source-over";
|
|
553
568
|
|
|
554
569
|
// Compute average background luminance for contrast enforcement
|
|
555
570
|
const bgLum = (luminance(bgStart) + luminance(bgEnd)) / 2;
|
|
556
571
|
|
|
557
572
|
// ── 1b. Layered background — archetype-coherent shapes ─────────
|
|
573
|
+
// Use source-over with pre-multiplied alpha instead of soft-light
|
|
574
|
+
// for much cheaper compositing (soft-light requires per-pixel blend)
|
|
558
575
|
const bgShapeCount = 3 + Math.floor(rng() * 4);
|
|
559
|
-
ctx.globalCompositeOperation = "soft-light";
|
|
560
576
|
for (let i = 0; i < bgShapeCount; i++) {
|
|
561
577
|
const bx = rng() * width;
|
|
562
578
|
const by = rng() * height;
|
|
563
579
|
const bSize = (width * 0.3 + rng() * width * 0.5);
|
|
564
580
|
const bColor = pickHierarchyColor(colorHierarchy, rng);
|
|
565
|
-
ctx.globalAlpha = 0.03 + rng() * 0.05;
|
|
581
|
+
ctx.globalAlpha = (0.03 + rng() * 0.05) * 0.5; // halved to compensate for source-over vs soft-light
|
|
566
582
|
ctx.fillStyle = hexWithAlpha(bColor, 0.15);
|
|
567
583
|
ctx.beginPath();
|
|
568
584
|
// Use archetype-appropriate background shapes
|
|
569
585
|
if (archetype.name === "geometric-precision" || archetype.name === "op-art") {
|
|
570
|
-
// Rectangular shapes for geometric archetypes
|
|
571
586
|
ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
|
|
572
587
|
} else {
|
|
573
588
|
ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
|
|
574
589
|
}
|
|
575
590
|
ctx.fill();
|
|
576
591
|
}
|
|
577
|
-
// Subtle concentric rings from center
|
|
592
|
+
// Subtle concentric rings from center — batched into single stroke
|
|
578
593
|
const ringCount = 2 + Math.floor(rng() * 3);
|
|
579
594
|
ctx.globalAlpha = 0.02 + rng() * 0.03;
|
|
580
595
|
ctx.strokeStyle = hexWithAlpha(colorHierarchy.dominant, 0.1);
|
|
581
596
|
ctx.lineWidth = 1 * scaleFactor;
|
|
597
|
+
ctx.beginPath();
|
|
582
598
|
for (let i = 1; i <= ringCount; i++) {
|
|
583
599
|
const r = (Math.min(width, height) * 0.15) * i;
|
|
584
|
-
ctx.
|
|
600
|
+
ctx.moveTo(cx + r, cy);
|
|
585
601
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
586
|
-
ctx.stroke();
|
|
587
602
|
}
|
|
588
|
-
ctx.
|
|
603
|
+
ctx.stroke();
|
|
589
604
|
|
|
590
605
|
// ── 1c. Background pattern layer — subtle textured paper ───────
|
|
591
606
|
const bgPatternRoll = rng();
|
|
592
607
|
if (bgPatternRoll < 0.6) {
|
|
593
608
|
ctx.save();
|
|
594
|
-
ctx.globalCompositeOperation = "soft-light";
|
|
595
609
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
596
610
|
const patternColor = hexWithAlpha(colorHierarchy.dominant, 0.15);
|
|
597
611
|
|
|
598
612
|
if (bgPatternRoll < 0.2) {
|
|
599
|
-
// Dot grid —
|
|
600
|
-
const dotSpacing = Math.max(
|
|
601
|
-
const
|
|
613
|
+
// Dot grid — use fillRect instead of arcs (much cheaper, no path building)
|
|
614
|
+
const dotSpacing = Math.max(12, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
615
|
+
const dotDiam = Math.max(1, Math.round(dotSpacing * 0.16));
|
|
602
616
|
ctx.globalAlpha = patternOpacity;
|
|
603
617
|
ctx.fillStyle = patternColor;
|
|
604
|
-
|
|
605
|
-
for (let px = 0; px < width; px += dotSpacing) {
|
|
606
|
-
for (let py = 0; py < height; py += dotSpacing) {
|
|
607
|
-
ctx.
|
|
608
|
-
|
|
618
|
+
let dotCount = 0;
|
|
619
|
+
for (let px = 0; px < width && dotCount < 2000; px += dotSpacing) {
|
|
620
|
+
for (let py = 0; py < height && dotCount < 2000; py += dotSpacing) {
|
|
621
|
+
ctx.fillRect(px, py, dotDiam, dotDiam);
|
|
622
|
+
dotCount++;
|
|
609
623
|
}
|
|
610
624
|
}
|
|
611
|
-
ctx.fill();
|
|
612
625
|
} else if (bgPatternRoll < 0.4) {
|
|
613
|
-
// Diagonal lines — batched into a single path
|
|
614
|
-
const lineSpacing = Math.max(
|
|
626
|
+
// Diagonal lines — batched into a single path, capped at 300 lines
|
|
627
|
+
const lineSpacing = Math.max(10, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
615
628
|
ctx.globalAlpha = patternOpacity;
|
|
616
629
|
ctx.strokeStyle = patternColor;
|
|
617
630
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
618
631
|
const diag = Math.hypot(width, height);
|
|
619
632
|
ctx.beginPath();
|
|
620
|
-
|
|
633
|
+
let lineCount = 0;
|
|
634
|
+
for (let d = -diag; d < diag && lineCount < 300; d += lineSpacing) {
|
|
621
635
|
ctx.moveTo(d, 0);
|
|
622
636
|
ctx.lineTo(d + height, height);
|
|
637
|
+
lineCount++;
|
|
623
638
|
}
|
|
624
639
|
ctx.stroke();
|
|
625
640
|
} else {
|
|
626
|
-
// Tessellation — hexagonal grid,
|
|
627
|
-
const tessSize = Math.max(
|
|
641
|
+
// Tessellation — hexagonal grid, capped at 500 hexagons
|
|
642
|
+
const tessSize = Math.max(15, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
628
643
|
const tessH = tessSize * Math.sqrt(3);
|
|
629
644
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
630
645
|
ctx.strokeStyle = patternColor;
|
|
631
646
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
647
|
+
// Pre-compute hex vertex offsets (avoid trig per vertex)
|
|
648
|
+
const hexVx: number[] = [];
|
|
649
|
+
const hexVy: number[] = [];
|
|
650
|
+
for (let s = 0; s < 6; s++) {
|
|
651
|
+
const angle = (Math.PI / 3) * s - Math.PI / 6;
|
|
652
|
+
hexVx.push(Math.cos(angle) * tessSize * 0.5);
|
|
653
|
+
hexVy.push(Math.sin(angle) * tessSize * 0.5);
|
|
654
|
+
}
|
|
632
655
|
ctx.beginPath();
|
|
633
|
-
|
|
656
|
+
let hexCount = 0;
|
|
657
|
+
for (let row = 0; row * tessH < height + tessH && hexCount < 500; row++) {
|
|
634
658
|
const offsetX = (row % 2) * tessSize * 0.75;
|
|
635
|
-
for (let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++) {
|
|
659
|
+
for (let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5 && hexCount < 500; col++) {
|
|
636
660
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
637
661
|
const hy = row * tessH;
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const vy = hy + Math.sin(angle) * tessSize * 0.5;
|
|
642
|
-
if (s === 0) ctx.moveTo(vx, vy);
|
|
643
|
-
else ctx.lineTo(vx, vy);
|
|
662
|
+
ctx.moveTo(hx + hexVx[0], hy + hexVy[0]);
|
|
663
|
+
for (let s = 1; s < 6; s++) {
|
|
664
|
+
ctx.lineTo(hx + hexVx[s], hy + hexVy[s]);
|
|
644
665
|
}
|
|
645
666
|
ctx.closePath();
|
|
667
|
+
hexCount++;
|
|
646
668
|
}
|
|
647
669
|
}
|
|
648
670
|
ctx.stroke();
|
|
@@ -651,6 +673,8 @@ export function renderHashArt(
|
|
|
651
673
|
}
|
|
652
674
|
ctx.globalCompositeOperation = "source-over";
|
|
653
675
|
|
|
676
|
+
_mark("1_background");
|
|
677
|
+
|
|
654
678
|
// ── 2. Composition mode — archetype-aware selection ──────────────
|
|
655
679
|
const compositionMode: CompositionMode = rng() < 0.7
|
|
656
680
|
? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)]
|
|
@@ -776,6 +800,8 @@ export function renderHashArt(
|
|
|
776
800
|
}
|
|
777
801
|
ctx.globalAlpha = 1;
|
|
778
802
|
|
|
803
|
+
_mark("2_3_composition_focal");
|
|
804
|
+
|
|
779
805
|
// ── 4. Flow field — simplex noise for organic variation ─────────
|
|
780
806
|
// Create a seeded simplex noise field (unique per hash)
|
|
781
807
|
const noiseFieldRng = createRng(seedFromHash(gitHash, 333));
|
|
@@ -855,6 +881,8 @@ export function renderHashArt(
|
|
|
855
881
|
}
|
|
856
882
|
|
|
857
883
|
|
|
884
|
+
_mark("4_flowfield_hero");
|
|
885
|
+
|
|
858
886
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
859
887
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
860
888
|
|
|
@@ -1337,6 +1365,8 @@ export function renderHashArt(
|
|
|
1337
1365
|
|
|
1338
1366
|
// Reset blend mode for post-processing passes
|
|
1339
1367
|
ctx.globalCompositeOperation = "source-over";
|
|
1368
|
+
if (_dt) { _dt.shapeCount = shapePositions.length; _dt.extraCount = extrasSpent; }
|
|
1369
|
+
_mark("5_shape_layers");
|
|
1340
1370
|
|
|
1341
1371
|
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
1342
1372
|
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
@@ -1405,6 +1435,8 @@ export function renderHashArt(
|
|
|
1405
1435
|
}
|
|
1406
1436
|
|
|
1407
1437
|
|
|
1438
|
+
_mark("5g_portals");
|
|
1439
|
+
|
|
1408
1440
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
1409
1441
|
// Optimized: collect all segments into width-quantized buckets, then
|
|
1410
1442
|
// render each bucket as a single batched path. This reduces
|
|
@@ -1546,6 +1578,8 @@ export function renderHashArt(
|
|
|
1546
1578
|
}
|
|
1547
1579
|
}
|
|
1548
1580
|
|
|
1581
|
+
_mark("6_flow_lines");
|
|
1582
|
+
|
|
1549
1583
|
// ── 6b. Motion/energy lines — short directional bursts ─────────
|
|
1550
1584
|
// Optimized: collect all burst segments, then batch by quantized alpha
|
|
1551
1585
|
const energyArchetypes = ["dense-chaotic", "cosmic", "neon-glow", "bold-graphic"];
|
|
@@ -1605,6 +1639,8 @@ export function renderHashArt(
|
|
|
1605
1639
|
}
|
|
1606
1640
|
}
|
|
1607
1641
|
|
|
1642
|
+
_mark("6b_energy_lines");
|
|
1643
|
+
|
|
1608
1644
|
// ── 6c. Apply symmetry mirroring ─────────────────────────────────
|
|
1609
1645
|
if (symmetryMode !== "none") {
|
|
1610
1646
|
const canvas = ctx.canvas;
|
|
@@ -1627,66 +1663,28 @@ export function renderHashArt(
|
|
|
1627
1663
|
}
|
|
1628
1664
|
|
|
1629
1665
|
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
//
|
|
1666
|
+
_mark("6c_symmetry");
|
|
1667
|
+
|
|
1668
|
+
// ── 7. Noise texture overlay ─────────────────────────────────────
|
|
1669
|
+
// With density capped at 2500 dots, direct fillRect calls are far cheaper
|
|
1670
|
+
// than the getImageData/putImageData round-trip which copies the entire
|
|
1671
|
+
// pixel buffer (4 × width × height bytes) twice.
|
|
1633
1672
|
const noiseRng = createRng(seedFromHash(gitHash, 777));
|
|
1634
1673
|
const rawNoiseDensity = Math.floor((width * height) / 800);
|
|
1635
|
-
// Cap at 2500 dots — beyond this the visual effect is indistinguishable
|
|
1636
|
-
// but getImageData/putImageData cost scales with canvas size
|
|
1637
1674
|
const noiseDensity = Math.min(rawNoiseDensity, 2500);
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
const
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
const ny = Math.floor(noiseRng() * height);
|
|
1648
|
-
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
1649
|
-
// srcA in range [0.01, 0.04] — multiply by 256 for fixed-point
|
|
1650
|
-
const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
|
|
1651
|
-
const invA256 = 256 - srcA256;
|
|
1652
|
-
const bSrc = brightness * srcA256; // pre-multiply brightness × alpha
|
|
1653
|
-
const idx = (ny * width + nx) << 2;
|
|
1654
|
-
data[idx] = (data[idx] * invA256 + bSrc) >> 8;
|
|
1655
|
-
data[idx + 1] = (data[idx + 1] * invA256 + bSrc) >> 8;
|
|
1656
|
-
data[idx + 2] = (data[idx + 2] * invA256 + bSrc) >> 8;
|
|
1657
|
-
}
|
|
1658
|
-
} else {
|
|
1659
|
-
for (let i = 0; i < noiseDensity; i++) {
|
|
1660
|
-
const nx = Math.floor(noiseRng() * width);
|
|
1661
|
-
const ny = Math.floor(noiseRng() * height);
|
|
1662
|
-
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
1663
|
-
const srcA256 = Math.round((0.01 + noiseRng() * 0.03) * 256);
|
|
1664
|
-
const invA256 = 256 - srcA256;
|
|
1665
|
-
const bSrc = brightness * srcA256;
|
|
1666
|
-
for (let dy = 0; dy < pixelScale && ny + dy < height; dy++) {
|
|
1667
|
-
for (let dx = 0; dx < pixelScale && nx + dx < width; dx++) {
|
|
1668
|
-
const idx = ((ny + dy) * width + (nx + dx)) << 2;
|
|
1669
|
-
data[idx] = (data[idx] * invA256 + bSrc) >> 8;
|
|
1670
|
-
data[idx + 1] = (data[idx + 1] * invA256 + bSrc) >> 8;
|
|
1671
|
-
data[idx + 2] = (data[idx + 2] * invA256 + bSrc) >> 8;
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
ctx.putImageData(imageData, 0, 0);
|
|
1677
|
-
} catch {
|
|
1678
|
-
// Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
|
|
1679
|
-
for (let i = 0; i < noiseDensity; i++) {
|
|
1680
|
-
const nx = noiseRng() * width;
|
|
1681
|
-
const ny = noiseRng() * height;
|
|
1682
|
-
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
1683
|
-
const alpha = 0.01 + noiseRng() * 0.03;
|
|
1684
|
-
ctx.globalAlpha = alpha;
|
|
1685
|
-
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
1686
|
-
ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
|
|
1687
|
-
}
|
|
1675
|
+
const pixelScale = Math.max(1, Math.round(scaleFactor));
|
|
1676
|
+
for (let i = 0; i < noiseDensity; i++) {
|
|
1677
|
+
const nx = noiseRng() * width;
|
|
1678
|
+
const ny = noiseRng() * height;
|
|
1679
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
1680
|
+
const alpha = 0.01 + noiseRng() * 0.03;
|
|
1681
|
+
ctx.globalAlpha = alpha;
|
|
1682
|
+
ctx.fillStyle = `rgb(${brightness},${brightness},${brightness})`;
|
|
1683
|
+
ctx.fillRect(nx, ny, pixelScale, pixelScale);
|
|
1688
1684
|
}
|
|
1689
1685
|
|
|
1686
|
+
_mark("7_noise_texture");
|
|
1687
|
+
|
|
1690
1688
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
1691
1689
|
ctx.globalAlpha = 1;
|
|
1692
1690
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
@@ -1702,6 +1700,8 @@ export function renderHashArt(
|
|
|
1702
1700
|
ctx.fillStyle = vigGrad;
|
|
1703
1701
|
ctx.fillRect(0, 0, width, height);
|
|
1704
1702
|
|
|
1703
|
+
_mark("8_vignette");
|
|
1704
|
+
|
|
1705
1705
|
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
1706
1706
|
// Optimized: batch all curves into alpha-quantized groups to reduce
|
|
1707
1707
|
// beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
|
|
@@ -1770,6 +1770,8 @@ export function renderHashArt(
|
|
|
1770
1770
|
}
|
|
1771
1771
|
}
|
|
1772
1772
|
|
|
1773
|
+
_mark("9_connecting_curves");
|
|
1774
|
+
|
|
1773
1775
|
// ── 10. Post-processing ────────────────────────────────────────
|
|
1774
1776
|
|
|
1775
1777
|
// 10a. Color grading — unified tone across the whole image
|
|
@@ -1827,6 +1829,8 @@ export function renderHashArt(
|
|
|
1827
1829
|
ctx.globalCompositeOperation = "source-over";
|
|
1828
1830
|
}
|
|
1829
1831
|
|
|
1832
|
+
_mark("10_post_processing");
|
|
1833
|
+
|
|
1830
1834
|
// ── 10e. Generative borders — archetype-driven decorative frames ──
|
|
1831
1835
|
{
|
|
1832
1836
|
ctx.save();
|
|
@@ -1973,6 +1977,8 @@ export function renderHashArt(
|
|
|
1973
1977
|
ctx.restore();
|
|
1974
1978
|
}
|
|
1975
1979
|
|
|
1980
|
+
_mark("10e_borders");
|
|
1981
|
+
|
|
1976
1982
|
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
1977
1983
|
{
|
|
1978
1984
|
const sigRng = createRng(seedFromHash(gitHash, 42));
|
|
@@ -2036,5 +2042,6 @@ export function renderHashArt(
|
|
|
2036
2042
|
}
|
|
2037
2043
|
|
|
2038
2044
|
ctx.globalAlpha = 1;
|
|
2045
|
+
_mark("11_signature");
|
|
2039
2046
|
|
|
2040
2047
|
}
|
package/src/types.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface GenerationConfig {
|
|
|
20
20
|
opacityReduction: number;
|
|
21
21
|
/** Base shapes per layer — defaults to gridSize² × 1.5 when 0 */
|
|
22
22
|
shapesPerLayer: number;
|
|
23
|
+
/** Internal: collect per-phase timing data when set (not part of public API) */
|
|
24
|
+
_debugTiming?: { phases: Record<string, number>; shapeCount: number; extraCount: number };
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export const DEFAULT_CONFIG: GenerationConfig = {
|