git-hash-art 0.2.0 → 0.4.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/dist/browser.js CHANGED
@@ -8,9 +8,21 @@ 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
+ * 1b. Layered background — large faint shapes / subtle pattern for depth
17
+ * 2. Composition mode — hash selects: radial, flow-field, spiral, grid-subdivision, or clustered
18
+ * 3. Focal points + void zones (negative space)
19
+ * 4. Flow field seed values
20
+ * 5. Shape layers — blend modes, render styles, weighted selection,
21
+ * focal-point placement, atmospheric depth, organic edges
22
+ * 5b. Recursive nesting
23
+ * 6. Flow-line pass — tapered brush-stroke curves
24
+ * 7. Noise texture overlay
25
+ * 8. Organic connecting curves
14
26
  */
15
27
  // declare module 'color-scheme';
16
28
 
@@ -68,46 +80,61 @@ class $616009579e3d72c5$export$da2372f11bc66b3f {
68
80
  }
69
81
 
70
82
 
71
- function $b5a262d09b87e373$export$f116a24fd288e742(gitHash) {
72
- const seed = (0, $616009579e3d72c5$export$39a95c82b20fdf81)(gitHash);
73
- const scheme = new (0, $4wRzV$colorscheme)();
74
- scheme.from_hue(seed % 360).scheme("analogic").variation("soft");
75
- let colors = scheme.colors().map((hex)=>`#${hex}`);
76
- const contrastingHue = (seed + 180) % 360;
77
- const contrastingScheme = new (0, $4wRzV$colorscheme)();
78
- contrastingScheme.from_hue(contrastingHue).scheme("mono").variation("soft");
79
- colors.push(`#${contrastingScheme.colors()[0]}`);
80
- return colors;
83
+ // ── Color variation modes ───────────────────────────────────────────
84
+ // The hash deterministically selects a variation, producing dramatically
85
+ // different palettes from the same hue.
86
+ const $b5a262d09b87e373$var$COLOR_VARIATIONS = [
87
+ "soft",
88
+ "hard",
89
+ "pastel",
90
+ "light",
91
+ "dark",
92
+ "default"
93
+ ];
94
+ /**
95
+ * Pick a color variation mode deterministically from a seed.
96
+ */ function $b5a262d09b87e373$var$pickVariation(seed) {
97
+ return $b5a262d09b87e373$var$COLOR_VARIATIONS[Math.abs(seed) % $b5a262d09b87e373$var$COLOR_VARIATIONS.length];
98
+ }
99
+ /**
100
+ * Scheme type also varies — some hashes get near-monochromatic palettes,
101
+ * others get high-contrast complementary schemes.
102
+ */ const $b5a262d09b87e373$var$SCHEME_TYPES = [
103
+ "analogic",
104
+ "mono",
105
+ "contrast",
106
+ "triade",
107
+ "tetrade"
108
+ ];
109
+ function $b5a262d09b87e373$var$pickSchemeType(seed) {
110
+ return $b5a262d09b87e373$var$SCHEME_TYPES[Math.abs(seed >> 4) % $b5a262d09b87e373$var$SCHEME_TYPES.length];
81
111
  }
82
112
  class $b5a262d09b87e373$export$ab958c550f521376 {
83
113
  constructor(gitHash){
84
114
  this.seed = (0, $616009579e3d72c5$export$39a95c82b20fdf81)(gitHash);
115
+ this.rng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 42));
116
+ // Hash-driven variation and scheme type for palette diversity
117
+ this.variation = $b5a262d09b87e373$var$pickVariation(this.seed);
118
+ this.schemeType = $b5a262d09b87e373$var$pickSchemeType(this.seed);
85
119
  this.baseScheme = this.generateBaseScheme();
86
120
  this.complementaryScheme = this.generateComplementaryScheme();
87
121
  this.triadicScheme = this.generateTriadicScheme();
88
- this.metallic = this.generateMetallicColors();
89
122
  }
90
123
  generateBaseScheme() {
91
124
  const scheme = new (0, $4wRzV$colorscheme)();
92
- return scheme.from_hue(this.seed % 360).scheme("analogic").variation("soft").colors().map((hex)=>`#${hex}`);
125
+ return scheme.from_hue(this.seed % 360).scheme(this.schemeType).variation(this.variation).colors().map((hex)=>`#${hex}`);
93
126
  }
94
127
  generateComplementaryScheme() {
95
128
  const complementaryHue = (this.seed + 180) % 360;
129
+ // Complementary uses a contrasting variation for tension
130
+ const compVariation = this.variation === "soft" ? "hard" : this.variation === "dark" ? "light" : this.variation;
96
131
  const scheme = new (0, $4wRzV$colorscheme)();
97
- return scheme.from_hue(complementaryHue).scheme("mono").variation("soft").colors().map((hex)=>`#${hex}`);
132
+ return scheme.from_hue(complementaryHue).scheme("mono").variation(compVariation).colors().map((hex)=>`#${hex}`);
98
133
  }
99
134
  generateTriadicScheme() {
100
135
  const triadicHue = (this.seed + 120) % 360;
101
136
  const scheme = new (0, $4wRzV$colorscheme)();
102
- return scheme.from_hue(triadicHue).scheme("triade").variation("soft").colors().map((hex)=>`#${hex}`);
103
- }
104
- generateMetallicColors() {
105
- return {
106
- gold: "#FFD700",
107
- silver: "#C0C0C0",
108
- copper: "#B87333",
109
- bronze: "#CD7F32"
110
- };
137
+ return scheme.from_hue(triadicHue).scheme("triade").variation(this.variation).colors().map((hex)=>`#${hex}`);
111
138
  }
112
139
  /**
113
140
  * Returns a flat array of hash-derived colors suitable for art generation.
@@ -165,6 +192,12 @@ function $b5a262d09b87e373$export$59539d800dbe6858(hex, rng, amount = 0.1) {
165
192
  const jit = ()=>(rng() - 0.5) * 2 * amount * 255;
166
193
  return $b5a262d09b87e373$var$rgbToHex(r + jit(), g + jit(), b + jit());
167
194
  }
195
+ function $b5a262d09b87e373$export$fb75607d98509d9(hex, amount) {
196
+ const [r, g, b] = $b5a262d09b87e373$var$hexToRgb(hex);
197
+ const gray = 0.299 * r + 0.587 * g + 0.114 * b;
198
+ const mix = (c)=>c + (gray - c) * amount;
199
+ return $b5a262d09b87e373$var$rgbToHex(mix(r), mix(g), mix(b));
200
+ }
168
201
 
169
202
 
170
203
 
@@ -952,6 +985,32 @@ const $e41b41d8dcf837ad$export$4ff7fc6f1af248b5 = {
952
985
  };
953
986
 
954
987
 
988
+ const $e0f99502ff383dd8$export$f821c68fe9beaecf = [
989
+ "source-over",
990
+ "screen",
991
+ "multiply",
992
+ "overlay",
993
+ "soft-light",
994
+ "color-dodge",
995
+ "color-burn",
996
+ "lighter"
997
+ ];
998
+ function $e0f99502ff383dd8$export$7bb7bff4e26fa06b(rng) {
999
+ if (rng() < 0.4) return "source-over";
1000
+ return $e0f99502ff383dd8$export$f821c68fe9beaecf[1 + Math.floor(rng() * ($e0f99502ff383dd8$export$f821c68fe9beaecf.length - 1))];
1001
+ }
1002
+ const $e0f99502ff383dd8$var$RENDER_STYLES = [
1003
+ "fill-and-stroke",
1004
+ "fill-and-stroke",
1005
+ "fill-only",
1006
+ "stroke-only",
1007
+ "double-stroke",
1008
+ "dashed",
1009
+ "watercolor"
1010
+ ];
1011
+ function $e0f99502ff383dd8$export$9fd4e64b2acd410e(rng) {
1012
+ return $e0f99502ff383dd8$var$RENDER_STYLES[Math.floor(rng() * $e0f99502ff383dd8$var$RENDER_STYLES.length)];
1013
+ }
955
1014
  function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
956
1015
  const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
957
1016
  ctx.save();
@@ -968,8 +1027,71 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
968
1027
  }
969
1028
  ctx.restore();
970
1029
  }
1030
+ /**
1031
+ * Apply the chosen render style to the current path.
1032
+ */ function $e0f99502ff383dd8$var$applyRenderStyle(ctx, style, fillColor, strokeColor, strokeWidth, size, rng) {
1033
+ switch(style){
1034
+ case "fill-only":
1035
+ ctx.fill();
1036
+ break;
1037
+ case "stroke-only":
1038
+ ctx.fill(); // transparent fill to define the path
1039
+ ctx.globalAlpha *= 0.3; // ghost fill
1040
+ ctx.fill();
1041
+ ctx.globalAlpha /= 0.3;
1042
+ ctx.stroke();
1043
+ break;
1044
+ case "double-stroke":
1045
+ ctx.fill();
1046
+ // Outer stroke
1047
+ ctx.lineWidth = strokeWidth * 2;
1048
+ ctx.globalAlpha *= 0.5;
1049
+ ctx.stroke();
1050
+ ctx.globalAlpha /= 0.5;
1051
+ // Inner stroke
1052
+ ctx.lineWidth = strokeWidth * 0.5;
1053
+ ctx.strokeStyle = fillColor;
1054
+ ctx.stroke();
1055
+ break;
1056
+ case "dashed":
1057
+ ctx.fill();
1058
+ ctx.setLineDash([
1059
+ size * 0.05,
1060
+ size * 0.03
1061
+ ]);
1062
+ ctx.stroke();
1063
+ ctx.setLineDash([]);
1064
+ break;
1065
+ case "watercolor":
1066
+ {
1067
+ // Draw 3-4 slightly offset passes at low opacity for a bleed effect
1068
+ const passes = 3 + (rng ? Math.floor(rng() * 2) : 0);
1069
+ const savedAlpha = ctx.globalAlpha;
1070
+ ctx.globalAlpha = savedAlpha * (0.3 / passes * 2);
1071
+ for(let p = 0; p < passes; p++){
1072
+ const jx = rng ? (rng() - 0.5) * size * 0.06 : 0;
1073
+ const jy = rng ? (rng() - 0.5) * size * 0.06 : 0;
1074
+ ctx.save();
1075
+ ctx.translate(jx, jy);
1076
+ ctx.fill();
1077
+ ctx.restore();
1078
+ }
1079
+ ctx.globalAlpha = savedAlpha;
1080
+ // Light stroke on top
1081
+ ctx.globalAlpha *= 0.4;
1082
+ ctx.stroke();
1083
+ ctx.globalAlpha /= 0.4;
1084
+ break;
1085
+ }
1086
+ case "fill-and-stroke":
1087
+ default:
1088
+ ctx.fill();
1089
+ ctx.stroke();
1090
+ break;
1091
+ }
1092
+ }
971
1093
  function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
972
- const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation, patterns: patterns = [], proportionType: proportionType = "GOLDEN_RATIO", baseOpacity: baseOpacity = 0.6, opacityReduction: opacityReduction = 0.1, glowRadius: glowRadius = 0, glowColor: glowColor, gradientFillEnd: gradientFillEnd } = config;
1094
+ const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation, patterns: patterns = [], proportionType: proportionType = "GOLDEN_RATIO", baseOpacity: baseOpacity = 0.6, opacityReduction: opacityReduction = 0.1, glowRadius: glowRadius = 0, glowColor: glowColor, gradientFillEnd: gradientFillEnd, renderStyle: renderStyle = "fill-and-stroke", rng: rng } = config;
973
1095
  ctx.save();
974
1096
  ctx.translate(x, y);
975
1097
  ctx.rotate(rotation * Math.PI / 180);
@@ -992,8 +1114,7 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
992
1114
  const drawFunction = (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[shape];
993
1115
  if (drawFunction) {
994
1116
  drawFunction(ctx, size);
995
- ctx.fill();
996
- ctx.stroke();
1117
+ $e0f99502ff383dd8$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
997
1118
  }
998
1119
  // Reset shadow so patterns aren't double-glowed
999
1120
  if (glowRadius > 0) ctx.shadowBlur = 0;
@@ -1025,6 +1146,138 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
1025
1146
  };
1026
1147
 
1027
1148
 
1149
+ // ── Shape categories for weighted selection ─────────────────────────
1150
+ const $1f63dc64b5593c73$var$BASIC_SHAPES = [
1151
+ "circle",
1152
+ "square",
1153
+ "triangle",
1154
+ "hexagon",
1155
+ "diamond",
1156
+ "cube"
1157
+ ];
1158
+ const $1f63dc64b5593c73$var$COMPLEX_SHAPES = [
1159
+ "star",
1160
+ "jacked-star",
1161
+ "heart",
1162
+ "platonicSolid",
1163
+ "fibonacciSpiral",
1164
+ "islamicPattern",
1165
+ "celticKnot",
1166
+ "merkaba",
1167
+ "fractal"
1168
+ ];
1169
+ const $1f63dc64b5593c73$var$SACRED_SHAPES = [
1170
+ "mandala",
1171
+ "flowerOfLife",
1172
+ "treeOfLife",
1173
+ "metatronsCube",
1174
+ "sriYantra",
1175
+ "seedOfLife",
1176
+ "vesicaPiscis",
1177
+ "torus",
1178
+ "eggOfLife"
1179
+ ];
1180
+ const $1f63dc64b5593c73$var$COMPOSITION_MODES = [
1181
+ "radial",
1182
+ "flow-field",
1183
+ "spiral",
1184
+ "grid-subdivision",
1185
+ "clustered"
1186
+ ];
1187
+ // ── Helper: pick shape with layer-aware weighting ───────────────────
1188
+ function $1f63dc64b5593c73$var$pickShape(rng, layerRatio, shapeNames) {
1189
+ const basicW = 1 - layerRatio * 0.6;
1190
+ const complexW = 0.3 + layerRatio * 0.3;
1191
+ const sacredW = 0.1 + layerRatio * 0.4;
1192
+ const total = basicW + complexW + sacredW;
1193
+ const roll = rng() * total;
1194
+ let pool;
1195
+ if (roll < basicW) pool = $1f63dc64b5593c73$var$BASIC_SHAPES;
1196
+ else if (roll < basicW + complexW) pool = $1f63dc64b5593c73$var$COMPLEX_SHAPES;
1197
+ else pool = $1f63dc64b5593c73$var$SACRED_SHAPES;
1198
+ const available = pool.filter((s)=>shapeNames.includes(s));
1199
+ if (available.length === 0) return shapeNames[Math.floor(rng() * shapeNames.length)];
1200
+ return available[Math.floor(rng() * available.length)];
1201
+ }
1202
+ // ── Helper: get position based on composition mode ──────────────────
1203
+ function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
1204
+ switch(mode){
1205
+ case "radial":
1206
+ {
1207
+ const angle = rng() * Math.PI * 2;
1208
+ const maxR = Math.min(width, height) * 0.45;
1209
+ const r = Math.pow(rng(), 0.7) * maxR;
1210
+ return {
1211
+ x: cx + Math.cos(angle) * r,
1212
+ y: cy + Math.sin(angle) * r
1213
+ };
1214
+ }
1215
+ case "spiral":
1216
+ {
1217
+ const t = shapeIndex / totalShapes;
1218
+ const turns = 3 + rng() * 2;
1219
+ const angle = t * Math.PI * 2 * turns;
1220
+ const maxR = Math.min(width, height) * 0.42;
1221
+ const r = t * maxR + (rng() - 0.5) * maxR * 0.15;
1222
+ return {
1223
+ x: cx + Math.cos(angle) * r,
1224
+ y: cy + Math.sin(angle) * r
1225
+ };
1226
+ }
1227
+ case "grid-subdivision":
1228
+ {
1229
+ const cells = 3 + Math.floor(rng() * 3);
1230
+ const cellW = width / cells;
1231
+ const cellH = height / cells;
1232
+ const gx = Math.floor(rng() * cells);
1233
+ const gy = Math.floor(rng() * cells);
1234
+ return {
1235
+ x: gx * cellW + rng() * cellW,
1236
+ y: gy * cellH + rng() * cellH
1237
+ };
1238
+ }
1239
+ case "clustered":
1240
+ {
1241
+ const numClusters = 3 + Math.floor(rng() * 3);
1242
+ const ci = Math.floor(rng() * numClusters);
1243
+ const clusterRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(String(ci), 999));
1244
+ const clx = width * (0.15 + clusterRng() * 0.7);
1245
+ const cly = height * (0.15 + clusterRng() * 0.7);
1246
+ const spread = Math.min(width, height) * 0.18;
1247
+ return {
1248
+ x: clx + (rng() - 0.5) * spread * 2,
1249
+ y: cly + (rng() - 0.5) * spread * 2
1250
+ };
1251
+ }
1252
+ case "flow-field":
1253
+ default:
1254
+ return {
1255
+ x: rng() * width,
1256
+ y: rng() * height
1257
+ };
1258
+ }
1259
+ }
1260
+ // ── Helper: positional color blending ───────────────────────────────
1261
+ function $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, colors, rng) {
1262
+ const nx = x / width;
1263
+ const ny = y / height;
1264
+ const posIndex = (nx * 0.6 + ny * 0.4) * (colors.length - 1);
1265
+ const baseIdx = Math.floor(posIndex) % colors.length;
1266
+ return (0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[baseIdx], rng, 0.08);
1267
+ }
1268
+ // ── Helper: check if a position is inside a void zone (Feature E) ───
1269
+ function $1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones) {
1270
+ for (const zone of voidZones){
1271
+ if (Math.hypot(x - zone.x, y - zone.y) < zone.radius) return true;
1272
+ }
1273
+ return false;
1274
+ }
1275
+ // ── Helper: density check for negative space (Feature E) ────────────
1276
+ function $1f63dc64b5593c73$var$localDensity(x, y, positions, radius) {
1277
+ let count = 0;
1278
+ for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
1279
+ return count;
1280
+ }
1028
1281
  function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1029
1282
  const finalConfig = {
1030
1283
  ...(0, $81c1b644006d48ec$export$c2f8e0cc249a8d8f),
@@ -1032,61 +1285,240 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1032
1285
  };
1033
1286
  const { width: width, height: height, gridSize: gridSize, layers: layers, minShapeSize: minShapeSize, maxShapeSize: maxShapeSize, baseOpacity: baseOpacity, opacityReduction: opacityReduction } = finalConfig;
1034
1287
  finalConfig.shapesPerLayer = finalConfig.shapesPerLayer || Math.floor(gridSize * gridSize * 1.5);
1035
- // --- Color scheme derived from hash ---
1036
1288
  const colorScheme = new (0, $b5a262d09b87e373$export$ab958c550f521376)(gitHash);
1037
1289
  const colors = colorScheme.getColors();
1038
1290
  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
1291
  const shapeNames = Object.keys((0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5));
1049
1292
  const scaleFactor = Math.min(width, height) / 1024;
1050
1293
  const adjustedMinSize = minShapeSize * scaleFactor;
1051
1294
  const adjustedMaxSize = maxShapeSize * scaleFactor;
1052
- // One master RNG seeded from the full hash — all randomness flows from here
1053
1295
  const rng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash));
1054
- // Track shape positions for organic connecting curves later
1296
+ const cx = width / 2;
1297
+ const cy = height / 2;
1298
+ // ── 1. Background ──────────────────────────────────────────────
1299
+ const bgRadius = Math.hypot(cx, cy);
1300
+ const bgGrad = ctx.createRadialGradient(cx, cy, 0, cx, cy, bgRadius);
1301
+ bgGrad.addColorStop(0, bgStart);
1302
+ bgGrad.addColorStop(1, bgEnd);
1303
+ ctx.fillStyle = bgGrad;
1304
+ ctx.fillRect(0, 0, width, height);
1305
+ // ── 1b. Layered background (Feature G) ─────────────────────────
1306
+ // Draw large, very faint shapes to give the background texture
1307
+ const bgShapeCount = 3 + Math.floor(rng() * 4);
1308
+ ctx.globalCompositeOperation = "soft-light";
1309
+ for(let i = 0; i < bgShapeCount; i++){
1310
+ const bx = rng() * width;
1311
+ const by = rng() * height;
1312
+ const bSize = width * 0.3 + rng() * width * 0.5;
1313
+ const bColor = colors[Math.floor(rng() * colors.length)];
1314
+ ctx.globalAlpha = 0.03 + rng() * 0.05;
1315
+ ctx.fillStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(bColor, 0.15);
1316
+ ctx.beginPath();
1317
+ ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
1318
+ ctx.fill();
1319
+ }
1320
+ // Subtle concentric rings from center
1321
+ const ringCount = 2 + Math.floor(rng() * 3);
1322
+ ctx.globalAlpha = 0.02 + rng() * 0.03;
1323
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colors[0], 0.1);
1324
+ ctx.lineWidth = 1 * scaleFactor;
1325
+ for(let i = 1; i <= ringCount; i++){
1326
+ const r = Math.min(width, height) * 0.15 * i;
1327
+ ctx.beginPath();
1328
+ ctx.arc(cx, cy, r, 0, Math.PI * 2);
1329
+ ctx.stroke();
1330
+ }
1331
+ ctx.globalCompositeOperation = "source-over";
1332
+ // ── 2. Composition mode ────────────────────────────────────────
1333
+ const compositionMode = $1f63dc64b5593c73$var$COMPOSITION_MODES[Math.floor(rng() * $1f63dc64b5593c73$var$COMPOSITION_MODES.length)];
1334
+ // ── 3. Focal points + void zones ───────────────────────────────
1335
+ const numFocal = 1 + Math.floor(rng() * 2);
1336
+ const focalPoints = [];
1337
+ for(let f = 0; f < numFocal; f++)focalPoints.push({
1338
+ x: width * (0.2 + rng() * 0.6),
1339
+ y: height * (0.2 + rng() * 0.6),
1340
+ strength: 0.3 + rng() * 0.4
1341
+ });
1342
+ // Feature E: 1-2 void zones where shapes are sparse (negative space)
1343
+ const numVoids = Math.floor(rng() * 2) + 1;
1344
+ const voidZones = [];
1345
+ for(let v = 0; v < numVoids; v++)voidZones.push({
1346
+ x: width * (0.15 + rng() * 0.7),
1347
+ y: height * (0.15 + rng() * 0.7),
1348
+ radius: Math.min(width, height) * (0.06 + rng() * 0.1)
1349
+ });
1350
+ function applyFocalBias(rx, ry) {
1351
+ let nearest = focalPoints[0];
1352
+ let minDist = Infinity;
1353
+ for (const fp of focalPoints){
1354
+ const d = Math.hypot(rx - fp.x, ry - fp.y);
1355
+ if (d < minDist) {
1356
+ minDist = d;
1357
+ nearest = fp;
1358
+ }
1359
+ }
1360
+ const pull = nearest.strength * rng() * 0.5;
1361
+ return [
1362
+ rx + (nearest.x - rx) * pull,
1363
+ ry + (nearest.y - ry) * pull
1364
+ ];
1365
+ }
1366
+ // ── 4. Flow field seed values ──────────────────────────────────
1367
+ const fieldAngleBase = rng() * Math.PI * 2;
1368
+ const fieldFreq = 0.5 + rng() * 2;
1369
+ function flowAngle(x, y) {
1370
+ 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;
1371
+ }
1372
+ // ── 5. Shape layers ────────────────────────────────────────────
1055
1373
  const shapePositions = [];
1374
+ const densityCheckRadius = Math.min(width, height) * 0.08;
1375
+ const maxLocalDensity = Math.ceil(finalConfig.shapesPerLayer * 0.15);
1056
1376
  for(let layer = 0; layer < layers; layer++){
1377
+ const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
1057
1378
  const numShapes = finalConfig.shapesPerLayer + Math.floor(rng() * finalConfig.shapesPerLayer * 0.3);
1058
- // Layer opacity decays gently so all layers remain visible
1059
1379
  const layerOpacity = Math.max(0.15, baseOpacity - layer * opacityReduction);
1060
- // Later layers use smaller shapes for depth
1061
1380
  const layerSizeScale = 1 - layer * 0.15;
1381
+ // Feature B: per-layer blend mode
1382
+ const layerBlend = (0, $e0f99502ff383dd8$export$7bb7bff4e26fa06b)(rng);
1383
+ ctx.globalCompositeOperation = layerBlend;
1384
+ // Feature C: per-layer render style bias
1385
+ const layerRenderStyle = (0, $e0f99502ff383dd8$export$9fd4e64b2acd410e)(rng);
1386
+ // Feature D: atmospheric desaturation for later layers
1387
+ const atmosphericDesat = layerRatio * 0.3; // 0 for first layer, up to 0.3 for last
1062
1388
  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
1389
+ // Position from composition mode, then focal bias
1390
+ const rawPos = $1f63dc64b5593c73$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
1391
+ const [x, y] = applyFocalBias(rawPos.x, rawPos.y);
1392
+ // Feature E: skip shapes in void zones, reduce in dense areas
1393
+ if ($1f63dc64b5593c73$var$isInVoidZone(x, y, voidZones)) {
1394
+ // 85% chance to skip — allows a few shapes to bleed in
1395
+ if (rng() < 0.85) continue;
1396
+ }
1397
+ if ($1f63dc64b5593c73$var$localDensity(x, y, shapePositions, densityCheckRadius) > maxLocalDensity) {
1398
+ if (rng() < 0.6) continue; // thin out dense areas
1399
+ }
1400
+ // Weighted shape selection
1401
+ const shape = $1f63dc64b5593c73$var$pickShape(rng, layerRatio, shapeNames);
1402
+ // Power distribution for size
1068
1403
  const sizeT = Math.pow(rng(), 1.8);
1069
1404
  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)];
1405
+ // Flow-field rotation in flow-field mode, random otherwise
1406
+ const rotation = compositionMode === "flow-field" ? flowAngle(x, y) * 180 / Math.PI + (rng() - 0.5) * 30 : rng() * 360;
1407
+ // Positional color blending + jitter
1408
+ let fillBase = $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, colors, rng);
1409
+ const strokeBase = colors[Math.floor(rng() * colors.length)];
1410
+ // Feature D: desaturate colors on later layers for depth
1411
+ if (atmosphericDesat > 0) fillBase = (0, $b5a262d09b87e373$export$fb75607d98509d9)(fillBase, atmosphericDesat);
1412
+ const fillColor = (0, $b5a262d09b87e373$export$59539d800dbe6858)(fillBase, rng, 0.06);
1413
+ const strokeColor = (0, $b5a262d09b87e373$export$59539d800dbe6858)(strokeBase, rng, 0.05);
1414
+ // Semi-transparent fill
1415
+ const fillAlpha = 0.2 + rng() * 0.5;
1416
+ const transparentFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, fillAlpha);
1073
1417
  const strokeWidth = (0.5 + rng() * 2.0) * scaleFactor;
1074
1418
  ctx.globalAlpha = layerOpacity * (0.5 + rng() * 0.5);
1419
+ // Glow on sacred shapes more often
1420
+ const isSacred = $1f63dc64b5593c73$var$SACRED_SHAPES.includes(shape);
1421
+ const glowChance = isSacred ? 0.45 : 0.2;
1422
+ const hasGlow = rng() < glowChance;
1423
+ const glowRadius = hasGlow ? (8 + rng() * 20) * scaleFactor : 0;
1424
+ // Gradient fill on ~30%
1425
+ const hasGradient = rng() < 0.3;
1426
+ const gradientEnd = hasGradient ? (0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1) : undefined;
1427
+ // Feature C: per-shape render style (70% use layer style, 30% pick their own)
1428
+ const shapeRenderStyle = rng() < 0.7 ? layerRenderStyle : (0, $e0f99502ff383dd8$export$9fd4e64b2acd410e)(rng);
1429
+ // Feature F: organic edge jitter — applied via watercolor style on ~15% of shapes
1430
+ const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
1431
+ const finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
1075
1432
  (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, x, y, {
1076
- fillColor: fillColor,
1433
+ fillColor: transparentFill,
1077
1434
  strokeColor: strokeColor,
1078
1435
  strokeWidth: strokeWidth,
1079
1436
  size: size,
1080
1437
  rotation: rotation,
1081
- proportionType: "GOLDEN_RATIO"
1438
+ proportionType: "GOLDEN_RATIO",
1439
+ glowRadius: glowRadius,
1440
+ glowColor: hasGlow ? (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.6) : undefined,
1441
+ gradientFillEnd: gradientEnd,
1442
+ renderStyle: finalRenderStyle,
1443
+ rng: rng
1082
1444
  });
1083
1445
  shapePositions.push({
1084
1446
  x: x,
1085
- y: y
1447
+ y: y,
1448
+ size: size
1086
1449
  });
1450
+ // ── 5b. Recursive nesting ──────────────────────────────────
1451
+ if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
1452
+ const innerCount = 1 + Math.floor(rng() * 3);
1453
+ for(let n = 0; n < innerCount; n++){
1454
+ const innerShape = $1f63dc64b5593c73$var$pickShape(rng, Math.min(1, layerRatio + 0.3), shapeNames);
1455
+ const innerSize = size * (0.15 + rng() * 0.25);
1456
+ const innerOffX = (rng() - 0.5) * size * 0.4;
1457
+ const innerOffY = (rng() - 0.5) * size * 0.4;
1458
+ const innerRot = rng() * 360;
1459
+ const innerFill = (0, $b5a262d09b87e373$export$f2121afcad3d553f)((0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1), 0.3 + rng() * 0.4);
1460
+ ctx.globalAlpha = layerOpacity * 0.7;
1461
+ (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, innerShape, x + innerOffX, y + innerOffY, {
1462
+ fillColor: innerFill,
1463
+ strokeColor: (0, $b5a262d09b87e373$export$f2121afcad3d553f)(strokeColor, 0.5),
1464
+ strokeWidth: strokeWidth * 0.6,
1465
+ size: innerSize,
1466
+ rotation: innerRot,
1467
+ proportionType: "GOLDEN_RATIO",
1468
+ renderStyle: shapeRenderStyle,
1469
+ rng: rng
1470
+ });
1471
+ }
1472
+ }
1473
+ }
1474
+ }
1475
+ // Reset blend mode for post-processing passes
1476
+ ctx.globalCompositeOperation = "source-over";
1477
+ // ── 6. Flow-line pass (Feature H: tapered brush strokes) ───────
1478
+ const numFlowLines = 6 + Math.floor(rng() * 10);
1479
+ for(let i = 0; i < numFlowLines; i++){
1480
+ let fx = rng() * width;
1481
+ let fy = rng() * height;
1482
+ const steps = 30 + Math.floor(rng() * 40);
1483
+ const stepLen = (3 + rng() * 5) * scaleFactor;
1484
+ const startWidth = (1 + rng() * 3) * scaleFactor;
1485
+ const lineColor = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colors[Math.floor(rng() * colors.length)], 0.4);
1486
+ const lineAlpha = 0.06 + rng() * 0.1;
1487
+ // Draw as individual segments with tapering width
1488
+ let prevX = fx;
1489
+ let prevY = fy;
1490
+ for(let s = 0; s < steps; s++){
1491
+ const angle = flowAngle(fx, fy) + (rng() - 0.5) * 0.3;
1492
+ fx += Math.cos(angle) * stepLen;
1493
+ fy += Math.sin(angle) * stepLen;
1494
+ if (fx < 0 || fx > width || fy < 0 || fy > height) break;
1495
+ // Taper: thick at start, thin at end
1496
+ const taper = 1 - s / steps * 0.8;
1497
+ ctx.globalAlpha = lineAlpha * taper;
1498
+ ctx.strokeStyle = lineColor;
1499
+ ctx.lineWidth = startWidth * taper;
1500
+ ctx.lineCap = "round";
1501
+ ctx.beginPath();
1502
+ ctx.moveTo(prevX, prevY);
1503
+ ctx.lineTo(fx, fy);
1504
+ ctx.stroke();
1505
+ prevX = fx;
1506
+ prevY = fy;
1087
1507
  }
1088
1508
  }
1089
- // --- Organic connecting curves between nearby shapes ---
1509
+ // ── 7. Noise texture overlay ───────────────────────────────────
1510
+ const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
1511
+ const noiseDensity = Math.floor(width * height / 800);
1512
+ for(let i = 0; i < noiseDensity; i++){
1513
+ const nx = noiseRng() * width;
1514
+ const ny = noiseRng() * height;
1515
+ const brightness = noiseRng() > 0.5 ? 255 : 0;
1516
+ const alpha = 0.01 + noiseRng() * 0.03;
1517
+ ctx.globalAlpha = alpha;
1518
+ ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
1519
+ ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
1520
+ }
1521
+ // ── 8. Organic connecting curves ───────────────────────────────
1090
1522
  if (shapePositions.length > 1) {
1091
1523
  const numCurves = Math.floor(8 * (width * height) / 1048576);
1092
1524
  ctx.lineWidth = 0.8 * scaleFactor;
@@ -1104,8 +1536,8 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1104
1536
  const bulge = (rng() - 0.5) * dist * 0.4;
1105
1537
  const cpx = mx + -dy / (dist || 1) * bulge;
1106
1538
  const cpy = my + dx / (dist || 1) * bulge;
1107
- ctx.globalAlpha = 0.08 + rng() * 0.12;
1108
- ctx.strokeStyle = colors[Math.floor(rng() * colors.length)];
1539
+ ctx.globalAlpha = 0.06 + rng() * 0.1;
1540
+ ctx.strokeStyle = (0, $b5a262d09b87e373$export$f2121afcad3d553f)(colors[Math.floor(rng() * colors.length)], 0.3);
1109
1541
  ctx.beginPath();
1110
1542
  ctx.moveTo(a.x, a.y);
1111
1543
  ctx.quadraticCurveTo(cpx, cpy, b.x, b.y);