@@ -2,6 +2,7 @@ import p5 from '../../../src/app.js';
2
2
import { server } from '@vitest/browser/context'
3
3
import { THRESHOLD , DIFFERENCE , ERODE } from '../../../src/core/constants.js' ;
4
4
const { readFile, writeFile } = server . commands
5
+ import pixelmatch from 'pixelmatch' ;
5
6
6
7
// By how much can each color channel value (0-255) differ before
7
8
// we call it a mismatch? This should be large enough to not trigger
@@ -88,55 +89,167 @@ export function visualSuite(
88
89
89
90
export async function checkMatch ( actual , expected , p5 ) {
90
91
let scale = Math . min ( MAX_SIDE / expected . width , MAX_SIDE / expected . height ) ;
91
-
92
- // Long screenshots end up super tiny when fit to a small square, so we
93
- // can double the max side length for these
94
92
const ratio = expected . width / expected . height ;
95
93
const narrow = ratio !== 1 ;
96
94
if ( narrow ) {
97
95
scale *= 2 ;
98
96
}
99
-
97
+
100
98
for ( const img of [ actual , expected ] ) {
101
99
img . resize (
102
100
Math . ceil ( img . width * scale ) ,
103
101
Math . ceil ( img . height * scale )
104
102
) ;
105
103
}
106
104
107
- const expectedWithBg = p5 . createGraphics ( expected . width , expected . height ) ;
108
- expectedWithBg . pixelDensity ( 1 ) ;
109
- expectedWithBg . background ( BG ) ;
110
- expectedWithBg . image ( expected , 0 , 0 ) ;
111
-
112
- const cnv = p5 . createGraphics ( actual . width , actual . height ) ;
113
- cnv . pixelDensity ( 1 ) ;
114
- cnv . background ( BG ) ;
115
- cnv . image ( actual , 0 , 0 ) ;
116
- cnv . blendMode ( DIFFERENCE ) ;
117
- cnv . image ( expectedWithBg , 0 , 0 ) ;
118
- for ( let i = 0 ; i < shiftThreshold ; i ++ ) {
119
- cnv . filter ( ERODE , false ) ;
105
+ // Ensure both images have the same dimensions
106
+ const width = expected . width ;
107
+ const height = expected . height ;
108
+
109
+ // Create canvases with background color
110
+ const actualCanvas = p5 . createGraphics ( width , height ) ;
111
+ const expectedCanvas = p5 . createGraphics ( width , height ) ;
112
+ actualCanvas . pixelDensity ( 1 ) ;
113
+ expectedCanvas . pixelDensity ( 1 ) ;
114
+
115
+ actualCanvas . background ( BG ) ;
116
+ expectedCanvas . background ( BG ) ;
117
+
118
+ actualCanvas . image ( actual , 0 , 0 ) ;
119
+ expectedCanvas . image ( expected , 0 , 0 ) ;
120
+
121
+ // Load pixel data
122
+ actualCanvas . loadPixels ( ) ;
123
+ expectedCanvas . loadPixels ( ) ;
124
+
125
+ // Create diff output canvas
126
+ const diffCanvas = p5 . createGraphics ( width , height ) ;
127
+ diffCanvas . pixelDensity ( 1 ) ;
128
+ diffCanvas . loadPixels ( ) ;
129
+
130
+ // Run pixelmatch
131
+ const diffCount = pixelmatch (
132
+ actualCanvas . pixels ,
133
+ expectedCanvas . pixels ,
134
+ diffCanvas . pixels ,
135
+ width ,
136
+ height ,
137
+ {
138
+ threshold : 0.3 ,
139
+ includeAA : false ,
140
+ alpha : 0.1
141
+ }
142
+ ) ;
143
+
144
+ // If no differences, return early
145
+ if ( diffCount === 0 ) {
146
+ actualCanvas . remove ( ) ;
147
+ expectedCanvas . remove ( ) ;
148
+ diffCanvas . updatePixels ( ) ;
149
+ return { ok : true , diff : diffCanvas } ;
120
150
}
121
- const diff = cnv . get ( ) ;
122
- cnv . remove ( ) ;
123
- diff . loadPixels ( ) ;
124
- expectedWithBg . remove ( ) ;
151
+
152
+ // Post-process to identify and filter out isolated differences
153
+ const visited = new Set ( ) ;
154
+ const clusterSizes = [ ] ;
155
+
156
+ for ( let y = 0 ; y < height ; y ++ ) {
157
+ for ( let x = 0 ; x < width ; x ++ ) {
158
+ const pos = ( y * width + x ) * 4 ;
159
+
160
+ // If this is a diff pixel (red in pixelmatch output) and not yet visited
161
+ if (
162
+ diffCanvas . pixels [ pos ] === 255 &&
163
+ diffCanvas . pixels [ pos + 1 ] === 0 &&
164
+ diffCanvas . pixels [ pos + 2 ] === 0 &&
165
+ ! visited . has ( pos )
166
+ ) {
167
+ // Find the connected cluster size using BFS
168
+ const clusterSize = findClusterSize ( diffCanvas . pixels , x , y , width , height , 1 , visited ) ;
169
+ clusterSizes . push ( clusterSize ) ;
170
+ }
171
+ }
172
+ }
173
+
174
+ // Define significance thresholds
175
+ const MIN_CLUSTER_SIZE = 4 ; // Minimum pixels in a significant cluster
176
+ const MAX_TOTAL_DIFF_PIXELS = 40 ; // Maximum total different pixels
177
+
178
+ // Determine if the differences are significant
179
+ const significantClusters = clusterSizes . filter ( size => size >= MIN_CLUSTER_SIZE ) ;
180
+ const significantDiffPixels = significantClusters . reduce ( ( sum , size ) => sum + size , 0 ) ;
125
181
126
- let ok = true ;
127
- for ( let i = 0 ; i < diff . pixels . length ; i += 4 ) {
128
- let diffSum = 0 ;
129
- for ( let off = 0 ; off < 3 ; off ++ ) {
130
- diffSum += diff . pixels [ i + off ]
182
+ // Update the diff canvas
183
+ diffCanvas . updatePixels ( ) ;
184
+
185
+ // Clean up canvases
186
+ actualCanvas . remove ( ) ;
187
+ expectedCanvas . remove ( ) ;
188
+
189
+ // Determine test result
190
+ const ok = (
191
+ diffCount === 0 || // No differences at all
192
+ (
193
+ significantDiffPixels === 0 || // No significant clusters
194
+ (
195
+ significantDiffPixels <= MAX_TOTAL_DIFF_PIXELS && // Total different pixels within tolerance
196
+ significantClusters . length <= 2 // Not too many significant clusters
197
+ )
198
+ )
199
+ ) ;
200
+
201
+ return {
202
+ ok,
203
+ diff : diffCanvas ,
204
+ details : {
205
+ totalDiffPixels : diffCount ,
206
+ significantDiffPixels,
207
+ clusters : clusterSizes ,
208
+ significantClusters
131
209
}
132
- diffSum /= 3 ;
133
- if ( diffSum > COLOR_THRESHOLD ) {
134
- ok = false ;
135
- break ;
210
+ } ;
211
+ }
212
+
213
+ /**
214
+ * Find the size of a connected cluster of diff pixels using BFS
215
+ */
216
+ function findClusterSize ( pixels , startX , startY , width , height , radius , visited ) {
217
+ const queue = [ { x : startX , y : startY } ] ;
218
+ let size = 0 ;
219
+
220
+ while ( queue . length > 0 ) {
221
+ const { x, y} = queue . shift ( ) ;
222
+ const pos = ( y * width + x ) * 4 ;
223
+
224
+ // Skip if already visited
225
+ if ( visited . has ( pos ) ) continue ;
226
+
227
+ // Skip if not a diff pixel
228
+ if ( pixels [ pos ] !== 255 || pixels [ pos + 1 ] !== 0 || pixels [ pos + 2 ] !== 0 ) continue ;
229
+
230
+ // Mark as visited
231
+ visited . add ( pos ) ;
232
+ size ++ ;
233
+
234
+ // Add neighbors to queue
235
+ for ( let dy = - radius ; dy <= radius ; dy ++ ) {
236
+ for ( let dx = - radius ; dx <= radius ; dx ++ ) {
237
+ const nx = x + dx ;
238
+ const ny = y + dy ;
239
+
240
+ // Skip if out of bounds
241
+ if ( nx < 0 || nx >= width || ny < 0 || ny >= height ) continue ;
242
+
243
+ // Skip if already visited
244
+ const npos = ( ny * width + nx ) * 4 ;
245
+ if ( ! visited . has ( npos ) ) {
246
+ queue . push ( { x : nx , y : ny } ) ;
247
+ }
248
+ }
136
249
}
137
250
}
138
-
139
- return { ok , diff } ;
251
+
252
+ return size ;
140
253
}
141
254
142
255
/**
0 commit comments