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/package.json
CHANGED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createCanvas } from "@napi-rs/canvas";
|
|
3
|
+
import { renderHashArt } from "../lib/render";
|
|
4
|
+
import * as utils from "../lib/utils";
|
|
5
|
+
import * as affinity from "../lib/canvas/shapes/affinity";
|
|
6
|
+
import * as colors from "../lib/canvas/colors";
|
|
7
|
+
import * as draw from "../lib/canvas/draw";
|
|
8
|
+
import { shapes } from "../lib/canvas/shapes";
|
|
9
|
+
import * as archetypes from "../lib/archetypes";
|
|
10
|
+
|
|
11
|
+
const TEST_HASH = "46192e59d42f741c761cbea79462a8b3815dd905";
|
|
12
|
+
const HASH_B = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
|
|
13
|
+
|
|
14
|
+
function timeMs(fn: () => void, iterations = 1): number {
|
|
15
|
+
const start = performance.now();
|
|
16
|
+
for (let i = 0; i < iterations; i++) fn();
|
|
17
|
+
return performance.now() - start;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createTestCtx(width = 512, height = 512) {
|
|
21
|
+
const canvas = createCanvas(width, height);
|
|
22
|
+
return canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── 1. Full pipeline benchmarks ─────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
describe("Full pipeline timing", () => {
|
|
28
|
+
const sizes: Array<[number, number, string]> = [
|
|
29
|
+
[128, 128, "128×128 (tiny)"],
|
|
30
|
+
[512, 512, "512×512 (small)"],
|
|
31
|
+
[1024, 1024, "1024×1024 (medium)"],
|
|
32
|
+
[2048, 2048, "2048×2048 (default)"],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
for (const [w, h, label] of sizes) {
|
|
36
|
+
it(`renders ${label} within budget`, () => {
|
|
37
|
+
const canvas = createCanvas(w, h);
|
|
38
|
+
const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
|
|
39
|
+
const ms = timeMs(() => renderHashArt(ctx, TEST_HASH, { width: w, height: h }));
|
|
40
|
+
console.log(` ${label}: ${ms.toFixed(1)} ms`);
|
|
41
|
+
expect(ms).toBeLessThan(30_000);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
it("renders 512×512 with gridSize=9 layers=5 (dense worst case)", () => {
|
|
46
|
+
const ctx = createTestCtx(512, 512);
|
|
47
|
+
const ms = timeMs(() =>
|
|
48
|
+
renderHashArt(ctx, TEST_HASH, { width: 512, height: 512, gridSize: 9, layers: 5 }),
|
|
49
|
+
);
|
|
50
|
+
console.log(` 512×512 grid=9 layers=5: ${ms.toFixed(1)} ms`);
|
|
51
|
+
expect(ms).toBeLessThan(15_000);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("renders 1024×1024 with multiple hashes consistently", () => {
|
|
55
|
+
const hashes = [
|
|
56
|
+
TEST_HASH,
|
|
57
|
+
HASH_B,
|
|
58
|
+
"ff00ff00ff00ff00ff00ff00ff00ff00ff00ff0",
|
|
59
|
+
"7777777777777777777777777777777777777777",
|
|
60
|
+
];
|
|
61
|
+
const times: number[] = [];
|
|
62
|
+
for (const hash of hashes) {
|
|
63
|
+
const canvas = createCanvas(1024, 1024);
|
|
64
|
+
const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
|
|
65
|
+
const ms = timeMs(() => renderHashArt(ctx, hash, { width: 1024, height: 1024 }));
|
|
66
|
+
times.push(ms);
|
|
67
|
+
console.log(` 1024×1024 hash=${hash.slice(0, 8)}: ${ms.toFixed(1)} ms`);
|
|
68
|
+
}
|
|
69
|
+
const min = Math.min(...times);
|
|
70
|
+
const max = Math.max(...times);
|
|
71
|
+
console.log(` Variance: fastest=${min.toFixed(1)} ms, slowest=${max.toFixed(1)} ms, ratio=${(max / min).toFixed(2)}×`);
|
|
72
|
+
expect(max / min).toBeLessThan(8);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── 2. Color pipeline micro-benchmarks ──────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe("Color pipeline timing", () => {
|
|
79
|
+
it("SacredColorScheme construction + getColorsByMode", () => {
|
|
80
|
+
const iterations = 1000;
|
|
81
|
+
const ms = timeMs(() => {
|
|
82
|
+
const scheme = new colors.SacredColorScheme(TEST_HASH);
|
|
83
|
+
scheme.getColorsByMode("harmonious");
|
|
84
|
+
}, iterations);
|
|
85
|
+
console.log(` SacredColorScheme: ${(ms / iterations).toFixed(3)} ms/call (${iterations} iterations)`);
|
|
86
|
+
expect(ms / iterations).toBeLessThan(5);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("buildColorHierarchy", () => {
|
|
90
|
+
const scheme = new colors.SacredColorScheme(TEST_HASH);
|
|
91
|
+
const palette = scheme.getColors();
|
|
92
|
+
const iterations = 10_000;
|
|
93
|
+
const rng = utils.createRng(utils.seedFromHash(TEST_HASH));
|
|
94
|
+
const ms = timeMs(() => colors.buildColorHierarchy(palette, rng), iterations);
|
|
95
|
+
console.log(` buildColorHierarchy: ${(ms / iterations).toFixed(4)} ms/call`);
|
|
96
|
+
expect(ms / iterations).toBeLessThan(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("jitterColorHSL (hot path — called 2-3× per shape)", () => {
|
|
100
|
+
const iterations = 50_000;
|
|
101
|
+
const rng = utils.createRng(utils.seedFromHash(TEST_HASH));
|
|
102
|
+
const ms = timeMs(() => colors.jitterColorHSL("#4a7fc1", rng, 6, 0.05), iterations);
|
|
103
|
+
console.log(` jitterColorHSL: ${(ms / iterations).toFixed(4)} ms/call`);
|
|
104
|
+
expect(ms / iterations).toBeLessThan(0.1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("enforceContrast (called per shape fill + stroke)", () => {
|
|
108
|
+
const iterations = 50_000;
|
|
109
|
+
const ms = timeMs(() => colors.enforceContrast("#4a7fc1", 0.15), iterations);
|
|
110
|
+
console.log(` enforceContrast: ${(ms / iterations).toFixed(4)} ms/call`);
|
|
111
|
+
expect(ms / iterations).toBeLessThan(0.1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("luminance (called inside enforceContrast)", () => {
|
|
115
|
+
const iterations = 100_000;
|
|
116
|
+
const ms = timeMs(() => colors.luminance("#4a7fc1"), iterations);
|
|
117
|
+
console.log(` luminance: ${(ms / iterations).toFixed(5)} ms/call`);
|
|
118
|
+
expect(ms / iterations).toBeLessThan(0.05);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("evolveHierarchy (called per layer)", () => {
|
|
122
|
+
const rng = utils.createRng(utils.seedFromHash(TEST_HASH));
|
|
123
|
+
const scheme = new colors.SacredColorScheme(TEST_HASH);
|
|
124
|
+
const palette = scheme.getColors();
|
|
125
|
+
const hierarchy = colors.buildColorHierarchy(palette, rng);
|
|
126
|
+
const iterations = 10_000;
|
|
127
|
+
const ms = timeMs(() => colors.evolveHierarchy(hierarchy, 0.5, 20), iterations);
|
|
128
|
+
console.log(` evolveHierarchy: ${(ms / iterations).toFixed(4)} ms/call`);
|
|
129
|
+
expect(ms / iterations).toBeLessThan(0.5);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("hexWithAlpha (called many times per shape)", () => {
|
|
133
|
+
const iterations = 100_000;
|
|
134
|
+
const ms = timeMs(() => colors.hexWithAlpha("#4a7fc1", 0.5), iterations);
|
|
135
|
+
console.log(` hexWithAlpha: ${(ms / iterations).toFixed(5)} ms/call`);
|
|
136
|
+
expect(ms / iterations).toBeLessThan(0.01);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── 3. Noise / flow field micro-benchmarks ──────────────────────────
|
|
141
|
+
|
|
142
|
+
describe("Noise and flow field timing", () => {
|
|
143
|
+
it("createSimplexNoise setup", () => {
|
|
144
|
+
const iterations = 1000;
|
|
145
|
+
const ms = timeMs(() => {
|
|
146
|
+
const rng = utils.createRng(utils.seedFromHash(TEST_HASH, 333));
|
|
147
|
+
utils.createSimplexNoise(rng);
|
|
148
|
+
}, iterations);
|
|
149
|
+
console.log(` createSimplexNoise: ${(ms / iterations).toFixed(3)} ms/call`);
|
|
150
|
+
expect(ms / iterations).toBeLessThan(2);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("FBM noise sampling (3 octaves — used for flow field)", () => {
|
|
154
|
+
const rng = utils.createRng(utils.seedFromHash(TEST_HASH, 333));
|
|
155
|
+
const noise = utils.createSimplexNoise(rng);
|
|
156
|
+
const fbm = utils.createFBM(noise, 3, 2.0, 0.5);
|
|
157
|
+
const iterations = 100_000;
|
|
158
|
+
let sink = 0;
|
|
159
|
+
const ms = timeMs(() => {
|
|
160
|
+
sink += fbm(Math.random() * 5, Math.random() * 5);
|
|
161
|
+
}, iterations);
|
|
162
|
+
console.log(` FBM (3 octaves): ${(ms / iterations).toFixed(5)} ms/call (sink=${sink.toFixed(2)})`);
|
|
163
|
+
expect(ms / iterations).toBeLessThan(0.02);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ── 4. Shape palette and selection ──────────────────────────────────
|
|
168
|
+
|
|
169
|
+
describe("Shape palette timing", () => {
|
|
170
|
+
it("buildShapePalette", () => {
|
|
171
|
+
const shapeNames = Object.keys(shapes);
|
|
172
|
+
const iterations = 5000;
|
|
173
|
+
const ms = timeMs(() => {
|
|
174
|
+
const rng = utils.createRng(utils.seedFromHash(TEST_HASH));
|
|
175
|
+
affinity.buildShapePalette(rng, shapeNames, "classic");
|
|
176
|
+
}, iterations);
|
|
177
|
+
console.log(` buildShapePalette: ${(ms / iterations).toFixed(3)} ms/call`);
|
|
178
|
+
expect(ms / iterations).toBeLessThan(2);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("pickShapeFromPalette (called per shape)", () => {
|
|
182
|
+
const rng = utils.createRng(utils.seedFromHash(TEST_HASH));
|
|
183
|
+
const shapeNames = Object.keys(shapes);
|
|
184
|
+
const palette = affinity.buildShapePalette(rng, shapeNames, "classic");
|
|
185
|
+
const iterations = 50_000;
|
|
186
|
+
const ms = timeMs(() => affinity.pickShapeFromPalette(palette, rng, 0.5), iterations);
|
|
187
|
+
console.log(` pickShapeFromPalette: ${(ms / iterations).toFixed(4)} ms/call`);
|
|
188
|
+
expect(ms / iterations).toBeLessThan(0.1);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ── 5. Archetype selection ──────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
describe("Archetype selection timing", () => {
|
|
195
|
+
it("selectArchetype (including ~15% blend chance)", () => {
|
|
196
|
+
const iterations = 10_000;
|
|
197
|
+
const ms = timeMs(() => {
|
|
198
|
+
const rng = utils.createRng(utils.seedFromHash(TEST_HASH));
|
|
199
|
+
archetypes.selectArchetype(rng);
|
|
200
|
+
}, iterations);
|
|
201
|
+
console.log(` selectArchetype: ${(ms / iterations).toFixed(4)} ms/call`);
|
|
202
|
+
expect(ms / iterations).toBeLessThan(0.5);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── 6. Canvas draw operations (shape rendering) ─────────────────────
|
|
207
|
+
|
|
208
|
+
describe("Shape rendering timing", () => {
|
|
209
|
+
const renderStyles = [
|
|
210
|
+
["fill-and-stroke", 5000, 2],
|
|
211
|
+
["watercolor", 2000, 5],
|
|
212
|
+
["stipple", 1000, 10],
|
|
213
|
+
["hatched", 2000, 5],
|
|
214
|
+
["noise-grain", 500, 15],
|
|
215
|
+
["fabric-weave", 1000, 5],
|
|
216
|
+
["hand-drawn", 2000, 5],
|
|
217
|
+
["marble-vein", 1000, 5],
|
|
218
|
+
["wood-grain", 1000, 5],
|
|
219
|
+
] as const;
|
|
220
|
+
|
|
221
|
+
for (const [style, iterations, maxMs] of renderStyles) {
|
|
222
|
+
it(`enhanceShapeGeneration — ${style}`, () => {
|
|
223
|
+
const ctx = createTestCtx(512, 512);
|
|
224
|
+
const rng = utils.createRng(utils.seedFromHash(TEST_HASH));
|
|
225
|
+
const size = style === "noise-grain" ? 200 : style === "stipple" ? 120 : 80;
|
|
226
|
+
const ms = timeMs(() => {
|
|
227
|
+
draw.enhanceShapeGeneration(ctx, "circle", 256, 256, {
|
|
228
|
+
fillColor: "rgba(74,127,193,0.5)",
|
|
229
|
+
strokeColor: "#333333",
|
|
230
|
+
strokeWidth: 2,
|
|
231
|
+
size,
|
|
232
|
+
rotation: 45,
|
|
233
|
+
renderStyle: style,
|
|
234
|
+
rng,
|
|
235
|
+
lightAngle: 1.2,
|
|
236
|
+
scaleFactor: 0.5,
|
|
237
|
+
});
|
|
238
|
+
}, iterations);
|
|
239
|
+
console.log(` enhanceShape (${style}): ${(ms / iterations).toFixed(3)} ms/call`);
|
|
240
|
+
expect(ms / iterations).toBeLessThan(maxMs);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-phase timing breakdown using _debugTiming instrumentation.
|
|
3
|
+
* This gives exact cost of each pipeline section inside renderHashArt.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { createCanvas } from "@napi-rs/canvas";
|
|
7
|
+
import { renderHashArt } from "../lib/render";
|
|
8
|
+
import type { GenerationConfig } from "../types";
|
|
9
|
+
|
|
10
|
+
const HASHES = [
|
|
11
|
+
{ label: "46192e59", hash: "46192e59d42f741c761cbea79462a8b3815dd905" },
|
|
12
|
+
{ label: "deadbeef", hash: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" },
|
|
13
|
+
{ label: "ff00ff00", hash: "ff00ff00ff00ff00ff00ff00ff00ff00ff00ff0" },
|
|
14
|
+
{ label: "77777777", hash: "7777777777777777777777777777777777777777" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function fmt(ms: number): string {
|
|
18
|
+
return ms < 1 ? `${(ms * 1000).toFixed(0)}µs` : `${ms.toFixed(1)}ms`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("Phase breakdown via _debugTiming", () => {
|
|
22
|
+
for (const { label, hash } of HASHES) {
|
|
23
|
+
it(`${label} at 1024×1024`, () => {
|
|
24
|
+
const canvas = createCanvas(1024, 1024);
|
|
25
|
+
const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
|
|
26
|
+
const timing: GenerationConfig["_debugTiming"] = { phases: {}, shapeCount: 0, extraCount: 0 };
|
|
27
|
+
const config: Partial<GenerationConfig> = { width: 1024, height: 1024, _debugTiming: timing };
|
|
28
|
+
|
|
29
|
+
const start = performance.now();
|
|
30
|
+
renderHashArt(ctx, hash, config);
|
|
31
|
+
const total = performance.now() - start;
|
|
32
|
+
|
|
33
|
+
console.log(`\n ═══ ${label} (${total.toFixed(0)}ms total, ${timing!.shapeCount} shapes, ${timing!.extraCount} extras) ═══`);
|
|
34
|
+
const phases = timing!.phases;
|
|
35
|
+
// Sort by cost descending
|
|
36
|
+
const sorted = Object.entries(phases).sort((a, b) => b[1] - a[1]);
|
|
37
|
+
for (const [name, ms] of sorted) {
|
|
38
|
+
const pct = ((ms / total) * 100).toFixed(1);
|
|
39
|
+
console.log(` ${name.padEnd(25)} ${fmt(ms).padStart(10)} (${pct}%)`);
|
|
40
|
+
}
|
|
41
|
+
expect(total).toBeLessThan(5_000); // was 30s, now targeting <5s
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-level timing profiler — measures cost of each pipeline section
|
|
3
|
+
* by running the full pipeline with instrumented timing hooks.
|
|
4
|
+
*
|
|
5
|
+
* Strategy: We can't easily instrument inside renderHashArt without
|
|
6
|
+
* modifying it, so instead we measure the cost of individual subsystems
|
|
7
|
+
* in isolation at realistic scales, then compare to full pipeline time.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect } from "vitest";
|
|
10
|
+
import { createCanvas } from "@napi-rs/canvas";
|
|
11
|
+
import { renderHashArt } from "../lib/render";
|
|
12
|
+
import * as utils from "../lib/utils";
|
|
13
|
+
import * as colors from "../lib/canvas/colors";
|
|
14
|
+
|
|
15
|
+
const HASHES = [
|
|
16
|
+
{ label: "46192e59", hash: "46192e59d42f741c761cbea79462a8b3815dd905" },
|
|
17
|
+
{ label: "deadbeef", hash: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" },
|
|
18
|
+
{ label: "ff00ff00", hash: "ff00ff00ff00ff00ff00ff00ff00ff00ff00ff0" },
|
|
19
|
+
{ label: "77777777", hash: "7777777777777777777777777777777777777777" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function fmt(ms: number): string {
|
|
23
|
+
return ms < 1 ? `${(ms * 1000).toFixed(0)}µs` : `${ms.toFixed(1)}ms`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("Phase cost breakdown", () => {
|
|
27
|
+
it("measures noise texture (phase 7) cost in isolation", () => {
|
|
28
|
+
// Phase 7 does getImageData → pixel loop → putImageData
|
|
29
|
+
// At 1024×1024 that's ~1.3M noise dots
|
|
30
|
+
const sizes = [512, 1024, 2048];
|
|
31
|
+
console.log("\n ═══ Noise texture (phase 7) cost ═══");
|
|
32
|
+
for (const size of sizes) {
|
|
33
|
+
const canvas = createCanvas(size, size);
|
|
34
|
+
const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
|
|
35
|
+
// Fill with something so getImageData has data
|
|
36
|
+
ctx.fillStyle = "#333";
|
|
37
|
+
ctx.fillRect(0, 0, size, size);
|
|
38
|
+
|
|
39
|
+
const noiseDensity = Math.floor((size * size) / 800);
|
|
40
|
+
const noiseRng = utils.createRng(utils.seedFromHash("test", 777));
|
|
41
|
+
|
|
42
|
+
const start = performance.now();
|
|
43
|
+
const imageData = ctx.getImageData(0, 0, size, size);
|
|
44
|
+
const getTime = performance.now() - start;
|
|
45
|
+
|
|
46
|
+
const data = imageData.data;
|
|
47
|
+
const loopStart = performance.now();
|
|
48
|
+
for (let i = 0; i < noiseDensity; i++) {
|
|
49
|
+
const nx = Math.floor(noiseRng() * size);
|
|
50
|
+
const ny = Math.floor(noiseRng() * size);
|
|
51
|
+
const brightness = noiseRng() > 0.5 ? 255 : 0;
|
|
52
|
+
const alpha = Math.floor((0.01 + noiseRng() * 0.03) * 255);
|
|
53
|
+
const idx = (ny * size + nx) * 4;
|
|
54
|
+
const srcA = alpha / 255;
|
|
55
|
+
const invA = 1 - srcA;
|
|
56
|
+
data[idx] = Math.round(data[idx] * invA + brightness * srcA);
|
|
57
|
+
data[idx + 1] = Math.round(data[idx + 1] * invA + brightness * srcA);
|
|
58
|
+
data[idx + 2] = Math.round(data[idx + 2] * invA + brightness * srcA);
|
|
59
|
+
}
|
|
60
|
+
const loopTime = performance.now() - loopStart;
|
|
61
|
+
|
|
62
|
+
const putStart = performance.now();
|
|
63
|
+
ctx.putImageData(imageData, 0, 0);
|
|
64
|
+
const putTime = performance.now() - putStart;
|
|
65
|
+
|
|
66
|
+
console.log(` ${size}×${size}: getImageData=${fmt(getTime)}, loop(${noiseDensity} dots)=${fmt(loopTime)}, putImageData=${fmt(putTime)}, total=${fmt(getTime + loopTime + putTime)}`);
|
|
67
|
+
}
|
|
68
|
+
expect(true).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("measures flow line cost (phase 6) — per-segment stroke vs batched", () => {
|
|
72
|
+
console.log("\n ═══ Flow line (phase 6) cost: per-segment vs batched ═══");
|
|
73
|
+
const size = 1024;
|
|
74
|
+
const canvas = createCanvas(size, size);
|
|
75
|
+
const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
|
|
76
|
+
|
|
77
|
+
// Simulate realistic flow line parameters
|
|
78
|
+
const numLines = 15;
|
|
79
|
+
const stepsPerLine = 50;
|
|
80
|
+
const totalSegments = numLines * stepsPerLine;
|
|
81
|
+
|
|
82
|
+
// Current approach: beginPath/moveTo/lineTo/stroke per segment
|
|
83
|
+
const start1 = performance.now();
|
|
84
|
+
for (let line = 0; line < numLines; line++) {
|
|
85
|
+
let px = Math.random() * size;
|
|
86
|
+
let py = Math.random() * size;
|
|
87
|
+
for (let s = 0; s < stepsPerLine; s++) {
|
|
88
|
+
const nx = px + (Math.random() - 0.5) * 10;
|
|
89
|
+
const ny = py + (Math.random() - 0.5) * 10;
|
|
90
|
+
ctx.globalAlpha = 0.1 * (1 - s / stepsPerLine);
|
|
91
|
+
ctx.strokeStyle = `rgba(100,150,200,0.3)`;
|
|
92
|
+
ctx.lineWidth = 2 * (1 - s / stepsPerLine * 0.8);
|
|
93
|
+
ctx.beginPath();
|
|
94
|
+
ctx.moveTo(px, py);
|
|
95
|
+
ctx.lineTo(nx, ny);
|
|
96
|
+
ctx.stroke();
|
|
97
|
+
px = nx;
|
|
98
|
+
py = ny;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const perSegTime = performance.now() - start1;
|
|
102
|
+
|
|
103
|
+
// Batched approach: group segments by quantized state
|
|
104
|
+
const start2 = performance.now();
|
|
105
|
+
// Quantize into alpha buckets (e.g. 5 buckets)
|
|
106
|
+
const ALPHA_BUCKETS = 5;
|
|
107
|
+
const buckets: Array<Array<[number, number, number, number]>> = [];
|
|
108
|
+
for (let i = 0; i < ALPHA_BUCKETS; i++) buckets.push([]);
|
|
109
|
+
|
|
110
|
+
for (let line = 0; line < numLines; line++) {
|
|
111
|
+
let px = Math.random() * size;
|
|
112
|
+
let py = Math.random() * size;
|
|
113
|
+
for (let s = 0; s < stepsPerLine; s++) {
|
|
114
|
+
const nx = px + (Math.random() - 0.5) * 10;
|
|
115
|
+
const ny = py + (Math.random() - 0.5) * 10;
|
|
116
|
+
const t = s / stepsPerLine;
|
|
117
|
+
const bucketIdx = Math.min(ALPHA_BUCKETS - 1, Math.floor(t * ALPHA_BUCKETS));
|
|
118
|
+
buckets[bucketIdx].push([px, py, nx, ny]);
|
|
119
|
+
px = nx;
|
|
120
|
+
py = ny;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (let b = 0; b < ALPHA_BUCKETS; b++) {
|
|
124
|
+
const t = (b + 0.5) / ALPHA_BUCKETS;
|
|
125
|
+
ctx.globalAlpha = 0.1 * (1 - t);
|
|
126
|
+
ctx.strokeStyle = `rgba(100,150,200,0.3)`;
|
|
127
|
+
ctx.lineWidth = 2 * (1 - t * 0.8);
|
|
128
|
+
ctx.beginPath();
|
|
129
|
+
for (const [x1, y1, x2, y2] of buckets[b]) {
|
|
130
|
+
ctx.moveTo(x1, y1);
|
|
131
|
+
ctx.lineTo(x2, y2);
|
|
132
|
+
}
|
|
133
|
+
ctx.stroke();
|
|
134
|
+
}
|
|
135
|
+
const batchedTime = performance.now() - start2;
|
|
136
|
+
|
|
137
|
+
console.log(` Per-segment (${totalSegments} segments): ${fmt(perSegTime)}`);
|
|
138
|
+
console.log(` Batched (${ALPHA_BUCKETS} buckets): ${fmt(batchedTime)}`);
|
|
139
|
+
console.log(` Speedup: ${(perSegTime / batchedTime).toFixed(1)}×`);
|
|
140
|
+
expect(true).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("measures energy line cost (phase 6b)", () => {
|
|
144
|
+
console.log("\n ═══ Energy lines (phase 6b) cost ═══");
|
|
145
|
+
const size = 1024;
|
|
146
|
+
const canvas = createCanvas(size, size);
|
|
147
|
+
const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
|
|
148
|
+
|
|
149
|
+
// Worst case: 15 energy sources × 6 bursts = 90 line segments
|
|
150
|
+
const energyCount = 15;
|
|
151
|
+
const burstsPerSource = 6;
|
|
152
|
+
|
|
153
|
+
// Current: per-segment beginPath/stroke
|
|
154
|
+
const start1 = performance.now();
|
|
155
|
+
for (let e = 0; e < energyCount; e++) {
|
|
156
|
+
for (let b = 0; b < burstsPerSource; b++) {
|
|
157
|
+
ctx.globalAlpha = 0.06;
|
|
158
|
+
ctx.strokeStyle = "rgba(100,150,200,0.3)";
|
|
159
|
+
ctx.lineWidth = 1.5;
|
|
160
|
+
ctx.beginPath();
|
|
161
|
+
ctx.moveTo(Math.random() * size, Math.random() * size);
|
|
162
|
+
ctx.lineTo(Math.random() * size, Math.random() * size);
|
|
163
|
+
ctx.stroke();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const perSegTime = performance.now() - start1;
|
|
167
|
+
|
|
168
|
+
// Batched: single path
|
|
169
|
+
const start2 = performance.now();
|
|
170
|
+
ctx.globalAlpha = 0.06;
|
|
171
|
+
ctx.strokeStyle = "rgba(100,150,200,0.3)";
|
|
172
|
+
ctx.lineWidth = 1.5;
|
|
173
|
+
ctx.beginPath();
|
|
174
|
+
for (let e = 0; e < energyCount; e++) {
|
|
175
|
+
for (let b = 0; b < burstsPerSource; b++) {
|
|
176
|
+
ctx.moveTo(Math.random() * size, Math.random() * size);
|
|
177
|
+
ctx.lineTo(Math.random() * size, Math.random() * size);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
ctx.stroke();
|
|
181
|
+
const batchedTime = performance.now() - start2;
|
|
182
|
+
|
|
183
|
+
console.log(` Per-segment (${energyCount * burstsPerSource} lines): ${fmt(perSegTime)}`);
|
|
184
|
+
console.log(` Batched (single path): ${fmt(batchedTime)}`);
|
|
185
|
+
console.log(` Speedup: ${(perSegTime / batchedTime).toFixed(1)}×`);
|
|
186
|
+
expect(true).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("measures connecting curves (phase 9) cost", () => {
|
|
190
|
+
console.log("\n ═══ Connecting curves (phase 9) cost ═══");
|
|
191
|
+
const size = 1024;
|
|
192
|
+
const canvas = createCanvas(size, size);
|
|
193
|
+
const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
|
|
194
|
+
|
|
195
|
+
const numCurves = 8; // typical for 1024×1024
|
|
196
|
+
// Current: per-curve beginPath/quadraticCurveTo/stroke
|
|
197
|
+
const start1 = performance.now();
|
|
198
|
+
for (let i = 0; i < numCurves; i++) {
|
|
199
|
+
ctx.globalAlpha = 0.08;
|
|
200
|
+
ctx.strokeStyle = "rgba(100,150,200,0.3)";
|
|
201
|
+
ctx.lineWidth = 0.8;
|
|
202
|
+
ctx.beginPath();
|
|
203
|
+
ctx.moveTo(Math.random() * size, Math.random() * size);
|
|
204
|
+
ctx.quadraticCurveTo(
|
|
205
|
+
Math.random() * size, Math.random() * size,
|
|
206
|
+
Math.random() * size, Math.random() * size,
|
|
207
|
+
);
|
|
208
|
+
ctx.stroke();
|
|
209
|
+
}
|
|
210
|
+
const perCurveTime = performance.now() - start1;
|
|
211
|
+
console.log(` Per-curve (${numCurves} curves): ${fmt(perCurveTime)}`);
|
|
212
|
+
expect(true).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("measures hexWithAlpha hot path cost", () => {
|
|
216
|
+
// hexWithAlpha is called thousands of times — check if caching helps
|
|
217
|
+
console.log("\n ═══ hexWithAlpha caching potential ═══");
|
|
218
|
+
const iterations = 100_000;
|
|
219
|
+
const colors_list = ["#4a7fc1", "#c14a7f", "#7fc14a", "#f0e0d0", "#333333"];
|
|
220
|
+
const alphas = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8];
|
|
221
|
+
|
|
222
|
+
const start = performance.now();
|
|
223
|
+
for (let i = 0; i < iterations; i++) {
|
|
224
|
+
colors.hexWithAlpha(colors_list[i % colors_list.length], alphas[i % alphas.length]);
|
|
225
|
+
}
|
|
226
|
+
const uncachedTime = performance.now() - start;
|
|
227
|
+
|
|
228
|
+
// Simple cache simulation
|
|
229
|
+
const cache = new Map<string, string>();
|
|
230
|
+
const start2 = performance.now();
|
|
231
|
+
for (let i = 0; i < iterations; i++) {
|
|
232
|
+
const c = colors_list[i % colors_list.length];
|
|
233
|
+
const a = alphas[i % alphas.length];
|
|
234
|
+
const key = `${c}|${a}`;
|
|
235
|
+
if (!cache.has(key)) {
|
|
236
|
+
cache.set(key, colors.hexWithAlpha(c, a));
|
|
237
|
+
}
|
|
238
|
+
cache.get(key);
|
|
239
|
+
}
|
|
240
|
+
const cachedTime = performance.now() - start2;
|
|
241
|
+
|
|
242
|
+
console.log(` Uncached: ${fmt(uncachedTime)} (${(uncachedTime / iterations * 1000).toFixed(2)}µs/call)`);
|
|
243
|
+
console.log(` Cached: ${fmt(cachedTime)} (${(cachedTime / iterations * 1000).toFixed(2)}µs/call)`);
|
|
244
|
+
console.log(` Speedup: ${(uncachedTime / cachedTime).toFixed(1)}×`);
|
|
245
|
+
expect(true).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("full pipeline breakdown — before vs after", () => {
|
|
249
|
+
console.log("\n ═══ Full pipeline timing (1024×1024) ═══");
|
|
250
|
+
for (const { label, hash } of HASHES) {
|
|
251
|
+
const canvas = createCanvas(1024, 1024);
|
|
252
|
+
const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D;
|
|
253
|
+
const start = performance.now();
|
|
254
|
+
renderHashArt(ctx, hash, { width: 1024, height: 1024 });
|
|
255
|
+
const ms = performance.now() - start;
|
|
256
|
+
console.log(` ${label}: ${fmt(ms)}`);
|
|
257
|
+
}
|
|
258
|
+
expect(true).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
});
|