@@ -226,73 +226,113 @@ export const ProbeCanvas = forwardRef<HTMLCanvasElement, ProbeCanvasProps>(
226226 const contactShapes = probe . contact_shapes ?? [ ] ;
227227 const contactShapeParams = probe . contact_shape_params ?? [ ] ;
228228
229- // Helper to draw a contact shape
230- const drawContactShape = (
231- x : number ,
232- y : number ,
233- shape : string ,
234- params : ContactShapeParams ,
235- ) => {
236- ctx . beginPath ( ) ;
229+ // A contact's pixel dimensions, used both to size the metallic gradient and
230+ // to skip the sheen on pads too small for it to register.
231+ const contactDims = ( shape : string , params : ContactShapeParams ) => {
237232 switch ( shape ) {
238233 case "circle" : {
239- const radius = ( params . radius ?? 5 ) * scale ;
240- ctx . arc ( x , y , radius , 0 , Math . PI * 2 ) ;
241- break ;
234+ const d = ( params . radius ?? 5 ) * 2 * scale ;
235+ return { w : d , h : d , minPx : d , gradient : true } ;
242236 }
243237 case "square" : {
244- const side = ( params . width ?? 10 ) * scale ;
245- ctx . rect ( x - side / 2 , y - side / 2 , side , side ) ;
246- break ;
238+ const s = ( params . width ?? 10 ) * scale ;
239+ return { w : s , h : s , minPx : s , gradient : true } ;
247240 }
248241 case "rect" : {
249242 const w = ( params . width ?? 10 ) * scale ;
250243 const h = ( params . height ?? 15 ) * scale ;
251- ctx . rect ( x - w / 2 , y - h / 2 , w , h ) ;
244+ return { w, h, minPx : Math . min ( w , h ) , gradient : true } ;
245+ }
246+ default :
247+ return { w : 0 , h : 0 , minPx : 0 , gradient : false } ;
248+ }
249+ } ;
250+
251+ // Draws one contact path centered on the current origin (callers translate
252+ // the context to the pad position first). Rectangular pads get lightly
253+ // rounded corners so they read as real electrode pads, not hard tiles.
254+ const drawContactShape = ( shape : string , params : ContactShapeParams ) => {
255+ ctx . beginPath ( ) ;
256+ switch ( shape ) {
257+ case "circle" : {
258+ const radius = ( params . radius ?? 5 ) * scale ;
259+ ctx . arc ( 0 , 0 , radius , 0 , Math . PI * 2 ) ;
260+ break ;
261+ }
262+ case "square" :
263+ case "rect" : {
264+ const w = ( params . width ?? 10 ) * scale ;
265+ const h = ( shape === "square" ? ( params . width ?? 10 ) : ( params . height ?? 15 ) ) * scale ;
266+ const r = Math . min ( w , h ) * 0.18 ;
267+ if ( typeof ctx . roundRect === "function" ) {
268+ ctx . roundRect ( - w / 2 , - h / 2 , w , h , r ) ;
269+ } else {
270+ ctx . rect ( - w / 2 , - h / 2 , w , h ) ;
271+ }
252272 break ;
253273 }
254274 default : {
255- // Unknown/missing shape: draw a dot with X to indicate missing data
275+ // Unknown/missing shape: a dot with an X to flag missing data.
256276 const markerSize = Math . max ( 3 , Math . min ( 10 , 7 * ( scale / 100 ) ) ) ;
257- // Draw small circle
258- ctx . arc ( x , y , markerSize * 0.4 , 0 , Math . PI * 2 ) ;
259- ctx . closePath ( ) ;
260- // Draw X through the center
261- ctx . moveTo ( x - markerSize , y - markerSize ) ;
262- ctx . lineTo ( x + markerSize , y + markerSize ) ;
263- ctx . moveTo ( x + markerSize , y - markerSize ) ;
264- ctx . lineTo ( x - markerSize , y + markerSize ) ;
277+ ctx . arc ( 0 , 0 , markerSize * 0.4 , 0 , Math . PI * 2 ) ;
278+ ctx . moveTo ( - markerSize , - markerSize ) ;
279+ ctx . lineTo ( markerSize , markerSize ) ;
280+ ctx . moveTo ( markerSize , - markerSize ) ;
281+ ctx . lineTo ( - markerSize , markerSize ) ;
265282 }
266283 }
267284 } ;
268285
269- // Shadow offset for depth effect - subtle, proportional to scale
270- const shadowOffset = 0.4 * scale ; // 0.4 micrometer offset for subtle depth
286+ // One metallic gold gradient per distinct pad size per frame. Contacts are
287+ // usually uniform, so this is built once and reused across all of them.
288+ const gradientCache = new Map < string , CanvasGradient > ( ) ;
289+ const goldGradient = ( w : number , h : number ) => {
290+ const key = `${ Math . round ( w ) } x${ Math . round ( h ) } ` ;
291+ let g = gradientCache . get ( key ) ;
292+ if ( ! g ) {
293+ g = ctx . createLinearGradient ( - w / 2 , - h / 2 , w / 2 , h / 2 ) ;
294+ g . addColorStop ( 0 , "rgba(248, 228, 156, 1)" ) ; // warm highlight (top-left)
295+ g . addColorStop ( 0.45 , "rgba(212, 175, 55, 1)" ) ; // gold body
296+ g . addColorStop ( 1 , "rgba(146, 108, 28, 1)" ) ; // deep bronze (bottom-right)
297+ gradientCache . set ( key , g ) ;
298+ }
299+ return g ;
300+ } ;
271301
272- // First pass: draw shadows (offset dark shapes)
273302 contactPositions . forEach ( ( position , index ) => {
274303 const [ x , y ] = projectPoint ( position ) ;
275304 const shape = contactShapes [ index ] ?? "" ;
276305 const params = contactShapeParams [ index ] ?? { } ;
306+ const dims = contactDims ( shape , params ) ;
277307
278- drawContactShape ( x + shadowOffset , y + shadowOffset , shape , params ) ;
279- ctx . fillStyle = "rgba(30, 20, 5, 0.7)" ; // Even darker and more opaque
308+ ctx . save ( ) ;
309+ ctx . translate ( x , y ) ;
310+ drawContactShape ( shape , params ) ;
311+
312+ // Metallic sheen when the pad is big enough to show it; below that a flat
313+ // gold that looks identical at that size but is cheaper.
314+ ctx . fillStyle =
315+ dims . gradient && dims . minPx >= 5
316+ ? goldGradient ( dims . w , dims . h )
317+ : "rgba(212, 175, 55, 1)" ;
318+
319+ // A soft, capped shadow lifts the pad off the silver shank without the
320+ // hard offset double-image the previous two-pass approach produced.
321+ ctx . shadowColor = "rgba(15, 12, 4, 0.35)" ;
322+ ctx . shadowBlur = Math . min ( 5 , Math . max ( 1.5 , dims . minPx * 0.12 ) ) ;
323+ ctx . shadowOffsetX = 0 ;
324+ ctx . shadowOffsetY = Math . min ( 2.5 , Math . max ( 0.4 , dims . minPx * 0.06 ) ) ;
280325 ctx . fill ( ) ;
281- } ) ;
282326
283- // Second pass: draw gold contacts on top
284- contactPositions . forEach ( ( position , index ) => {
285- const [ x , y ] = projectPoint ( position ) ;
286- const shape = contactShapes [ index ] ?? "" ;
287- const params = contactShapeParams [ index ] ?? { } ;
327+ // Clear the shadow before the rim so the outline stays crisp.
328+ ctx . shadowColor = "transparent" ;
329+ ctx . shadowBlur = 0 ;
330+ ctx . shadowOffsetY = 0 ;
288331
289- drawContactShape ( x , y , shape , params ) ;
290-
291- ctx . fillStyle = "rgba(212, 175, 55, 1.0)" ; // Gold contacts - fully opaque to cover shadow
292- ctx . strokeStyle = "rgba(80, 60, 15, 0.9)" ; // Dark bronze outline
293- ctx . lineWidth = Math . max ( 1.2 , 2.5 * ( scale / 150 ) ) ;
294- ctx . fill ( ) ;
332+ ctx . lineWidth = Math . min ( 2 , Math . max ( 0.8 , dims . minPx * 0.03 ) ) ;
333+ ctx . strokeStyle = "rgba(110, 80, 25, 0.85)" ;
295334 ctx . stroke ( ) ;
335+ ctx . restore ( ) ;
296336 } ) ;
297337
298338 if ( showContactIds && probe . contact_ids && idLabelInfo ) {
0 commit comments