Robert Crocker

Craft obsessed developer who designs.

← Back to the lab

05Transform Play

I · Interaction Studies

Pointer position drives rotation and scale through a single transform.

rotate(  0deg) scale(1.00)

Hover

Source

2 files
"use client";

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

import { clampedNormalize } from "./math";

export function TransformExhibit() {
  const canvasRef = useRef<HTMLDivElement>(null);
  const [rotate, setRotate] = useState(0);
  const [scale, setScale] = useState(1);
  const [active, setActive] = useState(false);

  const handlePointerMove = useCallback((e: React.PointerEvent) => {
    const el = canvasRef.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    setActive(true);
    // x position → rotation, y position → scale
    setRotate(clampedNormalize(e.clientX - rect.left, 0, rect.width, -24, 24));
    setScale(clampedNormalize(e.clientY - rect.top, 0, rect.height, 1.25, 0.75));
  }, []);

  const handlePointerLeave = useCallback(() => {
    setActive(false);
    setRotate(0);
    setScale(1);
  }, []);

  return (
    <div
      ref={canvasRef}
      onPointerMove={handlePointerMove}
      onPointerLeave={handlePointerLeave}
      className="absolute inset-0 flex cursor-crosshair items-center justify-center bg-[radial-gradient(hsl(var(--muted-foreground)/0.12)_1px,transparent_1px)] [background-size:24px_24px]"
    >
      {/* Ghost of the identity state */}
      <div
        aria-hidden
        className="pointer-events-none absolute size-20 rounded border border-dashed border-border"
      />

      <div
        className={`size-20 rounded bg-primary motion-reduce:transition-none ${
          active
            ? "transition-transform duration-75 ease-linear"
            : "transition-transform duration-300 ease-[cubic-bezier(0.165,0.84,0.44,1)]"
        }`}
        style={{ transform: `rotate(${rotate}deg) scale(${scale})` }}
      />

      <p className="pointer-events-none absolute bottom-3 left-1/2 -translate-x-1/2 whitespace-nowrap font-mono text-[11px] tracking-wide text-muted-foreground tabular-nums">
        rotate({rotate.toFixed(0).padStart(3, "\u2007")}deg) scale(
        {scale.toFixed(2)})
      </p>
    </div>
  );
}

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