@vizzly-testing/honeydiff 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -16
- package/README.md +181 -689
- package/assets/honeydiff-mascot-diff-drizzle.png +0 -0
- package/examples/color-blindness.js +18 -18
- package/examples/wcag.js +17 -17
- package/index.d.ts +59 -56
- package/index.js +1 -1
- package/package.json +2 -1
- package/platforms/index-aarch64-apple-darwin.node +0 -0
- package/platforms/index-aarch64-unknown-linux-gnu.node +0 -0
- package/platforms/index-x86_64-pc-windows-msvc.node +0 -0
- package/platforms/index-x86_64-unknown-linux-gnu.node +0 -0
package/README.md
CHANGED
|
@@ -4,780 +4,272 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@vizzly-testing/honeydiff)
|
|
5
5
|
[](https://github.com/vizzly-testing/honeydiff/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Fast image comparison for visual regression testing in Node.js.
|
|
8
|
+
|
|
9
|
+
Honeydiff is a native Rust image diff engine packaged for Node. It is built for
|
|
10
|
+
the messy parts of visual testing: anti-aliased text, full-page screenshots,
|
|
11
|
+
small rendering noise, diff artifacts, spatial clusters, perceptual metrics, and
|
|
12
|
+
accessibility checks.
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="./assets/honeydiff-mascot-diff-drizzle.png" alt="Diff Drizzle, the Honeydiff Vizzly bear mascot" width="320" />
|
|
16
|
+
</p>
|
|
8
17
|
|
|
9
18
|
```bash
|
|
10
19
|
npm install @vizzly-testing/honeydiff
|
|
11
20
|
```
|
|
12
21
|
|
|
13
|
-
Requires Node.js 22+.
|
|
14
|
-
|
|
15
|
-
## Why Honeydiff?
|
|
16
|
-
|
|
17
|
-
- **6x faster than odiff** - Processes 230M+ pixels/sec with multi-core parallelization
|
|
18
|
-
- **Smart anti-aliasing** - Filters out rendering artifacts, not actual changes
|
|
19
|
-
- **Accessibility built-in** - WCAG contrast analysis for catching accessibility violations
|
|
20
|
-
- **Flexible inputs** - Works with file paths or Buffers
|
|
21
|
-
- **Full TypeScript support** - Complete type definitions included
|
|
22
|
-
- **Advanced analytics** - SSIM perceptual scoring, spatial clustering, intensity stats
|
|
23
|
-
|
|
24
|
-
Built for production visual regression testing at scale.
|
|
22
|
+
Requires Node.js 22+. Prebuilt binaries are included for macOS ARM64, Linux
|
|
23
|
+
x64/ARM64, and Windows x64.
|
|
25
24
|
|
|
26
25
|
## Quick Start
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
```javascript
|
|
27
|
+
```js
|
|
31
28
|
import { compare, quickCompare } from '@vizzly-testing/honeydiff';
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
if (await quickCompare('baseline.png', 'current.png')) {
|
|
35
|
-
console.log('Visual regression detected!');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Detailed comparison with tolerance
|
|
39
|
-
const result = await compare('baseline.png', 'current.png', {
|
|
40
|
-
threshold: 10, // Ignore tiny differences
|
|
41
|
-
includeSSIM: true // Perceptual similarity score
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
console.log(`Diff: ${result.diffPercentage.toFixed(2)}%`);
|
|
45
|
-
console.log(`SSIM: ${result.perceptualScore}`);
|
|
46
|
-
// Output: Diff: 3.55%, SSIM: 0.9234
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### TypeScript
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
import { compare, CompareOptions, DiffResult } from '@vizzly-testing/honeydiff';
|
|
30
|
+
let changed = await quickCompare('baseline.png', 'current.png');
|
|
53
31
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
includeSSIM: true,
|
|
57
|
-
includeClusters: true
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const result: DiffResult = await compare('img1.png', 'img2.png', options);
|
|
61
|
-
|
|
62
|
-
if (result.diffClusters) {
|
|
63
|
-
console.log(`Found ${result.diffClusters.length} different regions`);
|
|
32
|
+
if (changed) {
|
|
33
|
+
console.log('Visual change detected');
|
|
64
34
|
}
|
|
65
|
-
```
|
|
66
35
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let screenshot = await page.screenshot();
|
|
75
|
-
let isDifferent = await quickCompare('baseline.png', screenshot);
|
|
76
|
-
expect(isDifferent).toBe(false);
|
|
36
|
+
let result = await compare('baseline.png', 'current.png', {
|
|
37
|
+
threshold: 2.0,
|
|
38
|
+
includeClusters: true,
|
|
39
|
+
diffPath: 'artifacts/diff.png',
|
|
40
|
+
maskPath: 'artifacts/mask.png',
|
|
41
|
+
overlayPath: 'artifacts/overlay.png',
|
|
42
|
+
overwrite: true,
|
|
77
43
|
});
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
**Playwright**
|
|
81
|
-
```javascript
|
|
82
|
-
import { compare } from '@vizzly-testing/honeydiff';
|
|
83
44
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
let result = await compare('baseline.png', screenshot, {
|
|
89
|
-
threshold: 10,
|
|
90
|
-
diffPath: './artifacts/diff.png'
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
expect(result.diffPercentage).toBeLessThan(1.0);
|
|
94
|
-
});
|
|
45
|
+
console.log(result.isDifferent);
|
|
46
|
+
console.log(result.diffPercentage);
|
|
47
|
+
console.log(result.diffClusters);
|
|
95
48
|
```
|
|
96
49
|
|
|
97
|
-
##
|
|
98
|
-
|
|
99
|
-
### 1. Performance at Scale
|
|
50
|
+
## Why Honeydiff?
|
|
100
51
|
|
|
101
|
-
|
|
52
|
+
Most image diff packages stop at basic pixel comparison. Honeydiff gives you the
|
|
53
|
+
pieces visual regression systems usually need once screenshots get real:
|
|
102
54
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
55
|
+
- CIEDE2000 perceptual color thresholds, with `2.0` as the default.
|
|
56
|
+
- Strict exact matching when you set `threshold: 0`.
|
|
57
|
+
- Conservative anti-aliasing detection for font and sub-pixel rendering noise.
|
|
58
|
+
- Variable-height screenshot support for full-page comparisons.
|
|
59
|
+
- Diff, mask, and overlay image artifacts for debugging failures.
|
|
60
|
+
- Spatial clusters, intensity stats, SSIM, GMSD, and diff fingerprints.
|
|
61
|
+
- WCAG contrast screening and color vision deficiency simulation.
|
|
62
|
+
- Async and sync APIs with TypeScript definitions included.
|
|
107
63
|
|
|
108
|
-
|
|
64
|
+
## Common Use
|
|
109
65
|
|
|
110
|
-
|
|
66
|
+
### Compare Two Images
|
|
111
67
|
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
antialiasing: true // Default: true
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
console.log(`Ignored ${result.aaPixelsIgnored} AA pixels`);
|
|
118
|
-
// Output: Ignored 1952904 AA pixels
|
|
119
|
-
```
|
|
68
|
+
```js
|
|
69
|
+
import { compare } from '@vizzly-testing/honeydiff';
|
|
120
70
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const result = await compare('img1.png', 'img2.png', {
|
|
124
|
-
antialiasing: false // Strict pixel matching
|
|
71
|
+
let result = await compare('before.png', 'after.png', {
|
|
72
|
+
threshold: 2.0,
|
|
125
73
|
});
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### 3. Variable Height Screenshots
|
|
129
|
-
|
|
130
|
-
Perfect for scrollable content - compare full-page screenshots with different heights.
|
|
131
|
-
|
|
132
|
-
```javascript
|
|
133
|
-
// 24,162px vs 24,412px tall images
|
|
134
|
-
const result = await compare('full-page-v1.png', 'full-page-v2.png');
|
|
135
74
|
|
|
136
|
-
if (result.
|
|
137
|
-
console.log(
|
|
138
|
-
console.log(`Extra pixels: ${result.heightDiff.extraPixels}`);
|
|
75
|
+
if (result.isDifferent) {
|
|
76
|
+
console.log(`${result.diffPercentage.toFixed(2)}% of pixels changed`);
|
|
139
77
|
}
|
|
140
78
|
```
|
|
141
79
|
|
|
142
|
-
###
|
|
80
|
+
### Use Buffers
|
|
143
81
|
|
|
144
|
-
|
|
82
|
+
```js
|
|
83
|
+
import { readFile } from 'node:fs/promises';
|
|
84
|
+
import { compare } from '@vizzly-testing/honeydiff';
|
|
145
85
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
diffPath: './artifacts/diff.png', // Highlighted differences
|
|
149
|
-
maskPath: './artifacts/mask.png', // Binary diff mask
|
|
150
|
-
overlayPath: './artifacts/overlay.png', // Side-by-side comparison
|
|
151
|
-
overwrite: true
|
|
152
|
-
});
|
|
86
|
+
let baseline = await readFile('baseline.png');
|
|
87
|
+
let current = await readFile('current.png');
|
|
153
88
|
|
|
154
|
-
|
|
89
|
+
let result = await compare(baseline, current);
|
|
155
90
|
```
|
|
156
91
|
|
|
157
|
-
###
|
|
92
|
+
### Generate Review Artifacts
|
|
158
93
|
|
|
159
|
-
|
|
94
|
+
```js
|
|
95
|
+
import { compare } from '@vizzly-testing/honeydiff';
|
|
160
96
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
97
|
+
let result = await compare('baseline.png', 'current.png', {
|
|
98
|
+
diffPath: 'artifacts/diff.png',
|
|
99
|
+
maskPath: 'artifacts/mask.png',
|
|
100
|
+
overlayPath: 'artifacts/overlay.png',
|
|
101
|
+
overwrite: true,
|
|
165
102
|
});
|
|
166
|
-
|
|
167
|
-
if (result.diffClusters) {
|
|
168
|
-
result.diffClusters.forEach((cluster, i) => {
|
|
169
|
-
console.log(`Region ${i + 1}:`);
|
|
170
|
-
console.log(` - ${cluster.pixelCount} pixels changed`);
|
|
171
|
-
console.log(` - Center at (${cluster.centerOfMass[0]}, ${cluster.centerOfMass[1]})`);
|
|
172
|
-
console.log(` - Bounding box: ${cluster.boundingBox.width}x${cluster.boundingBox.height}`);
|
|
173
|
-
console.log(` - Average intensity: ${cluster.avgIntensity}`);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
// Output:
|
|
177
|
-
// Region 1:
|
|
178
|
-
// - 12450 pixels changed
|
|
179
|
-
// - Center at (245, 189)
|
|
180
|
-
// - Bounding box: 124x89
|
|
181
|
-
// - Average intensity: 127.3
|
|
182
103
|
```
|
|
183
104
|
|
|
184
|
-
|
|
185
|
-
| Value | Behavior |
|
|
186
|
-
|-------|----------|
|
|
187
|
-
| 1 | Exact matching - any different pixel counts |
|
|
188
|
-
| 2 | Default - filters single isolated pixels as rendering noise |
|
|
189
|
-
| 3+ | More permissive - only larger clusters detected |
|
|
105
|
+
### Group Differences Into Regions
|
|
190
106
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
Text changes often fragment into many small clusters (one per character). Enable cluster merging to consolidate them into logical regions:
|
|
194
|
-
|
|
195
|
-
```javascript
|
|
196
|
-
// Simple: enable with sensible defaults
|
|
197
|
-
const result = await compare('img1.png', 'img2.png', {
|
|
198
|
-
includeClusters: true,
|
|
199
|
-
clusterMerge: true // Merge nearby clusters (great for text)
|
|
200
|
-
});
|
|
107
|
+
```js
|
|
108
|
+
import { compare } from '@vizzly-testing/honeydiff';
|
|
201
109
|
|
|
202
|
-
|
|
203
|
-
const result = await compare('img1.png', 'img2.png', {
|
|
110
|
+
let result = await compare('baseline.png', 'current.png', {
|
|
204
111
|
includeClusters: true,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
yBandTolerance: 5, // Vertical tolerance for "same line"
|
|
208
|
-
maxHeightRatio: 2.0, // Prevent merging very different sized clusters
|
|
209
|
-
maxWidthRatio: 3.0
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// Before: "2024-01-01" detected as 59 clusters (one per character/gap)
|
|
214
|
-
// After: "2024-01-01" detected as 1-2 logical regions
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
Uses SWT-inspired heuristics from text detection research (Epshtein et al. 2010).
|
|
218
|
-
|
|
219
|
-
### 6. Perceptual Similarity (SSIM & GMSD)
|
|
220
|
-
|
|
221
|
-
Beyond pixel counting - measure structural similarity from a human perception perspective.
|
|
222
|
-
|
|
223
|
-
**SSIM (Structural Similarity Index)** - Overall perceptual similarity:
|
|
224
|
-
```javascript
|
|
225
|
-
const result = await compare('img1.png', 'img2.png', {
|
|
226
|
-
includeSSIM: true // Note: Can be slow on large images
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
if (result.perceptualScore !== null) {
|
|
230
|
-
console.log(`SSIM: ${result.perceptualScore.toFixed(3)}`);
|
|
231
|
-
// Output: SSIM: 0.923 (0.0 = different, 1.0 = identical)
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
**GMSD (Gradient Magnitude Similarity Deviation)** - Fast edge-sensitive metric:
|
|
236
|
-
```javascript
|
|
237
|
-
const result = await compare('img1.png', 'img2.png', {
|
|
238
|
-
includeGMSD: true // Very fast, great for detecting structural changes
|
|
112
|
+
minClusterSize: 2,
|
|
113
|
+
clusterMerge: true,
|
|
239
114
|
});
|
|
240
115
|
|
|
241
|
-
|
|
242
|
-
console.log(
|
|
243
|
-
// Output: GMSD: 0.0234 (0.0 = identical, higher = more different)
|
|
244
|
-
|
|
245
|
-
if (result.gmsdScore < 0.05) {
|
|
246
|
-
console.log('Edges are very similar');
|
|
247
|
-
}
|
|
116
|
+
for (let cluster of result.diffClusters ?? []) {
|
|
117
|
+
console.log(cluster.pixelCount, cluster.boundingBox);
|
|
248
118
|
}
|
|
249
119
|
```
|
|
250
120
|
|
|
251
|
-
|
|
252
|
-
- Border thickness changes
|
|
253
|
-
- Font weight shifts
|
|
254
|
-
- Icon updates
|
|
255
|
-
- Any edge/outline regressions
|
|
256
|
-
|
|
257
|
-
**Reference:** Xue et al. 2014 - "Gradient Magnitude Similarity Deviation: A Highly Efficient Perceptual Image Quality Index"
|
|
258
|
-
|
|
259
|
-
### 7. Thresholds
|
|
260
|
-
|
|
261
|
-
Honeydiff uses CIEDE2000 Delta E thresholds for perceptual color difference:
|
|
262
|
-
|
|
263
|
-
```javascript
|
|
264
|
-
const result = await compare('img1.png', 'img2.png', {
|
|
265
|
-
threshold: 2.0 // Default: ignores tiny perceptual variance
|
|
266
|
-
});
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
Use exact mode when every changed pixel should count:
|
|
270
|
-
|
|
271
|
-
```javascript
|
|
272
|
-
const result = await compare('img1.png', 'img2.png', {
|
|
273
|
-
threshold: 0,
|
|
274
|
-
minClusterSize: 1
|
|
275
|
-
});
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
### 8. Buffer Support
|
|
279
|
-
|
|
280
|
-
Work with in-memory images from screenshots or API responses.
|
|
281
|
-
|
|
282
|
-
For large screenshots, prefer file paths when possible. Buffer inputs are copied into the native worker before async processing begins, so path inputs put less pressure on the Node.js event loop and heap.
|
|
283
|
-
|
|
284
|
-
```javascript
|
|
285
|
-
import fs from 'node:fs';
|
|
286
|
-
|
|
287
|
-
// Compare buffers directly
|
|
288
|
-
let img1 = fs.readFileSync('image1.png');
|
|
289
|
-
let img2 = fs.readFileSync('image2.png');
|
|
290
|
-
let result = await compare(img1, img2, { threshold: 5 });
|
|
291
|
-
|
|
292
|
-
// Mix paths and buffers
|
|
293
|
-
let screenshot = await page.screenshot(); // Returns Buffer
|
|
294
|
-
let result = await compare('baseline.png', screenshot);
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
### 9. Image Dimensions
|
|
298
|
-
|
|
299
|
-
Fast utility to get image dimensions without loading full image data.
|
|
300
|
-
|
|
301
|
-
```javascript
|
|
302
|
-
import { getDimensions } from '@vizzly-testing/honeydiff';
|
|
303
|
-
|
|
304
|
-
let dims = await getDimensions('screenshot.png');
|
|
305
|
-
console.log(`Image is ${dims.width}x${dims.height} pixels`);
|
|
306
|
-
// Output: Image is 1920x1080 pixels
|
|
307
|
-
|
|
308
|
-
// Works with buffers too
|
|
309
|
-
let buffer = fs.readFileSync('image.png');
|
|
310
|
-
let dims = await getDimensions(buffer);
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### 10. Image Metadata
|
|
314
|
-
|
|
315
|
-
Get image dimensions, file size, and format detection - useful for storage tracking and billing.
|
|
316
|
-
|
|
317
|
-
```javascript
|
|
318
|
-
import { getImageMetadata, getImageMetadataFromFile } from '@vizzly-testing/honeydiff';
|
|
319
|
-
|
|
320
|
-
// From buffer
|
|
321
|
-
let buffer = fs.readFileSync('screenshot.png');
|
|
322
|
-
let metadata = await getImageMetadata(buffer);
|
|
323
|
-
console.log(`Image: ${metadata.width}x${metadata.height}`);
|
|
324
|
-
console.log(`Size: ${metadata.fileSizeBytes} bytes`);
|
|
325
|
-
console.log(`Format: ${metadata.format}`);
|
|
326
|
-
// Output: Image: 1920x1080
|
|
327
|
-
// Size: 245760 bytes
|
|
328
|
-
// Format: png
|
|
329
|
-
|
|
330
|
-
// From file (gets accurate file size from disk)
|
|
331
|
-
let metadata = await getImageMetadataFromFile('screenshot.png');
|
|
332
|
-
console.log(`${metadata.width}x${metadata.height}, ${metadata.fileSizeBytes} bytes`);
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
### 11. Diff Fingerprinting
|
|
121
|
+
### Add Perceptual Metrics
|
|
336
122
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
```javascript
|
|
340
|
-
import {
|
|
341
|
-
compare,
|
|
342
|
-
computeFingerprintSync,
|
|
343
|
-
fingerprintSimilaritySync,
|
|
344
|
-
fingerprintHashSync
|
|
345
|
-
} from '@vizzly-testing/honeydiff';
|
|
123
|
+
```js
|
|
124
|
+
import { compare } from '@vizzly-testing/honeydiff';
|
|
346
125
|
|
|
347
|
-
// Compare images with clustering enabled
|
|
348
126
|
let result = await compare('baseline.png', 'current.png', {
|
|
349
|
-
|
|
127
|
+
includeSSIM: true,
|
|
128
|
+
includeGMSD: true,
|
|
350
129
|
});
|
|
351
130
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (fingerprint) {
|
|
356
|
-
console.log(`Hash: ${fingerprint.hash}`);
|
|
357
|
-
console.log(`Zones affected: ${fingerprint.zoneMask.toString(2).padStart(16, '0')}`);
|
|
358
|
-
console.log(`Cluster count: ${fingerprint.clusterCount}`);
|
|
359
|
-
console.log(`Magnitude: ${fingerprint.diffMagnitude}`);
|
|
360
|
-
}
|
|
131
|
+
console.log(result.perceptualScore);
|
|
132
|
+
console.log(result.gmsdScore);
|
|
361
133
|
```
|
|
362
134
|
|
|
363
|
-
|
|
364
|
-
```javascript
|
|
365
|
-
// Group diffs by hash for batch approval
|
|
366
|
-
let groups = new Map();
|
|
367
|
-
|
|
368
|
-
for (let comparison of comparisons) {
|
|
369
|
-
let fp = computeFingerprintSync(comparison.result, width, height);
|
|
370
|
-
if (fp) {
|
|
371
|
-
let hash = fingerprintHashSync(fp);
|
|
372
|
-
if (!groups.has(hash)) groups.set(hash, []);
|
|
373
|
-
groups.get(hash).push(comparison);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
135
|
+
### Screenshot Contrast
|
|
376
136
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (items.length > 1) {
|
|
380
|
-
console.log(`Found ${items.length} similar diffs with hash ${hash}`);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
```
|
|
137
|
+
```js
|
|
138
|
+
import { analyzeWcagContrast } from '@vizzly-testing/honeydiff';
|
|
384
139
|
|
|
385
|
-
|
|
386
|
-
```javascript
|
|
387
|
-
// Compare two fingerprints (0.0 = different, 1.0 = identical)
|
|
388
|
-
let similarity = fingerprintSimilaritySync(fp1, fp2);
|
|
140
|
+
let report = await analyzeWcagContrast('screenshot.png');
|
|
389
141
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
142
|
+
console.log(report.violations.length);
|
|
143
|
+
console.log(report.aaNormalPassPercentage);
|
|
144
|
+
console.log(report.violations);
|
|
393
145
|
```
|
|
394
146
|
|
|
395
|
-
|
|
396
|
-
- `clusterCount` - Number of distinct change regions
|
|
397
|
-
- `clusterPositions` - Normalized positions (0.0-1.0)
|
|
398
|
-
- `clusterSizes` - Relative sizes (area ratio)
|
|
399
|
-
- `zoneMask` - 4x4 grid (16 zones) showing affected regions
|
|
400
|
-
- `diffMagnitude` - Bucketed size: `tiny`, `small`, `medium`, `large`, `massive`
|
|
401
|
-
- `hash` - Pre-computed coarse hash for fast grouping
|
|
402
|
-
|
|
403
|
-
## Accessibility Features
|
|
404
|
-
|
|
405
|
-
Built-in WCAG color contrast analysis and color blindness simulation to catch accessibility violations in screenshots and UI designs.
|
|
406
|
-
|
|
407
|
-
### WCAG Contrast Analysis
|
|
147
|
+
### Simulate Color Vision Deficiency
|
|
408
148
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
```javascript
|
|
412
|
-
import { analyzeWcagContrast, saveWcagOverlay } from '@vizzly-testing/honeydiff';
|
|
413
|
-
|
|
414
|
-
// Analyze a screenshot for accessibility violations
|
|
415
|
-
let analysis = await analyzeWcagContrast('screenshot.png', {
|
|
416
|
-
edgeThreshold: 60, // Edge detection sensitivity (0-255)
|
|
417
|
-
minRegionSize: 50, // Minimum violation region size (pixels)
|
|
418
|
-
checkAA: true, // Check WCAG AA (4.5:1 normal, 3.0:1 large)
|
|
419
|
-
checkAAA: true // Check WCAG AAA (7.0:1 normal, 4.5:1 large)
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
console.log(`Total edges analyzed: ${analysis.totalEdges}`);
|
|
423
|
-
console.log(`AA normal text pass rate: ${analysis.aaNormalPassPercentage.toFixed(1)}%`);
|
|
424
|
-
console.log(`Found ${analysis.violations.length} violations`);
|
|
425
|
-
|
|
426
|
-
// Examine violations
|
|
427
|
-
analysis.violations.forEach((violation, i) => {
|
|
428
|
-
console.log(`\nViolation ${i + 1}:`);
|
|
429
|
-
console.log(` Location: (${violation.boundingBox.x}, ${violation.boundingBox.y})`);
|
|
430
|
-
console.log(` Contrast ratio: ${violation.contrastRatio.toFixed(2)}:1`);
|
|
431
|
-
console.log(` Foreground: rgb(${violation.foregroundColor.slice(0,3).join(', ')})`);
|
|
432
|
-
console.log(` Background: rgb(${violation.backgroundColor.slice(0,3).join(', ')})`);
|
|
433
|
-
console.log(` Fails AA normal: ${violation.failsAaNormal}`);
|
|
434
|
-
console.log(` Fails AAA normal: ${violation.failsAaaNormal}`);
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
// Generate visual overlay showing violations
|
|
438
|
-
await saveWcagOverlay('screenshot.png', analysis, 'violations.png', {
|
|
439
|
-
highlightColor: [255, 0, 0, 180] // Semi-transparent red
|
|
440
|
-
});
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
**Advanced filtering:**
|
|
444
|
-
```javascript
|
|
445
|
-
let analysis = await analyzeWcagContrast('screenshot.png', {
|
|
446
|
-
edgeThreshold: 80, // Higher = fewer false positives
|
|
447
|
-
minRegionSize: 100, // Ignore tiny regions
|
|
448
|
-
maxContrastThreshold: 3.5, // Only flag severe violations
|
|
449
|
-
checkAA: true,
|
|
450
|
-
checkAAA: false
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
console.log(`AA pass rate: ${analysis.aaNormalPassPercentage.toFixed(1)}%`);
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
### Color Blindness Simulation
|
|
457
|
-
|
|
458
|
-
Simulate how your UI appears to users with color vision deficiencies. Uses the scientifically accurate Brettel, Viénot & Mollon 1997 algorithm.
|
|
459
|
-
|
|
460
|
-
**Supported CVD types:**
|
|
461
|
-
- `protanopia` - Red-blind (~1% of males)
|
|
462
|
-
- `deuteranopia` - Green-blind (~1% of males, most common)
|
|
463
|
-
- `tritanopia` - Blue-blind (~0.003% of population)
|
|
464
|
-
- `achromatopsia` - Complete color blindness (grayscale only)
|
|
465
|
-
|
|
466
|
-
```javascript
|
|
149
|
+
```js
|
|
467
150
|
import {
|
|
468
|
-
simulateColorBlindness,
|
|
469
151
|
saveColorBlindnessSimulation,
|
|
470
|
-
saveAllColorBlindnessSimulations,
|
|
471
|
-
getColorBlindnessTypes
|
|
472
152
|
} from '@vizzly-testing/honeydiff';
|
|
473
153
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
checkAA: true,
|
|
503
|
-
checkAAA: false
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
if (analysis.violations.length > 0) {
|
|
507
|
-
console.log(`Found ${analysis.violations.length} contrast issues for red-green colorblind users`);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Comprehensive report for all CVD types
|
|
511
|
-
let report = await analyzeWcagAllCvd('dashboard.png', {
|
|
512
|
-
checkAA: true,
|
|
513
|
-
checkAAA: true
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
console.log('Accessibility Report:');
|
|
517
|
-
console.log(`Normal vision: ${report.normalVision.violations.length} violations`);
|
|
518
|
-
console.log(`Protanopia: ${report.protanopia.violations.length} violations`);
|
|
519
|
-
console.log(`Deuteranopia: ${report.deuteranopia.violations.length} violations`);
|
|
520
|
-
console.log(`Tritanopia: ${report.tritanopia.violations.length} violations`);
|
|
521
|
-
console.log(`Total: ${report.totalViolations} violations`);
|
|
522
|
-
console.log(`CVD-only issues: ~${report.cvdOnlyViolationCount}`);
|
|
523
|
-
|
|
524
|
-
if (report.hasAnyViolations) {
|
|
525
|
-
console.log('Accessibility issues detected!');
|
|
526
|
-
}
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
**Test integration:**
|
|
530
|
-
```javascript
|
|
531
|
-
test('UI colors work for colorblind users', async () => {
|
|
532
|
-
let screenshot = await page.screenshot();
|
|
533
|
-
|
|
534
|
-
// Comprehensive CVD accessibility check
|
|
535
|
-
let report = await analyzeWcagAllCvd(screenshot, { checkAA: true });
|
|
536
|
-
|
|
537
|
-
expect(report.hasAnyViolations).toBe(false);
|
|
538
|
-
expect(report.deuteranopia.aaNormalPassPercentage).toBeGreaterThan(95);
|
|
539
|
-
});
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
## Complete API Reference
|
|
543
|
-
|
|
544
|
-
### Async Functions (Recommended)
|
|
545
|
-
|
|
546
|
-
**`compare(img1, img2, options?): Promise<DiffResult>`**
|
|
547
|
-
|
|
548
|
-
Full comparison with detailed results.
|
|
549
|
-
|
|
550
|
-
**`quickCompare(img1, img2): Promise<boolean>`**
|
|
551
|
-
|
|
552
|
-
Fast boolean check - returns true if different, false if identical.
|
|
553
|
-
|
|
554
|
-
**`getDimensions(img): Promise<{ width: number, height: number }>`**
|
|
555
|
-
|
|
556
|
-
Get image dimensions without loading full image data.
|
|
557
|
-
|
|
558
|
-
**`getImageMetadata(buffer): Promise<ImageMetadata>`**
|
|
559
|
-
|
|
560
|
-
Get image metadata (dimensions, file size, format) from a buffer.
|
|
561
|
-
|
|
562
|
-
**`getImageMetadataFromFile(path): Promise<ImageMetadata>`**
|
|
563
|
-
|
|
564
|
-
Get image metadata from a file path. File size is read from disk for accuracy.
|
|
565
|
-
|
|
566
|
-
### Sync Functions (Blocks Event Loop)
|
|
567
|
-
|
|
568
|
-
**`compareSync(img1, img2, options?): DiffResult`**
|
|
569
|
-
|
|
570
|
-
Synchronous comparison. Use only when blocking behavior is required.
|
|
571
|
-
|
|
572
|
-
**`quickCompareSync(img1, img2): boolean`**
|
|
573
|
-
|
|
574
|
-
Synchronous boolean check.
|
|
575
|
-
|
|
576
|
-
**`getDimensionsSync(img): { width: number, height: number }`**
|
|
577
|
-
|
|
578
|
-
Synchronous dimension retrieval.
|
|
579
|
-
|
|
580
|
-
**`getImageMetadataSync(buffer): ImageMetadata`**
|
|
581
|
-
|
|
582
|
-
Synchronous metadata retrieval from buffer.
|
|
583
|
-
|
|
584
|
-
**`getImageMetadataFromFileSync(path): ImageMetadata`**
|
|
585
|
-
|
|
586
|
-
Synchronous metadata retrieval from file.
|
|
587
|
-
|
|
588
|
-
### Diff Fingerprint Functions (Sync only)
|
|
589
|
-
|
|
590
|
-
**`computeFingerprintSync(diffResult, width, height): DiffFingerprint | null`**
|
|
591
|
-
|
|
592
|
-
Compute a fingerprint from a diff result. Requires `includeClusters: true` in compare options. Returns null if no clusters.
|
|
593
|
-
|
|
594
|
-
**`fingerprintSimilaritySync(fp1, fp2): number`**
|
|
595
|
-
|
|
596
|
-
Compare two fingerprints. Returns 0.0 (different) to 1.0 (identical).
|
|
597
|
-
|
|
598
|
-
**`fingerprintHashSync(fingerprint): string`**
|
|
599
|
-
|
|
600
|
-
Get coarse hash for fast grouping. Returns 16-char hex string.
|
|
601
|
-
|
|
602
|
-
### Input Types
|
|
603
|
-
|
|
604
|
-
All functions accept:
|
|
605
|
-
- `string` - File path to PNG image
|
|
606
|
-
- `Buffer` - Image data in memory
|
|
607
|
-
|
|
608
|
-
### CompareOptions
|
|
609
|
-
|
|
610
|
-
```typescript
|
|
611
|
-
interface CompareOptions {
|
|
612
|
-
// Basic options
|
|
613
|
-
threshold?: number; // CIEDE2000 Delta E threshold (default: 2.0)
|
|
614
|
-
antialiasing?: boolean; // Ignore AA artifacts (default: true)
|
|
615
|
-
maxDiffs?: number; // Stop after N diffs (default: unlimited)
|
|
616
|
-
|
|
617
|
-
// Analysis options
|
|
618
|
-
includeDiffPixels?: boolean; // List all diff pixels (memory intensive, default: false)
|
|
619
|
-
includeClusters?: boolean; // Spatial clustering (default: false)
|
|
620
|
-
includeSSIM?: boolean; // SSIM perceptual score (slow, default: false)
|
|
621
|
-
includeGMSD?: boolean; // GMSD edge similarity (fast, default: false)
|
|
622
|
-
minClusterSize?: number; // Filter clusters smaller than this (default: 2)
|
|
623
|
-
clusterMerge?: boolean | { // Merge nearby clusters (default: false)
|
|
624
|
-
horizontalDistance?: number; // Max horizontal gap to merge (default: 15)
|
|
625
|
-
yBandTolerance?: number; // Vertical "same line" tolerance (default: 5)
|
|
626
|
-
maxHeightRatio?: number; // Max height ratio to merge (default: 2.0)
|
|
627
|
-
maxWidthRatio?: number; // Max width ratio to merge (default: 3.0)
|
|
628
|
-
};
|
|
629
|
-
|
|
630
|
-
// Output options
|
|
631
|
-
diffPath?: string; // Save diff image path
|
|
632
|
-
maskPath?: string; // Save mask image path
|
|
633
|
-
overlayPath?: string; // Save overlay image path
|
|
634
|
-
overwrite?: boolean; // Overwrite existing files (default: false)
|
|
635
|
-
diffMaskColor?: string | number[]; // Highlight color: hex or [r,g,b,a] (default: "ff0000")
|
|
636
|
-
}
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
### DiffResult
|
|
640
|
-
|
|
641
|
-
```typescript
|
|
154
|
+
await saveColorBlindnessSimulation(
|
|
155
|
+
'screenshot.png',
|
|
156
|
+
'deuteranopia',
|
|
157
|
+
'screenshot-deuteranopia.png'
|
|
158
|
+
);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Options
|
|
162
|
+
|
|
163
|
+
| Option | Default | Notes |
|
|
164
|
+
| --- | --- | --- |
|
|
165
|
+
| `threshold` | `2.0` | CIEDE2000 Delta E threshold. Use `0` for exact matching. |
|
|
166
|
+
| `antialiasing` | `true` | Ignore likely anti-aliased pixels. |
|
|
167
|
+
| `maxDiffs` | unlimited | Stop after a maximum number of differing pixels. |
|
|
168
|
+
| `includeDiffPixels` | `false` | Return individual differing pixel positions and intensities. |
|
|
169
|
+
| `includeClusters` | `false` | Return connected regions of visual change. |
|
|
170
|
+
| `minClusterSize` | `2` | Filter tiny isolated clusters as noise. |
|
|
171
|
+
| `clusterMerge` | disabled | Merge nearby clusters into logical text-like regions. |
|
|
172
|
+
| `includeSSIM` | `false` | Calculate structural similarity. More expensive on large images. |
|
|
173
|
+
| `includeGMSD` | `false` | Calculate fast edge-sensitive structural difference. |
|
|
174
|
+
| `diffPath` | unset | Save a highlighted diff image. |
|
|
175
|
+
| `maskPath` | unset | Save a binary diff mask. |
|
|
176
|
+
| `overlayPath` | unset | Save an overlay comparison image. |
|
|
177
|
+
| `overwrite` | `false` | Replace existing artifact files. |
|
|
178
|
+
|
|
179
|
+
## Result Shape
|
|
180
|
+
|
|
181
|
+
```ts
|
|
642
182
|
interface DiffResult {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
diffPercentage: number; // 0.0-100.0
|
|
183
|
+
isDifferent: boolean;
|
|
184
|
+
diffPercentage: number;
|
|
646
185
|
totalPixels: number;
|
|
647
|
-
diffPixels: number;
|
|
648
|
-
|
|
649
|
-
// Anti-aliasing
|
|
186
|
+
diffPixels: number;
|
|
650
187
|
aaPixelsIgnored: number;
|
|
651
|
-
aaPercentage: number;
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
diffClusters: DiffCluster[] | null; // Null unless includeClusters enabled
|
|
660
|
-
intensityStats: IntensityStats | null; // Null unless includeDiffPixels enabled
|
|
661
|
-
perceptualScore: number | null; // SSIM 0.0-1.0, null unless includeSSIM enabled
|
|
662
|
-
gmsdScore: number | null; // GMSD 0.0+, null unless includeGMSD enabled
|
|
188
|
+
aaPercentage: number;
|
|
189
|
+
boundingBox: BoundingBox | null;
|
|
190
|
+
heightDiff: HeightDiff | null;
|
|
191
|
+
diffPixelsList: DiffPixel[] | null;
|
|
192
|
+
diffClusters: DiffCluster[] | null;
|
|
193
|
+
intensityStats: IntensityStats | null;
|
|
194
|
+
perceptualScore: number | null;
|
|
195
|
+
gmsdScore: number | null;
|
|
663
196
|
}
|
|
664
197
|
```
|
|
665
198
|
|
|
666
|
-
See [index.d.ts](./index.d.ts) for
|
|
667
|
-
|
|
668
|
-
## Performance
|
|
199
|
+
See [index.d.ts](./index.d.ts) for the full API surface.
|
|
669
200
|
|
|
670
|
-
|
|
201
|
+
## Thresholds
|
|
671
202
|
|
|
672
|
-
|
|
673
|
-
|-----------|------|------------|
|
|
674
|
-
| Basic CIEDE2000 comparison | ~79ms | 231.8M pixels/sec |
|
|
675
|
-
| With anti-aliasing | ~88ms | 208M pixels/sec |
|
|
676
|
-
| With artifacts (diff/mask/overlay) | ~432ms | 42.4M pixels/sec |
|
|
203
|
+
Honeydiff uses CIEDE2000 Delta E for perceptual color difference.
|
|
677
204
|
|
|
678
|
-
|
|
205
|
+
| Threshold | Meaning |
|
|
206
|
+
| --- | --- |
|
|
207
|
+
| `0` | Exact pixel matching. |
|
|
208
|
+
| `1` | Barely noticeable color changes. |
|
|
209
|
+
| `2` | Recommended default for UI screenshots. |
|
|
210
|
+
| `3+` | More tolerant of rendering differences. |
|
|
679
211
|
|
|
680
|
-
|
|
212
|
+
The default is intentionally practical for browser and app screenshots: it
|
|
213
|
+
filters tiny rendering variance while still catching meaningful UI changes.
|
|
681
214
|
|
|
682
|
-
|
|
215
|
+
The cited algorithms and standards are listed in the repo's
|
|
216
|
+
[References](../../docs/REFERENCES.md), including CIEDE2000, SSIM, MS-SSIM,
|
|
217
|
+
GMSD, Brettel CVD simulation, sRGB, and WCAG contrast math.
|
|
683
218
|
|
|
684
|
-
##
|
|
219
|
+
## Performance
|
|
685
220
|
|
|
686
|
-
|
|
221
|
+
Current local benchmark snapshots:
|
|
687
222
|
|
|
688
|
-
|
|
689
|
-
|
|
223
|
+
| Scenario | Result |
|
|
224
|
+
| --- | ---: |
|
|
225
|
+
| Full HD default comparison | ~18.9ms |
|
|
226
|
+
| Full HD exact comparison | ~2.8ms |
|
|
227
|
+
| Identical image fast path | ~0.34ms |
|
|
228
|
+
| 4K default comparison | ~76.9ms |
|
|
229
|
+
| 4K exact comparison | ~14.0ms |
|
|
690
230
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
threshold: 10,
|
|
694
|
-
maxDiffs: 1000, // Early exit for fast failure
|
|
695
|
-
diffPath: process.env.CI ? './artifacts/diff.png' : undefined
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
if (result.isDifferent && result.diffPercentage > 1.0) {
|
|
699
|
-
console.error(`Visual regression: ${result.diffPercentage.toFixed(2)}% changed`);
|
|
700
|
-
process.exit(1);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
```
|
|
231
|
+
See [benchmarks/BENCHMARK_RESULTS.md](https://github.com/vizzly-testing/honeydiff/blob/main/benchmarks/BENCHMARK_RESULTS.md)
|
|
232
|
+
for the current benchmark notes.
|
|
704
233
|
|
|
705
|
-
|
|
234
|
+
## API Overview
|
|
706
235
|
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
236
|
+
```js
|
|
237
|
+
import {
|
|
238
|
+
analyzeWcagAllCvd,
|
|
239
|
+
analyzeWcagContrast,
|
|
240
|
+
analyzeWcagForCvd,
|
|
241
|
+
compare,
|
|
242
|
+
compareSync,
|
|
243
|
+
computeFingerprintSync,
|
|
244
|
+
fingerprintHashSync,
|
|
245
|
+
fingerprintSimilaritySync,
|
|
246
|
+
getColorBlindnessTypes,
|
|
247
|
+
getDimensions,
|
|
248
|
+
getDimensionsSync,
|
|
249
|
+
getImageMetadata,
|
|
250
|
+
getImageMetadataFromFile,
|
|
251
|
+
getImageMetadataFromFileSync,
|
|
252
|
+
getImageMetadataSync,
|
|
253
|
+
quickCompare,
|
|
254
|
+
quickCompareSync,
|
|
255
|
+
saveAllColorBlindnessSimulations,
|
|
256
|
+
saveColorBlindnessSimulation,
|
|
257
|
+
saveWcagOverlay,
|
|
258
|
+
simulateColorBlindness,
|
|
259
|
+
} from '@vizzly-testing/honeydiff';
|
|
720
260
|
```
|
|
721
261
|
|
|
722
|
-
|
|
262
|
+
## Development
|
|
723
263
|
|
|
724
|
-
```
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
if (result.diffPercentage > 5.0) {
|
|
730
|
-
console.error('FAIL: Major visual regression');
|
|
731
|
-
process.exit(1);
|
|
732
|
-
} else if (result.diffPercentage > 1.0) {
|
|
733
|
-
console.warn('WARN: Minor visual changes detected');
|
|
734
|
-
} else {
|
|
735
|
-
console.log('PASS: Visual test passed');
|
|
736
|
-
}
|
|
264
|
+
```bash
|
|
265
|
+
pnpm install
|
|
266
|
+
cargo build --release
|
|
267
|
+
cargo test
|
|
737
268
|
```
|
|
738
269
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
**Module not found errors**
|
|
742
|
-
- Ensure Node.js 22+ is installed
|
|
743
|
-
- Verify your platform is supported (macOS ARM64, Linux x64/ARM64, Windows x64)
|
|
744
|
-
- Try `npm install` again to download pre-built binaries
|
|
745
|
-
|
|
746
|
-
**Large memory usage with `includeDiffPixels`**
|
|
747
|
-
- This option stores every different pixel's position and intensity
|
|
748
|
-
- Disable for large images with many differences
|
|
749
|
-
- Only enable if you need detailed pixel-level data
|
|
750
|
-
|
|
751
|
-
**Slow performance with `includeSSIM`**
|
|
752
|
-
- SSIM calculation is O(width × height × window²)
|
|
753
|
-
- Only enable when perceptual scoring is needed
|
|
754
|
-
- Consider running SSIM only on failed comparisons
|
|
755
|
-
|
|
756
|
-
**Anti-aliasing too aggressive**
|
|
757
|
-
- Set `antialiasing: false` for stricter pixel matching
|
|
758
|
-
- Adjust `threshold` instead for controlled flexibility
|
|
759
|
-
|
|
760
|
-
## Use Cases
|
|
761
|
-
|
|
762
|
-
- **Visual Regression Testing** - Catch unintended UI changes in CI/CD
|
|
763
|
-
- **Screenshot Validation** - Compare generated screenshots against baselines
|
|
764
|
-
- **Accessibility Testing** - Validate WCAG contrast and color blindness visibility
|
|
765
|
-
- **Design QA** - Ensure implementations match design specifications
|
|
766
|
-
- **Cross-browser Testing** - Detect rendering differences across browsers
|
|
767
|
-
- **A/B Testing** - Measure visual differences between variants
|
|
768
|
-
- **Responsive Design** - Compare layouts across viewports
|
|
769
|
-
|
|
770
|
-
## Examples
|
|
771
|
-
|
|
772
|
-
See [examples/](./examples/) directory:
|
|
773
|
-
- [basic.js](./examples/basic.js) - Core features and comparison options
|
|
774
|
-
- [buffers.js](./examples/buffers.js) - Working with in-memory images
|
|
775
|
-
- [typescript.ts](./examples/typescript.ts) - TypeScript integration
|
|
776
|
-
- [wcag.js](./examples/wcag.js) - WCAG contrast analysis
|
|
777
|
-
- [color-blindness.js](./examples/color-blindness.js) - CVD simulation and accessibility
|
|
270
|
+
The package is ESM-first and release packages include native binaries under
|
|
271
|
+
`platforms/*.node`.
|
|
778
272
|
|
|
779
273
|
## License
|
|
780
274
|
|
|
781
|
-
MIT
|
|
782
|
-
|
|
783
|
-
Built with native Rust for maximum performance.
|
|
275
|
+
MIT
|