Skip to content

Commit a5ac7cc

Browse files
committed
probe looks better
1 parent 3713f0c commit a5ac7cc

1 file changed

Lines changed: 81 additions & 41 deletions

File tree

apps/probe-viewer/src/components/ProbeCanvas.tsx

Lines changed: 81 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)