text-shaper 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +8 -3
- package/dist/index.js +10 -10
- package/dist/index.js.map +13 -8
- package/dist/perf/benchmark-runner-eksh2b26.js +0 -0
- package/dist/perf/benchmark-runner.html +517 -0
- package/dist/perf/comparison-tests.js +22676 -0
- package/dist/perf/comparison-tests.js.map +111 -0
- package/dist/perf/cpu/cpu-perf.js +21920 -0
- package/dist/perf/cpu/cpu-perf.js.map +108 -0
- package/dist/perf/gpu/webgl-perf.js +470 -0
- package/dist/perf/gpu/webgl-perf.js.map +11 -0
- package/dist/perf/gpu/webgpu-perf.js +442 -0
- package/dist/perf/gpu/webgpu-perf.js.map +11 -0
- package/dist/perf/index.html +258 -0
- package/dist/perf/module-loader.js +18 -0
- package/dist/perf/utils/perf-utils.js +170 -0
- package/dist/perf/utils/perf-utils.js.map +10 -0
- package/dist/raster/asymmetric-stroke.d.ts +57 -0
- package/dist/raster/bitmap-utils.d.ts +57 -0
- package/dist/raster/cascade-blur.d.ts +45 -0
- package/dist/raster/msdf.d.ts +125 -0
- package/dist/raster/types.d.ts +15 -0
- package/dist/render/outline-transform.d.ts +169 -0
- package/package.json +2 -2
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Text Shaper Performance Tests</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
10
|
+
max-width: 800px;
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
padding: 40px 20px;
|
|
13
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
color: white;
|
|
16
|
+
}
|
|
17
|
+
.container {
|
|
18
|
+
background: rgba(255, 255, 255, 0.1);
|
|
19
|
+
backdrop-filter: blur(10px);
|
|
20
|
+
border-radius: 20px;
|
|
21
|
+
padding: 40px;
|
|
22
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
23
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
24
|
+
}
|
|
25
|
+
h1 {
|
|
26
|
+
text-align: center;
|
|
27
|
+
margin-bottom: 10px;
|
|
28
|
+
font-size: 2.5rem;
|
|
29
|
+
font-weight: 700;
|
|
30
|
+
}
|
|
31
|
+
.subtitle {
|
|
32
|
+
text-align: center;
|
|
33
|
+
margin-bottom: 40px;
|
|
34
|
+
opacity: 0.9;
|
|
35
|
+
font-size: 1.1rem;
|
|
36
|
+
}
|
|
37
|
+
.feature-grid {
|
|
38
|
+
display: grid;
|
|
39
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
40
|
+
gap: 20px;
|
|
41
|
+
margin: 40px 0;
|
|
42
|
+
}
|
|
43
|
+
.feature-card {
|
|
44
|
+
background: rgba(255, 255, 255, 0.1);
|
|
45
|
+
border-radius: 15px;
|
|
46
|
+
padding: 25px;
|
|
47
|
+
text-align: center;
|
|
48
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
49
|
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
50
|
+
}
|
|
51
|
+
.feature-card:hover {
|
|
52
|
+
transform: translateY(-5px);
|
|
53
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
|
54
|
+
}
|
|
55
|
+
.feature-icon {
|
|
56
|
+
font-size: 3rem;
|
|
57
|
+
margin-bottom: 15px;
|
|
58
|
+
display: block;
|
|
59
|
+
}
|
|
60
|
+
.feature-title {
|
|
61
|
+
font-size: 1.2rem;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
margin-bottom: 10px;
|
|
64
|
+
}
|
|
65
|
+
.feature-description {
|
|
66
|
+
opacity: 0.8;
|
|
67
|
+
font-size: 0.9rem;
|
|
68
|
+
line-height: 1.5;
|
|
69
|
+
}
|
|
70
|
+
.cta-section {
|
|
71
|
+
text-align: center;
|
|
72
|
+
margin-top: 40px;
|
|
73
|
+
}
|
|
74
|
+
.cta-button {
|
|
75
|
+
display: inline-block;
|
|
76
|
+
background: rgba(255, 255, 255, 0.2);
|
|
77
|
+
color: white;
|
|
78
|
+
padding: 15px 30px;
|
|
79
|
+
border-radius: 50px;
|
|
80
|
+
text-decoration: none;
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
font-size: 1.1rem;
|
|
83
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
84
|
+
transition: all 0.3s ease;
|
|
85
|
+
margin: 10px;
|
|
86
|
+
}
|
|
87
|
+
.cta-button:hover {
|
|
88
|
+
background: rgba(255, 255, 255, 0.3);
|
|
89
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
90
|
+
transform: translateY(-2px);
|
|
91
|
+
}
|
|
92
|
+
.tech-info {
|
|
93
|
+
background: rgba(0, 0, 0, 0.2);
|
|
94
|
+
border-radius: 15px;
|
|
95
|
+
padding: 20px;
|
|
96
|
+
margin: 30px 0;
|
|
97
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
98
|
+
}
|
|
99
|
+
.tech-info h3 {
|
|
100
|
+
margin-top: 0;
|
|
101
|
+
color: #fff;
|
|
102
|
+
}
|
|
103
|
+
.tech-list {
|
|
104
|
+
display: flex;
|
|
105
|
+
flex-wrap: wrap;
|
|
106
|
+
gap: 10px;
|
|
107
|
+
margin-top: 15px;
|
|
108
|
+
}
|
|
109
|
+
.tech-tag {
|
|
110
|
+
background: rgba(255, 255, 255, 0.2);
|
|
111
|
+
padding: 5px 12px;
|
|
112
|
+
border-radius: 20px;
|
|
113
|
+
font-size: 0.85rem;
|
|
114
|
+
font-weight: 500;
|
|
115
|
+
}
|
|
116
|
+
.warning {
|
|
117
|
+
background: rgba(255, 193, 7, 0.2);
|
|
118
|
+
border: 1px solid rgba(255, 193, 7, 0.3);
|
|
119
|
+
color: #ffc107;
|
|
120
|
+
padding: 15px;
|
|
121
|
+
border-radius: 10px;
|
|
122
|
+
margin: 20px 0;
|
|
123
|
+
text-align: center;
|
|
124
|
+
}
|
|
125
|
+
</style>
|
|
126
|
+
</head>
|
|
127
|
+
<body>
|
|
128
|
+
<div class="container">
|
|
129
|
+
<h1>🚀 Text Shaper Performance Lab</h1>
|
|
130
|
+
<p class="subtitle">Benchmarking CPU vs GPU Performance for Text Rasterization</p>
|
|
131
|
+
|
|
132
|
+
<div class="feature-grid">
|
|
133
|
+
<div class="feature-card">
|
|
134
|
+
<span class="feature-icon">⚡</span>
|
|
135
|
+
<div class="feature-title">CPU Performance</div>
|
|
136
|
+
<div class="feature-description">
|
|
137
|
+
Test the performance of your current TypeScript implementation with various optimization levels
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="feature-card">
|
|
141
|
+
<span class="feature-icon">🎮</span>
|
|
142
|
+
<div class="feature-title">WebGL Acceleration</div>
|
|
143
|
+
<div class="feature-description">
|
|
144
|
+
Compare WebGL shader performance against CPU implementation for real-time rendering
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="feature-card">
|
|
148
|
+
<span class="feature-icon">🔥</span>
|
|
149
|
+
<div class="feature-title">WebGPU Compute</div>
|
|
150
|
+
<div class="feature-description">
|
|
151
|
+
Leverage modern GPU compute shaders for parallel text processing and SDF generation
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="feature-card">
|
|
155
|
+
<span class="feature-icon">📊</span>
|
|
156
|
+
<div class="feature-title">Detailed Analytics</div>
|
|
157
|
+
<div class="feature-description">
|
|
158
|
+
Get comprehensive performance reports with timing breakdowns and speedup analysis
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div class="tech-info">
|
|
164
|
+
<h3>🛠️ Technologies Tested</h3>
|
|
165
|
+
<div class="tech-list">
|
|
166
|
+
<span class="tech-tag">TypeScript</span>
|
|
167
|
+
<span class="tech-tag">WebGL</span>
|
|
168
|
+
<span class="tech-tag">WebGPU</span>
|
|
169
|
+
<span class="tech-tag">SDF Generation</span>
|
|
170
|
+
<span class="tech-tag">Texture Atlas</span>
|
|
171
|
+
<span class="tech-tag">Glyph Rasterization</span>
|
|
172
|
+
<span class="tech-tag">Text Shaping</span>
|
|
173
|
+
<span class="tech-tag">Performance Benchmarking</span>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="cta-section">
|
|
178
|
+
<a href="benchmark-runner.html" class="cta-button">
|
|
179
|
+
🎯 Start Benchmarking
|
|
180
|
+
</a>
|
|
181
|
+
<a href="#" onclick="runQuickTest()" class="cta-button">
|
|
182
|
+
⚡ Quick Test
|
|
183
|
+
</a>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div id="quick-results" style="display: none; margin-top: 30px;">
|
|
187
|
+
<div class="tech-info">
|
|
188
|
+
<h3>🖥️ System Information</h3>
|
|
189
|
+
<div id="system-info"></div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<script type="module">
|
|
195
|
+
import { getGPUInfo } from '../utils/perf-utils.js';
|
|
196
|
+
|
|
197
|
+
// Quick system check
|
|
198
|
+
async function runQuickTest() {
|
|
199
|
+
const resultsDiv = document.getElementById('quick-results');
|
|
200
|
+
const systemInfo = document.getElementById('system-info');
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const gpuInfo = await getGPUInfo();
|
|
204
|
+
|
|
205
|
+
systemInfo.innerHTML = `
|
|
206
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
|
|
207
|
+
<div>
|
|
208
|
+
<strong>GPU Vendor:</strong><br>
|
|
209
|
+
${gpuInfo.vendor}
|
|
210
|
+
</div>
|
|
211
|
+
<div>
|
|
212
|
+
<strong>GPU Renderer:</strong><br>
|
|
213
|
+
${gpuInfo.renderer}
|
|
214
|
+
</div>
|
|
215
|
+
<div>
|
|
216
|
+
<strong>WebGPU Support:</strong><br>
|
|
217
|
+
${gpuInfo.webgpu ? '✅ Available' : '❌ Not Available'}
|
|
218
|
+
</div>
|
|
219
|
+
<div>
|
|
220
|
+
<strong>WebGL Support:</strong><br>
|
|
221
|
+
${gpuInfo.webgl ? '✅ Available' : '❌ Not Available'}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(255,255,255,0.2);">
|
|
225
|
+
<strong>Recommendation:</strong>
|
|
226
|
+
${gpuInfo.webgpu ? 'Use WebGPU for best performance' :
|
|
227
|
+
gpuInfo.webgl ? 'Use WebGL for GPU acceleration' :
|
|
228
|
+
'CPU-only benchmarks recommended'}
|
|
229
|
+
</div>
|
|
230
|
+
`;
|
|
231
|
+
|
|
232
|
+
resultsDiv.style.display = 'block';
|
|
233
|
+
} catch (error) {
|
|
234
|
+
systemInfo.innerHTML = `
|
|
235
|
+
<div class="warning">
|
|
236
|
+
Unable to detect GPU information. This might be due to browser security restrictions.
|
|
237
|
+
<br><br>
|
|
238
|
+
<strong>Try the full benchmark instead:</strong>
|
|
239
|
+
<br>
|
|
240
|
+
<a href="benchmark-runner.html" style="color: #ffc107; text-decoration: underline;">
|
|
241
|
+
Go to Benchmark Runner
|
|
242
|
+
</a>
|
|
243
|
+
</div>
|
|
244
|
+
`;
|
|
245
|
+
resultsDiv.style.display = 'block';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Make it available globally
|
|
250
|
+
window.runQuickTest = runQuickTest;
|
|
251
|
+
|
|
252
|
+
// Auto-run quick test on load
|
|
253
|
+
window.addEventListener('load', () => {
|
|
254
|
+
setTimeout(runQuickTest, 1000);
|
|
255
|
+
});
|
|
256
|
+
</script>
|
|
257
|
+
</body>
|
|
258
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
// Performance test module loader
|
|
3
|
+
// This helps with module resolution in browser
|
|
4
|
+
|
|
5
|
+
// Export all modules globally for easy access
|
|
6
|
+
window.PerformanceTests = {
|
|
7
|
+
// Modules will be loaded by the HTML files
|
|
8
|
+
modules: new Map(),
|
|
9
|
+
|
|
10
|
+
loadModule: function(name, module) {
|
|
11
|
+
this.modules.set(name, module);
|
|
12
|
+
console.log('Loaded module:', name);
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
getModule: function(name) {
|
|
16
|
+
return this.modules.get(name);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, {
|
|
5
|
+
get: all[name],
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
set: (newValue) => all[name] = () => newValue
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
12
|
+
|
|
13
|
+
// tests/performance/utils/perf-utils.ts
|
|
14
|
+
class PerformanceTimer {
|
|
15
|
+
startTime = 0;
|
|
16
|
+
endTime = 0;
|
|
17
|
+
start() {
|
|
18
|
+
this.startTime = performance.now();
|
|
19
|
+
}
|
|
20
|
+
end() {
|
|
21
|
+
this.endTime = performance.now();
|
|
22
|
+
return this.duration();
|
|
23
|
+
}
|
|
24
|
+
duration() {
|
|
25
|
+
return this.endTime - this.startTime;
|
|
26
|
+
}
|
|
27
|
+
static now() {
|
|
28
|
+
return performance.now();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function benchmark(name, fn, options = {}) {
|
|
32
|
+
const {
|
|
33
|
+
iterations = 100,
|
|
34
|
+
warmupIterations = 10,
|
|
35
|
+
gcBetweenRuns = false
|
|
36
|
+
} = options;
|
|
37
|
+
for (let i = 0;i < warmupIterations; i++) {
|
|
38
|
+
await fn();
|
|
39
|
+
}
|
|
40
|
+
if (gcBetweenRuns && globalThis.gc) {
|
|
41
|
+
globalThis.gc();
|
|
42
|
+
}
|
|
43
|
+
const samples = [];
|
|
44
|
+
let totalTime = 0;
|
|
45
|
+
let minTime = Infinity;
|
|
46
|
+
let maxTime = -Infinity;
|
|
47
|
+
for (let i = 0;i < iterations; i++) {
|
|
48
|
+
const start = PerformanceTimer.now();
|
|
49
|
+
await fn();
|
|
50
|
+
const end = PerformanceTimer.now();
|
|
51
|
+
const duration = end - start;
|
|
52
|
+
samples.push(duration);
|
|
53
|
+
totalTime += duration;
|
|
54
|
+
minTime = Math.min(minTime, duration);
|
|
55
|
+
maxTime = Math.max(maxTime, duration);
|
|
56
|
+
if (i % 10 === 9) {
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
iterations,
|
|
63
|
+
totalTime,
|
|
64
|
+
avgTime: totalTime / iterations,
|
|
65
|
+
minTime,
|
|
66
|
+
maxTime,
|
|
67
|
+
samples
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function comparePerformance(name, impl1, impl2, options = {}) {
|
|
71
|
+
const result1 = await benchmark(`${name}-impl1`, impl1, options);
|
|
72
|
+
const result2 = await benchmark(`${name}-impl2`, impl2, options);
|
|
73
|
+
const speedup = result1.avgTime / result2.avgTime;
|
|
74
|
+
const winner = result1.avgTime < result2.avgTime ? "impl1" : "impl2";
|
|
75
|
+
return {
|
|
76
|
+
impl1: result1,
|
|
77
|
+
impl2: result2,
|
|
78
|
+
speedup: Math.max(speedup, 1 / speedup),
|
|
79
|
+
winner
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class MemoryTracker {
|
|
84
|
+
initialUsage = 0;
|
|
85
|
+
start() {
|
|
86
|
+
if (globalThis.performance?.memory) {
|
|
87
|
+
this.initialUsage = globalThis.performance.memory.usedJSHeapSize;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
getUsage() {
|
|
91
|
+
if (globalThis.performance?.memory) {
|
|
92
|
+
return globalThis.performance.memory.usedJSHeapSize - this.initialUsage;
|
|
93
|
+
}
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
format(bytes) {
|
|
97
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function createPerformanceReport(results) {
|
|
101
|
+
const report = [];
|
|
102
|
+
report.push("=== Performance Benchmark Report ===");
|
|
103
|
+
report.push(`Generated: ${new Date().toISOString()}`);
|
|
104
|
+
report.push("");
|
|
105
|
+
results.forEach((result) => {
|
|
106
|
+
report.push(`${result.name}:`);
|
|
107
|
+
report.push(` Iterations: ${result.iterations}`);
|
|
108
|
+
report.push(` Average: ${result.avgTime.toFixed(3)} ms`);
|
|
109
|
+
report.push(` Min: ${result.minTime.toFixed(3)} ms`);
|
|
110
|
+
report.push(` Max: ${result.maxTime.toFixed(3)} ms`);
|
|
111
|
+
report.push(` Total: ${result.totalTime.toFixed(3)} ms`);
|
|
112
|
+
report.push("");
|
|
113
|
+
});
|
|
114
|
+
return report.join(`
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
function isWebGPUSupported() {
|
|
118
|
+
return !!(navigator.gpu && navigator.gpu.requestAdapter);
|
|
119
|
+
}
|
|
120
|
+
function isWebGLSupported() {
|
|
121
|
+
try {
|
|
122
|
+
const canvas = document.createElement("canvas");
|
|
123
|
+
return !!(canvas.getContext("webgl") || canvas.getContext("webgl2"));
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function getGPUInfo() {
|
|
129
|
+
const info = {
|
|
130
|
+
vendor: "Unknown",
|
|
131
|
+
renderer: "Unknown",
|
|
132
|
+
webgpu: isWebGPUSupported(),
|
|
133
|
+
webgl: isWebGLSupported()
|
|
134
|
+
};
|
|
135
|
+
if (info.webgpu) {
|
|
136
|
+
try {
|
|
137
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
138
|
+
if (adapter) {
|
|
139
|
+
info.vendor = adapter.info?.vendor || "Unknown";
|
|
140
|
+
info.renderer = adapter.info?.architecture || "Unknown";
|
|
141
|
+
}
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
if (info.webgl && info.vendor === "Unknown") {
|
|
145
|
+
try {
|
|
146
|
+
const canvas = document.createElement("canvas");
|
|
147
|
+
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
|
148
|
+
if (gl) {
|
|
149
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
150
|
+
if (debugInfo) {
|
|
151
|
+
info.vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
152
|
+
info.renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
return info;
|
|
158
|
+
}
|
|
159
|
+
export {
|
|
160
|
+
isWebGPUSupported,
|
|
161
|
+
isWebGLSupported,
|
|
162
|
+
getGPUInfo,
|
|
163
|
+
createPerformanceReport,
|
|
164
|
+
comparePerformance,
|
|
165
|
+
benchmark,
|
|
166
|
+
PerformanceTimer,
|
|
167
|
+
MemoryTracker
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
//# debugId=BE03333C43482BE064756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../tests/performance/utils/perf-utils.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Performance testing utilities for browser-based benchmarks\n */\n\nexport interface PerformanceResult {\n name: string;\n iterations: number;\n totalTime: number;\n avgTime: number;\n minTime: number;\n maxTime: number;\n samples: number[];\n}\n\nexport interface BenchmarkOptions {\n iterations?: number;\n warmupIterations?: number;\n gcBetweenRuns?: boolean;\n}\n\n/**\n * High-precision timing utility\n */\nexport class PerformanceTimer {\n private startTime = 0;\n private endTime = 0;\n\n start(): void {\n this.startTime = performance.now();\n }\n\n end(): number {\n this.endTime = performance.now();\n return this.duration();\n }\n\n duration(): number {\n return this.endTime - this.startTime;\n }\n\n static now(): number {\n return performance.now();\n }\n}\n\n/**\n * Run a performance benchmark on a function\n */\nexport async function benchmark(\n name: string,\n fn: () => void | Promise<void>,\n options: BenchmarkOptions = {}\n): Promise<PerformanceResult> {\n const {\n iterations = 100,\n warmupIterations = 10,\n gcBetweenRuns = false\n } = options;\n\n // Warmup\n for (let i = 0; i < warmupIterations; i++) {\n await fn();\n }\n\n // Force GC if available and requested\n if (gcBetweenRuns && (globalThis as any).gc) {\n (globalThis as any).gc();\n }\n\n const samples: number[] = [];\n let totalTime = 0;\n let minTime = Infinity;\n let maxTime = -Infinity;\n\n // Run benchmark\n for (let i = 0; i < iterations; i++) {\n const start = PerformanceTimer.now();\n await fn();\n const end = PerformanceTimer.now();\n \n const duration = end - start;\n samples.push(duration);\n totalTime += duration;\n minTime = Math.min(minTime, duration);\n maxTime = Math.max(maxTime, duration);\n\n // Small delay between iterations to prevent overheating\n if (i % 10 === 9) {\n await new Promise(resolve => setTimeout(resolve, 1));\n }\n }\n\n return {\n name,\n iterations,\n totalTime,\n avgTime: totalTime / iterations,\n minTime,\n maxTime,\n samples\n };\n}\n\n/**\n * Compare performance between two implementations\n */\nexport async function comparePerformance(\n name: string,\n impl1: () => void | Promise<void>,\n impl2: () => void | Promise<void>,\n options: BenchmarkOptions = {}\n): Promise<{\n impl1: PerformanceResult;\n impl2: PerformanceResult;\n speedup: number;\n winner: string;\n}> {\n const result1 = await benchmark(`${name}-impl1`, impl1, options);\n const result2 = await benchmark(`${name}-impl2`, impl2, options);\n\n const speedup = result1.avgTime / result2.avgTime;\n const winner = result1.avgTime < result2.avgTime ? 'impl1' : 'impl2';\n\n return {\n impl1: result1,\n impl2: result2,\n speedup: Math.max(speedup, 1 / speedup),\n winner\n };\n}\n\n/**\n * Memory usage tracker\n */\nexport class MemoryTracker {\n private initialUsage: number = 0;\n\n start(): void {\n if ((globalThis as any).performance?.memory) {\n this.initialUsage = (globalThis as any).performance.memory.usedJSHeapSize;\n }\n }\n\n getUsage(): number {\n if ((globalThis as any).performance?.memory) {\n return (globalThis as any).performance.memory.usedJSHeapSize - this.initialUsage;\n }\n return 0;\n }\n\n format(bytes: number): string {\n return `${(bytes / 1024 / 1024).toFixed(2)} MB`;\n }\n}\n\n/**\n * Create a performance report\n */\nexport function createPerformanceReport(results: PerformanceResult[]): string {\n const report: string[] = [];\n \n report.push('=== Performance Benchmark Report ===');\n report.push(`Generated: ${new Date().toISOString()}`);\n report.push('');\n\n results.forEach(result => {\n report.push(`${result.name}:`);\n report.push(` Iterations: ${result.iterations}`);\n report.push(` Average: ${result.avgTime.toFixed(3)} ms`);\n report.push(` Min: ${result.minTime.toFixed(3)} ms`);\n report.push(` Max: ${result.maxTime.toFixed(3)} ms`);\n report.push(` Total: ${result.totalTime.toFixed(3)} ms`);\n report.push('');\n });\n\n return report.join('\\n');\n}\n\n/**\n * WebGPU support detection\n */\nexport function isWebGPUSupported(): boolean {\n return !!(navigator.gpu && navigator.gpu.requestAdapter);\n}\n\n/**\n * WebGL support detection\n */\nexport function isWebGLSupported(): boolean {\n try {\n const canvas = document.createElement('canvas');\n return !!(canvas.getContext('webgl') || canvas.getContext('webgl2'));\n } catch {\n return false;\n }\n}\n\n/**\n * Get GPU information\n */\nexport async function getGPUInfo(): Promise<{\n vendor: string;\n renderer: string;\n webgpu: boolean;\n webgl: boolean;\n}> {\n const info = {\n vendor: 'Unknown',\n renderer: 'Unknown',\n webgpu: isWebGPUSupported(),\n webgl: isWebGLSupported()\n };\n\n // Try WebGPU\n if (info.webgpu) {\n try {\n const adapter = await navigator.gpu.requestAdapter();\n if (adapter) {\n info.vendor = adapter.info?.vendor || 'Unknown';\n info.renderer = adapter.info?.architecture || 'Unknown';\n }\n } catch {\n // Ignore\n }\n }\n\n // Fallback to WebGL\n if (info.webgl && info.vendor === 'Unknown') {\n try {\n const canvas = document.createElement('canvas');\n const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');\n if (gl) {\n const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');\n if (debugInfo) {\n info.vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) as string;\n info.renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) as string;\n }\n }\n } catch {\n // Ignore\n }\n }\n\n return info;\n}"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "BE03333C43482BE064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asymmetric Stroke
|
|
3
|
+
*
|
|
4
|
+
* Generates stroked outlines with independent X and Y border widths.
|
|
5
|
+
* This enables effects like directional shadows, stretched borders,
|
|
6
|
+
* and other asymmetric outline effects.
|
|
7
|
+
*
|
|
8
|
+
* The algorithm:
|
|
9
|
+
* 1. Flatten curves to polylines with configurable precision
|
|
10
|
+
* 2. For each segment, compute offset vectors scaled by (xBorder, yBorder)
|
|
11
|
+
* 3. Handle line joins (miter, round, bevel)
|
|
12
|
+
* 4. Generate both inner and outer contours for closed paths
|
|
13
|
+
* 5. Handle caps for open paths
|
|
14
|
+
*/
|
|
15
|
+
import type { GlyphPath } from "../render/path.ts";
|
|
16
|
+
/**
|
|
17
|
+
* Options for asymmetric stroking
|
|
18
|
+
*/
|
|
19
|
+
export interface AsymmetricStrokeOptions {
|
|
20
|
+
/** X-axis border width (in font units) */
|
|
21
|
+
xBorder: number;
|
|
22
|
+
/** Y-axis border width (in font units) */
|
|
23
|
+
yBorder: number;
|
|
24
|
+
/** Precision for curve flattening (smaller = more accurate, default: 1) */
|
|
25
|
+
eps?: number;
|
|
26
|
+
/** Line join style (default: "round") */
|
|
27
|
+
lineJoin?: "miter" | "round" | "bevel";
|
|
28
|
+
/** Miter limit for miter joins (default: 4) */
|
|
29
|
+
miterLimit?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Stroke a path with asymmetric X/Y borders
|
|
33
|
+
*
|
|
34
|
+
* @param path Input path to stroke
|
|
35
|
+
* @param options Stroke options including xBorder and yBorder
|
|
36
|
+
* @returns Two paths: outer (positive offset) and inner (negative offset)
|
|
37
|
+
*
|
|
38
|
+
* For filled text with border:
|
|
39
|
+
* - Combine outer outline with original fill
|
|
40
|
+
* - Or use outer outline alone for hollow border effect
|
|
41
|
+
*/
|
|
42
|
+
export declare function strokeAsymmetric(path: GlyphPath, options: AsymmetricStrokeOptions): {
|
|
43
|
+
outer: GlyphPath;
|
|
44
|
+
inner: GlyphPath;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Create a combined stroke path (both inner and outer as single path)
|
|
48
|
+
* This creates a ring/donut shape that can be filled
|
|
49
|
+
*/
|
|
50
|
+
export declare function strokeAsymmetricCombined(path: GlyphPath, options: AsymmetricStrokeOptions): GlyphPath;
|
|
51
|
+
/**
|
|
52
|
+
* Stroke with uniform border (convenience function)
|
|
53
|
+
*/
|
|
54
|
+
export declare function strokeUniform(path: GlyphPath, border: number, options?: Omit<AsymmetricStrokeOptions, "xBorder" | "yBorder">): {
|
|
55
|
+
outer: GlyphPath;
|
|
56
|
+
inner: GlyphPath;
|
|
57
|
+
};
|
|
@@ -23,3 +23,60 @@ export declare function copyBitmap(bitmap: Bitmap): Bitmap;
|
|
|
23
23
|
* Resize bitmap using nearest-neighbor interpolation
|
|
24
24
|
*/
|
|
25
25
|
export declare function resizeBitmap(bitmap: Bitmap, newWidth: number, newHeight: number): Bitmap;
|
|
26
|
+
/**
|
|
27
|
+
* Add two bitmaps together (additive blend)
|
|
28
|
+
* Result: dst = clamp(dst + src, 0, 255)
|
|
29
|
+
* Used for combining glyph with shadow/glow
|
|
30
|
+
*/
|
|
31
|
+
export declare function addBitmaps(dst: Bitmap, src: Bitmap, srcX?: number, srcY?: number): void;
|
|
32
|
+
/**
|
|
33
|
+
* Multiply two bitmaps (multiplicative blend)
|
|
34
|
+
* Result: dst = (dst * src) / 255
|
|
35
|
+
* Used for masking operations
|
|
36
|
+
*/
|
|
37
|
+
export declare function mulBitmaps(dst: Bitmap, src: Bitmap, srcX?: number, srcY?: number): void;
|
|
38
|
+
/**
|
|
39
|
+
* Subtract src from dst (subtractive blend)
|
|
40
|
+
* Result: dst = clamp(dst - src, 0, 255)
|
|
41
|
+
* Used for outline effects
|
|
42
|
+
*/
|
|
43
|
+
export declare function subBitmaps(dst: Bitmap, src: Bitmap, srcX?: number, srcY?: number): void;
|
|
44
|
+
/**
|
|
45
|
+
* Alpha composite src over dst using src as alpha
|
|
46
|
+
* Result: dst = src + dst * (1 - src/255)
|
|
47
|
+
* Standard Porter-Duff "over" operation
|
|
48
|
+
*/
|
|
49
|
+
export declare function compositeBitmaps(dst: Bitmap, src: Bitmap, srcX?: number, srcY?: number): void;
|
|
50
|
+
/**
|
|
51
|
+
* Shift bitmap position by integer offset
|
|
52
|
+
* Creates a new bitmap with the content shifted
|
|
53
|
+
*/
|
|
54
|
+
export declare function shiftBitmap(bitmap: Bitmap, shiftX: number, shiftY: number): Bitmap;
|
|
55
|
+
/**
|
|
56
|
+
* Fix outline bitmap by removing glyph interior
|
|
57
|
+
* Used when you want only the border, not the filled shape
|
|
58
|
+
* Result: outline = outline - glyph (where glyph coverage > threshold)
|
|
59
|
+
*/
|
|
60
|
+
export declare function fixOutline(outlineBitmap: Bitmap, glyphBitmap: Bitmap, glyphX?: number, glyphY?: number, threshold?: number): void;
|
|
61
|
+
/**
|
|
62
|
+
* Maximum blend: take the maximum of two bitmaps
|
|
63
|
+
* Result: dst = max(dst, src)
|
|
64
|
+
* Used for combining multiple layers
|
|
65
|
+
*/
|
|
66
|
+
export declare function maxBitmaps(dst: Bitmap, src: Bitmap, srcX?: number, srcY?: number): void;
|
|
67
|
+
/**
|
|
68
|
+
* Create a padded copy of a bitmap with extra space around edges
|
|
69
|
+
* Useful before blur operations to prevent edge artifacts
|
|
70
|
+
*/
|
|
71
|
+
export declare function padBitmap(bitmap: Bitmap, padLeft: number, padTop: number, padRight: number, padBottom: number): Bitmap;
|
|
72
|
+
/**
|
|
73
|
+
* Create an expanded bitmap that can contain both dst and src
|
|
74
|
+
* Returns the expanded bitmap and the offsets for both original bitmaps
|
|
75
|
+
*/
|
|
76
|
+
export declare function expandToFit(dst: Bitmap, src: Bitmap, srcX: number, srcY: number): {
|
|
77
|
+
expanded: Bitmap;
|
|
78
|
+
dstOffsetX: number;
|
|
79
|
+
dstOffsetY: number;
|
|
80
|
+
srcOffsetX: number;
|
|
81
|
+
srcOffsetY: number;
|
|
82
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cascade Blur Algorithm
|
|
3
|
+
*
|
|
4
|
+
* High-performance Gaussian blur for large radii using pyramid scaling.
|
|
5
|
+
*
|
|
6
|
+
* The key insight: to blur with large radius efficiently:
|
|
7
|
+
* 1. Scale down the image by factor of 2 (with smooth kernel)
|
|
8
|
+
* 2. Apply blur with smaller kernel on reduced image
|
|
9
|
+
* 3. Scale back up (with smooth kernel)
|
|
10
|
+
*
|
|
11
|
+
* The shrink/expand kernels [1, 5, 10, 10, 5, 1]/32 provide sufficient
|
|
12
|
+
* smoothness to maintain 8-bit precision through multiple cascade levels.
|
|
13
|
+
*
|
|
14
|
+
* Performance: O(n) per pixel regardless of blur radius
|
|
15
|
+
* Traditional Gaussian: O(r) per pixel where r is radius
|
|
16
|
+
*/
|
|
17
|
+
import { type Bitmap } from "./types.ts";
|
|
18
|
+
/**
|
|
19
|
+
* Cascade Gaussian blur with asymmetric X/Y radii
|
|
20
|
+
*
|
|
21
|
+
* @param bitmap Input bitmap
|
|
22
|
+
* @param radiusX Blur radius along X axis
|
|
23
|
+
* @param radiusY Blur radius along Y axis (defaults to radiusX)
|
|
24
|
+
* @returns New bitmap with blur applied (dimensions may change)
|
|
25
|
+
*/
|
|
26
|
+
export declare function cascadeBlur(bitmap: Bitmap, radiusX: number, radiusY?: number): Bitmap;
|
|
27
|
+
/**
|
|
28
|
+
* Fast Gaussian blur using cascade algorithm
|
|
29
|
+
* This is the recommended blur function for large radii (> 3 pixels)
|
|
30
|
+
*
|
|
31
|
+
* @param bitmap Input bitmap
|
|
32
|
+
* @param radius Blur radius in pixels
|
|
33
|
+
* @returns New bitmap with blur applied (dimensions may change)
|
|
34
|
+
*/
|
|
35
|
+
export declare function fastGaussianBlur(bitmap: Bitmap, radius: number): Bitmap;
|
|
36
|
+
/**
|
|
37
|
+
* Adaptive blur that chooses the best algorithm based on radius
|
|
38
|
+
* - For small radii (≤ 3): uses simple separable Gaussian (more precise)
|
|
39
|
+
* - For large radii (> 3): uses cascade algorithm (faster)
|
|
40
|
+
*
|
|
41
|
+
* @param bitmap Input bitmap
|
|
42
|
+
* @param radiusX Horizontal blur radius in pixels
|
|
43
|
+
* @param radiusY Vertical blur radius in pixels (defaults to radiusX)
|
|
44
|
+
*/
|
|
45
|
+
export declare function adaptiveBlur(bitmap: Bitmap, radiusX: number, radiusY?: number): Bitmap;
|