git-hash-art 0.10.0 → 0.11.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/.github/workflows/deploy-www.yml +57 -0
- package/ALGORITHM.md +108 -8
- package/CHANGELOG.md +20 -0
- package/dist/browser.js +873 -64
- package/dist/browser.js.map +1 -1
- package/dist/main.js +875 -64
- package/dist/main.js.map +1 -1
- package/dist/module.js +875 -64
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/archetypes.ts +33 -1
- package/src/lib/canvas/colors.ts +52 -5
- package/src/lib/canvas/draw.ts +94 -5
- package/src/lib/render.ts +501 -67
- package/src/lib/utils.ts +109 -0
package/package.json
CHANGED
package/src/lib/archetypes.ts
CHANGED
|
@@ -27,10 +27,21 @@ export type PaletteMode =
|
|
|
27
27
|
| "neon" // high saturation on dark
|
|
28
28
|
| "pastel-light" // soft pastels on light background
|
|
29
29
|
| "earth" // muted warm naturals
|
|
30
|
-
| "high-contrast"
|
|
30
|
+
| "high-contrast" // black + white + one accent
|
|
31
|
+
| "split-complementary" // base hue + two flanking complements
|
|
32
|
+
| "analogous-accent" // tight analogous cluster + one distant accent
|
|
33
|
+
| "limited-palette"; // 3 colors only, risograph-print feel
|
|
31
34
|
|
|
32
35
|
// ── Archetype definition ────────────────────────────────────────────
|
|
33
36
|
|
|
37
|
+
export type CompositionMode =
|
|
38
|
+
| "radial"
|
|
39
|
+
| "flow-field"
|
|
40
|
+
| "spiral"
|
|
41
|
+
| "grid-subdivision"
|
|
42
|
+
| "clustered"
|
|
43
|
+
| "golden-spiral";
|
|
44
|
+
|
|
34
45
|
export interface Archetype {
|
|
35
46
|
name: string;
|
|
36
47
|
/** Override gridSize (controls shape count) */
|
|
@@ -51,6 +62,8 @@ export interface Archetype {
|
|
|
51
62
|
paletteMode: PaletteMode;
|
|
52
63
|
/** Preferred render styles (weighted toward these) */
|
|
53
64
|
preferredStyles: RenderStyle[];
|
|
65
|
+
/** Preferred composition modes (70% chance of using one of these) */
|
|
66
|
+
preferredCompositions: CompositionMode[];
|
|
54
67
|
/** Flow line count multiplier (1 = default) */
|
|
55
68
|
flowLineMultiplier: number;
|
|
56
69
|
/** Whether to draw the hero shape */
|
|
@@ -77,6 +90,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
77
90
|
backgroundStyle: "radial-dark",
|
|
78
91
|
paletteMode: "harmonious",
|
|
79
92
|
preferredStyles: ["fill-and-stroke", "watercolor", "fill-only"],
|
|
93
|
+
preferredCompositions: ["clustered", "flow-field", "radial"],
|
|
80
94
|
flowLineMultiplier: 2.5,
|
|
81
95
|
heroShape: false,
|
|
82
96
|
glowMultiplier: 0.5,
|
|
@@ -94,6 +108,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
94
108
|
backgroundStyle: "solid-light",
|
|
95
109
|
paletteMode: "duotone",
|
|
96
110
|
preferredStyles: ["fill-and-stroke", "stroke-only", "incomplete"],
|
|
111
|
+
preferredCompositions: ["golden-spiral", "grid-subdivision"],
|
|
97
112
|
flowLineMultiplier: 0.3,
|
|
98
113
|
heroShape: true,
|
|
99
114
|
glowMultiplier: 0,
|
|
@@ -111,6 +126,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
111
126
|
backgroundStyle: "radial-dark",
|
|
112
127
|
paletteMode: "earth",
|
|
113
128
|
preferredStyles: ["watercolor", "fill-only", "incomplete"],
|
|
129
|
+
preferredCompositions: ["flow-field", "golden-spiral", "spiral"],
|
|
114
130
|
flowLineMultiplier: 4,
|
|
115
131
|
heroShape: false,
|
|
116
132
|
glowMultiplier: 0.3,
|
|
@@ -128,6 +144,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
128
144
|
backgroundStyle: "solid-dark",
|
|
129
145
|
paletteMode: "high-contrast",
|
|
130
146
|
preferredStyles: ["stroke-only", "dashed", "double-stroke", "hatched"],
|
|
147
|
+
preferredCompositions: ["grid-subdivision", "radial"],
|
|
131
148
|
flowLineMultiplier: 0,
|
|
132
149
|
heroShape: false,
|
|
133
150
|
glowMultiplier: 0,
|
|
@@ -145,6 +162,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
145
162
|
backgroundStyle: "radial-light",
|
|
146
163
|
paletteMode: "pastel-light",
|
|
147
164
|
preferredStyles: ["watercolor", "incomplete", "fill-only"],
|
|
165
|
+
preferredCompositions: ["golden-spiral", "radial", "spiral"],
|
|
148
166
|
flowLineMultiplier: 1.5,
|
|
149
167
|
heroShape: true,
|
|
150
168
|
glowMultiplier: 2,
|
|
@@ -162,6 +180,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
162
180
|
backgroundStyle: "linear-diagonal",
|
|
163
181
|
paletteMode: "duotone",
|
|
164
182
|
preferredStyles: ["fill-and-stroke", "double-stroke"],
|
|
183
|
+
preferredCompositions: ["grid-subdivision", "golden-spiral"],
|
|
165
184
|
flowLineMultiplier: 0,
|
|
166
185
|
heroShape: true,
|
|
167
186
|
glowMultiplier: 0,
|
|
@@ -179,6 +198,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
179
198
|
backgroundStyle: "solid-dark",
|
|
180
199
|
paletteMode: "neon",
|
|
181
200
|
preferredStyles: ["stroke-only", "double-stroke", "dashed"],
|
|
201
|
+
preferredCompositions: ["radial", "spiral", "clustered"],
|
|
182
202
|
flowLineMultiplier: 2,
|
|
183
203
|
heroShape: true,
|
|
184
204
|
glowMultiplier: 3,
|
|
@@ -196,6 +216,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
196
216
|
backgroundStyle: "solid-light",
|
|
197
217
|
paletteMode: "monochrome",
|
|
198
218
|
preferredStyles: ["hatched", "incomplete", "stroke-only", "dashed"],
|
|
219
|
+
preferredCompositions: ["flow-field", "grid-subdivision", "clustered"],
|
|
199
220
|
flowLineMultiplier: 1.5,
|
|
200
221
|
heroShape: false,
|
|
201
222
|
glowMultiplier: 0,
|
|
@@ -213,6 +234,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
213
234
|
backgroundStyle: "radial-dark",
|
|
214
235
|
paletteMode: "neon",
|
|
215
236
|
preferredStyles: ["fill-only", "watercolor", "fill-and-stroke"],
|
|
237
|
+
preferredCompositions: ["radial", "spiral", "golden-spiral"],
|
|
216
238
|
flowLineMultiplier: 3,
|
|
217
239
|
heroShape: true,
|
|
218
240
|
glowMultiplier: 2.5,
|
|
@@ -230,6 +252,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
230
252
|
backgroundStyle: "radial-light",
|
|
231
253
|
paletteMode: "harmonious",
|
|
232
254
|
preferredStyles: ["watercolor", "fill-only", "incomplete"],
|
|
255
|
+
preferredCompositions: ["golden-spiral", "flow-field", "radial"],
|
|
233
256
|
flowLineMultiplier: 0.5,
|
|
234
257
|
heroShape: false,
|
|
235
258
|
glowMultiplier: 0.3,
|
|
@@ -247,6 +270,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
247
270
|
backgroundStyle: "solid-light",
|
|
248
271
|
paletteMode: "high-contrast",
|
|
249
272
|
preferredStyles: ["fill-and-stroke", "stroke-only", "dashed"],
|
|
273
|
+
preferredCompositions: ["grid-subdivision", "radial"],
|
|
250
274
|
flowLineMultiplier: 0,
|
|
251
275
|
heroShape: false,
|
|
252
276
|
glowMultiplier: 0,
|
|
@@ -264,6 +288,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
264
288
|
backgroundStyle: "solid-light",
|
|
265
289
|
paletteMode: "duotone",
|
|
266
290
|
preferredStyles: ["fill-and-stroke", "fill-only", "double-stroke"],
|
|
291
|
+
preferredCompositions: ["grid-subdivision", "clustered"],
|
|
267
292
|
flowLineMultiplier: 0,
|
|
268
293
|
heroShape: true,
|
|
269
294
|
glowMultiplier: 0,
|
|
@@ -281,6 +306,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
281
306
|
backgroundStyle: "radial-dark",
|
|
282
307
|
paletteMode: "harmonious",
|
|
283
308
|
preferredStyles: ["fill-and-stroke", "watercolor", "fill-only"],
|
|
309
|
+
preferredCompositions: ["radial", "golden-spiral", "flow-field"],
|
|
284
310
|
flowLineMultiplier: 1,
|
|
285
311
|
heroShape: true,
|
|
286
312
|
glowMultiplier: 1,
|
|
@@ -298,6 +324,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
298
324
|
backgroundStyle: "solid-dark",
|
|
299
325
|
paletteMode: "high-contrast",
|
|
300
326
|
preferredStyles: ["fill-and-stroke", "stroke-only", "fill-only"],
|
|
327
|
+
preferredCompositions: ["clustered", "grid-subdivision", "radial"],
|
|
301
328
|
flowLineMultiplier: 0,
|
|
302
329
|
heroShape: false,
|
|
303
330
|
glowMultiplier: 0.3,
|
|
@@ -315,6 +342,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
315
342
|
backgroundStyle: "radial-light",
|
|
316
343
|
paletteMode: "earth",
|
|
317
344
|
preferredStyles: ["watercolor", "fill-only", "incomplete"],
|
|
345
|
+
preferredCompositions: ["flow-field", "golden-spiral", "spiral"],
|
|
318
346
|
flowLineMultiplier: 3,
|
|
319
347
|
heroShape: true,
|
|
320
348
|
glowMultiplier: 0.2,
|
|
@@ -332,6 +360,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
332
360
|
backgroundStyle: "solid-light",
|
|
333
361
|
paletteMode: "monochrome",
|
|
334
362
|
preferredStyles: ["stipple", "fill-only", "hatched"],
|
|
363
|
+
preferredCompositions: ["radial", "clustered", "flow-field"],
|
|
335
364
|
flowLineMultiplier: 0,
|
|
336
365
|
heroShape: false,
|
|
337
366
|
glowMultiplier: 0,
|
|
@@ -349,6 +378,7 @@ const ARCHETYPES: Archetype[] = [
|
|
|
349
378
|
backgroundStyle: "radial-dark",
|
|
350
379
|
paletteMode: "neon",
|
|
351
380
|
preferredStyles: ["fill-only", "watercolor", "stroke-only", "incomplete"],
|
|
381
|
+
preferredCompositions: ["spiral", "radial", "golden-spiral"],
|
|
352
382
|
flowLineMultiplier: 2,
|
|
353
383
|
heroShape: true,
|
|
354
384
|
glowMultiplier: 2.5,
|
|
@@ -371,6 +401,7 @@ function lerpNum(a: number, b: number, t: number): number {
|
|
|
371
401
|
function blendArchetypes(a: Archetype, b: Archetype, t: number): Archetype {
|
|
372
402
|
// Merge preferred styles — unique union, primary archetype first
|
|
373
403
|
const mergedStyles = [...new Set([...a.preferredStyles, ...b.preferredStyles])] as RenderStyle[];
|
|
404
|
+
const mergedCompositions = [...new Set([...a.preferredCompositions, ...b.preferredCompositions])] as CompositionMode[];
|
|
374
405
|
|
|
375
406
|
return {
|
|
376
407
|
name: `${a.name}+${b.name}`,
|
|
@@ -383,6 +414,7 @@ function blendArchetypes(a: Archetype, b: Archetype, t: number): Archetype {
|
|
|
383
414
|
backgroundStyle: t < 0.5 ? a.backgroundStyle : b.backgroundStyle,
|
|
384
415
|
paletteMode: t < 0.5 ? a.paletteMode : b.paletteMode,
|
|
385
416
|
preferredStyles: mergedStyles,
|
|
417
|
+
preferredCompositions: mergedCompositions,
|
|
386
418
|
flowLineMultiplier: lerpNum(a.flowLineMultiplier, b.flowLineMultiplier, t),
|
|
387
419
|
heroShape: t < 0.5 ? a.heroShape : b.heroShape,
|
|
388
420
|
glowMultiplier: lerpNum(a.glowMultiplier, b.glowMultiplier, t),
|
package/src/lib/canvas/colors.ts
CHANGED
|
@@ -194,6 +194,45 @@ export class SacredColorScheme {
|
|
|
194
194
|
const accent = hslToHex(baseHue, 0.9, 0.5);
|
|
195
195
|
return ["#111111", "#eeeeee", accent, hslToHex(baseHue, 0.7, 0.35)];
|
|
196
196
|
}
|
|
197
|
+
case "split-complementary": {
|
|
198
|
+
// Base hue + two colors flanking the complement (±30°)
|
|
199
|
+
const comp = (baseHue + 180) % 360;
|
|
200
|
+
const split1 = (comp - 30 + 360) % 360;
|
|
201
|
+
const split2 = (comp + 30) % 360;
|
|
202
|
+
const sat = 0.55 + this.rng() * 0.25;
|
|
203
|
+
return [
|
|
204
|
+
hslToHex(baseHue, sat, 0.5),
|
|
205
|
+
hslToHex(baseHue, sat * 0.8, 0.65),
|
|
206
|
+
hslToHex(split1, sat, 0.5),
|
|
207
|
+
hslToHex(split2, sat, 0.5),
|
|
208
|
+
hslToHex(split1, sat * 0.7, 0.7),
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
case "analogous-accent": {
|
|
212
|
+
// Tight cluster of 3 analogous hues + 1 distant accent
|
|
213
|
+
const step = 15 + this.rng() * 20; // 15-35° apart
|
|
214
|
+
const h1 = (baseHue - step + 360) % 360;
|
|
215
|
+
const h2 = (baseHue + step) % 360;
|
|
216
|
+
const accentHue = (baseHue + 150 + this.rng() * 60) % 360;
|
|
217
|
+
const sat = 0.5 + this.rng() * 0.3;
|
|
218
|
+
return [
|
|
219
|
+
hslToHex(baseHue, sat, 0.5),
|
|
220
|
+
hslToHex(h1, sat, 0.55),
|
|
221
|
+
hslToHex(h2, sat, 0.45),
|
|
222
|
+
hslToHex(accentHue, sat + 0.15, 0.5),
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
case "limited-palette": {
|
|
226
|
+
// Only 3 colors — like a risograph print
|
|
227
|
+
const h2 = (baseHue + 120 + this.rng() * 40) % 360;
|
|
228
|
+
const h3 = (baseHue + 220 + this.rng() * 40) % 360;
|
|
229
|
+
const sat = 0.6 + this.rng() * 0.2;
|
|
230
|
+
return [
|
|
231
|
+
hslToHex(baseHue, sat, 0.5),
|
|
232
|
+
hslToHex(h2, sat, 0.5),
|
|
233
|
+
hslToHex(h3, sat * 0.9, 0.55),
|
|
234
|
+
];
|
|
235
|
+
}
|
|
197
236
|
case "harmonious":
|
|
198
237
|
default:
|
|
199
238
|
return this.getColors();
|
|
@@ -210,6 +249,11 @@ export class SacredColorScheme {
|
|
|
210
249
|
case "high-contrast":
|
|
211
250
|
case "monochrome-ink":
|
|
212
251
|
return ["#f5f5f0", "#e8e8e0"];
|
|
252
|
+
case "split-complementary":
|
|
253
|
+
case "analogous-accent":
|
|
254
|
+
return this.getBackgroundColors();
|
|
255
|
+
case "limited-palette":
|
|
256
|
+
return [hslToHex(this.seed % 360, 0.08, 0.94), hslToHex((this.seed + 20) % 360, 0.06, 0.90)];
|
|
213
257
|
case "neon":
|
|
214
258
|
return ["#0a0a12", "#050510"];
|
|
215
259
|
case "earth":
|
|
@@ -344,14 +388,16 @@ export function buildColorHierarchy(colors: string[], rng: () => number): ColorH
|
|
|
344
388
|
all: colors,
|
|
345
389
|
};
|
|
346
390
|
}
|
|
347
|
-
// Pick dominant as the color
|
|
391
|
+
// Pick dominant as the color with the highest chroma (saturation × distance from gray)
|
|
392
|
+
// This selects the most visually prominent color rather than the average
|
|
348
393
|
const hsls = colors.map((c) => hexToHsl(c));
|
|
349
|
-
const avgHue = hsls.reduce((s, h) => s + h[0], 0) / hsls.length;
|
|
350
394
|
let dominantIdx = 0;
|
|
351
|
-
let
|
|
395
|
+
let maxChroma = -1;
|
|
352
396
|
for (let i = 0; i < hsls.length; i++) {
|
|
353
|
-
|
|
354
|
-
|
|
397
|
+
// Chroma approximation: saturation × how far lightness is from 50% (gray)
|
|
398
|
+
const lightnessVibrancy = 1 - Math.abs(hsls[i][2] - 0.5) * 2; // peaks at L=0.5
|
|
399
|
+
const chroma = hsls[i][1] * lightnessVibrancy;
|
|
400
|
+
if (chroma > maxChroma) { maxChroma = chroma; dominantIdx = i; }
|
|
355
401
|
}
|
|
356
402
|
// Accent is the color most distant from dominant in hue
|
|
357
403
|
let accentIdx = 0;
|
|
@@ -533,5 +579,6 @@ export function evolveHierarchy(
|
|
|
533
579
|
dominant: hueRotate(base.dominant, shift),
|
|
534
580
|
secondary: hueRotate(base.secondary, shift * 0.7),
|
|
535
581
|
accent: hueRotate(base.accent, shift * 0.5),
|
|
582
|
+
all: base.all.map(c => hueRotate(c, shift * 0.6)),
|
|
536
583
|
};
|
|
537
584
|
}
|
package/src/lib/canvas/draw.ts
CHANGED
|
@@ -91,6 +91,10 @@ interface EnhanceShapeConfig extends DrawShapeConfig {
|
|
|
91
91
|
renderStyle?: RenderStyle;
|
|
92
92
|
/** RNG for watercolor jitter (required for "watercolor" style). */
|
|
93
93
|
rng?: () => number;
|
|
94
|
+
/** Light direction angle in radians — used for shadow & highlight. */
|
|
95
|
+
lightAngle?: number;
|
|
96
|
+
/** Scale factor for resolution-independent sizing. */
|
|
97
|
+
scaleFactor?: number;
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
export function drawShape(
|
|
@@ -209,6 +213,28 @@ function applyRenderStyle(
|
|
|
209
213
|
ctx.fillStyle = origFill;
|
|
210
214
|
ctx.restore();
|
|
211
215
|
|
|
216
|
+
// Pass 4: Organic edge erosion — irregular bites along the boundary
|
|
217
|
+
if (rng && size > 20) {
|
|
218
|
+
const erosionBites = 6 + Math.floor(rng() * 8);
|
|
219
|
+
const edgeRadius = size * 0.45;
|
|
220
|
+
ctx.save();
|
|
221
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
222
|
+
ctx.globalAlpha = 0.6 + rng() * 0.3;
|
|
223
|
+
for (let eb = 0; eb < erosionBites; eb++) {
|
|
224
|
+
const biteAngle = rng() * Math.PI * 2;
|
|
225
|
+
const biteDist = edgeRadius * (0.85 + rng() * 0.25);
|
|
226
|
+
const biteR = size * (0.02 + rng() * 0.04);
|
|
227
|
+
ctx.beginPath();
|
|
228
|
+
ctx.arc(
|
|
229
|
+
Math.cos(biteAngle) * biteDist,
|
|
230
|
+
Math.sin(biteAngle) * biteDist,
|
|
231
|
+
biteR, 0, Math.PI * 2,
|
|
232
|
+
);
|
|
233
|
+
ctx.fill();
|
|
234
|
+
}
|
|
235
|
+
ctx.restore();
|
|
236
|
+
}
|
|
237
|
+
|
|
212
238
|
ctx.globalAlpha = savedAlpha;
|
|
213
239
|
// Soft stroke on top — thinner than normal for delicacy
|
|
214
240
|
ctx.globalAlpha *= 0.25;
|
|
@@ -532,6 +558,29 @@ function applyRenderStyle(
|
|
|
532
558
|
ctx.stroke();
|
|
533
559
|
ctx.restore();
|
|
534
560
|
}
|
|
561
|
+
|
|
562
|
+
// Organic edge erosion — small irregular bites for rough paper feel
|
|
563
|
+
if (rng && size > 20) {
|
|
564
|
+
const erosionBites = 4 + Math.floor(rng() * 6);
|
|
565
|
+
const edgeRadius = size * 0.42;
|
|
566
|
+
ctx.save();
|
|
567
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
568
|
+
ctx.globalAlpha = 0.5 + rng() * 0.3;
|
|
569
|
+
for (let eb = 0; eb < erosionBites; eb++) {
|
|
570
|
+
const biteAngle = rng() * Math.PI * 2;
|
|
571
|
+
const biteDist = edgeRadius * (0.9 + rng() * 0.2);
|
|
572
|
+
const biteR = size * (0.015 + rng() * 0.03);
|
|
573
|
+
ctx.beginPath();
|
|
574
|
+
ctx.arc(
|
|
575
|
+
Math.cos(biteAngle) * biteDist,
|
|
576
|
+
Math.sin(biteAngle) * biteDist,
|
|
577
|
+
biteR, 0, Math.PI * 2,
|
|
578
|
+
);
|
|
579
|
+
ctx.fill();
|
|
580
|
+
}
|
|
581
|
+
ctx.restore();
|
|
582
|
+
}
|
|
583
|
+
|
|
535
584
|
ctx.globalAlpha = savedAlphaHD;
|
|
536
585
|
break;
|
|
537
586
|
}
|
|
@@ -570,14 +619,24 @@ export function enhanceShapeGeneration(
|
|
|
570
619
|
gradientFillEnd,
|
|
571
620
|
renderStyle = "fill-and-stroke",
|
|
572
621
|
rng,
|
|
622
|
+
lightAngle,
|
|
623
|
+
scaleFactor = 1,
|
|
573
624
|
} = config;
|
|
574
625
|
|
|
575
626
|
ctx.save();
|
|
576
627
|
ctx.translate(x, y);
|
|
577
628
|
ctx.rotate((rotation * Math.PI) / 180);
|
|
578
629
|
|
|
579
|
-
//
|
|
580
|
-
if (
|
|
630
|
+
// ── Drop shadow — soft colored shadow offset along light direction ──
|
|
631
|
+
if (lightAngle !== undefined && size > 10) {
|
|
632
|
+
const shadowDist = size * 0.035;
|
|
633
|
+
const shadowBlurR = size * 0.06;
|
|
634
|
+
ctx.shadowOffsetX = Math.cos(lightAngle + Math.PI) * shadowDist;
|
|
635
|
+
ctx.shadowOffsetY = Math.sin(lightAngle + Math.PI) * shadowDist;
|
|
636
|
+
ctx.shadowBlur = shadowBlurR;
|
|
637
|
+
ctx.shadowColor = "rgba(0,0,0,0.12)";
|
|
638
|
+
} else if (glowRadius > 0) {
|
|
639
|
+
// Glow / shadow effect (legacy path)
|
|
581
640
|
ctx.shadowBlur = glowRadius;
|
|
582
641
|
ctx.shadowColor = glowColor || fillColor;
|
|
583
642
|
ctx.shadowOffsetX = 0;
|
|
@@ -603,9 +662,39 @@ export function enhanceShapeGeneration(
|
|
|
603
662
|
applyRenderStyle(ctx, renderStyle, fillColor, strokeColor, strokeWidth, size, rng);
|
|
604
663
|
}
|
|
605
664
|
|
|
606
|
-
// Reset shadow so patterns aren't double-
|
|
607
|
-
|
|
608
|
-
|
|
665
|
+
// Reset shadow so patterns and highlight aren't double-shadowed
|
|
666
|
+
ctx.shadowBlur = 0;
|
|
667
|
+
ctx.shadowOffsetX = 0;
|
|
668
|
+
ctx.shadowOffsetY = 0;
|
|
669
|
+
ctx.shadowColor = "transparent";
|
|
670
|
+
|
|
671
|
+
// ── Specular highlight — tinted arc on the light-facing side ──
|
|
672
|
+
if (lightAngle !== undefined && size > 15 && rng) {
|
|
673
|
+
const hlRadius = size * 0.35;
|
|
674
|
+
const hlDist = size * 0.15;
|
|
675
|
+
const hlX = Math.cos(lightAngle) * hlDist;
|
|
676
|
+
const hlY = Math.sin(lightAngle) * hlDist;
|
|
677
|
+
const hlGrad = ctx.createRadialGradient(hlX, hlY, 0, hlX, hlY, hlRadius);
|
|
678
|
+
// Tint highlight warm/cool based on fill color for cohesion
|
|
679
|
+
// Parse fill to detect warmth — fallback to white for non-parseable
|
|
680
|
+
let hlBase = "255,255,255";
|
|
681
|
+
if (typeof fillColor === "string" && fillColor.startsWith("#") && fillColor.length >= 7) {
|
|
682
|
+
const r = parseInt(fillColor.slice(1, 3), 16);
|
|
683
|
+
const g = parseInt(fillColor.slice(3, 5), 16);
|
|
684
|
+
const b = parseInt(fillColor.slice(5, 7), 16);
|
|
685
|
+
// Blend toward white but keep a hint of the fill's warmth
|
|
686
|
+
hlBase = `${Math.round(r * 0.15 + 255 * 0.85)},${Math.round(g * 0.15 + 255 * 0.85)},${Math.round(b * 0.15 + 255 * 0.85)}`;
|
|
687
|
+
}
|
|
688
|
+
hlGrad.addColorStop(0, `rgba(${hlBase},0.18)`);
|
|
689
|
+
hlGrad.addColorStop(0.5, `rgba(${hlBase},0.05)`);
|
|
690
|
+
hlGrad.addColorStop(1, `rgba(${hlBase},0)`);
|
|
691
|
+
const savedOp = ctx.globalCompositeOperation;
|
|
692
|
+
ctx.globalCompositeOperation = "soft-light";
|
|
693
|
+
ctx.fillStyle = hlGrad;
|
|
694
|
+
ctx.beginPath();
|
|
695
|
+
ctx.arc(hlX, hlY, hlRadius, 0, Math.PI * 2);
|
|
696
|
+
ctx.fill();
|
|
697
|
+
ctx.globalCompositeOperation = savedOp;
|
|
609
698
|
}
|
|
610
699
|
|
|
611
700
|
// Layer additional patterns if specified
|