LAWSOFUX · motion lab
Concept · fittss-law

Fitts's Law

Center-out radial pulse palette · forest / cream takeaways · 3

Definition

The time it takes a user to hit a target scales with how far the cursor must travel and how small the target is. Big nearby targets win; tiny far ones lose. The law is mathematical, not aesthetic — every increase in target size or proximity is a real reduction in friction.

Why it matters for ShurIQ reports

Filter chips, viewport switchers, and stack-rank row actions live or die by hit-target geometry. On a brand-intelligence dashboard with dense data, a 36-pixel button next to a 12-pixel chart label means the executive will mis-tap and disengage. Fitts's Law is what justifies generous click areas around every interactive element on a viz hub.

Takeaways

Visual motion language

On approach, interactive elements show a subtle magnetic-pull (1–2px shift toward cursor) and a scale-pulse on press. Snap-grid alignment ensures targets land where the eye expects.

Cavalry recreation seed. 4 concentric ring strokes (ø480, ø340, ø220, ø100), 12px cream stroke. Center cream dot ø24. Animate a 60px cream blurred circle (opacity 0.4) traveling along a diagonal arc from off-canvas top-right to the bullseye over 1.2s, scale at landing 1→0.7 then disappear. 800ms hold, repeat from a new angle.

Origins

Paul Fitts, 1954 — formalized as a movement-time equation in human-factors research.

Cavalry scene

The script below builds this concept's motion in Cavalry through the Stallion bridge. Pipe to cavalry_run_script via MCP, or paste into Cavalry's JavaScript Editor.

// Laws of UX · fittss-law · Cavalry scene
// Motion family: center-out radial pulse (target rings + traveling cursor)
// Palette: olive-green + cream
// To run: pipe to cavalry_run_script tool, or paste into Cavalry's JavaScript Editor
// Built 2026-04-30 by ShurAI

(function () {
  var PREFIX = "claude_lawofux_fittss-law_";

  var existing = api.getAllSceneLayers();
  for (var i = 0; i < existing.length; i++) {
    try {
      var nm = api.getNiceName(existing[i]);
      if (nm && nm.indexOf("claude_lawofux_") === 0) api.deleteLayer(existing[i]);
    } catch (e) {}
  }

  var BG    = "#4A5A2C";
  var CREAM = "#D6CDB0";

  var bg = api.primitive("rectangle", PREFIX + "bg");
  api.set(bg, { "generator.dimensions": [1080, 1080] });
  api.setFill(bg, true);
  api.set(bg, { "material.materialColor": BG });

  // 4 concentric ring strokes
  var radii = [240, 170, 110, 50];
  for (var i = 0; i < radii.length; i++) {
    var ring = api.primitive("ellipse", PREFIX + "ring_" + i);
    api.set(ring, {
      "generator.radius": [radii[i], radii[i]],
      "position.x": 0, "position.y": 0,
      "opacity": 0
    });
    api.setFill(ring, false);
    api.setStroke(ring, true);
    api.set(ring, {
      "stroke.strokeColor": CREAM,
      "stroke.width": 12
    });
    api.keyframe(ring, i * 4,       { "opacity": 0   });
    api.keyframe(ring, i * 4 + 12,  { "opacity": 100 });
  }

  // Bullseye dot
  var bull = api.primitive("ellipse", PREFIX + "bullseye");
  api.set(bull, {
    "generator.radius": [12, 12],
    "position.x": 0, "position.y": 0,
    "opacity": 0
  });
  api.setFill(bull, true);
  api.set(bull, { "material.materialColor": CREAM });
  api.keyframe(bull, 16, { "opacity": 0   });
  api.keyframe(bull, 28, { "opacity": 100 });

  // Traveling cursor blob from off-canvas top-right to bullseye
  var cursor = api.primitive("ellipse", PREFIX + "cursor");
  api.set(cursor, {
    "generator.radius": [30, 30],
    "position.x": 380, "position.y": 380,
    "opacity": 0
  });
  api.setFill(cursor, true);
  api.set(cursor, { "material.materialColor": CREAM });

  api.keyframe(cursor, 30,  { "opacity": 0,  "position.x": 380, "position.y": 380, "scale.x": 1.0, "scale.y": 1.0 });
  api.keyframe(cursor, 36,  { "opacity": 40 });
  api.keyframe(cursor, 60,  { "opacity": 60, "position.x": 0,   "position.y": 0,   "scale.x": 0.7, "scale.y": 0.7 });
  api.keyframe(cursor, 72,  { "opacity": 0 });
  api.magicEasing(cursor, "position.x", 60, "EaseOut", "");
  api.magicEasing(cursor, "position.y", 60, "EaseOut", "");

  var layerCount = api.getAllSceneLayers().length;
  console.log("scene built: fittss-law (" + layerCount + " layers)");
})();