1+ import { Locator } from "@playwright/test" ;
2+
3+ export type Pixel = { x : number ; y : number } ;
4+
5+ export interface CanvasAnalysisResult {
6+ red : Array < { x : number ; y : number ; radius : number } > ;
7+ yellow : Array < { x : number ; y : number ; radius : number } > ;
8+ green : Array < { x : number ; y : number ; radius : number } > ;
9+ }
10+
11+ export async function analyzeCanvasWithLocator ( locator : Locator ) {
12+ const canvasHandle = await locator . evaluateHandle ( ( canvas ) => canvas as HTMLCanvasElement ) ;
13+ const canvasElement = await canvasHandle . asElement ( ) ;
14+ if ( ! canvasElement ) {
15+ throw new Error ( "Failed to retrieve canvas element" ) ;
16+ }
17+
18+ // Retrieve the original canvas width
19+ const originalCanvasWidth = await canvasElement . evaluate ( ( canvas ) => canvas . width ) ;
20+
21+ const result = await canvasElement . evaluate (
22+ ( canvas , originalWidth ) => {
23+ const ctx = canvas ?. getContext ( "2d" ) ;
24+ if ( ! ctx ) {
25+ throw new Error ( "Failed to get 2D context" ) ;
26+ }
27+
28+ const imageData = ctx . getImageData ( 0 , 0 , canvas . width , canvas . height ) ;
29+ const { data, width, height } = imageData ;
30+
31+ const scaleFactor = canvas . width / originalWidth ;
32+ const adjustedRadius = 3 / scaleFactor ;
33+ const adjustedMergeRadius = 10 / scaleFactor ;
34+
35+ type Pixel = { x : number ; y : number } ;
36+
37+ const redPixels : Pixel [ ] = [ ] ;
38+ const yellowPixels : Pixel [ ] = [ ] ;
39+ const greenPixels : Pixel [ ] = [ ] ;
40+
41+ const isRedPixel = ( r : number , g : number , b : number ) => r > 170 && g < 120 && b < 120 ;
42+ const isYellowPixel = ( r : number , g : number , b : number ) => r > 170 && g > 170 && b < 130 ;
43+ const isGreenPixel = ( r : number , g : number , b : number ) => g > 120 && g > r && g > b && r < 50 && b < 160 ;
44+
45+ for ( let y = 0 ; y < height ; y ++ ) {
46+ for ( let x = 0 ; x < width ; x ++ ) {
47+ const i = ( y * width + x ) * 4 ;
48+ const r = data [ i ] ;
49+ const g = data [ i + 1 ] ;
50+ const b = data [ i + 2 ] ;
51+
52+ if ( isRedPixel ( r , g , b ) ) redPixels . push ( { x, y } ) ;
53+ if ( isYellowPixel ( r , g , b ) ) yellowPixels . push ( { x, y } ) ;
54+ if ( isGreenPixel ( r , g , b ) ) greenPixels . push ( { x, y } ) ;
55+ }
56+ }
57+
58+ const clusterNodes = ( pixels : Pixel [ ] , radius : number ) : Pixel [ ] [ ] => {
59+ const visited = new Set < string > ( ) ;
60+ const clusters : Pixel [ ] [ ] = [ ] ;
61+
62+ pixels . forEach ( ( pixel ) => {
63+ const key = `${ pixel . x } ,${ pixel . y } ` ;
64+ if ( visited . has ( key ) ) return ;
65+
66+ const cluster : Pixel [ ] = [ ] ;
67+ const stack : Pixel [ ] = [ pixel ] ;
68+
69+ while ( stack . length > 0 ) {
70+ const current = stack . pop ( ) ! ;
71+ const currentKey = `${ current . x } ,${ current . y } ` ;
72+ if ( visited . has ( currentKey ) ) continue ;
73+
74+ visited . add ( currentKey ) ;
75+ cluster . push ( current ) ;
76+
77+ pixels . forEach ( ( neighbor ) => {
78+ const dist = Math . sqrt (
79+ ( current . x - neighbor . x ) ** 2 + ( current . y - neighbor . y ) ** 2
80+ ) ;
81+ if ( dist <= radius ) stack . push ( neighbor ) ;
82+ } ) ;
83+ }
84+
85+ clusters . push ( cluster ) ;
86+ } ) ;
87+
88+ return clusters ;
89+ } ;
90+
91+ const mergeCloseClusters = ( clusters : Pixel [ ] [ ] , mergeRadius : number ) : Pixel [ ] [ ] => {
92+ const mergedClusters : Pixel [ ] [ ] = [ ] ;
93+ const used = new Set < number > ( ) ;
94+
95+ for ( let i = 0 ; i < clusters . length ; i ++ ) {
96+ if ( used . has ( i ) ) continue ;
97+
98+ let merged = [ ...clusters [ i ] ] ;
99+ for ( let j = i + 1 ; j < clusters . length ; j ++ ) {
100+ if ( used . has ( j ) ) continue ;
101+
102+ const dist = Math . sqrt (
103+ ( merged [ 0 ] . x - clusters [ j ] [ 0 ] . x ) ** 2 +
104+ ( merged [ 0 ] . y - clusters [ j ] [ 0 ] . y ) ** 2
105+ ) ;
106+
107+ if ( dist <= mergeRadius ) {
108+ merged = merged . concat ( clusters [ j ] ) ;
109+ used . add ( j ) ;
110+ }
111+ }
112+
113+ mergedClusters . push ( merged ) ;
114+ used . add ( i ) ;
115+ }
116+
117+ return mergedClusters ;
118+ } ;
119+
120+ const redClusters = clusterNodes ( redPixels , adjustedRadius ) ;
121+ const yellowClusters = clusterNodes ( yellowPixels , adjustedRadius ) ;
122+ const greenClusters = clusterNodes ( greenPixels , adjustedRadius ) ;
123+
124+ const mergedGreenClusters = mergeCloseClusters ( greenClusters , adjustedMergeRadius ) ;
125+
126+ const filteredRedClusters = redClusters . filter ( ( cluster ) => cluster . length >= 5 ) ;
127+ const filteredYellowClusters = yellowClusters . filter ( ( cluster ) => cluster . length >= 5 ) ;
128+ const filteredGreenClusters = mergedGreenClusters . filter ( ( cluster ) => cluster . length >= 5 ) ;
129+
130+ const calculateRadius = ( cluster : Pixel [ ] , scaleFactor : number ) => {
131+ const rawRadius = Math . sqrt ( cluster . length / Math . PI ) / scaleFactor ;
132+ return Math . round ( rawRadius * 1000 ) / 1000 ;
133+ } ;
134+
135+ return {
136+ red : filteredRedClusters . map ( cluster => ( {
137+ x : cluster . reduce ( ( sum , p ) => sum + p . x , 0 ) / cluster . length ,
138+ y : cluster . reduce ( ( sum , p ) => sum + p . y , 0 ) / cluster . length ,
139+ radius : calculateRadius ( cluster , scaleFactor )
140+ } ) ) ,
141+ yellow : filteredYellowClusters . map ( cluster => ( {
142+ x : cluster . reduce ( ( sum , p ) => sum + p . x , 0 ) / cluster . length ,
143+ y : cluster . reduce ( ( sum , p ) => sum + p . y , 0 ) / cluster . length ,
144+ radius : calculateRadius ( cluster , scaleFactor )
145+ } ) ) ,
146+ green : filteredGreenClusters . map ( cluster => ( {
147+ x : cluster . reduce ( ( sum , p ) => sum + p . x , 0 ) / cluster . length ,
148+ y : cluster . reduce ( ( sum , p ) => sum + p . y , 0 ) / cluster . length ,
149+ radius : calculateRadius ( cluster , scaleFactor )
150+ } ) )
151+ } ;
152+ } ,
153+ originalCanvasWidth
154+ ) ;
155+
156+ await canvasHandle . dispose ( ) ;
157+ return result ;
158+ }
0 commit comments