git-hash-art 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ALGORITHM.md +76 -24
- package/CHANGELOG.md +17 -1
- package/dist/browser.js +707 -324
- package/dist/browser.js.map +1 -1
- package/dist/main.js +707 -324
- package/dist/main.js.map +1 -1
- package/dist/module.js +707 -324
- package/dist/module.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/performance.test.ts +243 -0
- package/src/__tests__/phase-breakdown.test.ts +44 -0
- package/src/__tests__/phase-timing.test.ts +260 -0
- package/src/__tests__/profile-pipeline.test.ts +160 -0
- package/src/lib/canvas/colors.ts +23 -6
- package/src/lib/canvas/draw.ts +149 -62
- package/src/lib/canvas/shapes/complex.ts +19 -10
- package/src/lib/canvas/shapes/sacred.ts +16 -17
- package/src/lib/render.ts +521 -244
- package/src/types.ts +2 -0
package/src/lib/render.ts
CHANGED
|
@@ -38,8 +38,7 @@ import {
|
|
|
38
38
|
drawMirroredShape,
|
|
39
39
|
pickMirrorAxis,
|
|
40
40
|
pickBlendMode,
|
|
41
|
-
pickRenderStyle,
|
|
42
|
-
type RenderStyle
|
|
41
|
+
pickRenderStyle, type RenderStyle
|
|
43
42
|
} from "./canvas/draw";
|
|
44
43
|
import { shapes } from "./canvas/shapes";
|
|
45
44
|
import {
|
|
@@ -52,6 +51,40 @@ import { createRng, seedFromHash, createSimplexNoise, createFBM } from "./utils"
|
|
|
52
51
|
import { DEFAULT_CONFIG, type GenerationConfig } from "../types";
|
|
53
52
|
import { selectArchetype, type BackgroundStyle, type CompositionMode } from "./archetypes";
|
|
54
53
|
|
|
54
|
+
// ── Render style cost weights (normalized: fill-and-stroke = 1) ─────
|
|
55
|
+
// Based on benchmark measurements. Used by the complexity budget to
|
|
56
|
+
// cap total rendering work and downgrade expensive styles when needed.
|
|
57
|
+
const RENDER_STYLE_COST: Record<RenderStyle, number> = {
|
|
58
|
+
"fill-and-stroke": 1,
|
|
59
|
+
"fill-only": 0.5,
|
|
60
|
+
"stroke-only": 1,
|
|
61
|
+
"double-stroke": 1.5,
|
|
62
|
+
"dashed": 1,
|
|
63
|
+
"watercolor": 7,
|
|
64
|
+
"hatched": 3,
|
|
65
|
+
"incomplete": 1,
|
|
66
|
+
"stipple": 90,
|
|
67
|
+
"stencil": 2,
|
|
68
|
+
"noise-grain": 400,
|
|
69
|
+
"wood-grain": 10,
|
|
70
|
+
"marble-vein": 4,
|
|
71
|
+
"fabric-weave": 6,
|
|
72
|
+
"hand-drawn": 5,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function downgradeRenderStyle(style: RenderStyle): RenderStyle {
|
|
76
|
+
switch (style) {
|
|
77
|
+
case "noise-grain": return "hatched";
|
|
78
|
+
case "stipple": return "dashed";
|
|
79
|
+
case "wood-grain": return "hatched";
|
|
80
|
+
case "watercolor": return "fill-and-stroke";
|
|
81
|
+
case "fabric-weave": return "hatched";
|
|
82
|
+
case "hand-drawn": return "fill-and-stroke";
|
|
83
|
+
case "marble-vein": return "stroke-only";
|
|
84
|
+
default: return style;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
55
88
|
|
|
56
89
|
// ── Shape categories for weighted selection (legacy fallback) ───────
|
|
57
90
|
|
|
@@ -446,6 +479,15 @@ export function renderHashArt(
|
|
|
446
479
|
config: Partial<GenerationConfig> = {},
|
|
447
480
|
): void {
|
|
448
481
|
const finalConfig: GenerationConfig = { ...DEFAULT_CONFIG, ...config };
|
|
482
|
+
const _dt = finalConfig._debugTiming;
|
|
483
|
+
const _t = _dt ? () => performance.now() : undefined;
|
|
484
|
+
let _p = _t ? _t() : 0;
|
|
485
|
+
function _mark(name: string) {
|
|
486
|
+
if (!_dt || !_t) return;
|
|
487
|
+
const now = _t();
|
|
488
|
+
_dt.phases[name] = (now - _p);
|
|
489
|
+
_p = now;
|
|
490
|
+
}
|
|
449
491
|
|
|
450
492
|
const rng = createRng(seedFromHash(gitHash));
|
|
451
493
|
|
|
@@ -497,13 +539,16 @@ export function renderHashArt(
|
|
|
497
539
|
const cx = width / 2;
|
|
498
540
|
const cy = height / 2;
|
|
499
541
|
|
|
542
|
+
_mark("0_setup");
|
|
543
|
+
|
|
500
544
|
// ── 1. Background ──────────────────────────────────────────────
|
|
501
545
|
const bgRadius = Math.hypot(cx, cy);
|
|
502
546
|
drawBackground(ctx, archetype.backgroundStyle, bgStart, bgEnd, width, height, cx, cy, bgRadius, rng, colors);
|
|
503
547
|
|
|
504
548
|
// Gradient mesh overlay — 3-4 color control points for richer backgrounds
|
|
549
|
+
// Use source-over instead of soft-light for cheaper compositing
|
|
505
550
|
const meshPoints = 3 + Math.floor(rng() * 2);
|
|
506
|
-
ctx.
|
|
551
|
+
ctx.globalAlpha = 1;
|
|
507
552
|
for (let i = 0; i < meshPoints; i++) {
|
|
508
553
|
const mx = rng() * width;
|
|
509
554
|
const my = rng() * height;
|
|
@@ -512,104 +557,114 @@ export function renderHashArt(
|
|
|
512
557
|
const grad = ctx.createRadialGradient(mx, my, 0, mx, my, mRadius);
|
|
513
558
|
grad.addColorStop(0, hexWithAlpha(mColor, 0.08 + rng() * 0.06));
|
|
514
559
|
grad.addColorStop(1, "rgba(0,0,0,0)");
|
|
515
|
-
ctx.globalAlpha = 1;
|
|
516
560
|
ctx.fillStyle = grad;
|
|
517
|
-
|
|
561
|
+
// Clip to gradient bounding box — avoids blending transparent pixels
|
|
562
|
+
const gx = Math.max(0, mx - mRadius);
|
|
563
|
+
const gy = Math.max(0, my - mRadius);
|
|
564
|
+
const gw = Math.min(width, mx + mRadius) - gx;
|
|
565
|
+
const gh = Math.min(height, my + mRadius) - gy;
|
|
566
|
+
ctx.fillRect(gx, gy, gw, gh);
|
|
518
567
|
}
|
|
519
|
-
ctx.globalCompositeOperation = "source-over";
|
|
520
568
|
|
|
521
569
|
// Compute average background luminance for contrast enforcement
|
|
522
570
|
const bgLum = (luminance(bgStart) + luminance(bgEnd)) / 2;
|
|
523
571
|
|
|
524
572
|
// ── 1b. Layered background — archetype-coherent shapes ─────────
|
|
573
|
+
// Use source-over with pre-multiplied alpha instead of soft-light
|
|
574
|
+
// for much cheaper compositing (soft-light requires per-pixel blend)
|
|
525
575
|
const bgShapeCount = 3 + Math.floor(rng() * 4);
|
|
526
|
-
ctx.globalCompositeOperation = "soft-light";
|
|
527
576
|
for (let i = 0; i < bgShapeCount; i++) {
|
|
528
577
|
const bx = rng() * width;
|
|
529
578
|
const by = rng() * height;
|
|
530
579
|
const bSize = (width * 0.3 + rng() * width * 0.5);
|
|
531
580
|
const bColor = pickHierarchyColor(colorHierarchy, rng);
|
|
532
|
-
ctx.globalAlpha = 0.03 + rng() * 0.05;
|
|
581
|
+
ctx.globalAlpha = (0.03 + rng() * 0.05) * 0.5; // halved to compensate for source-over vs soft-light
|
|
533
582
|
ctx.fillStyle = hexWithAlpha(bColor, 0.15);
|
|
534
583
|
ctx.beginPath();
|
|
535
584
|
// Use archetype-appropriate background shapes
|
|
536
585
|
if (archetype.name === "geometric-precision" || archetype.name === "op-art") {
|
|
537
|
-
// Rectangular shapes for geometric archetypes
|
|
538
586
|
ctx.rect(bx - bSize / 2, by - bSize / 2, bSize, bSize * (0.5 + rng() * 0.5));
|
|
539
587
|
} else {
|
|
540
588
|
ctx.arc(bx, by, bSize / 2, 0, Math.PI * 2);
|
|
541
589
|
}
|
|
542
590
|
ctx.fill();
|
|
543
591
|
}
|
|
544
|
-
// Subtle concentric rings from center
|
|
592
|
+
// Subtle concentric rings from center — batched into single stroke
|
|
545
593
|
const ringCount = 2 + Math.floor(rng() * 3);
|
|
546
594
|
ctx.globalAlpha = 0.02 + rng() * 0.03;
|
|
547
595
|
ctx.strokeStyle = hexWithAlpha(colorHierarchy.dominant, 0.1);
|
|
548
596
|
ctx.lineWidth = 1 * scaleFactor;
|
|
597
|
+
ctx.beginPath();
|
|
549
598
|
for (let i = 1; i <= ringCount; i++) {
|
|
550
599
|
const r = (Math.min(width, height) * 0.15) * i;
|
|
551
|
-
ctx.
|
|
600
|
+
ctx.moveTo(cx + r, cy);
|
|
552
601
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
553
|
-
ctx.stroke();
|
|
554
602
|
}
|
|
555
|
-
ctx.
|
|
603
|
+
ctx.stroke();
|
|
556
604
|
|
|
557
605
|
// ── 1c. Background pattern layer — subtle textured paper ───────
|
|
558
606
|
const bgPatternRoll = rng();
|
|
559
607
|
if (bgPatternRoll < 0.6) {
|
|
560
608
|
ctx.save();
|
|
561
|
-
ctx.globalCompositeOperation = "soft-light";
|
|
562
609
|
const patternOpacity = 0.02 + rng() * 0.04;
|
|
563
610
|
const patternColor = hexWithAlpha(colorHierarchy.dominant, 0.15);
|
|
564
611
|
|
|
565
612
|
if (bgPatternRoll < 0.2) {
|
|
566
|
-
// Dot grid —
|
|
567
|
-
const dotSpacing = Math.max(
|
|
568
|
-
const
|
|
613
|
+
// Dot grid — use fillRect instead of arcs (much cheaper, no path building)
|
|
614
|
+
const dotSpacing = Math.max(12, Math.min(width, height) * (0.015 + rng() * 0.015));
|
|
615
|
+
const dotDiam = Math.max(1, Math.round(dotSpacing * 0.16));
|
|
569
616
|
ctx.globalAlpha = patternOpacity;
|
|
570
617
|
ctx.fillStyle = patternColor;
|
|
571
|
-
|
|
572
|
-
for (let px = 0; px < width; px += dotSpacing) {
|
|
573
|
-
for (let py = 0; py < height; py += dotSpacing) {
|
|
574
|
-
ctx.
|
|
575
|
-
|
|
618
|
+
let dotCount = 0;
|
|
619
|
+
for (let px = 0; px < width && dotCount < 2000; px += dotSpacing) {
|
|
620
|
+
for (let py = 0; py < height && dotCount < 2000; py += dotSpacing) {
|
|
621
|
+
ctx.fillRect(px, py, dotDiam, dotDiam);
|
|
622
|
+
dotCount++;
|
|
576
623
|
}
|
|
577
624
|
}
|
|
578
|
-
ctx.fill();
|
|
579
625
|
} else if (bgPatternRoll < 0.4) {
|
|
580
|
-
// Diagonal lines — batched into a single path
|
|
581
|
-
const lineSpacing = Math.max(
|
|
626
|
+
// Diagonal lines — batched into a single path, capped at 300 lines
|
|
627
|
+
const lineSpacing = Math.max(10, Math.min(width, height) * (0.02 + rng() * 0.02));
|
|
582
628
|
ctx.globalAlpha = patternOpacity;
|
|
583
629
|
ctx.strokeStyle = patternColor;
|
|
584
630
|
ctx.lineWidth = 0.5 * scaleFactor;
|
|
585
631
|
const diag = Math.hypot(width, height);
|
|
586
632
|
ctx.beginPath();
|
|
587
|
-
|
|
633
|
+
let lineCount = 0;
|
|
634
|
+
for (let d = -diag; d < diag && lineCount < 300; d += lineSpacing) {
|
|
588
635
|
ctx.moveTo(d, 0);
|
|
589
636
|
ctx.lineTo(d + height, height);
|
|
637
|
+
lineCount++;
|
|
590
638
|
}
|
|
591
639
|
ctx.stroke();
|
|
592
640
|
} else {
|
|
593
|
-
// Tessellation — hexagonal grid,
|
|
594
|
-
const tessSize = Math.max(
|
|
641
|
+
// Tessellation — hexagonal grid, capped at 500 hexagons
|
|
642
|
+
const tessSize = Math.max(15, Math.min(width, height) * (0.025 + rng() * 0.02));
|
|
595
643
|
const tessH = tessSize * Math.sqrt(3);
|
|
596
644
|
ctx.globalAlpha = patternOpacity * 0.7;
|
|
597
645
|
ctx.strokeStyle = patternColor;
|
|
598
646
|
ctx.lineWidth = 0.4 * scaleFactor;
|
|
647
|
+
// Pre-compute hex vertex offsets (avoid trig per vertex)
|
|
648
|
+
const hexVx: number[] = [];
|
|
649
|
+
const hexVy: number[] = [];
|
|
650
|
+
for (let s = 0; s < 6; s++) {
|
|
651
|
+
const angle = (Math.PI / 3) * s - Math.PI / 6;
|
|
652
|
+
hexVx.push(Math.cos(angle) * tessSize * 0.5);
|
|
653
|
+
hexVy.push(Math.sin(angle) * tessSize * 0.5);
|
|
654
|
+
}
|
|
599
655
|
ctx.beginPath();
|
|
600
|
-
|
|
656
|
+
let hexCount = 0;
|
|
657
|
+
for (let row = 0; row * tessH < height + tessH && hexCount < 500; row++) {
|
|
601
658
|
const offsetX = (row % 2) * tessSize * 0.75;
|
|
602
|
-
for (let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5; col++) {
|
|
659
|
+
for (let col = 0; col * tessSize * 1.5 < width + tessSize * 1.5 && hexCount < 500; col++) {
|
|
603
660
|
const hx = col * tessSize * 1.5 + offsetX;
|
|
604
661
|
const hy = row * tessH;
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const vy = hy + Math.sin(angle) * tessSize * 0.5;
|
|
609
|
-
if (s === 0) ctx.moveTo(vx, vy);
|
|
610
|
-
else ctx.lineTo(vx, vy);
|
|
662
|
+
ctx.moveTo(hx + hexVx[0], hy + hexVy[0]);
|
|
663
|
+
for (let s = 1; s < 6; s++) {
|
|
664
|
+
ctx.lineTo(hx + hexVx[s], hy + hexVy[s]);
|
|
611
665
|
}
|
|
612
666
|
ctx.closePath();
|
|
667
|
+
hexCount++;
|
|
613
668
|
}
|
|
614
669
|
}
|
|
615
670
|
ctx.stroke();
|
|
@@ -618,6 +673,8 @@ export function renderHashArt(
|
|
|
618
673
|
}
|
|
619
674
|
ctx.globalCompositeOperation = "source-over";
|
|
620
675
|
|
|
676
|
+
_mark("1_background");
|
|
677
|
+
|
|
621
678
|
// ── 2. Composition mode — archetype-aware selection ──────────────
|
|
622
679
|
const compositionMode: CompositionMode = rng() < 0.7
|
|
623
680
|
? archetype.preferredCompositions[Math.floor(rng() * archetype.preferredCompositions.length)]
|
|
@@ -707,23 +764,27 @@ export function renderHashArt(
|
|
|
707
764
|
ctx.arc(zone.x, zone.y, zone.radius, 0, Math.PI * 2);
|
|
708
765
|
ctx.stroke();
|
|
709
766
|
|
|
710
|
-
// ~50% chance: scatter tiny dots inside the void
|
|
767
|
+
// ~50% chance: scatter tiny dots inside the void — batched into single path
|
|
711
768
|
if (rng() < 0.5) {
|
|
712
769
|
const dotCount = 3 + Math.floor(rng() * 6);
|
|
713
770
|
ctx.globalAlpha = 0.06 + rng() * 0.04;
|
|
714
771
|
ctx.fillStyle = hexWithAlpha(colorHierarchy.secondary, 0.15);
|
|
772
|
+
ctx.beginPath();
|
|
715
773
|
for (let d = 0; d < dotCount; d++) {
|
|
716
774
|
const angle = rng() * Math.PI * 2;
|
|
717
775
|
const dist = rng() * zone.radius * 0.7;
|
|
718
776
|
const dotR = (1 + rng() * 3) * scaleFactor;
|
|
719
|
-
ctx.
|
|
777
|
+
ctx.moveTo(
|
|
778
|
+
zone.x + Math.cos(angle) * dist + dotR,
|
|
779
|
+
zone.y + Math.sin(angle) * dist,
|
|
780
|
+
);
|
|
720
781
|
ctx.arc(
|
|
721
782
|
zone.x + Math.cos(angle) * dist,
|
|
722
783
|
zone.y + Math.sin(angle) * dist,
|
|
723
784
|
dotR, 0, Math.PI * 2,
|
|
724
785
|
);
|
|
725
|
-
ctx.fill();
|
|
726
786
|
}
|
|
787
|
+
ctx.fill();
|
|
727
788
|
}
|
|
728
789
|
|
|
729
790
|
// ~30% chance: thin concentric ring inside
|
|
@@ -739,6 +800,8 @@ export function renderHashArt(
|
|
|
739
800
|
}
|
|
740
801
|
ctx.globalAlpha = 1;
|
|
741
802
|
|
|
803
|
+
_mark("2_3_composition_focal");
|
|
804
|
+
|
|
742
805
|
// ── 4. Flow field — simplex noise for organic variation ─────────
|
|
743
806
|
// Create a seeded simplex noise field (unique per hash)
|
|
744
807
|
const noiseFieldRng = createRng(seedFromHash(gitHash, 333));
|
|
@@ -818,9 +881,33 @@ export function renderHashArt(
|
|
|
818
881
|
}
|
|
819
882
|
|
|
820
883
|
|
|
884
|
+
_mark("4_flowfield_hero");
|
|
885
|
+
|
|
821
886
|
// ── 5. Shape layers ────────────────────────────────────────────
|
|
822
887
|
const maxLocalDensity = Math.ceil(shapesPerLayer * 0.15);
|
|
823
888
|
|
|
889
|
+
// ── Complexity budget — caps total rendering work ──────────────
|
|
890
|
+
// Budget scales with pixel area so larger canvases get proportionally
|
|
891
|
+
// more headroom. The multiplier extras (glazing, echoes, nesting,
|
|
892
|
+
// constellations, rhythm) are gated behind the budget; when it runs
|
|
893
|
+
// low they are skipped. When it's exhausted, expensive render styles
|
|
894
|
+
// are downgraded to cheaper alternatives.
|
|
895
|
+
//
|
|
896
|
+
// RNG values are always consumed even when skipping, so the
|
|
897
|
+
// deterministic sequence for shapes that *do* render is preserved.
|
|
898
|
+
const pixelArea = width * height;
|
|
899
|
+
const BUDGET_PER_MEGAPIXEL = 6000; // cost units per 1M pixels
|
|
900
|
+
let complexityBudget = (pixelArea / 1_000_000) * BUDGET_PER_MEGAPIXEL;
|
|
901
|
+
const totalBudget = complexityBudget;
|
|
902
|
+
const budgetForExtras = complexityBudget * 0.25; // reserve 25% for multiplier extras
|
|
903
|
+
let extrasSpent = 0;
|
|
904
|
+
|
|
905
|
+
// Hard cap on clip-heavy render styles (stipple, noise-grain).
|
|
906
|
+
// These generate O(size²) fillRect calls per shape and dominate
|
|
907
|
+
// worst-case render time. Cap scales with pixel area.
|
|
908
|
+
const MAX_CLIP_HEAVY_SHAPES = Math.max(4, Math.floor(8 * (pixelArea / 1_000_000)));
|
|
909
|
+
let clipHeavyCount = 0;
|
|
910
|
+
|
|
824
911
|
for (let layer = 0; layer < layers; layer++) {
|
|
825
912
|
const layerRatio = layers > 1 ? layer / (layers - 1) : 0;
|
|
826
913
|
const numShapes =
|
|
@@ -957,7 +1044,30 @@ export function renderHashArt(
|
|
|
957
1044
|
|
|
958
1045
|
// Organic edge jitter — applied via watercolor style on ~15% of shapes
|
|
959
1046
|
const useOrganicEdges = rng() < 0.15 && shapeRenderStyle === "fill-and-stroke";
|
|
960
|
-
|
|
1047
|
+
let finalRenderStyle = useOrganicEdges ? "watercolor" as RenderStyle : shapeRenderStyle;
|
|
1048
|
+
|
|
1049
|
+
// Budget check: downgrade expensive styles proportionally —
|
|
1050
|
+
// the more expensive the style, the earlier it gets downgraded.
|
|
1051
|
+
// noise-grain (400) downgrades when budget < 20% remaining,
|
|
1052
|
+
// stipple (90) when < 82%, wood-grain (10) when < 98%.
|
|
1053
|
+
let styleCost = RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
1054
|
+
if (styleCost > 3) {
|
|
1055
|
+
const downgradeThreshold = Math.min(0.85, styleCost / 500);
|
|
1056
|
+
if (complexityBudget < totalBudget * (1 - downgradeThreshold)) {
|
|
1057
|
+
finalRenderStyle = downgradeRenderStyle(finalRenderStyle);
|
|
1058
|
+
styleCost = RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
// Hard cap: clip-heavy styles (stipple, noise-grain) are limited
|
|
1062
|
+
// to MAX_CLIP_HEAVY_SHAPES total across the entire render.
|
|
1063
|
+
if ((finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") &&
|
|
1064
|
+
clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) {
|
|
1065
|
+
finalRenderStyle = downgradeRenderStyle(finalRenderStyle);
|
|
1066
|
+
styleCost = RENDER_STYLE_COST[finalRenderStyle] ?? 1;
|
|
1067
|
+
}
|
|
1068
|
+
if (finalRenderStyle === "stipple" || finalRenderStyle === "noise-grain") {
|
|
1069
|
+
clipHeavyCount++;
|
|
1070
|
+
}
|
|
961
1071
|
|
|
962
1072
|
// Consistent light direction — subtle shadow offset
|
|
963
1073
|
const shadowDist = hasGlow ? 0 : (size * 0.02);
|
|
@@ -1016,28 +1126,37 @@ export function renderHashArt(
|
|
|
1016
1126
|
mirrorAxis: mirrorAxis!,
|
|
1017
1127
|
mirrorGap: size * (0.1 + rng() * 0.3),
|
|
1018
1128
|
});
|
|
1129
|
+
complexityBudget -= styleCost * 2; // mirrored = 2 shapes
|
|
1019
1130
|
} else {
|
|
1020
1131
|
enhanceShapeGeneration(ctx, shape, finalX, finalY, shapeConfig);
|
|
1132
|
+
complexityBudget -= styleCost;
|
|
1021
1133
|
}
|
|
1022
1134
|
|
|
1135
|
+
// ── Extras budget gate — skip multiplier sections when over budget ──
|
|
1136
|
+
const extrasAllowed = extrasSpent < budgetForExtras;
|
|
1137
|
+
|
|
1023
1138
|
// ── Glazing — luminous multi-pass transparency on ~20% of shapes ──
|
|
1024
1139
|
if (rng() < 0.2 && size > adjustedMinSize * 2) {
|
|
1025
1140
|
const glazePasses = 2 + Math.floor(rng() * 2);
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1141
|
+
if (extrasAllowed) {
|
|
1142
|
+
for (let g = 0; g < glazePasses; g++) {
|
|
1143
|
+
const glazeScale = 1 - (g + 1) * 0.12;
|
|
1144
|
+
const glazeAlpha = 0.08 + g * 0.04;
|
|
1145
|
+
ctx.globalAlpha = glazeAlpha;
|
|
1146
|
+
enhanceShapeGeneration(ctx, shape, finalX, finalY, {
|
|
1147
|
+
fillColor: hexWithAlpha(fillColor, 0.15 + g * 0.1),
|
|
1148
|
+
strokeColor: "rgba(0,0,0,0)",
|
|
1149
|
+
strokeWidth: 0,
|
|
1150
|
+
size: size * glazeScale,
|
|
1151
|
+
rotation,
|
|
1152
|
+
proportionType: "GOLDEN_RATIO",
|
|
1153
|
+
renderStyle: "fill-only",
|
|
1154
|
+
rng,
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
extrasSpent += glazePasses;
|
|
1040
1158
|
}
|
|
1159
|
+
// RNG consumed by glazePasses calculation above regardless
|
|
1041
1160
|
}
|
|
1042
1161
|
|
|
1043
1162
|
shapePositions.push({ x: finalX, y: finalY, size, shape });
|
|
@@ -1047,29 +1166,33 @@ export function renderHashArt(
|
|
|
1047
1166
|
if (size > adjustedMaxSize * 0.5 && rng() < 0.2) {
|
|
1048
1167
|
const echoCount = 2 + Math.floor(rng() * 2);
|
|
1049
1168
|
const echoAngle = rng() * Math.PI * 2;
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1169
|
+
if (extrasAllowed) {
|
|
1170
|
+
for (let e = 0; e < echoCount; e++) {
|
|
1171
|
+
const echoScale = 0.3 - e * 0.08;
|
|
1172
|
+
const echoDist = size * (0.6 + e * 0.4);
|
|
1173
|
+
const echoX = finalX + Math.cos(echoAngle) * echoDist;
|
|
1174
|
+
const echoY = finalY + Math.sin(echoAngle) * echoDist;
|
|
1175
|
+
const echoSize = size * Math.max(0.1, echoScale);
|
|
1176
|
+
|
|
1177
|
+
if (echoX < 0 || echoX > width || echoY < 0 || echoY > height) continue;
|
|
1178
|
+
|
|
1179
|
+
ctx.globalAlpha = layerOpacity * (0.4 - e * 0.1);
|
|
1180
|
+
enhanceShapeGeneration(ctx, shape, echoX, echoY, {
|
|
1181
|
+
fillColor: hexWithAlpha(fillColor, fillAlpha * 0.6),
|
|
1182
|
+
strokeColor: hexWithAlpha(strokeColor, 0.4),
|
|
1183
|
+
strokeWidth: strokeWidth * 0.6,
|
|
1184
|
+
size: echoSize,
|
|
1185
|
+
rotation: rotation + (e + 1) * 15,
|
|
1186
|
+
proportionType: "GOLDEN_RATIO",
|
|
1187
|
+
renderStyle: finalRenderStyle,
|
|
1188
|
+
rng,
|
|
1189
|
+
});
|
|
1190
|
+
shapePositions.push({ x: echoX, y: echoY, size: echoSize, shape });
|
|
1191
|
+
spatialGrid.insert({ x: echoX, y: echoY, size: echoSize, shape });
|
|
1192
|
+
}
|
|
1193
|
+
extrasSpent += echoCount * styleCost;
|
|
1072
1194
|
}
|
|
1195
|
+
// RNG for echoCount + echoAngle consumed above regardless
|
|
1073
1196
|
}
|
|
1074
1197
|
|
|
1075
1198
|
// ── 5d. Recursive nesting ──────────────────────────────────
|
|
@@ -1078,36 +1201,51 @@ export function renderHashArt(
|
|
|
1078
1201
|
const nestingChance = 0.15 + focalProximity * 0.15; // 15-30% near focal
|
|
1079
1202
|
if (size > adjustedMaxSize * 0.4 && rng() < nestingChance) {
|
|
1080
1203
|
const innerCount = 1 + Math.floor(rng() * 3);
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1204
|
+
if (extrasAllowed) {
|
|
1205
|
+
for (let n = 0; n < innerCount; n++) {
|
|
1206
|
+
// Pick inner shape from palette affinities
|
|
1207
|
+
const innerSizeFraction = (size * 0.25) / adjustedMaxSize;
|
|
1208
|
+
const innerShape = pickShapeFromPalette(shapePalette, rng, innerSizeFraction);
|
|
1209
|
+
const innerSize = size * (0.15 + rng() * 0.25);
|
|
1210
|
+
const innerOffX = (rng() - 0.5) * size * 0.4;
|
|
1211
|
+
const innerOffY = (rng() - 0.5) * size * 0.4;
|
|
1212
|
+
const innerRot = rng() * 360;
|
|
1213
|
+
const innerFill = hexWithAlpha(
|
|
1214
|
+
jitterColorHSL(pickHierarchyColor(colorHierarchy, rng), rng, 10, 0.1),
|
|
1215
|
+
0.3 + rng() * 0.4,
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1218
|
+
let innerStyle = pickStyleForShape(innerShape, layerRenderStyle, rng) as RenderStyle;
|
|
1219
|
+
// Apply clip-heavy cap to nested shapes too
|
|
1220
|
+
if ((innerStyle === "stipple" || innerStyle === "noise-grain") &&
|
|
1221
|
+
clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) {
|
|
1222
|
+
innerStyle = downgradeRenderStyle(innerStyle);
|
|
1223
|
+
}
|
|
1224
|
+
if (innerStyle === "stipple" || innerStyle === "noise-grain") clipHeavyCount++;
|
|
1225
|
+
ctx.globalAlpha = layerOpacity * 0.7;
|
|
1226
|
+
enhanceShapeGeneration(
|
|
1227
|
+
ctx,
|
|
1228
|
+
innerShape,
|
|
1229
|
+
finalX + innerOffX,
|
|
1230
|
+
finalY + innerOffY,
|
|
1231
|
+
{
|
|
1232
|
+
fillColor: innerFill,
|
|
1233
|
+
strokeColor: hexWithAlpha(strokeColor, 0.5),
|
|
1234
|
+
strokeWidth: strokeWidth * 0.6,
|
|
1235
|
+
size: innerSize,
|
|
1236
|
+
rotation: innerRot,
|
|
1237
|
+
proportionType: "GOLDEN_RATIO",
|
|
1238
|
+
renderStyle: innerStyle,
|
|
1239
|
+
rng,
|
|
1240
|
+
},
|
|
1241
|
+
);
|
|
1242
|
+
extrasSpent += RENDER_STYLE_COST[innerStyle] ?? 1;
|
|
1243
|
+
}
|
|
1244
|
+
} else {
|
|
1245
|
+
// Drain RNG to keep determinism — each nested shape consumes ~8 rng calls
|
|
1246
|
+
for (let n = 0; n < innerCount; n++) {
|
|
1247
|
+
rng(); rng(); rng(); rng(); rng(); rng(); rng(); rng();
|
|
1248
|
+
}
|
|
1111
1249
|
}
|
|
1112
1250
|
}
|
|
1113
1251
|
|
|
@@ -1117,42 +1255,58 @@ export function renderHashArt(
|
|
|
1117
1255
|
const constellation = CONSTELLATIONS[Math.floor(rng() * CONSTELLATIONS.length)];
|
|
1118
1256
|
const members = constellation.build(rng, size);
|
|
1119
1257
|
const groupRotation = rng() * Math.PI * 2;
|
|
1120
|
-
const cosR = Math.cos(groupRotation);
|
|
1121
|
-
const sinR = Math.sin(groupRotation);
|
|
1122
|
-
|
|
1123
|
-
for (const member of members) {
|
|
1124
|
-
// Rotate the group offset by the group rotation
|
|
1125
|
-
const mx = finalX + member.dx * cosR - member.dy * sinR;
|
|
1126
|
-
const my = finalY + member.dx * sinR + member.dy * cosR;
|
|
1127
|
-
|
|
1128
|
-
if (mx < 0 || mx > width || my < 0 || my > height) continue;
|
|
1129
|
-
|
|
1130
|
-
const memberFill = hexWithAlpha(
|
|
1131
|
-
jitterColorHSL(pickHierarchyColor(colorHierarchy, rng), rng, 8, 0.06),
|
|
1132
|
-
fillAlpha * 0.8,
|
|
1133
|
-
);
|
|
1134
|
-
const memberStroke = enforceContrast(
|
|
1135
|
-
jitterColorHSL(strokeBase, rng, 5, 0.04), bgLum,
|
|
1136
|
-
);
|
|
1137
1258
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1259
|
+
if (extrasAllowed) {
|
|
1260
|
+
const cosR = Math.cos(groupRotation);
|
|
1261
|
+
const sinR = Math.sin(groupRotation);
|
|
1262
|
+
|
|
1263
|
+
for (const member of members) {
|
|
1264
|
+
// Rotate the group offset by the group rotation
|
|
1265
|
+
const mx = finalX + member.dx * cosR - member.dy * sinR;
|
|
1266
|
+
const my = finalY + member.dx * sinR + member.dy * cosR;
|
|
1267
|
+
|
|
1268
|
+
if (mx < 0 || mx > width || my < 0 || my > height) continue;
|
|
1269
|
+
|
|
1270
|
+
const memberFill = hexWithAlpha(
|
|
1271
|
+
jitterColorHSL(pickHierarchyColor(colorHierarchy, rng), rng, 8, 0.06),
|
|
1272
|
+
fillAlpha * 0.8,
|
|
1273
|
+
);
|
|
1274
|
+
const memberStroke = enforceContrast(
|
|
1275
|
+
jitterColorHSL(strokeBase, rng, 5, 0.04), bgLum,
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
ctx.globalAlpha = layerOpacity * 0.6;
|
|
1279
|
+
// Use the member's shape if available, otherwise fall back to palette
|
|
1280
|
+
const memberShape = shapeNames.includes(member.shape)
|
|
1281
|
+
? member.shape
|
|
1282
|
+
: pickShapeFromPalette(shapePalette, rng, member.size / adjustedMaxSize);
|
|
1283
|
+
|
|
1284
|
+
let memberStyle = pickStyleForShape(memberShape, layerRenderStyle, rng) as RenderStyle;
|
|
1285
|
+
// Apply clip-heavy cap to constellation members too
|
|
1286
|
+
if ((memberStyle === "stipple" || memberStyle === "noise-grain") &&
|
|
1287
|
+
clipHeavyCount >= MAX_CLIP_HEAVY_SHAPES) {
|
|
1288
|
+
memberStyle = downgradeRenderStyle(memberStyle);
|
|
1289
|
+
}
|
|
1290
|
+
if (memberStyle === "stipple" || memberStyle === "noise-grain") clipHeavyCount++;
|
|
1291
|
+
enhanceShapeGeneration(ctx, memberShape, mx, my, {
|
|
1292
|
+
fillColor: memberFill,
|
|
1293
|
+
strokeColor: memberStroke,
|
|
1294
|
+
strokeWidth: strokeWidth * 0.7,
|
|
1295
|
+
size: member.size,
|
|
1296
|
+
rotation: member.rotation + (groupRotation * 180) / Math.PI,
|
|
1297
|
+
proportionType: "GOLDEN_RATIO",
|
|
1298
|
+
renderStyle: memberStyle,
|
|
1299
|
+
rng,
|
|
1300
|
+
});
|
|
1301
|
+
shapePositions.push({ x: mx, y: my, size: member.size, shape: memberShape });
|
|
1302
|
+
spatialGrid.insert({ x: mx, y: my, size: member.size, shape: memberShape });
|
|
1303
|
+
extrasSpent += RENDER_STYLE_COST[memberStyle] ?? 1;
|
|
1304
|
+
}
|
|
1305
|
+
} else {
|
|
1306
|
+
// Drain RNG — each member consumes ~6 rng calls for colors/style
|
|
1307
|
+
for (let m = 0; m < members.length; m++) {
|
|
1308
|
+
rng(); rng(); rng(); rng(); rng(); rng();
|
|
1309
|
+
}
|
|
1156
1310
|
}
|
|
1157
1311
|
}
|
|
1158
1312
|
|
|
@@ -1165,37 +1319,45 @@ export function renderHashArt(
|
|
|
1165
1319
|
const rhythmDecay = 0.7 + rng() * 0.15; // size multiplier per step
|
|
1166
1320
|
const rhythmShape = shape; // same shape for visual rhythm
|
|
1167
1321
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1322
|
+
if (extrasAllowed) {
|
|
1323
|
+
let rhythmSize = size * 0.6;
|
|
1324
|
+
for (let r = 0; r < rhythmCount; r++) {
|
|
1325
|
+
const rx = finalX + Math.cos(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
1326
|
+
const ry = finalY + Math.sin(rhythmAngle) * rhythmSpacing * (r + 1);
|
|
1172
1327
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1328
|
+
if (rx < 0 || rx > width || ry < 0 || ry > height) break;
|
|
1329
|
+
if (isInVoidZone(rx, ry, voidZones)) break;
|
|
1175
1330
|
|
|
1176
|
-
|
|
1177
|
-
|
|
1331
|
+
rhythmSize *= rhythmDecay;
|
|
1332
|
+
if (rhythmSize < adjustedMinSize) break;
|
|
1178
1333
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1334
|
+
const rhythmAlpha = layerOpacity * (0.6 - r * 0.08);
|
|
1335
|
+
ctx.globalAlpha = Math.max(0.1, rhythmAlpha);
|
|
1181
1336
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1337
|
+
const rhythmFill = hexWithAlpha(
|
|
1338
|
+
jitterColorHSL(pickHierarchyColor(layerHierarchy, rng), rng, 5, 0.04),
|
|
1339
|
+
fillAlpha * 0.7,
|
|
1340
|
+
);
|
|
1186
1341
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1342
|
+
enhanceShapeGeneration(ctx, rhythmShape, rx, ry, {
|
|
1343
|
+
fillColor: rhythmFill,
|
|
1344
|
+
strokeColor: hexWithAlpha(strokeColor, 0.5),
|
|
1345
|
+
strokeWidth: strokeWidth * 0.7,
|
|
1346
|
+
size: rhythmSize,
|
|
1347
|
+
rotation: rotation + (r + 1) * 12,
|
|
1348
|
+
proportionType: "GOLDEN_RATIO",
|
|
1349
|
+
renderStyle: finalRenderStyle,
|
|
1350
|
+
rng,
|
|
1351
|
+
});
|
|
1352
|
+
shapePositions.push({ x: rx, y: ry, size: rhythmSize, shape: rhythmShape });
|
|
1353
|
+
spatialGrid.insert({ x: rx, y: ry, size: rhythmSize, shape: rhythmShape });
|
|
1354
|
+
}
|
|
1355
|
+
extrasSpent += rhythmCount * styleCost;
|
|
1356
|
+
} else {
|
|
1357
|
+
// Drain RNG — each rhythm step consumes ~3 rng calls for colors
|
|
1358
|
+
for (let r = 0; r < rhythmCount; r++) {
|
|
1359
|
+
rng(); rng(); rng();
|
|
1360
|
+
}
|
|
1199
1361
|
}
|
|
1200
1362
|
}
|
|
1201
1363
|
}
|
|
@@ -1203,6 +1365,8 @@ export function renderHashArt(
|
|
|
1203
1365
|
|
|
1204
1366
|
// Reset blend mode for post-processing passes
|
|
1205
1367
|
ctx.globalCompositeOperation = "source-over";
|
|
1368
|
+
if (_dt) { _dt.shapeCount = shapePositions.length; _dt.extraCount = extrasSpent; }
|
|
1369
|
+
_mark("5_shape_layers");
|
|
1206
1370
|
|
|
1207
1371
|
// ── 5g. Layered masking / cutout portals ───────────────────────
|
|
1208
1372
|
// ~18% of images get 1-3 portal windows that paint over foreground
|
|
@@ -1271,16 +1435,33 @@ export function renderHashArt(
|
|
|
1271
1435
|
}
|
|
1272
1436
|
|
|
1273
1437
|
|
|
1438
|
+
_mark("5g_portals");
|
|
1439
|
+
|
|
1274
1440
|
// ── 6. Flow-line pass — variable color, branching, pressure ────
|
|
1441
|
+
// Optimized: collect all segments into width-quantized buckets, then
|
|
1442
|
+
// render each bucket as a single batched path. This reduces
|
|
1443
|
+
// beginPath/stroke calls from O(segments) to O(buckets).
|
|
1275
1444
|
const baseFlowLines = 6 + Math.floor(rng() * 10);
|
|
1276
1445
|
const numFlowLines = Math.round(baseFlowLines * archetype.flowLineMultiplier);
|
|
1277
1446
|
|
|
1447
|
+
// Width buckets — 6 buckets cover the taper×pressure range
|
|
1448
|
+
const FLOW_WIDTH_BUCKETS = 6;
|
|
1449
|
+
type FlowSeg = { x1: number; y1: number; x2: number; y2: number; color: string; alpha: number };
|
|
1450
|
+
const flowBuckets: Array<FlowSeg[]> = [];
|
|
1451
|
+
for (let b = 0; b < FLOW_WIDTH_BUCKETS; b++) flowBuckets.push([]);
|
|
1452
|
+
// Track the representative width for each bucket
|
|
1453
|
+
const flowBucketWidths: number[] = new Array(FLOW_WIDTH_BUCKETS);
|
|
1454
|
+
|
|
1455
|
+
// Pre-compute max possible width for bucket assignment
|
|
1456
|
+
let globalMaxFlowWidth = 0;
|
|
1457
|
+
|
|
1278
1458
|
for (let i = 0; i < numFlowLines; i++) {
|
|
1279
1459
|
let fx = rng() * width;
|
|
1280
1460
|
let fy = rng() * height;
|
|
1281
1461
|
const steps = 30 + Math.floor(rng() * 40);
|
|
1282
1462
|
const stepLen = (3 + rng() * 5) * scaleFactor;
|
|
1283
1463
|
const startWidth = (1 + rng() * 3) * scaleFactor;
|
|
1464
|
+
if (startWidth > globalMaxFlowWidth) globalMaxFlowWidth = startWidth;
|
|
1284
1465
|
|
|
1285
1466
|
// Variable color: interpolate between two hierarchy colors along the stroke
|
|
1286
1467
|
const lineColorStart = enforceContrast(pickHierarchyColor(colorHierarchy, rng), bgLum);
|
|
@@ -1308,23 +1489,21 @@ export function renderHashArt(
|
|
|
1308
1489
|
}
|
|
1309
1490
|
|
|
1310
1491
|
const t = s / steps;
|
|
1311
|
-
// Taper + pressure
|
|
1312
1492
|
const taper = 1 - t * 0.8;
|
|
1313
1493
|
const pressure = 0.6 + 0.4 * Math.sin(t * pressureFreq * Math.PI + pressurePhase);
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
// Interpolate color along stroke
|
|
1494
|
+
const segWidth = startWidth * taper * pressure;
|
|
1495
|
+
const segAlpha = lineAlpha * taper;
|
|
1317
1496
|
const lineColor = t < 0.5
|
|
1318
1497
|
? hexWithAlpha(lineColorStart, 0.4 + t * 0.2)
|
|
1319
1498
|
: hexWithAlpha(lineColorEnd, 0.4 + (1 - t) * 0.2);
|
|
1320
|
-
ctx.strokeStyle = lineColor;
|
|
1321
|
-
ctx.lineWidth = startWidth * taper * pressure;
|
|
1322
|
-
ctx.lineCap = "round";
|
|
1323
1499
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1500
|
+
// Quantize width into bucket
|
|
1501
|
+
const bucketIdx = Math.min(
|
|
1502
|
+
FLOW_WIDTH_BUCKETS - 1,
|
|
1503
|
+
Math.floor((segWidth / (globalMaxFlowWidth || 1)) * FLOW_WIDTH_BUCKETS),
|
|
1504
|
+
);
|
|
1505
|
+
flowBuckets[bucketIdx].push({ x1: prevX, y1: prevY, x2: fx, y2: fy, color: lineColor, alpha: segAlpha });
|
|
1506
|
+
flowBucketWidths[bucketIdx] = segWidth;
|
|
1328
1507
|
|
|
1329
1508
|
// Branching: ~12% chance per step to spawn a thinner child stroke
|
|
1330
1509
|
if (rng() < 0.12 && s > 5 && s < steps - 10) {
|
|
@@ -1341,12 +1520,14 @@ export function renderHashArt(
|
|
|
1341
1520
|
by += Math.sin(bAngle) * stepLen * 0.8;
|
|
1342
1521
|
if (bx < 0 || bx > width || by < 0 || by > height) break;
|
|
1343
1522
|
const bTaper = 1 - (bs / branchSteps) * 0.9;
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1523
|
+
const bSegWidth = branchWidth * bTaper;
|
|
1524
|
+
const bAlpha = lineAlpha * taper * bTaper * 0.6;
|
|
1525
|
+
const bBucket = Math.min(
|
|
1526
|
+
FLOW_WIDTH_BUCKETS - 1,
|
|
1527
|
+
Math.floor((bSegWidth / (globalMaxFlowWidth || 1)) * FLOW_WIDTH_BUCKETS),
|
|
1528
|
+
);
|
|
1529
|
+
flowBuckets[bBucket].push({ x1: bPrevX, y1: bPrevY, x2: bx, y2: by, color: lineColor, alpha: bAlpha });
|
|
1530
|
+
flowBucketWidths[bBucket] = bSegWidth;
|
|
1350
1531
|
bPrevX = bx;
|
|
1351
1532
|
bPrevY = by;
|
|
1352
1533
|
}
|
|
@@ -1357,14 +1538,63 @@ export function renderHashArt(
|
|
|
1357
1538
|
}
|
|
1358
1539
|
}
|
|
1359
1540
|
|
|
1541
|
+
// Render flow line buckets — one batched path per width bucket
|
|
1542
|
+
// Within each bucket, further sub-batch by quantized alpha (4 levels)
|
|
1543
|
+
ctx.lineCap = "round";
|
|
1544
|
+
const FLOW_ALPHA_BUCKETS = 4;
|
|
1545
|
+
for (let wb = 0; wb < FLOW_WIDTH_BUCKETS; wb++) {
|
|
1546
|
+
const segs = flowBuckets[wb];
|
|
1547
|
+
if (segs.length === 0) continue;
|
|
1548
|
+
ctx.lineWidth = flowBucketWidths[wb];
|
|
1549
|
+
|
|
1550
|
+
// Sub-bucket by alpha
|
|
1551
|
+
const alphaSubs: FlowSeg[][] = [];
|
|
1552
|
+
for (let a = 0; a < FLOW_ALPHA_BUCKETS; a++) alphaSubs.push([]);
|
|
1553
|
+
let maxAlpha = 0;
|
|
1554
|
+
for (let j = 0; j < segs.length; j++) {
|
|
1555
|
+
if (segs[j].alpha > maxAlpha) maxAlpha = segs[j].alpha;
|
|
1556
|
+
}
|
|
1557
|
+
for (let j = 0; j < segs.length; j++) {
|
|
1558
|
+
const ai = Math.min(
|
|
1559
|
+
FLOW_ALPHA_BUCKETS - 1,
|
|
1560
|
+
Math.floor((segs[j].alpha / (maxAlpha || 1)) * FLOW_ALPHA_BUCKETS),
|
|
1561
|
+
);
|
|
1562
|
+
alphaSubs[ai].push(segs[j]);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
for (let ai = 0; ai < FLOW_ALPHA_BUCKETS; ai++) {
|
|
1566
|
+
const sub = alphaSubs[ai];
|
|
1567
|
+
if (sub.length === 0) continue;
|
|
1568
|
+
// Use the median segment's alpha and color as representative
|
|
1569
|
+
const rep = sub[Math.floor(sub.length / 2)];
|
|
1570
|
+
ctx.globalAlpha = rep.alpha;
|
|
1571
|
+
ctx.strokeStyle = rep.color;
|
|
1572
|
+
ctx.beginPath();
|
|
1573
|
+
for (let j = 0; j < sub.length; j++) {
|
|
1574
|
+
ctx.moveTo(sub[j].x1, sub[j].y1);
|
|
1575
|
+
ctx.lineTo(sub[j].x2, sub[j].y2);
|
|
1576
|
+
}
|
|
1577
|
+
ctx.stroke();
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
_mark("6_flow_lines");
|
|
1582
|
+
|
|
1360
1583
|
// ── 6b. Motion/energy lines — short directional bursts ─────────
|
|
1584
|
+
// Optimized: collect all burst segments, then batch by quantized alpha
|
|
1361
1585
|
const energyArchetypes = ["dense-chaotic", "cosmic", "neon-glow", "bold-graphic"];
|
|
1362
1586
|
const hasEnergyLines = energyArchetypes.some(a => archetype.name.includes(a)) || rng() < 0.25;
|
|
1363
1587
|
if (hasEnergyLines && shapePositions.length > 0) {
|
|
1364
1588
|
const energyCount = 5 + Math.floor(rng() * 10);
|
|
1365
1589
|
ctx.lineCap = "round";
|
|
1590
|
+
|
|
1591
|
+
// Collect all energy segments with their computed state
|
|
1592
|
+
const ENERGY_ALPHA_BUCKETS = 3;
|
|
1593
|
+
const energyBuckets: Array<Array<{ x1: number; y1: number; x2: number; y2: number; color: string; lw: number }>> = [];
|
|
1594
|
+
for (let b = 0; b < ENERGY_ALPHA_BUCKETS; b++) energyBuckets.push([]);
|
|
1595
|
+
const energyAlphas: number[] = new Array(ENERGY_ALPHA_BUCKETS).fill(0);
|
|
1596
|
+
|
|
1366
1597
|
for (let e = 0; e < energyCount; e++) {
|
|
1367
|
-
// Pick a random shape to radiate from
|
|
1368
1598
|
const source = shapePositions[Math.floor(rng() * shapePositions.length)];
|
|
1369
1599
|
const burstCount = 2 + Math.floor(rng() * 4);
|
|
1370
1600
|
const baseAngle = flowAngle(source.x, source.y);
|
|
@@ -1378,19 +1608,39 @@ export function renderHashArt(
|
|
|
1378
1608
|
const ex = sx + Math.cos(angle) * lineLen;
|
|
1379
1609
|
const ey = sy + Math.sin(angle) * lineLen;
|
|
1380
1610
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1611
|
+
const eAlpha = 0.04 + rng() * 0.06;
|
|
1612
|
+
const eColor = hexWithAlpha(
|
|
1383
1613
|
enforceContrast(pickHierarchyColor(colorHierarchy, rng), bgLum), 0.3,
|
|
1384
1614
|
);
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1615
|
+
const eLw = (0.5 + rng() * 1.5) * scaleFactor;
|
|
1616
|
+
|
|
1617
|
+
// Quantize alpha into bucket
|
|
1618
|
+
const bi = Math.min(ENERGY_ALPHA_BUCKETS - 1, Math.floor((eAlpha - 0.04) / 0.06 * ENERGY_ALPHA_BUCKETS));
|
|
1619
|
+
energyBuckets[bi].push({ x1: sx, y1: sy, x2: ex, y2: ey, color: eColor, lw: eLw });
|
|
1620
|
+
energyAlphas[bi] = eAlpha;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Render batched energy lines
|
|
1625
|
+
for (let bi = 0; bi < ENERGY_ALPHA_BUCKETS; bi++) {
|
|
1626
|
+
const segs = energyBuckets[bi];
|
|
1627
|
+
if (segs.length === 0) continue;
|
|
1628
|
+
ctx.globalAlpha = energyAlphas[bi];
|
|
1629
|
+
// Use median segment's color and width as representative
|
|
1630
|
+
const rep = segs[Math.floor(segs.length / 2)];
|
|
1631
|
+
ctx.strokeStyle = rep.color;
|
|
1632
|
+
ctx.lineWidth = rep.lw;
|
|
1633
|
+
ctx.beginPath();
|
|
1634
|
+
for (let j = 0; j < segs.length; j++) {
|
|
1635
|
+
ctx.moveTo(segs[j].x1, segs[j].y1);
|
|
1636
|
+
ctx.lineTo(segs[j].x2, segs[j].y2);
|
|
1390
1637
|
}
|
|
1638
|
+
ctx.stroke();
|
|
1391
1639
|
}
|
|
1392
1640
|
}
|
|
1393
1641
|
|
|
1642
|
+
_mark("6b_energy_lines");
|
|
1643
|
+
|
|
1394
1644
|
// ── 6c. Apply symmetry mirroring ─────────────────────────────────
|
|
1395
1645
|
if (symmetryMode !== "none") {
|
|
1396
1646
|
const canvas = ctx.canvas;
|
|
@@ -1413,46 +1663,28 @@ export function renderHashArt(
|
|
|
1413
1663
|
}
|
|
1414
1664
|
|
|
1415
1665
|
|
|
1416
|
-
|
|
1666
|
+
_mark("6c_symmetry");
|
|
1667
|
+
|
|
1668
|
+
// ── 7. Noise texture overlay ─────────────────────────────────────
|
|
1669
|
+
// With density capped at 2500 dots, direct fillRect calls are far cheaper
|
|
1670
|
+
// than the getImageData/putImageData round-trip which copies the entire
|
|
1671
|
+
// pixel buffer (4 × width × height bytes) twice.
|
|
1417
1672
|
const noiseRng = createRng(seedFromHash(gitHash, 777));
|
|
1418
|
-
const
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
for (let dy = 0; dy < pixelScale && ny + dy < height; dy++) {
|
|
1430
|
-
for (let dx = 0; dx < pixelScale && nx + dx < width; dx++) {
|
|
1431
|
-
const idx = ((ny + dy) * width + (nx + dx)) * 4;
|
|
1432
|
-
// Alpha-blend the noise dot onto existing pixel data
|
|
1433
|
-
const srcA = alpha / 255;
|
|
1434
|
-
const invA = 1 - srcA;
|
|
1435
|
-
data[idx] = Math.round(data[idx] * invA + brightness * srcA);
|
|
1436
|
-
data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
|
|
1437
|
-
data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
|
|
1438
|
-
// Keep existing alpha
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
ctx.putImageData(imageData, 0, 0);
|
|
1443
|
-
} catch {
|
|
1444
|
-
// Fallback for environments where getImageData isn't available (e.g. some OffscreenCanvas)
|
|
1445
|
-
for (let i = 0; i < noiseDensity; i++) {
|
|
1446
|
-
const nx = noiseRng() * width;
|
|
1447
|
-
const ny = noiseRng() * height;
|
|
1448
|
-
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
1449
|
-
const alpha = 0.01 + noiseRng() * 0.03;
|
|
1450
|
-
ctx.globalAlpha = alpha;
|
|
1451
|
-
ctx.fillStyle = `rgba(${brightness},${brightness},${brightness},1)`;
|
|
1452
|
-
ctx.fillRect(nx, ny, 1 * scaleFactor, 1 * scaleFactor);
|
|
1453
|
-
}
|
|
1673
|
+
const rawNoiseDensity = Math.floor((width * height) / 800);
|
|
1674
|
+
const noiseDensity = Math.min(rawNoiseDensity, 2500);
|
|
1675
|
+
const pixelScale = Math.max(1, Math.round(scaleFactor));
|
|
1676
|
+
for (let i = 0; i < noiseDensity; i++) {
|
|
1677
|
+
const nx = noiseRng() * width;
|
|
1678
|
+
const ny = noiseRng() * height;
|
|
1679
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
1680
|
+
const alpha = 0.01 + noiseRng() * 0.03;
|
|
1681
|
+
ctx.globalAlpha = alpha;
|
|
1682
|
+
ctx.fillStyle = `rgb(${brightness},${brightness},${brightness})`;
|
|
1683
|
+
ctx.fillRect(nx, ny, pixelScale, pixelScale);
|
|
1454
1684
|
}
|
|
1455
1685
|
|
|
1686
|
+
_mark("7_noise_texture");
|
|
1687
|
+
|
|
1456
1688
|
// ── 8. Vignette — darken edges to draw the eye inward ───────────
|
|
1457
1689
|
ctx.globalAlpha = 1;
|
|
1458
1690
|
const vignetteStrength = 0.25 + rng() * 0.2;
|
|
@@ -1468,12 +1700,23 @@ export function renderHashArt(
|
|
|
1468
1700
|
ctx.fillStyle = vigGrad;
|
|
1469
1701
|
ctx.fillRect(0, 0, width, height);
|
|
1470
1702
|
|
|
1703
|
+
_mark("8_vignette");
|
|
1704
|
+
|
|
1471
1705
|
// ── 9. Organic connecting curves — proximity-aware ───────────────
|
|
1706
|
+
// Optimized: batch all curves into alpha-quantized groups to reduce
|
|
1707
|
+
// beginPath/stroke calls from O(numCurves) to O(alphaBuckets).
|
|
1472
1708
|
if (shapePositions.length > 1) {
|
|
1473
1709
|
const numCurves = Math.floor((8 * (width * height)) / (1024 * 1024));
|
|
1474
1710
|
const maxCurveDist = Math.hypot(width, height) * 0.2; // only connect nearby shapes
|
|
1475
1711
|
ctx.lineWidth = 0.8 * scaleFactor;
|
|
1476
1712
|
|
|
1713
|
+
// Collect curves into 3 alpha buckets
|
|
1714
|
+
const CURVE_ALPHA_BUCKETS = 3;
|
|
1715
|
+
const curveBuckets: Array<Array<{ ax: number; ay: number; cpx: number; cpy: number; bx: number; by: number }>> = [];
|
|
1716
|
+
const curveColors: string[] = [];
|
|
1717
|
+
const curveAlphas: number[] = new Array(CURVE_ALPHA_BUCKETS).fill(0);
|
|
1718
|
+
for (let b = 0; b < CURVE_ALPHA_BUCKETS; b++) curveBuckets.push([]);
|
|
1719
|
+
|
|
1477
1720
|
for (let i = 0; i < numCurves; i++) {
|
|
1478
1721
|
const idxA = Math.floor(rng() * shapePositions.length);
|
|
1479
1722
|
const offset =
|
|
@@ -1488,7 +1731,9 @@ export function renderHashArt(
|
|
|
1488
1731
|
const dist = Math.hypot(dx, dy);
|
|
1489
1732
|
|
|
1490
1733
|
// Skip connections between distant shapes
|
|
1491
|
-
if (dist > maxCurveDist)
|
|
1734
|
+
if (dist > maxCurveDist) {
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1492
1737
|
|
|
1493
1738
|
const mx = (a.x + b.x) / 2;
|
|
1494
1739
|
const my = (a.y + b.y) / 2;
|
|
@@ -1497,19 +1742,36 @@ export function renderHashArt(
|
|
|
1497
1742
|
const cpx = mx + (-dy / (dist || 1)) * bulge;
|
|
1498
1743
|
const cpy = my + (dx / (dist || 1)) * bulge;
|
|
1499
1744
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1745
|
+
const curveAlpha = 0.06 + rng() * 0.1;
|
|
1746
|
+
const curveColor = hexWithAlpha(
|
|
1502
1747
|
enforceContrast(pickHierarchyColor(colorHierarchy, rng), bgLum),
|
|
1503
1748
|
0.3,
|
|
1504
1749
|
);
|
|
1505
1750
|
|
|
1751
|
+
const bi = Math.min(CURVE_ALPHA_BUCKETS - 1, Math.floor((curveAlpha - 0.06) / 0.1 * CURVE_ALPHA_BUCKETS));
|
|
1752
|
+
curveBuckets[bi].push({ ax: a.x, ay: a.y, cpx, cpy, bx: b.x, by: b.y });
|
|
1753
|
+
curveAlphas[bi] = curveAlpha;
|
|
1754
|
+
if (!curveColors[bi]) curveColors[bi] = curveColor;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// Render batched curves
|
|
1758
|
+
for (let bi = 0; bi < CURVE_ALPHA_BUCKETS; bi++) {
|
|
1759
|
+
const curves = curveBuckets[bi];
|
|
1760
|
+
if (curves.length === 0) continue;
|
|
1761
|
+
ctx.globalAlpha = curveAlphas[bi];
|
|
1762
|
+
ctx.strokeStyle = curveColors[bi];
|
|
1506
1763
|
ctx.beginPath();
|
|
1507
|
-
|
|
1508
|
-
|
|
1764
|
+
for (let j = 0; j < curves.length; j++) {
|
|
1765
|
+
const c = curves[j];
|
|
1766
|
+
ctx.moveTo(c.ax, c.ay);
|
|
1767
|
+
ctx.quadraticCurveTo(c.cpx, c.cpy, c.bx, c.by);
|
|
1768
|
+
}
|
|
1509
1769
|
ctx.stroke();
|
|
1510
1770
|
}
|
|
1511
1771
|
}
|
|
1512
1772
|
|
|
1773
|
+
_mark("9_connecting_curves");
|
|
1774
|
+
|
|
1513
1775
|
// ── 10. Post-processing ────────────────────────────────────────
|
|
1514
1776
|
|
|
1515
1777
|
// 10a. Color grading — unified tone across the whole image
|
|
@@ -1567,6 +1829,8 @@ export function renderHashArt(
|
|
|
1567
1829
|
ctx.globalCompositeOperation = "source-over";
|
|
1568
1830
|
}
|
|
1569
1831
|
|
|
1832
|
+
_mark("10_post_processing");
|
|
1833
|
+
|
|
1570
1834
|
// ── 10e. Generative borders — archetype-driven decorative frames ──
|
|
1571
1835
|
{
|
|
1572
1836
|
ctx.save();
|
|
@@ -1613,12 +1877,15 @@ export function renderHashArt(
|
|
|
1613
1877
|
}
|
|
1614
1878
|
} else if (archName.includes("botanical") || archName.includes("organic") || archName.includes("watercolor")) {
|
|
1615
1879
|
// Vine tendrils — organic curving lines along edges
|
|
1880
|
+
// Optimized: batch all tendrils into a single path
|
|
1616
1881
|
ctx.strokeStyle = hexWithAlpha(colorHierarchy.secondary, 0.15);
|
|
1617
1882
|
ctx.lineWidth = Math.max(0.8, 1.2 * scaleFactor);
|
|
1618
1883
|
ctx.globalAlpha = 0.12 + borderRng() * 0.08;
|
|
1619
1884
|
ctx.lineCap = "round";
|
|
1620
1885
|
|
|
1621
1886
|
const tendrilCount = 8 + Math.floor(borderRng() * 8);
|
|
1887
|
+
ctx.beginPath();
|
|
1888
|
+
const leafPositions: Array<{ x: number; y: number; r: number }> = [];
|
|
1622
1889
|
for (let t = 0; t < tendrilCount; t++) {
|
|
1623
1890
|
// Start from a random edge point
|
|
1624
1891
|
const edge = Math.floor(borderRng() * 4);
|
|
@@ -1628,7 +1895,6 @@ export function renderHashArt(
|
|
|
1628
1895
|
else if (edge === 2) { tx = borderPad; ty = borderRng() * height; }
|
|
1629
1896
|
else { tx = width - borderPad; ty = borderRng() * height; }
|
|
1630
1897
|
|
|
1631
|
-
ctx.beginPath();
|
|
1632
1898
|
ctx.moveTo(tx, ty);
|
|
1633
1899
|
const segs = 3 + Math.floor(borderRng() * 4);
|
|
1634
1900
|
for (let s = 0; s < segs; s++) {
|
|
@@ -1642,16 +1908,24 @@ export function renderHashArt(
|
|
|
1642
1908
|
ty = cpy3;
|
|
1643
1909
|
ctx.quadraticCurveTo(cpx2, cpy2, tx, ty);
|
|
1644
1910
|
}
|
|
1645
|
-
ctx.stroke();
|
|
1646
1911
|
|
|
1647
|
-
//
|
|
1912
|
+
// Collect leaf positions for batch fill
|
|
1648
1913
|
if (borderRng() < 0.6) {
|
|
1649
|
-
|
|
1650
|
-
ctx.arc(tx, ty, borderPad * (0.15 + borderRng() * 0.2), 0, Math.PI * 2);
|
|
1651
|
-
ctx.fillStyle = hexWithAlpha(colorHierarchy.secondary, 0.08);
|
|
1652
|
-
ctx.fill();
|
|
1914
|
+
leafPositions.push({ x: tx, y: ty, r: borderPad * (0.15 + borderRng() * 0.2) });
|
|
1653
1915
|
}
|
|
1654
1916
|
}
|
|
1917
|
+
ctx.stroke();
|
|
1918
|
+
|
|
1919
|
+
// Batch all leaf dots into a single fill
|
|
1920
|
+
if (leafPositions.length > 0) {
|
|
1921
|
+
ctx.fillStyle = hexWithAlpha(colorHierarchy.secondary, 0.08);
|
|
1922
|
+
ctx.beginPath();
|
|
1923
|
+
for (const leaf of leafPositions) {
|
|
1924
|
+
ctx.moveTo(leaf.x + leaf.r, leaf.y);
|
|
1925
|
+
ctx.arc(leaf.x, leaf.y, leaf.r, 0, Math.PI * 2);
|
|
1926
|
+
}
|
|
1927
|
+
ctx.fill();
|
|
1928
|
+
}
|
|
1655
1929
|
} else if (archName.includes("celestial") || archName.includes("cosmic") || archName.includes("neon")) {
|
|
1656
1930
|
// Star-studded arcs along edges
|
|
1657
1931
|
ctx.globalAlpha = 0.1 + borderRng() * 0.08;
|
|
@@ -1667,8 +1941,9 @@ export function renderHashArt(
|
|
|
1667
1941
|
ctx.arc(cx, height * 1.3, height * 0.6, Math.PI + 0.3, -0.3);
|
|
1668
1942
|
ctx.stroke();
|
|
1669
1943
|
|
|
1670
|
-
// Scatter small stars along the border region
|
|
1944
|
+
// Scatter small stars along the border region — batched into single path
|
|
1671
1945
|
const starCount = 15 + Math.floor(borderRng() * 15);
|
|
1946
|
+
ctx.beginPath();
|
|
1672
1947
|
for (let s = 0; s < starCount; s++) {
|
|
1673
1948
|
const edge = Math.floor(borderRng() * 4);
|
|
1674
1949
|
let sx: number, sy: number;
|
|
@@ -1679,7 +1954,6 @@ export function renderHashArt(
|
|
|
1679
1954
|
|
|
1680
1955
|
const starR = (1 + borderRng() * 2.5) * scaleFactor;
|
|
1681
1956
|
// 4-point star
|
|
1682
|
-
ctx.beginPath();
|
|
1683
1957
|
for (let p = 0; p < 8; p++) {
|
|
1684
1958
|
const a = (p / 8) * Math.PI * 2;
|
|
1685
1959
|
const r = p % 2 === 0 ? starR : starR * 0.4;
|
|
@@ -1689,8 +1963,8 @@ export function renderHashArt(
|
|
|
1689
1963
|
else ctx.lineTo(px2, py2);
|
|
1690
1964
|
}
|
|
1691
1965
|
ctx.closePath();
|
|
1692
|
-
ctx.fill();
|
|
1693
1966
|
}
|
|
1967
|
+
ctx.fill();
|
|
1694
1968
|
} else if (archName.includes("minimal") || archName.includes("monochrome") || archName.includes("stipple")) {
|
|
1695
1969
|
// Thin single rule — understated elegance
|
|
1696
1970
|
ctx.strokeStyle = hexWithAlpha(colorHierarchy.dominant, 0.1);
|
|
@@ -1703,6 +1977,8 @@ export function renderHashArt(
|
|
|
1703
1977
|
ctx.restore();
|
|
1704
1978
|
}
|
|
1705
1979
|
|
|
1980
|
+
_mark("10e_borders");
|
|
1981
|
+
|
|
1706
1982
|
// ── 11. Signature mark — placed in the least-dense corner ──────
|
|
1707
1983
|
{
|
|
1708
1984
|
const sigRng = createRng(seedFromHash(gitHash, 42));
|
|
@@ -1766,5 +2042,6 @@ export function renderHashArt(
|
|
|
1766
2042
|
}
|
|
1767
2043
|
|
|
1768
2044
|
ctx.globalAlpha = 1;
|
|
2045
|
+
_mark("11_signature");
|
|
1769
2046
|
|
|
1770
2047
|
}
|