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/browser.js CHANGED
@@ -12,14 +12,17 @@ import $4wRzV$colorscheme from "color-scheme";
12
12
  * identically in Node (@napi-rs/canvas) and browsers.
13
13
  *
14
14
  * Generation pipeline:
15
- * 1. Background — radial gradient from hash-derived dark palette
16
- * 2. Composition modehash 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
15
+ * 1. Background — radial gradient from hash-derived dark palette
16
+ * 1b. Layered backgroundlarge faint shapes / subtle pattern for depth
17
+ * 2. Composition modehash 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 layersblend 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
23
26
  */
24
27
  // declare module 'color-scheme';
25
28
 
@@ -77,46 +80,61 @@ class $616009579e3d72c5$export$da2372f11bc66b3f {
77
80
  }
78
81
 
79
82
 
80
- function $b5a262d09b87e373$export$f116a24fd288e742(gitHash) {
81
- const seed = (0, $616009579e3d72c5$export$39a95c82b20fdf81)(gitHash);
82
- const scheme = new (0, $4wRzV$colorscheme)();
83
- scheme.from_hue(seed % 360).scheme("analogic").variation("soft");
84
- let colors = scheme.colors().map((hex)=>`#${hex}`);
85
- const contrastingHue = (seed + 180) % 360;
86
- const contrastingScheme = new (0, $4wRzV$colorscheme)();
87
- contrastingScheme.from_hue(contrastingHue).scheme("mono").variation("soft");
88
- colors.push(`#${contrastingScheme.colors()[0]}`);
89
- 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];
90
111
  }
91
112
  class $b5a262d09b87e373$export$ab958c550f521376 {
92
113
  constructor(gitHash){
93
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);
94
119
  this.baseScheme = this.generateBaseScheme();
95
120
  this.complementaryScheme = this.generateComplementaryScheme();
96
121
  this.triadicScheme = this.generateTriadicScheme();
97
- this.metallic = this.generateMetallicColors();
98
122
  }
99
123
  generateBaseScheme() {
100
124
  const scheme = new (0, $4wRzV$colorscheme)();
101
- 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}`);
102
126
  }
103
127
  generateComplementaryScheme() {
104
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;
105
131
  const scheme = new (0, $4wRzV$colorscheme)();
106
- 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}`);
107
133
  }
108
134
  generateTriadicScheme() {
109
135
  const triadicHue = (this.seed + 120) % 360;
110
136
  const scheme = new (0, $4wRzV$colorscheme)();
111
- return scheme.from_hue(triadicHue).scheme("triade").variation("soft").colors().map((hex)=>`#${hex}`);
112
- }
113
- generateMetallicColors() {
114
- return {
115
- gold: "#FFD700",
116
- silver: "#C0C0C0",
117
- copper: "#B87333",
118
- bronze: "#CD7F32"
119
- };
137
+ return scheme.from_hue(triadicHue).scheme("triade").variation(this.variation).colors().map((hex)=>`#${hex}`);
120
138
  }
121
139
  /**
122
140
  * Returns a flat array of hash-derived colors suitable for art generation.
@@ -174,6 +192,12 @@ function $b5a262d09b87e373$export$59539d800dbe6858(hex, rng, amount = 0.1) {
174
192
  const jit = ()=>(rng() - 0.5) * 2 * amount * 255;
175
193
  return $b5a262d09b87e373$var$rgbToHex(r + jit(), g + jit(), b + jit());
176
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
+ }
177
201
 
178
202
 
179
203
 
@@ -961,6 +985,32 @@ const $e41b41d8dcf837ad$export$4ff7fc6f1af248b5 = {
961
985
  };
962
986
 
963
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
+ }
964
1014
  function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
965
1015
  const { fillColor: fillColor, strokeColor: strokeColor, strokeWidth: strokeWidth, size: size, rotation: rotation } = config;
966
1016
  ctx.save();
@@ -977,8 +1027,71 @@ function $e0f99502ff383dd8$export$71b514a25c47df50(ctx, shape, x, y, config) {
977
1027
  }
978
1028
  ctx.restore();
979
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
+ }
980
1093
  function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
981
- 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;
982
1095
  ctx.save();
983
1096
  ctx.translate(x, y);
984
1097
  ctx.rotate(rotation * Math.PI / 180);
@@ -1001,8 +1114,7 @@ function $e0f99502ff383dd8$export$bb35a6995ddbf32d(ctx, shape, x, y, config) {
1001
1114
  const drawFunction = (0, $e41b41d8dcf837ad$export$4ff7fc6f1af248b5)[shape];
1002
1115
  if (drawFunction) {
1003
1116
  drawFunction(ctx, size);
1004
- ctx.fill();
1005
- ctx.stroke();
1117
+ $e0f99502ff383dd8$var$applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
1006
1118
  }
1007
1119
  // Reset shadow so patterns aren't double-glowed
1008
1120
  if (glowRadius > 0) ctx.shadowBlur = 0;
@@ -1087,13 +1199,6 @@ function $1f63dc64b5593c73$var$pickShape(rng, layerRatio, shapeNames) {
1087
1199
  if (available.length === 0) return shapeNames[Math.floor(rng() * shapeNames.length)];
1088
1200
  return available[Math.floor(rng() * available.length)];
1089
1201
  }
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
1202
  // ── Helper: get position based on composition mode ──────────────────
1098
1203
  function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height, shapeIndex, totalShapes, cx, cy) {
1099
1204
  switch(mode){
@@ -1133,10 +1238,8 @@ function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height,
1133
1238
  }
1134
1239
  case "clustered":
1135
1240
  {
1136
- // Pick one of 3-5 cluster centers, then scatter around it
1137
1241
  const numClusters = 3 + Math.floor(rng() * 3);
1138
1242
  const ci = Math.floor(rng() * numClusters);
1139
- // Deterministic cluster center from index
1140
1243
  const clusterRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(String(ci), 999));
1141
1244
  const clx = width * (0.15 + clusterRng() * 0.7);
1142
1245
  const cly = height * (0.15 + clusterRng() * 0.7);
@@ -1148,7 +1251,6 @@ function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height,
1148
1251
  }
1149
1252
  case "flow-field":
1150
1253
  default:
1151
- // Random position, will be adjusted by flow field direction later
1152
1254
  return {
1153
1255
  x: rng() * width,
1154
1256
  y: rng() * height
@@ -1157,15 +1259,25 @@ function $1f63dc64b5593c73$var$getCompositionPosition(mode, rng, width, height,
1157
1259
  }
1158
1260
  // ── Helper: positional color blending ───────────────────────────────
1159
1261
  function $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, colors, rng) {
1160
- // Blend between palette colors based on position
1161
1262
  const nx = x / width;
1162
1263
  const ny = y / height;
1163
- // Use position to bias which palette color is chosen
1164
1264
  const posIndex = (nx * 0.6 + ny * 0.4) * (colors.length - 1);
1165
1265
  const baseIdx = Math.floor(posIndex) % colors.length;
1166
- // Then jitter it slightly
1167
1266
  return (0, $b5a262d09b87e373$export$59539d800dbe6858)(colors[baseIdx], rng, 0.08);
1168
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
+ }
1169
1281
  function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1170
1282
  const finalConfig = {
1171
1283
  ...(0, $81c1b644006d48ec$export$c2f8e0cc249a8d8f),
@@ -1190,9 +1302,36 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1190
1302
  bgGrad.addColorStop(1, bgEnd);
1191
1303
  ctx.fillStyle = bgGrad;
1192
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";
1193
1332
  // ── 2. Composition mode ────────────────────────────────────────
1194
1333
  const compositionMode = $1f63dc64b5593c73$var$COMPOSITION_MODES[Math.floor(rng() * $1f63dc64b5593c73$var$COMPOSITION_MODES.length)];
1195
- // ── 3. Focal points ────────────────────────────────────────────
1334
+ // ── 3. Focal points + void zones ───────────────────────────────
1196
1335
  const numFocal = 1 + Math.floor(rng() * 2);
1197
1336
  const focalPoints = [];
1198
1337
  for(let f = 0; f < numFocal; f++)focalPoints.push({
@@ -1200,6 +1339,14 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1200
1339
  y: height * (0.2 + rng() * 0.6),
1201
1340
  strength: 0.3 + rng() * 0.4
1202
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
+ });
1203
1350
  function applyFocalBias(rx, ry) {
1204
1351
  let nearest = focalPoints[0];
1205
1352
  let minDist = Infinity;
@@ -1216,7 +1363,7 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1216
1363
  ry + (nearest.y - ry) * pull
1217
1364
  ];
1218
1365
  }
1219
- // ── 4. Flow field seed values (for flow-field mode & line pass) ─
1366
+ // ── 4. Flow field seed values ──────────────────────────────────
1220
1367
  const fieldAngleBase = rng() * Math.PI * 2;
1221
1368
  const fieldFreq = 0.5 + rng() * 2;
1222
1369
  function flowAngle(x, y) {
@@ -1224,15 +1371,32 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1224
1371
  }
1225
1372
  // ── 5. Shape layers ────────────────────────────────────────────
1226
1373
  const shapePositions = [];
1374
+ const densityCheckRadius = Math.min(width, height) * 0.08;
1375
+ const maxLocalDensity = Math.ceil(finalConfig.shapesPerLayer * 0.15);
1227
1376
  for(let layer = 0; layer < layers; layer++){
1228
1377
  const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
1229
1378
  const numShapes = finalConfig.shapesPerLayer + Math.floor(rng() * finalConfig.shapesPerLayer * 0.3);
1230
1379
  const layerOpacity = Math.max(0.15, baseOpacity - layer * opacityReduction);
1231
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
1232
1388
  for(let i = 0; i < numShapes; i++){
1233
1389
  // Position from composition mode, then focal bias
1234
1390
  const rawPos = $1f63dc64b5593c73$var$getCompositionPosition(compositionMode, rng, width, height, i, numShapes, cx, cy);
1235
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
+ }
1236
1400
  // Weighted shape selection
1237
1401
  const shape = $1f63dc64b5593c73$var$pickShape(rng, layerRatio, shapeNames);
1238
1402
  // Power distribution for size
@@ -1241,8 +1405,10 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1241
1405
  // Flow-field rotation in flow-field mode, random otherwise
1242
1406
  const rotation = compositionMode === "flow-field" ? flowAngle(x, y) * 180 / Math.PI + (rng() - 0.5) * 30 : rng() * 360;
1243
1407
  // Positional color blending + jitter
1244
- const fillBase = $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, colors, rng);
1408
+ let fillBase = $1f63dc64b5593c73$var$getPositionalColor(x, y, width, height, colors, rng);
1245
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);
1246
1412
  const fillColor = (0, $b5a262d09b87e373$export$59539d800dbe6858)(fillBase, rng, 0.06);
1247
1413
  const strokeColor = (0, $b5a262d09b87e373$export$59539d800dbe6858)(strokeBase, rng, 0.05);
1248
1414
  // Semi-transparent fill
@@ -1258,6 +1424,11 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1258
1424
  // Gradient fill on ~30%
1259
1425
  const hasGradient = rng() < 0.3;
1260
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;
1261
1432
  (0, $e0f99502ff383dd8$export$bb35a6995ddbf32d)(ctx, shape, x, y, {
1262
1433
  fillColor: transparentFill,
1263
1434
  strokeColor: strokeColor,
@@ -1267,14 +1438,16 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1267
1438
  proportionType: "GOLDEN_RATIO",
1268
1439
  glowRadius: glowRadius,
1269
1440
  glowColor: hasGlow ? (0, $b5a262d09b87e373$export$f2121afcad3d553f)(fillColor, 0.6) : undefined,
1270
- gradientFillEnd: gradientEnd
1441
+ gradientFillEnd: gradientEnd,
1442
+ renderStyle: finalRenderStyle,
1443
+ rng: rng
1271
1444
  });
1272
1445
  shapePositions.push({
1273
1446
  x: x,
1274
1447
  y: y,
1275
1448
  size: size
1276
1449
  });
1277
- // ── 5b. Recursive nesting: ~15% of larger shapes get inner shapes ──
1450
+ // ── 5b. Recursive nesting ──────────────────────────────────
1278
1451
  if (size > adjustedMaxSize * 0.4 && rng() < 0.15) {
1279
1452
  const innerCount = 1 + Math.floor(rng() * 3);
1280
1453
  for(let n = 0; n < innerCount; n++){
@@ -1291,37 +1464,49 @@ function $1f63dc64b5593c73$export$29a844702096332e(ctx, gitHash, config = {}) {
1291
1464
  strokeWidth: strokeWidth * 0.6,
1292
1465
  size: innerSize,
1293
1466
  rotation: innerRot,
1294
- proportionType: "GOLDEN_RATIO"
1467
+ proportionType: "GOLDEN_RATIO",
1468
+ renderStyle: shapeRenderStyle,
1469
+ rng: rng
1295
1470
  });
1296
1471
  }
1297
1472
  }
1298
1473
  }
1299
1474
  }
1300
- // ── 6. Flow-line pass ──────────────────────────────────────────
1301
- // Draw flowing curves that follow the hash-derived vector field
1475
+ // Reset blend mode for post-processing passes
1476
+ ctx.globalCompositeOperation = "source-over";
1477
+ // ── 6. Flow-line pass (Feature H: tapered brush strokes) ───────
1302
1478
  const numFlowLines = 6 + Math.floor(rng() * 10);
1303
1479
  for(let i = 0; i < numFlowLines; i++){
1304
1480
  let fx = rng() * width;
1305
1481
  let fy = rng() * height;
1306
1482
  const steps = 30 + Math.floor(rng() * 40);
1307
1483
  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);
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;
1313
1490
  for(let s = 0; s < steps; s++){
1314
1491
  const angle = flowAngle(fx, fy) + (rng() - 0.5) * 0.3;
1315
1492
  fx += Math.cos(angle) * stepLen;
1316
1493
  fy += Math.sin(angle) * stepLen;
1317
- // Stay in bounds
1318
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);
1319
1503
  ctx.lineTo(fx, fy);
1504
+ ctx.stroke();
1505
+ prevX = fx;
1506
+ prevY = fy;
1320
1507
  }
1321
- ctx.stroke();
1322
1508
  }
1323
1509
  // ── 7. Noise texture overlay ───────────────────────────────────
1324
- // Subtle grain rendered as tiny semi-transparent dots
1325
1510
  const noiseRng = (0, $616009579e3d72c5$export$eaf9227667332084)((0, $616009579e3d72c5$export$e9cc707de01b7042)(gitHash, 777));
1326
1511
  const noiseDensity = Math.floor(width * height / 800);
1327
1512
  for(let i = 0; i < noiseDensity; i++){