git-hash-art 0.11.0 → 0.12.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 CHANGED
@@ -26,14 +26,15 @@ Hash String
26
26
  0c. Shape Palette (curated via affinity graph)
27
27
  0d. Color Grade (hue + intensity for final tone)
28
28
  0e. Light Direction (consistent shadow angle)
29
+ 0f. Palette Evolution Direction (±20° hue drift across layers)
29
30
  1. Background Layer (7 styles: radial, linear, solid, multi-stop)
30
31
  └─ Gradient Mesh Overlay (3-4 radial color control points)
31
32
  1a. Background Luminance → contrast enforcement threshold
32
33
  1b. Layered Background (archetype-coherent shapes + concentric rings)
33
34
  1c. Background Pattern Layer (dot grid / diagonal lines / tessellation)
34
- 2. Composition Mode Selection (6 modes including golden-spiral)
35
+ 2. Composition Mode Selection (archetype-aware, 6 modes including golden-spiral)
35
36
  2b. Symmetry Mode Selection (none / bilateral / quad)
36
- 3. Focal Points (rule-of-thirds biased) + Void Zones
37
+ 3. Focal Points (rule-of-thirds biased) + Void Zones (archetype-aware density)
37
38
  3b. Void Zone Decoration (halos, scattered dots, concentric rings)
38
39
  4. Flow Field Initialization (simplex noise FBM)
39
40
  4a. Noise Size Modulation (terrain-like size variation from noise field)
@@ -54,26 +55,27 @@ Hash String
54
55
  │ ├─ Shadow & Highlight (drop shadow + specular highlight per shape)
55
56
  │ ├─ Organic Edges (~15% watercolor bleed)
56
57
  │ ├─ Edge Erosion (watercolor + hand-drawn styles get irregular boundary bites)
57
- │ ├─ 5a. Tangent Placement (~25% nudge toward nearest shape edge)
58
+ │ ├─ 5a. Tangent Placement (~25% nudge toward nearest shape edge, spatial grid lookup)
58
59
  │ ├─ 5b. Shape Mirroring (~40% of basic shapes get reflected copies)
59
60
  │ ├─ 5c. Size Echo (~20% of large shapes spawn trailing copies)
60
61
  │ ├─ 5d. Recursive Nesting (~15% of large shapes, palette-aware)
61
- └─ 5e. Shape Constellations (~12% of large shapes, pre-composed groups)
62
- 5f. Layered Masking / Cutout Portals (~18% of images)
63
- 6. Flow-Line Pass (variable color, pressure, branching)
62
+ ├─ 5e. Shape Constellations (~12% of large shapes, pre-composed groups)
63
+ │ └─ 5f. Rhythm Placement (~12% of medium-large shapes, geometric progressions)
64
+ 5g. Layered Masking / Cutout Portals (~18% of images)
65
+ 6. Flow-Line Pass (variable color, pressure, branching, void-aware)
64
66
  6b. Motion/Energy Lines (directional bursts from shapes)
65
67
  6c. Layered Transparency / Glazing (~20% of shapes get multi-pass redraws)
66
68
  7. Symmetry Mirroring (bilateral-x, bilateral-y, or quad)
67
- 8. Noise Texture Overlay
68
- 9. Vignette (radial edge darkening)
69
- 10. Organic Connecting Curves
69
+ 8. Noise Texture Overlay (batched ImageData buffer)
70
+ 9. Vignette (palette-tinted radial edge darkening)
71
+ 10. Organic Connecting Curves (proximity-aware)
70
72
  11. Post-Processing
71
73
  ├─ Color Grading (unified tone overlay)
72
74
  ├─ Chromatic Aberration (neon/cosmic/ethereal only)
73
75
  ├─ Bloom (neon/cosmic only)
74
76
  └─ Gradient Map (~35% chance, luminance-mapped two-color overlay)
75
77
  11b. Generative Borders (archetype-driven decorative frames)
76
- 12. Signature Mark (deterministic geometric chop mark)
78
+ 12. Signature Mark (density-aware placement, deterministic geometric chop mark)
77
79
  ```
78
80
 
79
81
  ## 1. Deterministic RNG
@@ -116,6 +118,7 @@ Each archetype controls:
116
118
  | `heroShape` | Whether to draw a dominant focal shape |
117
119
  | `glowMultiplier` | Glow probability scaling (0 = none, 3 = heavy) |
118
120
  | `sizePower` | Size distribution curve (0.5 = uniform, 2.8 = many tiny) |
121
+ | `preferredCompositions` | Weighted composition mode selection (e.g., radial, golden-spiral) |
119
122
 
120
123
  ### The 17 Archetypes
121
124
 
@@ -155,7 +158,7 @@ Each archetype controls:
155
158
 
156
159
  - **Blend ratio:** 25–50% (the secondary archetype never dominates)
157
160
  - **Numeric parameters** (`gridSize`, `layers`, `baseOpacity`, `minShapeSize`, etc.) are linearly interpolated between the two archetypes
158
- - **Style arrays** (`preferredStyles`) are merged — the primary's styles come first, then any unique styles from the secondary
161
+ - **Style arrays** (`preferredStyles`, `preferredCompositions`) are merged — the primary's entries come first, then any unique entries from the secondary
159
162
  - **Categorical parameters** (`backgroundStyle`, `paletteMode`) use the primary's value when the blend ratio is below 50%, otherwise the secondary's
160
163
  - **Boolean parameters** (`heroShape`) use the primary's value
161
164
 
@@ -192,9 +195,9 @@ Color generation uses the `color-scheme` library seeded from the hash, then appl
192
195
 
193
196
  After generating the raw palette, colors are organized into a **hierarchy** with weighted selection:
194
197
 
195
- - **Dominant (60%)** — the most-used color, selected as the palette entry closest to the average hue
196
- - **Secondary (25%)** — the color most distant from the dominant
197
- - **Accent (15%)** — a remaining color for visual punctuation
198
+ - **Dominant (60%)** — the most-used color, selected as the palette entry with the highest **chroma** (saturation × lightness vibrancy, where vibrancy peaks at L=0.5). This picks the most visually prominent color rather than the one closest to the average hue.
199
+ - **Accent (15%)** — the color most distant from the dominant in hue, providing maximum contrast for visual punctuation
200
+ - **Secondary (25%)** — the remaining color with the highest saturation
198
201
 
199
202
  `pickHierarchyColor(hierarchy, rng)` rolls against these weights so compositions naturally converge on a dominant tone without being monotonous.
200
203
 
@@ -328,7 +331,7 @@ The average luminance of the two background colors is computed and stored. This
328
331
 
329
332
  ### Composition Modes
330
333
 
331
- Each image uses one of 5 composition strategies for shape placement:
334
+ Each image uses one of 6 composition strategies for shape placement. Composition mode selection is **archetype-aware**: each archetype declares a `preferredCompositions` array, and selection is 70% from the archetype's preferred modes / 30% from the full set. This ensures archetypes get compositions that suit their character while preserving variety.
332
335
 
333
336
  | Mode | Behavior |
334
337
  | ---- | -------- |
@@ -358,7 +361,12 @@ Symmetry is applied by drawing the canvas image onto itself with `scale(-1, 1)`
358
361
 
359
362
  1–2 **focal points** are placed with 70% bias toward rule-of-thirds intersections (the remaining 30% are placed randomly within the central 60% of the canvas). Each focal point has a `strength` (0.3–0.7) that pulls nearby shapes toward it via `applyFocalBias`.
360
363
 
361
- 1–2 **void zones** are placed randomly. Shapes landing inside a void zone have an 85% chance of being skipped, creating breathing room in the composition.
364
+ **Void zones** are placed with **archetype-aware logic**:
365
+ - **Dense archetypes** (gridSize ≥ 8, e.g., `dense-chaotic`, `cosmic`, `op-art`, `stipple-portrait`): **0 void zones** — these archetypes thrive on density, and voids would undermine their character
366
+ - **Minimal archetypes** (gridSize ≤ 3, e.g., `minimal-spacious`, `bold-graphic`, `watercolor-wash`): 1–2 void zones placed at **golden-ratio positions** (1/φ and 1−1/φ along each axis, with slight jitter) for intentional, harmonious negative space
367
+ - **Other archetypes**: 1–2 void zones placed randomly within the central 70% of the canvas
368
+
369
+ Shapes landing inside a void zone have an 85% chance of being skipped, creating breathing room in the composition.
362
370
 
363
371
  ### Void Zone Decoration
364
372
 
@@ -410,7 +418,7 @@ For each shape in a layer:
410
418
 
411
419
  1. **Position:** Composition mode generates a candidate position, then `applyFocalBias` pulls it toward the nearest focal point
412
420
  2. **Void check:** 85% skip chance if inside a void zone
413
- 3. **Density check:** If local density exceeds 15% of `shapesPerLayer`, 60% skip chance
421
+ 3. **Density check:** Uses the **spatial hash grid** (see Performance section) for O(1) lookups — if local density exceeds 15% of `shapesPerLayer`, 60% skip chance
414
422
  4. **Size:** Power-curve distribution controlled by `archetype.sizePower` — higher values produce more small shapes
415
423
  5. **Shape selection:** `pickShapeFromPalette` with size-constraint filtering
416
424
  6. **Rotation:** Flow-field angle in flow-field mode (±15° jitter); random otherwise
@@ -435,7 +443,7 @@ Every shape receives lighting effects based on a single consistent light directi
435
443
  **Specular Highlight:**
436
444
  - Position: 15% of shape size along the light direction from center
437
445
  - Radius: 35% of shape size
438
- - Gradient: white at 18% opacity → 5% → 0% (radial falloff)
446
+ - Gradient: **tinted** highlight at 18% opacity → 5% → 0% (radial falloff). The highlight color blends 15% of the shape's fill color RGB with 85% white, producing warm or cool highlights that match the shape rather than pure white spots.
439
447
  - Composited via `soft-light` to interact naturally with the shape's fill color
440
448
  - Only applied to shapes larger than 15px (tiny shapes don't benefit)
441
449
 
@@ -443,7 +451,7 @@ The combination creates a subtle 3D effect where shapes appear to float above th
443
451
 
444
452
  ### 5a. Tangent Placement
445
453
 
446
- ~25% of shapes are nudged toward the nearest previously-placed shape so their edges "kiss." The algorithm finds the nearest shape, computes the target distance (sum of half-sizes), and repositions the current shape along the angle between them. This creates organic clustering where shapes feel intentionally arranged rather than randomly scattered.
454
+ ~25% of shapes are nudged toward the nearest previously-placed shape so their edges "kiss." The algorithm uses the **spatial hash grid** for O(1) nearest-neighbor lookup (searching within 3× `adjustedMaxSize`), computes the target distance (sum of half-sizes), and repositions the current shape along the angle between them. This creates organic clustering where shapes feel intentionally arranged rather than randomly scattered.
447
455
 
448
456
  ### 5b. Shape Mirroring
449
457
 
@@ -480,7 +488,20 @@ The mirror copy is drawn at 70% opacity and 95% size (decreasing for radial-4),
480
488
 
481
489
  Each member shape uses hierarchy colors with HSL jitter and affinity-aware render styles. Members that fall outside the canvas bounds are skipped.
482
490
 
483
- ### 5f. Layered Masking / Cutout Portals
491
+ ### 5f. Rhythm Placement
492
+
493
+ ~12% of shapes larger than 25% of `adjustedMaxSize` spawn a **rhythmic sequence** — a deliberate geometric progression of 3–6 copies of the same shape along a line:
494
+
495
+ - **Direction:** Random angle from the parent shape
496
+ - **Spacing:** 80–140% of the parent shape's size between each copy
497
+ - **Size decay:** Each copy is 70–85% the size of the previous one, creating a diminishing progression
498
+ - **Rotation:** Progressive rotation (+12° per step) adds a gentle twist to the sequence
499
+ - **Void awareness:** The sequence stops if a copy would land inside a void zone
500
+ - **Bounds check:** Copies that would fall outside the canvas are skipped, ending the sequence
501
+
502
+ Rhythm placement creates intentional visual patterns — like a trail of stepping stones or a musical crescendo rendered in geometry. The consistent shape and progressive scaling give the eye a clear path to follow.
503
+
504
+ ### 5g. Layered Masking / Cutout Portals
484
505
 
485
506
  ~18% of images receive 1–3 **portal cutouts** — shape-sized windows that paint over the foreground layers with a background wash, creating a "peek through" depth effect.
486
507
 
@@ -587,6 +608,7 @@ Flow lines follow a **simplex noise** vector field defined by `flowAngle(x, y)`.
587
608
  Each flow line:
588
609
  - Starts at a random position
589
610
  - Takes 30–70 steps along the flow field (±0.3 radians jitter per step)
611
+ - **Void zone awareness:** Segments that pass through void zones are skipped (the line continues on the other side, maintaining continuity without drawing through negative space)
590
612
  - **Variable color:** Interpolates between two hierarchy colors along the stroke length
591
613
  - **Pressure simulation:** Line width oscillates sinusoidally (frequency 2–6 cycles, amplitude ±40%) to simulate pen pressure
592
614
  - **Taper:** Width and opacity decrease toward the end (80% taper over the stroke length)
@@ -600,14 +622,22 @@ After all shapes, flow lines, and symmetry mirroring:
600
622
 
601
623
  Deterministic noise (separate RNG seeded from hash + salt 777) scatters single-pixel dots across the canvas. Density scales with canvas area (~1 dot per 800px²). Each dot is black or white at 1–4% opacity, adding subtle film grain.
602
624
 
625
+ The noise is applied via **ImageData buffer manipulation** — reading the canvas pixels into a typed array, alpha-blending each noise dot directly in memory, then writing the buffer back with `putImageData`. This is significantly faster than thousands of individual `fillRect` calls. A try/catch fallback uses the old per-pixel `fillRect` approach for environments where `getImageData` isn't available (e.g., some OffscreenCanvas implementations).
626
+
603
627
  ### Vignette
604
628
 
605
- A radial gradient darkens the edges: transparent at center, ramping to 25–45% black at the corners. This draws the eye inward toward the focal points.
629
+ A radial gradient darkens the edges: transparent at center, ramping to 25–45% opacity at the corners. The vignette color is **palette-tinted** based on background luminance:
630
+ - **Light backgrounds** (luminance > 0.5): warm sepia tone `rgba(80,60,30,...)` — avoids the harsh look of black vignette on light art
631
+ - **Dark backgrounds**: classic black `rgba(0,0,0,...)` — the traditional approach
632
+
633
+ This draws the eye inward toward the focal points while maintaining tonal coherence with the background.
606
634
 
607
635
  ### Organic Connecting Curves
608
636
 
609
637
  Quadratic Bézier curves connect random pairs of placed shapes. The control point is offset perpendicular to the line between shapes by up to 40% of their distance, creating organic arcs. Count scales with canvas area (~8 per megapixel). Drawn at 6–16% opacity using hierarchy colors.
610
638
 
639
+ Connections are **proximity-aware** — only shapes within 20% of the canvas diagonal are connected. Distant pairs are skipped, preventing long straight-ish lines that cross the entire composition and break the organic feel.
640
+
611
641
  ### Color Grading
612
642
 
613
643
  A `soft-light` overlay in a single hue (random, 40% saturation, 50% lightness) at low intensity (grade intensity × 25% opacity). This unifies the image's color temperature — like applying a photo filter.
@@ -647,15 +677,37 @@ Border elements use hierarchy colors at very low opacity (10–25%) so they fram
647
677
 
648
678
  ## 13. Signature Mark
649
679
 
650
- The final rendering step places a small deterministic **chop mark** (inspired by East Asian seal stamps) in the bottom-right corner of the canvas:
680
+ The final rendering step places a small deterministic **chop mark** (inspired by East Asian seal stamps) in the **least-dense corner** of the canvas:
651
681
 
652
682
  - **RNG isolation:** Uses a separate RNG seeded with a salt of 42, so the signature is independent of all other rendering decisions
653
- - **Position:** Bottom-right corner with a small margin (3% of canvas size)
683
+ - **Position:** The spatial hash grid evaluates all 4 corners (top-left, top-right, bottom-left, bottom-right) via `countNear()` within a 5× signature-size radius. The corner with the lowest shape density is selected. Falls back to bottom-right if all corners are equally empty.
654
684
  - **Size:** 1.5–2.5% of the shorter canvas dimension
655
685
  - **Structure:**
656
686
  1. Outer circle ring (stroke-only) at 12–20% opacity
657
- 2. 24 inner lines crossing the circle at unique angles derived from the hash, creating a distinctive geometric pattern
687
+ 2. 47 inner lines crossing the circle at unique angles derived from the hash, creating a distinctive geometric pattern
658
688
  3. Center dot at slightly higher opacity
659
689
  - **Color:** Uses the accent color from the hierarchy at very low opacity so it's visible but never distracting
660
690
 
661
691
  The signature is unique per hash (different inner line patterns) but always recognizable as a chop mark, giving each generated image a subtle artist's stamp.
692
+
693
+ ## 14. Performance
694
+
695
+ Several hot paths in the rendering pipeline are optimized for throughput:
696
+
697
+ ### Spatial Hash Grid
698
+
699
+ A `SpatialGrid` class provides O(1) amortized density checks and nearest-neighbor lookups, replacing the original O(n²) linear scans over all placed shapes. The grid cell size matches the density check radius (~8% of the shorter canvas dimension). Three operations are used throughout the pipeline:
700
+
701
+ - `insert(item)` — adds a shape to the grid cell containing its position
702
+ - `countNear(x, y, radius)` — counts shapes within a radius by scanning only the relevant grid cells
703
+ - `findNearest(x, y, searchRadius)` — returns the closest shape within a search radius
704
+
705
+ The grid is used for: density-based shape skipping (step 5), tangent placement nearest-neighbor lookup (step 5a), and signature corner selection (step 12).
706
+
707
+ ### Batched Background Patterns
708
+
709
+ The three background pattern types (dot grid, diagonal lines, hexagonal tessellation) batch all geometry into a single `beginPath()` / `fill()` or `stroke()` call instead of issuing per-element draw commands. For a 2048×2048 canvas with a dot grid at 30px spacing, this reduces ~4,600 individual `arc()`+`fill()` calls to a single batched path.
710
+
711
+ ### ImageData Noise Overlay
712
+
713
+ The noise texture pass reads the canvas into an `ImageData` buffer, alpha-blends each noise dot directly in the typed array, then writes the buffer back with a single `putImageData()`. This avoids thousands of individual `fillRect()` calls and the associated canvas state changes (globalAlpha, fillStyle). A try/catch fallback preserves compatibility with environments that don't support `getImageData`.
package/CHANGELOG.md CHANGED
@@ -4,12 +4,20 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### [0.12.0](https://github.com/gfargo/git-hash-art/compare/0.11.0...0.12.0)
8
+
9
+ - perf: cross-env rendering optimizations round 2 [`#22`](https://github.com/gfargo/git-hash-art/pull/22)
10
+ - perf: optimize rendering pipeline — batch flow lines, cap clip-heavy styles, cache color parsing [`#21`](https://github.com/gfargo/git-hash-art/pull/21)
11
+ - docs: update ALGORITHM.md to reflect rendering pipeline changes [`2631e0c`](https://github.com/gfargo/git-hash-art/commit/2631e0c10b2b6bfb0a98c6a31d71d7ddaa8e7511)
12
+
7
13
  #### [0.11.0](https://github.com/gfargo/git-hash-art/compare/0.10.1...0.11.0)
8
14
 
15
+ > 19 March 2026
16
+
9
17
  - feat: rendering pipeline performance & artistic quality improvements [`#20`](https://github.com/gfargo/git-hash-art/pull/20)
10
18
  - Allow manual version input for deployment [`94df05e`](https://github.com/gfargo/git-hash-art/commit/94df05e6d6dddc36f126c9cb0043718aad8f828f)
19
+ - chore: release v0.11.0 [`eadf11e`](https://github.com/gfargo/git-hash-art/commit/eadf11e9b402d35c85ec3e445c82d7d882f63d5c)
11
20
  - update deploy workflow to correct repo [`7e89a0f`](https://github.com/gfargo/git-hash-art/commit/7e89a0f92866bc77f7aa1d7f6c5802350031d56f)
12
- - update pnpm to v9 in deployment workflow [`0ab50de`](https://github.com/gfargo/git-hash-art/commit/0ab50debb9b2f7ca1a1137a21913a1e64396df4e)
13
21
 
14
22
  #### [0.10.1](https://github.com/gfargo/git-hash-art/compare/0.10.0...0.10.1)
15
23