git-hash-art 0.3.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/module.js CHANGED
@@ -13,14 +13,17 @@ import $4RUNL$colorscheme from "color-scheme";
13
13
  * identically in Node (@napi-rs/canvas) and browsers.
14
14
  *
15
15
  * Generation pipeline:
16
- * 1. Background — radial gradient from hash-derived dark palette
17
- * 2. Composition modehash selects: radial, flow-field, spiral, grid-subdivision, or clustered
18
- * 3. Color field smooth positional color blending across the canvas
19
- * 4. Shape layers weighted selection, focal-point placement, transparency, glow, gradients, jitter
20
- * 5. Recursive nesting — some shapes contain smaller shapes inside
21
- * 6. Flow-line pass bezier curves following a hash-derived vector field
22
- * 7. Noise texture overlay — subtle grain for organic feel
23
- * 8. Organic connecting curves — beziers between nearby shapes
16
+ * 1. Background — radial gradient from hash-derived dark palette
17
+ * 1b. Layered backgroundlarge faint shapes / subtle pattern for depth
18
+ * 2. Composition modehash selects: radial, flow-field, spiral, grid-subdivision, or clustered
19
+ * 3. Focal points + void zones (negative space)
20
+ * 4. Flow field seed values
21
+ * 5. Shape layersblend modes, render styles, weighted selection,
22
+ * focal-point placement, atmospheric depth, organic edges
23
+ * 5b. Recursive nesting
24
+ * 6. Flow-line pass — tapered brush-stroke curves
25
+ * 7. Noise texture overlay
26
+ * 8. Organic connecting curves
24
27
  */
25
28
  // declare module 'color-scheme';
26
29
 
@@ -78,51 +81,68 @@ class $461134e0b6ce0619$export$da2372f11bc66b3f {
78
81
  }
79
82
 
80
83
 
81
- function $9d614e7d77fc2947$export$f116a24fd288e742(gitHash) {
82
- const seed = (0, $461134e0b6ce0619$export$39a95c82b20fdf81)(gitHash);
83
- const scheme = new (0, $4RUNL$colorscheme)();
84
- scheme.from_hue(seed % 360).scheme("analogic").variation("soft");
85
- let colors = scheme.colors().map((hex)=>`#${hex}`);
86
- const contrastingHue = (seed + 180) % 360;
87
- const contrastingScheme = new (0, $4RUNL$colorscheme)();
88
- contrastingScheme.from_hue(contrastingHue).scheme("mono").variation("soft");
89
- colors.push(`#${contrastingScheme.colors()[0]}`);
90
- return colors;
84
+ // ── Color variation modes ───────────────────────────────────────────
85
+ // The hash deterministically selects a variation, producing dramatically
86
+ // different palettes from the same hue.
87
+ const $9d614e7d77fc2947$var$COLOR_VARIATIONS = [
88
+ "soft",
89
+ "hard",
90
+ "pastel",
91
+ "light",
92
+ "dark",
93
+ "default"
94
+ ];
95
+ /**
96
+ * Pick a color variation mode deterministically from a seed.
97
+ */ function $9d614e7d77fc2947$var$pickVariation(seed) {
98
+ return $9d614e7d77fc2947$var$COLOR_VARIATIONS[Math.abs(seed) % $9d614e7d77fc2947$var$COLOR_VARIATIONS.length];
99
+ }
100
+ /**
101
+ * Scheme type also varies — some hashes get near-monochromatic palettes,
102
+ * others get high-contrast complementary schemes.
103
+ */ const $9d614e7d77fc2947$var$SCHEME_TYPES = [
104
+ "analogic",
105
+ "mono",
106
+ "contrast",
107
+ "triade",
108
+ "tetrade"
109
+ ];
110
+ function $9d614e7d77fc2947$var$pickSchemeType(seed) {
111
+ return $9d614e7d77fc2947$var$SCHEME_TYPES[Math.abs(seed >> 4) % $9d614e7d77fc2947$var$SCHEME_TYPES.length];
91
112
  }
92
113
  class $9d614e7d77fc2947$export$ab958c550f521376 {
93
114
  seed;
115
+ rng;
116
+ variation;
117
+ schemeType;
94
118
  baseScheme;
95
119
  complementaryScheme;
96
120
  triadicScheme;
97
- metallic;
98
121
  constructor(gitHash){
99
122
  this.seed = (0, $461134e0b6ce0619$export$39a95c82b20fdf81)(gitHash);
123
+ this.rng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 42));
124
+ // Hash-driven variation and scheme type for palette diversity
125
+ this.variation = $9d614e7d77fc2947$var$pickVariation(this.seed);
126
+ this.schemeType = $9d614e7d77fc2947$var$pickSchemeType(this.seed);
100
127
  this.baseScheme = this.generateBaseScheme();
101
128
  this.complementaryScheme = this.generateComplementaryScheme();
102
129
  this.triadicScheme = this.generateTriadicScheme();
103
- this.metallic = this.generateMetallicColors();
104
130
  }
105
131
  generateBaseScheme() {
106
132
  const scheme = new (0, $4RUNL$colorscheme)();
107
- return scheme.from_hue(this.seed % 360).scheme("analogic").variation("soft").colors().map((hex)=>`#${hex}`);
133
+ return scheme.from_hue(this.seed % 360).scheme(this.schemeType).variation(this.variation).colors().map((hex)=>`#${hex}`);
108
134
  }
109
135
  generateComplementaryScheme() {
110
136
  const complementaryHue = (this.seed + 180) % 360;
137
+ // Complementary uses a contrasting variation for tension
138
+ const compVariation = this.variation === "soft" ? "hard" : this.variation === "dark" ? "light" : this.variation;
111
139
  const scheme = new (0, $4RUNL$colorscheme)();
112
- return scheme.from_hue(complementaryHue).scheme("mono").variation("soft").colors().map((hex)=>`#${hex}`);
140
+ return scheme.from_hue(complementaryHue).scheme("mono").variation(compVariation).colors().map((hex)=>`#${hex}`);
113
141
  }
114
142
  generateTriadicScheme() {
115
143
  const triadicHue = (this.seed + 120) % 360;
116
144
  const scheme = new (0, $4RUNL$colorscheme)();
117
- return scheme.from_hue(triadicHue).scheme("triade").variation("soft").colors().map((hex)=>`#${hex}`);
118
- }
119
- generateMetallicColors() {
120
- return {
121
- gold: "#FFD700",
122
- silver: "#C0C0C0",
123
- copper: "#B87333",
124
- bronze: "#CD7F32"
125
- };
145
+ return scheme.from_hue(triadicHue).scheme("triade").variation(this.variation).colors().map((hex)=>`#${hex}`);
126
146
  }
127
147
  /**
128
148
  * Returns a flat array of hash-derived colors suitable for art generation.
@@ -180,6 +200,12 @@ function $9d614e7d77fc2947$export$59539d800dbe6858(hex, rng, amount = 0.1) {
180
200
  const jit = ()=>(rng() - 0.5) * 2 * amount * 255;
181
201
  return $9d614e7d77fc2947$var$rgbToHex(r + jit(), g + jit(), b + jit());
182
202
  }
203
+ function $9d614e7d77fc2947$export$fb75607d98509d9(hex, amount) {
204
+ const [r, g, b] = $9d614e7d77fc2947$var$hexToRgb(hex);
205
+ const gray = 0.299 * r + 0.587 * g + 0.114 * b;
206
+ const mix = (c)=>c + (gray - c) * amount;
207
+ return $9d614e7d77fc2947$var$rgbToHex(mix(r), mix(g), mix(b));
208
+ }
183
209
 
184
210
 
185
211
 
@@ -967,6 +993,32 @@ const $701ba7c7229ef06d$export$4ff7fc6f1af248b5 = {
967
993
  };
968
994
 
969
995
 
996
+ const $9beb8f41637c29fd$export$f821c68fe9beaecf = [
997
+ "source-over",
998
+ "screen",
999
+ "multiply",
1000
+ "overlay",
1001
+ "soft-light",
1002
+ "color-dodge",
1003
+ "color-burn",
1004
+ "lighter"
1005
+ ];
1006
+ function $9beb8f41637c29fd$export$7bb7bff4e26fa06b(rng) {
1007
+ if (rng() < 0.4) return "source-over";
1008
+ return $9beb8f41637c29fd$export$f821c68fe9beaecf[1 + Math.floor(rng() * ($9beb8f41637c29fd$export$f821c68fe9beaecf.length - 1))];
1009
+ }
1010
+ const $9beb8f41637c29fd$var$RENDER_STYLES = [
1011
+ "fill-and-stroke",
1012
+ "fill-and-stroke",
1013
+ "fill-only",
1014
+ "stroke-only",
1015
+ "double-stroke",
1016
+ "dashed",
1017
+ "watercolor"
1018
+ ];
1019
+ function $9beb8f41637c29fd$export$9fd4e64b2acd410e(rng) {
1020
+ return $9beb8f41637c29fd$var$RENDER_STYLES[Math.floor(rng() * $9beb8f41637c29fd$var$RENDER_STYLES.length)];
1021
+ }
970
1022
  function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
971
1023
  const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
972
1024
  ctx.save();
@@ -983,8 +1035,71 @@ function $9beb8f41637c29fd$export$71b514a25c47df50(ctx, shape, x, y, config) {
983
1035
  }
984
1036
  ctx.restore();
985
1037
  }
1038
+ /**
1039
+ * Apply the chosen render style to the current path.
1040
+ */ function $9beb8f41637c29fd$var$applyRenderStyle(ctx, style, fillColor, strokeColor, strokeWidth, size, rng) {
1041
+ switch(style){
1042
+ case "fill-only":
1043
+ ctx.fill();
1044
+ break;
1045
+ case "stroke-only":
1046
+ ctx.fill(); // transparent fill to define the path
1047
+ ctx.globalAlpha *= 0.3; // ghost fill
1048
+ ctx.fill();
1049
+ ctx.globalAlpha /= 0.3;
1050
+ ctx.stroke();
1051
+ break;
1052
+ case "double-stroke":
1053
+ ctx.fill();
1054
+ // Outer stroke
1055
+ ctx.lineWidth = strokeWidth * 2;
1056
+ ctx.globalAlpha *= 0.5;
1057
+ ctx.stroke();
1058
+ ctx.globalAlpha /= 0.5;
1059
+ // Inner stroke
1060
+ ctx.lineWidth = strokeWidth * 0.5;
1061
+ ctx.strokeStyle = fillColor;
1062
+ ctx.stroke();
1063
+ break;
1064
+ case "dashed":
1065
+ ctx.fill();
1066
+ ctx.setLineDash([
1067
+ size * 0.05,
1068
+ size * 0.03
1069
+ ]);
1070
+ ctx.stroke();
1071
+ ctx.setLineDash([]);
1072
+ break;
1073
+ case "watercolor":
1074
+ {
1075
+ // Draw 3-4 slightly offset passes at low opacity for a bleed effect
1076
+ const passes = 3 + (rng ? Math.floor(rng() * 2) : 0);
1077
+ const savedAlpha = ctx.globalAlpha;
1078
+ ctx.globalAlpha = savedAlpha * (0.3 / passes * 2);
1079
+ for(let p = 0; p < passes; p++){
1080
+ const jx = rng ? (rng() - 0.5) * size * 0.06 : 0;
1081
+ const jy = rng ? (rng() - 0.5) * size * 0.06 : 0;
1082
+ ctx.save();
1083
+ ctx.translate(jx, jy);
1084
+ ctx.fill();
1085
+ ctx.restore();
1086
+ }
1087
+ ctx.globalAlpha = savedAlpha;
1088
+ // Light stroke on top
1089
+ ctx.globalAlpha *= 0.4;
1090
+ ctx.stroke();
1091
+ ctx.globalAlpha /= 0.4;
1092
+ break;
1093
+ }
1094
+ case "fill-and-stroke":
1095
+ default:
1096
+ ctx.fill();
1097
+ ctx.stroke();
1098
+ break;
1099
+ }
1100
+ }
986
1101
  function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
987
- 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;
1102
+ 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;
988
1103
  ctx.save();
989
1104
  ctx.translate(x, y);
990
1105
  ctx.rotate(rotation * Math.PI / 180);
@@ -1007,8 +1122,7 @@ function $9beb8f41637c29fd$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
1007
1122
  const drawFunction = (0, $701ba7c7229ef06d$export$4ff7fc6f1af248b5)[shape];
1008
1123
  if (drawFunction) {
1009
1124
  drawFunction(ctx, size);
1010
- ctx.fill();
1011
- ctx.stroke();
1125
+ $9beb8f41637c29fd$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
1012
1126
  }
1013
1127
  // Reset shadow so patterns aren't double-glowed
1014
1128
  if (glowRadius > 0) ctx.shadowBlur = 0;
@@ -1093,13 +1207,6 @@ function $b623126c6e9cbb71$var$pickShape(rng, layerRatio, shapeNames) {
1093
1207
  if (available.length === 0) return shapeNames[Math.floor(rng() * shapeNames.length)];
1094
1208
  return available[Math.floor(rng() * available.length)];
1095
1209
  }
1096
- // ── Helper: simple 2D value noise (hash-seeded) ─────────────────────
1097
- function $b623126c6e9cbb71$var$valueNoise(x, y, scale, rng) {
1098
- // Cheap pseudo-noise: combine sin waves at different frequencies
1099
- const nx = x / scale;
1100
- const ny = y / scale;
1101
- 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;
1102
- }
1103
1210
  // ── Helper: get position based on composition mode ──────────────────
1104
1211
  function $b623126c6e9cbb71$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
1105
1212
  switch(mode){
@@ -1139,10 +1246,8 @@ function $b623126c6e9cbb71$var$getCompositionPosition(mode, rng, width, height,
1139
1246
  }
1140
1247
  case "clustered":
1141
1248
  {
1142
- // Pick one of 3-5 cluster centers, then scatter around it
1143
1249
  const numClusters = 3 + Math.floor(rng() * 3);
1144
1250
  const ci = Math.floor(rng() * numClusters);
1145
- // Deterministic cluster center from index
1146
1251
  const clusterRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(String(ci), 999));
1147
1252
  const clx = width * (0.15 + clusterRng() * 0.7);
1148
1253
  const cly = height * (0.15 + clusterRng() * 0.7);
@@ -1154,7 +1259,6 @@ function $b623126c6e9cbb71$var$getCompositionPosition(mode, rng, width, height,
1154
1259
  }
1155
1260
  case "flow-field":
1156
1261
  default:
1157
- // Random position, will be adjusted by flow field direction later
1158
1262
  return {
1159
1263
  x: rng() * width,
1160
1264
  y: rng() * height
@@ -1163,15 +1267,25 @@ function $b623126c6e9cbb71$var$getCompositionPosition(mode, rng, width, height,
1163
1267
  }
1164
1268
  // ── Helper: positional color blending ───────────────────────────────
1165
1269
  function $b623126c6e9cbb71$var$getPositionalColor(x, y, width, height, colors, rng) {
1166
- // Blend between palette colors based on position
1167
1270
  const nx = x / width;
1168
1271
  const ny = y / height;
1169
- // Use position to bias which palette color is chosen
1170
1272
  const posIndex = (nx * 0.6 + ny * 0.4) * (colors.length - 1);
1171
1273
  const baseIdx = Math.floor(posIndex) % colors.length;
1172
- // Then jitter it slightly
1173
1274
  return (0, $9d614e7d77fc2947$export$59539d800dbe6858)(colors[baseIdx], rng, 0.08);
1174
1275
  }
1276
+ // ── Helper: check if a position is inside a void zone (Feature E) ───
1277
+ function $b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones) {
1278
+ for (const zone of voidZones){
1279
+ if (Math.hypot(x - zone.x, y - zone.y) < zone.radius) return true;
1280
+ }
1281
+ return false;
1282
+ }
1283
+ // ── Helper: density check for negative space (Feature E) ────────────
1284
+ function $b623126c6e9cbb71$var$localDensity(x, y, positions, radius) {
1285
+ let count = 0;
1286
+ for (const p of positions)if (Math.hypot(x - p.x, y - p.y) < radius) count++;
1287
+ return count;
1288
+ }
1175
1289
  function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1176
1290
  const finalConfig = {
1177
1291
  ...(0, $2bfb6a1ccb7a82ae$export$c2f8e0cc249a8d8f),
@@ -1196,9 +1310,36 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1196
1310
  bgGrad.addColorStop(1, bgEnd);
1197
1311
  ctx.fillStyle = bgGrad;
1198
1312
  ctx.fillRect(0, 0, width, height);
1313
+ // ── 1b. Layered background (Feature G) ─────────────────────────
1314
+ // Draw large, very faint shapes to give the background texture
1315
+ const bgShapeCount = 3 + Math.floor(rng() * 4);
1316
+ ctx.globalCompositeOperation = "soft-light";
1317
+ for(let i = 0; i < bgShapeCount; i++){
1318
+ const bx = rng() * width;
1319
+ const by = rng() * height;
1320
+ const bSize = width * 0.3 + rng() * width * 0.5;
1321
+ const bColor = colors[Math.floor(rng() * colors.length)];
1322
+ ctx.globalAlpha = 0.03 + rng() * 0.05;
1323
+ ctx.fillStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(bColor, 0.15);
1324
+ ctx.beginPath();
1325
+ ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
1326
+ ctx.fill();
1327
+ }
1328
+ // Subtle concentric rings from center
1329
+ const ringCount = 2 + Math.floor(rng() * 3);
1330
+ ctx.globalAlpha = 0.02 + rng() * 0.03;
1331
+ ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colors[0], 0.1);
1332
+ ctx.lineWidth = 1 * scaleFactor;
1333
+ for(let i = 1; i <= ringCount; i++){
1334
+ const r = Math.min(width, height) * 0.15 * i;
1335
+ ctx.beginPath();
1336
+ ctx.arc(cx, cy, r, 0, Math.PI * 2);
1337
+ ctx.stroke();
1338
+ }
1339
+ ctx.globalCompositeOperation = "source-over";
1199
1340
  // ── 2. Composition mode ────────────────────────────────────────
1200
1341
  const compositionMode = $b623126c6e9cbb71$var$COMPOSITION_MODES[Math.floor(rng() * $b623126c6e9cbb71$var$COMPOSITION_MODES.length)];
1201
- // ── 3. Focal points ────────────────────────────────────────────
1342
+ // ── 3. Focal points + void zones ───────────────────────────────
1202
1343
  const numFocal = 1 + Math.floor(rng() * 2);
1203
1344
  const focalPoints = [];
1204
1345
  for(let f = 0; f < numFocal; f++)focalPoints.push({
@@ -1206,6 +1347,14 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1206
1347
  y: height * (0.2 + rng() * 0.6),
1207
1348
  strength: 0.3 + rng() * 0.4
1208
1349
  });
1350
+ // Feature E: 1-2 void zones where shapes are sparse (negative space)
1351
+ const numVoids = Math.floor(rng() * 2) + 1;
1352
+ const voidZones = [];
1353
+ for(let v = 0; v < numVoids; v++)voidZones.push({
1354
+ x: width * (0.15 + rng() * 0.7),
1355
+ y: height * (0.15 + rng() * 0.7),
1356
+ radius: Math.min(width, height) * (0.06 + rng() * 0.1)
1357
+ });
1209
1358
  function applyFocalBias(rx, ry) {
1210
1359
  let nearest = focalPoints[0];
1211
1360
  let minDist = Infinity;
@@ -1222,7 +1371,7 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1222
1371
  ry + (nearest.y - ry) * pull
1223
1372
  ];
1224
1373
  }
1225
- // ── 4. Flow field seed values (for flow-field mode & line pass) ─
1374
+ // ── 4. Flow field seed values ──────────────────────────────────
1226
1375
  const fieldAngleBase = rng() * Math.PI * 2;
1227
1376
  const fieldFreq = 0.5 + rng() * 2;
1228
1377
  function flowAngle(x, y) {
@@ -1230,15 +1379,32 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1230
1379
  }
1231
1380
  // ── 5. Shape layers ────────────────────────────────────────────
1232
1381
  const shapePositions = [];
1382
+ const densityCheckRadius = Math.min(width, height) * 0.08;
1383
+ const maxLocalDensity = Math.ceil(finalConfig.shapesPerLayer * 0.15);
1233
1384
  for(let layer = 0; layer < layers; layer++){
1234
1385
  const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
1235
1386
  const numShapes = finalConfig.shapesPerLayer + Math.floor(rng() * finalConfig.shapesPerLayer * 0.3);
1236
1387
  const layerOpacity = Math.max(0.15, baseOpacity - layer * opacityReduction);
1237
1388
  const layerSizeScale = 1 - layer * 0.15;
1389
+ // Feature B: per-layer blend mode
1390
+ const layerBlend = (0, $9beb8f41637c29fd$export$7bb7bff4e26fa06b)(rng);
1391
+ ctx.globalCompositeOperation = layerBlend;
1392
+ // Feature C: per-layer render style bias
1393
+ const layerRenderStyle = (0, $9beb8f41637c29fd$export$9fd4e64b2acd410e)(rng);
1394
+ // Feature D: atmospheric desaturation for later layers
1395
+ const atmosphericDesat = layerRatio * 0.3; // 0 for first layer, up to 0.3 for last
1238
1396
  for(let i = 0; i < numShapes; i++){
1239
1397
  // Position from composition mode, then focal bias
1240
1398
  const rawPos = $b623126c6e9cbb71$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
1241
1399
  const [x, y] = applyFocalBias(rawPos.x, rawPos.y);
1400
+ // Feature E: skip shapes in void zones, reduce in dense areas
1401
+ if ($b623126c6e9cbb71$var$isInVoidZone(x, y, voidZones)) {
1402
+ // 85% chance to skip — allows a few shapes to bleed in
1403
+ if (rng() < 0.85) continue;
1404
+ }
1405
+ if ($b623126c6e9cbb71$var$localDensity(x, y, shapePositions, densityCheckRadius) > maxLocalDensity) {
1406
+ if (rng() < 0.6) continue; // thin out dense areas
1407
+ }
1242
1408
  // Weighted shape selection
1243
1409
  const shape = $b623126c6e9cbb71$var$pickShape(rng, layerRatio, shapeNames);
1244
1410
  // Power distribution for size
@@ -1247,8 +1413,10 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1247
1413
  // Flow-field rotation in flow-field mode, random otherwise
1248
1414
  const rotation = compositionMode === "flow-field" ? flowAngle(x, y) * 180 / Math.PI + (rng() - 0.5) * 30 : rng() * 360;
1249
1415
  // Positional color blending + jitter
1250
- const fillBase = $b623126c6e9cbb71$var$getPositionalColor(x, y, width, height, colors, rng);
1416
+ let fillBase = $b623126c6e9cbb71$var$getPositionalColor(x, y, width, height, colors, rng);
1251
1417
  const strokeBase = colors[Math.floor(rng() * colors.length)];
1418
+ // Feature D: desaturate colors on later layers for depth
1419
+ if (atmosphericDesat > 0) fillBase = (0, $9d614e7d77fc2947$export$fb75607d98509d9)(fillBase, atmosphericDesat);
1252
1420
  const fillColor = (0, $9d614e7d77fc2947$export$59539d800dbe6858)(fillBase, rng, 0.06);
1253
1421
  const strokeColor = (0, $9d614e7d77fc2947$export$59539d800dbe6858)(strokeBase, rng, 0.05);
1254
1422
  // Semi-transparent fill
@@ -1264,6 +1432,11 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1264
1432
  // Gradient fill on ~30%
1265
1433
  const hasGradient = rng() < 0.3;
1266
1434
  const gradientEnd = hasGradient ? (0, $9d614e7d77fc2947$export$59539d800dbe6858)(colors[Math.floor(rng() * colors.length)], rng, 0.1) : undefined;
1435
+ // Feature C: per-shape render style (70% use layer style, 30% pick their own)
1436
+ const shapeRenderStyle = rng() < 0.7 ? layerRenderStyle : (0, $9beb8f41637c29fd$export$9fd4e64b2acd410e)(rng);
1437
+ // Feature F: organic edge jitter — applied via watercolor style on ~15% of shapes
1438
+ const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
1439
+ const finalRenderStyle = useOrganicEdges ? "watercolor" : shapeRenderStyle;
1267
1440
  (0, $9beb8f41637c29fd$export$bb35a6995ddbf32d)(ctx, shape, x, y, {
1268
1441
  fillColor: transparentFill,
1269
1442
  strokeColor: strokeColor,
@@ -1273,14 +1446,16 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1273
1446
  proportionType: "GOLDEN_RATIO",
1274
1447
  glowRadius: glowRadius,
1275
1448
  glowColor: hasGlow ? (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(fillColor, 0.6) : undefined,
1276
- gradientFillEnd: gradientEnd
1449
+ gradientFillEnd: gradientEnd,
1450
+ renderStyle: finalRenderStyle,
1451
+ rng: rng
1277
1452
  });
1278
1453
  shapePositions.push({
1279
1454
  x: x,
1280
1455
  y: y,
1281
1456
  size: size
1282
1457
  });
1283
- // ── 5b. Recursive nesting: ~15% of larger shapes get inner shapes ──
1458
+ // ── 5b. Recursive nesting ──────────────────────────────────
1284
1459
  if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
1285
1460
  const innerCount = 1 + Math.floor(rng() * 3);
1286
1461
  for(let n = 0; n < innerCount; n++){
@@ -1297,37 +1472,49 @@ function $b623126c6e9cbb71$export$29a844702096332e(ctx, gitHash, config = {}) {
1297
1472
  strokeWidth: strokeWidth * 0.6,
1298
1473
  size: innerSize,
1299
1474
  rotation: innerRot,
1300
- proportionType: "GOLDEN_RATIO"
1475
+ proportionType: "GOLDEN_RATIO",
1476
+ renderStyle: shapeRenderStyle,
1477
+ rng: rng
1301
1478
  });
1302
1479
  }
1303
1480
  }
1304
1481
  }
1305
1482
  }
1306
- // ── 6. Flow-line pass ──────────────────────────────────────────
1307
- // Draw flowing curves that follow the hash-derived vector field
1483
+ // Reset blend mode for post-processing passes
1484
+ ctx.globalCompositeOperation = "source-over";
1485
+ // ── 6. Flow-line pass (Feature H: tapered brush strokes) ───────
1308
1486
  const numFlowLines = 6 + Math.floor(rng() * 10);
1309
1487
  for(let i = 0; i < numFlowLines; i++){
1310
1488
  let fx = rng() * width;
1311
1489
  let fy = rng() * height;
1312
1490
  const steps = 30 + Math.floor(rng() * 40);
1313
1491
  const stepLen = (3 + rng() * 5) * scaleFactor;
1314
- ctx.globalAlpha = 0.06 + rng() * 0.1;
1315
- ctx.strokeStyle = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colors[Math.floor(rng() * colors.length)], 0.4);
1316
- ctx.lineWidth = (0.5 + rng() * 1.5) * scaleFactor;
1317
- ctx.beginPath();
1318
- ctx.moveTo(fx, fy);
1492
+ const startWidth = (1 + rng() * 3) * scaleFactor;
1493
+ const lineColor = (0, $9d614e7d77fc2947$export$f2121afcad3d553f)(colors[Math.floor(rng() * colors.length)], 0.4);
1494
+ const lineAlpha = 0.06 + rng() * 0.1;
1495
+ // Draw as individual segments with tapering width
1496
+ let prevX = fx;
1497
+ let prevY = fy;
1319
1498
  for(let s = 0; s < steps; s++){
1320
1499
  const angle = flowAngle(fx, fy) + (rng() - 0.5) * 0.3;
1321
1500
  fx += Math.cos(angle) * stepLen;
1322
1501
  fy += Math.sin(angle) * stepLen;
1323
- // Stay in bounds
1324
1502
  if (fx < 0 || fx > width || fy < 0 || fy > height) break;
1503
+ // Taper: thick at start, thin at end
1504
+ const taper = 1 - s / steps * 0.8;
1505
+ ctx.globalAlpha = lineAlpha * taper;
1506
+ ctx.strokeStyle = lineColor;
1507
+ ctx.lineWidth = startWidth * taper;
1508
+ ctx.lineCap = "round";
1509
+ ctx.beginPath();
1510
+ ctx.moveTo(prevX, prevY);
1325
1511
  ctx.lineTo(fx, fy);
1512
+ ctx.stroke();
1513
+ prevX = fx;
1514
+ prevY = fy;
1326
1515
  }
1327
- ctx.stroke();
1328
1516
  }
1329
1517
  // ── 7. Noise texture overlay ───────────────────────────────────
1330
- // Subtle grain rendered as tiny semi-transparent dots
1331
1518
  const noiseRng = (0, $461134e0b6ce0619$export$eaf9227667332084)((0, $461134e0b6ce0619$export$e9cc707de01b7042)(gitHash, 777));
1332
1519
  const noiseDensity = Math.floor(width * height / 800);
1333
1520
  for(let i = 0; i < noiseDensity; i++){