git-hash-art 0.2.0 → 0.3.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 ADDED
@@ -0,0 +1,198 @@
1
+ # Art Generation Algorithm
2
+
3
+ This document describes the deterministic art generation pipeline used by `git-hash-art`. Every step derives its randomness from the input hash via a seeded mulberry32 PRNG, guaranteeing identical output for identical input.
4
+
5
+ ## Pipeline Overview
6
+
7
+ ```
8
+ Hash String
9
+
10
+ ├─► Seed (mulberry32 PRNG)
11
+
12
+ ├─► Color Scheme (analogic + complementary + triadic palettes)
13
+
14
+ └─► Rendering Pipeline
15
+
16
+ 1. Background Layer
17
+ 2. Composition Mode Selection
18
+ 3. Focal Point Generation
19
+ 4. Flow Field Initialization
20
+ 5. Shape Layers (× N layers)
21
+ │ ├─ Position (composition mode + focal bias)
22
+ │ ├─ Shape Selection (layer-weighted)
23
+ │ ├─ Styling (transparency, glow, gradients, color jitter)
24
+ │ └─ Recursive Nesting (~15% of large shapes)
25
+ 6. Flow-Line Pass
26
+ 7. Noise Texture Overlay
27
+ 8. Organic Connecting Curves
28
+ ```
29
+
30
+ ## 1. Deterministic RNG
31
+
32
+ All randomness flows from a single **mulberry32** PRNG seeded by hashing the full input string:
33
+
34
+ ```
35
+ seed = hash(gitHash) → mulberry32 state
36
+ rng() → float in [0, 1)
37
+ ```
38
+
39
+ The old approach extracted 2-char hex pairs from the hash (only ~20 unique values in a 40-char hash). Mulberry32 produces a full 32-bit uniform stream from any seed, eliminating correlation artifacts.
40
+
41
+ ## 2. Color Scheme
42
+
43
+ The `SacredColorScheme` class derives three harmonious palettes from the hash:
44
+
45
+ | Palette | Method | Purpose |
46
+ |---------|--------|---------|
47
+ | Base (analogic) | `color-scheme` lib, hue = seed % 360 | Primary shape colors |
48
+ | Complementary (mono) | hue = seed + 180° | Contrast accents |
49
+ | Triadic | hue = seed + 120° | Additional variety |
50
+
51
+ These are merged and deduplicated into a single 6-8 color palette. Background colors are darkened variants (65% and 55% brightness) of the base scheme.
52
+
53
+ ### Color Utilities
54
+
55
+ - **`hexWithAlpha(hex, alpha)`** — converts hex to `rgba()` for transparency
56
+ - **`jitterColor(hex, rng, amount)`** — applies ±amount RGB jitter per channel for organic variation
57
+ - **Positional blending** — shape fill color is biased by canvas position, creating smooth color flow across the image
58
+
59
+ ## 3. Background
60
+
61
+ A radial gradient fills the canvas from center to corners using two darkened base-scheme colors. This creates depth before any shapes are drawn.
62
+
63
+ ## 4. Composition Modes
64
+
65
+ The hash deterministically selects one of five composition strategies that control how shapes are positioned on the canvas:
66
+
67
+ | Mode | Description |
68
+ |------|-------------|
69
+ | **Radial** | Shapes emanate from the center with distance following a power curve (denser near center) |
70
+ | **Flow-field** | Random positions; shapes are rotated to align with a hash-derived vector field |
71
+ | **Spiral** | Shapes follow a multi-turn spiral path outward from center with slight scatter |
72
+ | **Grid-subdivision** | Canvas is divided into cells; shapes are placed randomly within cells |
73
+ | **Clustered** | 3-5 cluster centers are generated; shapes scatter around the nearest cluster |
74
+
75
+ Each mode produces fundamentally different visual character from the same shape set.
76
+
77
+ ## 5. Focal Points
78
+
79
+ 1-2 focal points are placed on the canvas (kept away from edges). Every shape position is pulled toward the nearest focal point by a strength factor (30-70%), creating areas of visual density and intentional-looking composition rather than uniform scatter.
80
+
81
+ ## 6. Shape Layers
82
+
83
+ The image is built in N layers (default: 4). Each layer has its own characteristics:
84
+
85
+ ### Layer Properties
86
+
87
+ | Property | Behavior |
88
+ |----------|----------|
89
+ | Opacity | Decays gently per layer (0.7 → 0.58 → 0.46 → 0.34), minimum 0.15 |
90
+ | Size scale | Later layers use progressively smaller shapes (×0.85, ×0.70, ×0.55) |
91
+ | Shape weights | Early layers favor basic shapes; later layers favor complex/sacred |
92
+ | Per-shape opacity | Additional random jitter (50-100% of layer opacity) |
93
+
94
+ ### Shape Selection (Layer-Weighted)
95
+
96
+ Shapes are divided into three categories with weights that shift across layers:
97
+
98
+ | Category | Shapes | Early layers | Late layers |
99
+ |----------|--------|-------------|-------------|
100
+ | **Basic** | circle, square, triangle, hexagon, diamond, cube | High weight | Low weight |
101
+ | **Complex** | star, platonic solid, fibonacci spiral, islamic pattern, celtic knot, merkaba, fractal | Medium | Medium-high |
102
+ | **Sacred** | mandala, flower of life, tree of life, Metatron's cube, Sri Yantra, seed of life, vesica piscis, torus, egg of life | Low | High |
103
+
104
+ ### Size Distribution
105
+
106
+ Shape sizes follow a **power distribution** (`Math.pow(rng(), 1.8)`) — producing many small shapes and few large ones, which creates natural visual hierarchy.
107
+
108
+ ### Styling Per Shape
109
+
110
+ Each shape receives:
111
+
112
+ - **Semi-transparent fill** — alpha between 0.2-0.7, creating watercolor-style blending where shapes overlap
113
+ - **Color jitter** — ±8% RGB variation on fills, ±5% on strokes, so no two shapes using the "same" palette color are pixel-identical
114
+ - **Positional color** — fill color is biased by the shape's canvas position, creating smooth color flow
115
+ - **Glow effect** — 45% of sacred shapes and 20% of others get a `shadowBlur` glow (8-28px scaled), with glow color at 60% opacity
116
+ - **Gradient fill** — ~30% of shapes get a radial gradient between two jittered palette colors instead of a flat fill
117
+ - **Variable stroke width** — 0.5-2.5px scaled to canvas size
118
+
119
+ ### Recursive Nesting
120
+
121
+ ~15% of shapes larger than 40% of max size spawn 1-3 inner shapes:
122
+ - Inner shapes are drawn at the parent's position with small random offsets
123
+ - They use more complex/sacred shape types (layer ratio biased +0.3)
124
+ - Sized at 15-40% of the parent
125
+ - More transparent than the parent layer
126
+
127
+ ## 7. Flow-Line Pass
128
+
129
+ 6-16 flowing curves are drawn across the canvas, following the hash-derived vector field:
130
+
131
+ - Each line starts at a random position and takes 30-70 steps
132
+ - At each step, direction is determined by the flow field angle at that position plus slight random wobble
133
+ - Lines stop if they leave the canvas bounds
134
+ - Drawn at very low opacity (6-16%) with thin strokes for subtle movement
135
+
136
+ The flow field is defined by:
137
+ ```
138
+ angle(x, y) = baseAngle + sin(x/w × freq × 2π) × π/2 + cos(y/h × freq × 2π) × π/2
139
+ ```
140
+
141
+ ## 8. Noise Texture Overlay
142
+
143
+ A dedicated noise RNG (seeded separately from the main RNG to avoid affecting shape generation) renders thousands of 1px dots across the canvas:
144
+
145
+ - Density: ~1 dot per 800 square pixels
146
+ - Each dot is either black or white (50/50)
147
+ - Very low opacity (1-4%)
148
+ - Creates subtle film-grain texture that adds organic depth
149
+
150
+ ## 9. Organic Connecting Curves
151
+
152
+ Quadratic bezier curves connect nearby shapes:
153
+
154
+ - Number of curves scales with canvas area (~8 per megapixel)
155
+ - Each curve connects two shapes that were drawn near each other in sequence
156
+ - Control points are offset perpendicular to the connecting line with random bulge
157
+ - Drawn at low opacity (6-16%) with palette colors at 30% alpha
158
+
159
+ ## Shape Implementations
160
+
161
+ ### Basic Shapes
162
+ Standard geometric primitives drawn as canvas paths (beginPath → moveTo/lineTo/arc → closePath). The draw pipeline calls `fill()` and `stroke()` after the path is defined.
163
+
164
+ ### Complex Shapes
165
+ More intricate geometry including:
166
+ - **Platonic solids** — 2D projections with all edges drawn between vertices
167
+ - **Fibonacci spiral** — iterative arc segments following the golden ratio
168
+ - **Islamic pattern** — 8-pointed star grid at intersections
169
+ - **Celtic knot** — bezier over/under weaving pattern
170
+ - **Mandala** — concentric circles with radial lines
171
+ - **Fractal tree** — recursive branching at ±30° with 0.7× length decay
172
+
173
+ ### Sacred Geometry
174
+ Mathematically precise sacred geometry patterns:
175
+ - **Flower of Life** — 7 overlapping circles in hexagonal arrangement
176
+ - **Tree of Life** — 10 Sephirot nodes with connecting paths
177
+ - **Metatron's Cube** — 13 vertices (center + inner/outer hexagons) fully connected
178
+ - **Sri Yantra** — 9 interlocking triangles at two radii
179
+ - **Seed of Life** — 7 circles (same as Flower of Life, different scale)
180
+ - **Vesica Piscis** — two overlapping circles
181
+ - **Torus** — 2D projection of a torus via line segments
182
+ - **Egg of Life** — 7 circles in tight hexagonal packing
183
+
184
+ ## Configuration
185
+
186
+ All parameters are exposed via `GenerationConfig`:
187
+
188
+ | Parameter | Default | Effect |
189
+ |-----------|---------|--------|
190
+ | `width` | 2048 | Canvas width in pixels |
191
+ | `height` | 2048 | Canvas height in pixels |
192
+ | `gridSize` | 5 | Base shape count = gridSize² × 1.5 |
193
+ | `layers` | 4 | Number of rendering layers |
194
+ | `minShapeSize` | 30 | Minimum shape size (scaled to canvas) |
195
+ | `maxShapeSize` | 400 | Maximum shape size (scaled to canvas) |
196
+ | `baseOpacity` | 0.7 | First layer opacity |
197
+ | `opacityReduction` | 0.12 | Opacity decay per layer |
198
+ | `shapesPerLayer` | auto | Override auto-calculated shape count |
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.3.0](https://github.com/gfargo/git-hash-art/compare/0.2.0...0.3.0)
8
+
9
+ - feat: evolve art generation with composition modes, flow fields, nesting, and texture [`#9`](https://github.com/gfargo/git-hash-art/pull/9)
10
+ - refactor(browser): simplify export statement [`dc193c9`](https://github.com/gfargo/git-hash-art/commit/dc193c972511c5b0eb9e6a781c423aa005453007)
11
+
7
12
  #### [0.2.0](https://github.com/gfargo/git-hash-art/compare/0.1.0...0.2.0)
8
13
 
14
+ > 19 March 2026
15
+
9
16
  - feat: add cross-platform browser support [`#8`](https://github.com/gfargo/git-hash-art/pull/8)
10
17
  - feat: add visual effects for richer art generation [`#6`](https://github.com/gfargo/git-hash-art/pull/6)
11
18
  - fix: improve art generation quality [`#5`](https://github.com/gfargo/git-hash-art/pull/5)
12
19
  - docs: update README to reflect current algorithm and features [`15879c6`](https://github.com/gfargo/git-hash-art/commit/15879c62480a2d418c4ef8dcb479d912a7160345)
20
+ - chore: release v0.2.0 [`6867e06`](https://github.com/gfargo/git-hash-art/commit/6867e067f6dda0f4a4fd444b412772ddecb01555)
13
21
  - fix(shapes): handle missing shape type default [`860bfd3`](https://github.com/gfargo/git-hash-art/commit/860bfd36caab97d6ed44dd441ea1a995863b6068)
14
22
 
15
23
  #### [0.1.0](https://github.com/gfargo/git-hash-art/compare/0.0.4...0.1.0)
package/dist/browser.js CHANGED
@@ -8,9 +8,18 @@ import $4wRzV$colorscheme from "color-scheme";
8
8
  */ /**
9
9
  * Pure rendering logic — environment-agnostic.
10
10
  *
11
- * This module only uses the standard CanvasRenderingContext2D API,
12
- * so it works identically in Node (@napi-rs/canvas) and browsers
13
- * (HTMLCanvasElement).
11
+ * Uses only the standard CanvasRenderingContext2D API so it works
12
+ * identically in Node (@napi-rs/canvas) and browsers.
13
+ *
14
+ * Generation pipeline:
15
+ * 1. Background — radial gradient from hash-derived dark palette
16
+ * 2. Composition mode — hash selects: radial, flow-field, spiral, grid-subdivision, or clustered
17
+ * 3. Color field — smooth positional color blending across the canvas
18
+ * 4. Shape layers — weighted selection, focal-point placement, transparency, glow, gradients, jitter
19
+ * 5. Recursive nesting — some shapes contain smaller shapes inside
20
+ * 6. Flow-line pass — bezier curves following a hash-derived vector field
21
+ * 7. Noise texture overlay — subtle grain for organic feel
22
+ * 8. Organic connecting curves — beziers between nearby shapes
14
23
  */
15
24
  // declare module 'color-scheme';
16
25
 
@@ -1025,6 +1034,138 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
1025
1034
  };
1026
1035
 
1027
1036
 
1037
+ // ── Shape categories for weighted selection ─────────────────────────
1038
+ const $1f63dc64b5593c73$var$BASIC_SHAPES = [
1039
+ "circle",
1040
+ "square",
1041
+ "triangle",
1042
+ "hexagon",
1043
+ "diamond",
1044
+ "cube"
1045
+ ];
1046
+ const $1f63dc64b5593c73$var$COMPLEX_SHAPES = [
1047
+ "star",
1048
+ "jacked-star",
1049
+ "heart",
1050
+ "platonicSolid",
1051
+ "fibonacciSpiral",
1052
+ "islamicPattern",
1053
+ "celticKnot",
1054
+ "merkaba",
1055
+ "fractal"
1056
+ ];
1057
+ const $1f63dc64b5593c73$var$SACRED_SHAPES = [
1058
+ "mandala",
1059
+ "flowerOfLife",
1060
+ "treeOfLife",
1061
+ "metatronsCube",
1062
+ "sriYantra",
1063
+ "seedOfLife",
1064
+ "vesicaPiscis",
1065
+ "torus",
1066
+ "eggOfLife"
1067
+ ];
1068
+ const $1f63dc64b5593c73$var$COMPOSITION_MODES = [
1069
+ "radial",
1070
+ "flow-field",
1071
+ "spiral",
1072
+ "grid-subdivision",
1073
+ "clustered"
1074
+ ];
1075
+ // ── Helper: pick shape with layer-aware weighting ───────────────────
1076
+ function $1f63dc64b5593c73$var$pickShape(rng, layerRatio, shapeNames) {
1077
+ const basicW = 1 - layerRatio * 0.6;
1078
+ const complexW = 0.3 + layerRatio * 0.3;
1079
+ const sacredW = 0.1 + layerRatio * 0.4;
1080
+ const total = basicW + complexW + sacredW;
1081
+ const roll = rng() * total;
1082
+ let pool;
1083
+ if (roll < basicW) pool = $1f63dc64b5593c73$var$BASIC_SHAPES;
1084
+ else if (roll < basicW + complexW) pool = $1f63dc64b5593c73$var$COMPLEX_SHAPES;
1085
+ else pool = $1f63dc64b5593c73$var$SACRED_SHAPES;
1086
+ const available = pool.filter((s)=>shapeNames.includes(s));
1087
+ if (available.length === 0) return shapeNames[Math.floor(rng() * shapeNames.length)];
1088
+ return available[Math.floor(rng() * available.length)];
1089
+ }
1090
+ // ── Helper: simple 2D value noise (hash-seeded) ─────────────────────
1091
+ function $1f63dc64b5593c73$var$valueNoise(x, y, scale, rng) {
1092
+ // Cheap pseudo-noise: combine sin waves at different frequencies
1093
+ const nx = x / scale;
1094
+ const ny = y / scale;
1095
+ return (Math.sin(nx * 1.7 + ny * 2.3 + rng() * 0.001) * 0.5 + Math.sin(nx * 3.1 - ny * 1.9 + rng() * 0.001) * 0.3 + Math.sin(nx * 5.3 + ny * 4.7 + rng() * 0.001) * 0.2) * 0.5 + 0.5;
1096
+ }
1097
+ // ── Helper: get position based on composition mode ──────────────────
1098
+ function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
1099
+ switch(mode){
1100
+ case "radial":
1101
+ {
1102
+ const angle = rng() * Math.PI * 2;
1103
+ const maxR = Math.min(width, height) * 0.45;
1104
+ const r = Math.pow(rng(), 0.7) * maxR;
1105
+ return {
1106
+ x: cx + Math.cos(angle) * r,
1107
+ y: cy + Math.sin(angle) * r
1108
+ };
1109
+ }
1110
+ case "spiral":
1111
+ {
1112
+ const t = shapeIndex / totalShapes;
1113
+ const turns = 3 + rng() * 2;
1114
+ const angle = t * Math.PI * 2 * turns;
1115
+ const maxR = Math.min(width, height) * 0.42;
1116
+ const r = t * maxR + (rng() - 0.5) * maxR * 0.15;
1117
+ return {
1118
+ x: cx + Math.cos(angle) * r,
1119
+ y: cy + Math.sin(angle) * r
1120
+ };
1121
+ }
1122
+ case "grid-subdivision":
1123
+ {
1124
+ const cells = 3 + Math.floor(rng() * 3);
1125
+ const cellW = width / cells;
1126
+ const cellH = height / cells;
1127
+ const gx = Math.floor(rng() * cells);
1128
+ const gy = Math.floor(rng() * cells);
1129
+ return {
1130
+ x: gx * cellW + rng() * cellW,
1131
+ y: gy * cellH + rng() * cellH
1132
+ };
1133
+ }
1134
+ case "clustered":
1135
+ {
1136
+ // Pick one of 3-5 cluster centers, then scatter around it
1137
+ const numClusters = 3 + Math.floor(rng() * 3);
1138
+ const ci = Math.floor(rng() * numClusters);
1139
+ // Deterministic cluster center from index
1140
+ const clusterRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(String(ci), 999));
1141
+ const clx = width * (0.15 + clusterRng() * 0.7);
1142
+ const cly = height * (0.15 + clusterRng() * 0.7);
1143
+ const spread = Math.min(width, height) * 0.18;
1144
+ return {
1145
+ x: clx + (rng() - 0.5) * spread * 2,
1146
+ y: cly + (rng() - 0.5) * spread * 2
1147
+ };
1148
+ }
1149
+ case "flow-field":
1150
+ default:
1151
+ // Random position, will be adjusted by flow field direction later
1152
+ return {
1153
+ x: rng() * width,
1154
+ y: rng() * height
1155
+ };
1156
+ }
1157
+ }
1158
+ // ── Helper: positional color blending ───────────────────────────────
1159
+ function $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, colors, rng) {
1160
+ // Blend between palette colors based on position
1161
+ const nx = x / width;
1162
+ const ny = y / height;
1163
+ // Use position to bias which palette color is chosen
1164
+ const posIndex = (nx * 0.6 + ny * 0.4) * (colors.length - 1);
1165
+ const baseIdx = Math.floor(posIndex) % colors.length;
1166
+ // Then jitter it slightly
1167
+ return (0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[baseIdx], rng, 0.08);
1168
+ }
1028
1169
  function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1029
1170
  const finalConfig = {
1030
1171
  ...(0, $81c1b644006d48ec$export$c2f8e0cc249a8d8f),
@@ -1032,61 +1173,167 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1032
1173
  };
1033
1174
  const { width: width, height: height, gridSize: gridSize, layers: layers, minShapeSize: minShapeSize, maxShapeSize: maxShapeSize, baseOpacity: baseOpacity, opacityReduction: opacityReduction } = finalConfig;
1034
1175
  finalConfig.shapesPerLayer = finalConfig.shapesPerLayer || Math.floor(gridSize * gridSize * 1.5);
1035
- // --- Color scheme derived from hash ---
1036
1176
  const colorScheme = new (0, $b5a262d09b87e373$export$ab958c550f521376)(gitHash);
1037
1177
  const colors = colorScheme.getColors();
1038
1178
  const [bgStart, bgEnd] = colorScheme.getBackgroundColors();
1039
- // --- Radial gradient background for depth ---
1040
- const cx = width / 2;
1041
- const cy = height / 2;
1042
- const bgRadius = Math.hypot(cx, cy);
1043
- const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, bgRadius);
1044
- gradient.addColorStop(0, bgStart);
1045
- gradient.addColorStop(1, bgEnd);
1046
- ctx.fillStyle = gradient;
1047
- ctx.fillRect(0, 0, width, height);
1048
1179
  const shapeNames = Object.keys((0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5));
1049
1180
  const scaleFactor = Math.min(width, height) / 1024;
1050
1181
  const adjustedMinSize = minShapeSize * scaleFactor;
1051
1182
  const adjustedMaxSize = maxShapeSize * scaleFactor;
1052
- // One master RNG seeded from the full hash — all randomness flows from here
1053
1183
  const rng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash));
1054
- // Track shape positions for organic connecting curves later
1184
+ const cx = width / 2;
1185
+ const cy = height / 2;
1186
+ // ── 1. Background ──────────────────────────────────────────────
1187
+ const bgRadius = Math.hypot(cx, cy);
1188
+ const bgGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, bgRadius);
1189
+ bgGrad.addColorStop(0, bgStart);
1190
+ bgGrad.addColorStop(1, bgEnd);
1191
+ ctx.fillStyle = bgGrad;
1192
+ ctx.fillRect(0, 0, width, height);
1193
+ // ── 2. Composition mode ────────────────────────────────────────
1194
+ const compositionMode = $1f63dc64b5593c73$var$COMPOSITION_MODES[Math.floor(rng() * $1f63dc64b5593c73$var$COMPOSITION_MODES.length)];
1195
+ // ── 3. Focal points ────────────────────────────────────────────
1196
+ const numFocal = 1 + Math.floor(rng() * 2);
1197
+ const focalPoints = [];
1198
+ for(let f = 0; f < numFocal; f++)focalPoints.push({
1199
+ x: width * (0.2 + rng() * 0.6),
1200
+ y: height * (0.2 + rng() * 0.6),
1201
+ strength: 0.3 + rng() * 0.4
1202
+ });
1203
+ function applyFocalBias(rx, ry) {
1204
+ let nearest = focalPoints[0];
1205
+ let minDist = Infinity;
1206
+ for (const fp of focalPoints){
1207
+ const d = Math.hypot(rx - fp.x, ry - fp.y);
1208
+ if (d < minDist) {
1209
+ minDist = d;
1210
+ nearest = fp;
1211
+ }
1212
+ }
1213
+ const pull = nearest.strength * rng() * 0.5;
1214
+ return [
1215
+ rx + (nearest.x - rx) * pull,
1216
+ ry + (nearest.y - ry) * pull
1217
+ ];
1218
+ }
1219
+ // ── 4. Flow field seed values (for flow-field mode & line pass) ─
1220
+ const fieldAngleBase = rng() * Math.PI * 2;
1221
+ const fieldFreq = 0.5 + rng() * 2;
1222
+ function flowAngle(x, y) {
1223
+ return fieldAngleBase + Math.sin(x / width * fieldFreq * Math.PI * 2) * Math.PI * 0.5 + Math.cos(y / height * fieldFreq * Math.PI * 2) * Math.PI * 0.5;
1224
+ }
1225
+ // ── 5. Shape layers ────────────────────────────────────────────
1055
1226
  const shapePositions = [];
1056
1227
  for(let layer = 0; layer < layers; layer++){
1228
+ const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
1057
1229
  const numShapes = finalConfig.shapesPerLayer + Math.floor(rng() * finalConfig.shapesPerLayer * 0.3);
1058
- // Layer opacity decays gently so all layers remain visible
1059
1230
  const layerOpacity = Math.max(0.15, baseOpacity - layer * opacityReduction);
1060
- // Later layers use smaller shapes for depth
1061
1231
  const layerSizeScale = 1 - layer * 0.15;
1062
1232
  for(let i = 0; i < numShapes; i++){
1063
- const x = rng() * width;
1064
- const y = rng() * height;
1065
- const shapeIdx = Math.floor(rng() * shapeNames.length);
1066
- const shape = shapeNames[shapeIdx];
1067
- // Shape size follows a power distribution — many small, few large
1233
+ // Position from composition mode, then focal bias
1234
+ const rawPos = $1f63dc64b5593c73$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
1235
+ const [x, y] = applyFocalBias(rawPos.x, rawPos.y);
1236
+ // Weighted shape selection
1237
+ const shape = $1f63dc64b5593c73$var$pickShape(rng, layerRatio, shapeNames);
1238
+ // Power distribution for size
1068
1239
  const sizeT = Math.pow(rng(), 1.8);
1069
1240
  const size = (adjustedMinSize + sizeT * (adjustedMaxSize - adjustedMinSize)) * layerSizeScale;
1070
- const rotation = rng() * 360;
1071
- const fillColor = colors[Math.floor(rng() * colors.length)];
1072
- const strokeColor = colors[Math.floor(rng() * colors.length)];
1241
+ // Flow-field rotation in flow-field mode, random otherwise
1242
+ const rotation = compositionMode === "flow-field" ? flowAngle(x, y) * 180 / Math.PI + (rng() - 0.5) * 30 : rng() * 360;
1243
+ // Positional color blending + jitter
1244
+ const fillBase = $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, colors, rng);
1245
+ const strokeBase = colors[Math.floor(rng() * colors.length)];
1246
+ const fillColor = (0, $b5a262d09b87e373$export$59539d800dbe6858)(fillBase, rng, 0.06);
1247
+ const strokeColor = (0, $b5a262d09b87e373$export$59539d800dbe6858)(strokeBase, rng, 0.05);
1248
+ // Semi-transparent fill
1249
+ const fillAlpha = 0.2 + rng() * 0.5;
1250
+ const transparentFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, fillAlpha);
1073
1251
  const strokeWidth = (0.5 + rng() * 2.0) * scaleFactor;
1074
1252
  ctx.globalAlpha = layerOpacity * (0.5 + rng() * 0.5);
1253
+ // Glow on sacred shapes more often
1254
+ const isSacred = $1f63dc64b5593c73$var$SACRED_SHAPES.includes(shape);
1255
+ const glowChance = isSacred ? 0.45 : 0.2;
1256
+ const hasGlow = rng() < glowChance;
1257
+ const glowRadius = hasGlow ? (8 + rng() * 20) * scaleFactor : 0;
1258
+ // Gradient fill on ~30%
1259
+ const hasGradient = rng() < 0.3;
1260
+ const gradientEnd = hasGradient ? (0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1) : undefined;
1075
1261
  (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, x, y, {
1076
- fillColor: fillColor,
1262
+ fillColor: transparentFill,
1077
1263
  strokeColor: strokeColor,
1078
1264
  strokeWidth: strokeWidth,
1079
1265
  size: size,
1080
1266
  rotation: rotation,
1081
- proportionType: "GOLDEN_RATIO"
1267
+ proportionType: "GOLDEN_RATIO",
1268
+ glowRadius: glowRadius,
1269
+ glowColor: hasGlow ? (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.6) : undefined,
1270
+ gradientFillEnd: gradientEnd
1082
1271
  });
1083
1272
  shapePositions.push({
1084
1273
  x: x,
1085
- y: y
1274
+ y: y,
1275
+ size: size
1086
1276
  });
1277
+ // ── 5b. Recursive nesting: ~15% of larger shapes get inner shapes ──
1278
+ if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
1279
+ const innerCount = 1 + Math.floor(rng() * 3);
1280
+ for(let n = 0; n < innerCount; n++){
1281
+ const innerShape = $1f63dc64b5593c73$var$pickShape(rng, Math.min(1, layerRatio + 0.3), shapeNames);
1282
+ const innerSize = size * (0.15 + rng() * 0.25);
1283
+ const innerOffX = (rng() - 0.5) * size * 0.4;
1284
+ const innerOffY = (rng() - 0.5) * size * 0.4;
1285
+ const innerRot = rng() * 360;
1286
+ const innerFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1), 0.3 + rng() * 0.4);
1287
+ ctx.globalAlpha = layerOpacity * 0.7;
1288
+ (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, innerShape, x + innerOffX, y + innerOffY, {
1289
+ fillColor: innerFill,
1290
+ strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.5),
1291
+ strokeWidth: strokeWidth * 0.6,
1292
+ size: innerSize,
1293
+ rotation: innerRot,
1294
+ proportionType: "GOLDEN_RATIO"
1295
+ });
1296
+ }
1297
+ }
1087
1298
  }
1088
1299
  }
1089
- // --- Organic connecting curves between nearby shapes ---
1300
+ // ── 6. Flow-line pass ──────────────────────────────────────────
1301
+ // Draw flowing curves that follow the hash-derived vector field
1302
+ const numFlowLines = 6 + Math.floor(rng() * 10);
1303
+ for(let i = 0; i < numFlowLines; i++){
1304
+ let fx = rng() * width;
1305
+ let fy = rng() * height;
1306
+ const steps = 30 + Math.floor(rng() * 40);
1307
+ const stepLen = (3 + rng() * 5) * scaleFactor;
1308
+ ctx.globalAlpha = 0.06 + rng() * 0.1;
1309
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colors[Math.floor(rng() * colors.length)], 0.4);
1310
+ ctx.lineWidth = (0.5 + rng() * 1.5) * scaleFactor;
1311
+ ctx.beginPath();
1312
+ ctx.moveTo(fx, fy);
1313
+ for(let s = 0; s < steps; s++){
1314
+ const angle = flowAngle(fx, fy) + (rng() - 0.5) * 0.3;
1315
+ fx += Math.cos(angle) * stepLen;
1316
+ fy += Math.sin(angle) * stepLen;
1317
+ // Stay in bounds
1318
+ if (fx < 0 || fx > width || fy < 0 || fy > height) break;
1319
+ ctx.lineTo(fx, fy);
1320
+ }
1321
+ ctx.stroke();
1322
+ }
1323
+ // ── 7. Noise texture overlay ───────────────────────────────────
1324
+ // Subtle grain rendered as tiny semi-transparent dots
1325
+ const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
1326
+ const noiseDensity = Math.floor(width * height / 800);
1327
+ for(let i = 0; i < noiseDensity; i++){
1328
+ const nx = noiseRng() * width;
1329
+ const ny = noiseRng() * height;
1330
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
1331
+ const alpha = 0.01 + noiseRng() * 0.03;
1332
+ ctx.globalAlpha = alpha;
1333
+ ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
1334
+ ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
1335
+ }
1336
+ // ── 8. Organic connecting curves ───────────────────────────────
1090
1337
  if (shapePositions.length > 1) {
1091
1338
  const numCurves = Math.floor(8 * (width * height) / 1048576);
1092
1339
  ctx.lineWidth = 0.8 * scaleFactor;
@@ -1104,8 +1351,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1104
1351
  const bulge = (rng() - 0.5) * dist * 0.4;
1105
1352
  const cpx = mx + -dy / (dist || 1) * bulge;
1106
1353
  const cpy = my + dx / (dist || 1) * bulge;
1107
- ctx.globalAlpha = 0.08 + rng() * 0.12;
1108
- ctx.strokeStyle = colors[Math.floor(rng() * colors.length)];
1354
+ ctx.globalAlpha = 0.06 + rng() * 0.1;
1355
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colors[Math.floor(rng() * colors.length)], 0.3);
1109
1356
  ctx.beginPath();
1110
1357
  ctx.moveTo(a.x, a.y);
1111
1358
  ctx.quadraticCurveTo(cpx, cpy, b.x, b.y);