Robert Crocker

Craft obsessed developer who designs.

← Back to the lab

01Distance Triangle

I · Interaction Studies

Pythagoras, live — the right triangle behind every cursor distance.

trace the hypotenuse

Hover

Source

2 files
"use client";

import { useCallback, useEffect, useRef, useState } from "react";

import { distanceBetween, type Point } from "./math";

const TICK = 8;

export function DistanceTriangleExhibit() {
  const canvasRef = useRef<HTMLDivElement>(null);
  const [cursor, setCursor] = useState<Point | null>(null);
  const [center, setCenter] = useState<Point>({ x: 0, y: 0 });

  const updateCenter = useCallback(() => {
    const el = canvasRef.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    setCenter({ x: rect.width / 2, y: rect.height / 2 });
  }, []);

  useEffect(() => {
    updateCenter();
    const el = canvasRef.current;
    if (!el) return;
    const ro = new ResizeObserver(updateCenter);
    ro.observe(el);
    return () => ro.disconnect();
  }, [updateCenter]);

  const handlePointerMove = useCallback((e: React.PointerEvent) => {
    const el = canvasRef.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    setCursor({ x: e.clientX - rect.left, y: e.clientY - rect.top });
  }, []);

  const deltaX = cursor ? Math.round(cursor.x - center.x) : 0;
  const deltaY = cursor ? Math.round(cursor.y - center.y) : 0;
  const distance = cursor ? Math.round(distanceBetween(cursor, center)) : 0;
  const corner: Point = cursor
    ? { x: cursor.x, y: center.y }
    : { x: center.x, y: center.y };

  return (
    <div
      ref={canvasRef}
      onPointerMove={handlePointerMove}
      onPointerLeave={() => setCursor(null)}
      className="absolute inset-0 cursor-crosshair bg-[radial-gradient(hsl(var(--muted-foreground)/0.12)_1px,transparent_1px)] [background-size:24px_24px]"
    >
      <svg className="absolute inset-0 size-full overflow-visible">
        <circle cx={center.x} cy={center.y} r={4} className="fill-foreground" />

        {cursor && (
          <>
            {/* deltaX leg */}
            <line
              x1={center.x}
              y1={center.y}
              x2={corner.x}
              y2={corner.y}
              strokeWidth={1.5}
              strokeDasharray="5 4"
              className="stroke-muted-foreground/50"
            />
            {/* deltaY leg */}
            <line
              x1={corner.x}
              y1={corner.y}
              x2={cursor.x}
              y2={cursor.y}
              strokeWidth={1.5}
              strokeDasharray="5 4"
              className="stroke-muted-foreground/50"
            />
            {/* hypotenuse */}
            <line
              x1={center.x}
              y1={center.y}
              x2={cursor.x}
              y2={cursor.y}
              strokeWidth={2}
              className="stroke-primary"
            />
            {/* right-angle tick */}
            <polyline
              points={`
                ${corner.x + (deltaX >= 0 ? -TICK : TICK)},${corner.y}
                ${corner.x + (deltaX >= 0 ? -TICK : TICK)},${corner.y + (deltaY >= 0 ? TICK : -TICK)}
                ${corner.x},${corner.y + (deltaY >= 0 ? TICK : -TICK)}
              `}
              fill="none"
              strokeWidth={1.5}
              className="stroke-muted-foreground/40"
            />
            <circle cx={cursor.x} cy={cursor.y} r={4} className="fill-primary" />
          </>
        )}
      </svg>

      {/* Readout — fixed position, no layout shift */}
      <p className="absolute bottom-3 left-1/2 -translate-x-1/2 whitespace-nowrap font-mono text-[11px] tracking-wide text-muted-foreground tabular-nums">
        {cursor
          ? `√(${Math.abs(deltaX)}² + ${Math.abs(deltaY)}²) = ${distance}px`
          : "trace the hypotenuse"}
      </p>
    </div>
  );
}

This study grew into a long-form write-up — read the essay.