LAWSOFUX · motion lab
Concept · law-of-proximity

Law of Proximity

Grid stagger reveal palette · warm tan / cream takeaways · 3

Definition

Items placed near one another are read as related. Whitespace alone, with no border or color cue, is enough to forge a group in the reader's perception. Distance is meaning.

Why it matters for ShurIQ reports

Stack-rank rows, gap card grids, and viewport tile layouts all encode relationships through spacing. A 16-pixel gap inside a card and a 32-pixel gap between cards tells the eye what belongs together before the reader has read a word. Proximity discipline is the cheapest way to make a dense brief feel orderly.

Takeaways

Visual motion language

On layout-shift, related elements use magnetic-pull (50–80ms ease) to settle into proximity together; cross-group elements snap to the grid independently. The motion makes group structure legible during transitions.

Cavalry recreation seed. 4×4 grid of 70px circles, 16px gap inside the 3-wide cluster, 60px gap to the rightmost column. Animate the left 12 cream dots fading in together with a soft scale 0.8→1 over 700ms. After 600ms hold, fade the right 4 dark dots in over 600ms. Highlights the proximity-based grouping. Loop after 2s.

Origins

Gestalt psychology — early 20th century perceptual organization 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 · law-of-proximity · Cavalry scene
// Motion family: grid-stagger (grouped reveal)
// Palette: burnt-orange + 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_law-of-proximity_";

  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    = "#B85A22";
  var DARK  = "#6E3414";
  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 });

  // 4x4 grid of 70px circles. Cluster of 12 dots on left, 4 on right with bigger gap.
  var ROWS = 4;
  var R = 35;
  var SMALL_GAP = 30;
  var WIDE_GAP = 100;

  // x positions for cols: 3 left tightly, then 1 right wider
  var leftStart = -200;
  var xCols = [
    leftStart,
    leftStart + (R*2 + SMALL_GAP),
    leftStart + 2 * (R*2 + SMALL_GAP),
    leftStart + 2 * (R*2 + SMALL_GAP) + (R*2 + WIDE_GAP)
  ];
  var startY = ((ROWS - 1) * (R*2 + SMALL_GAP)) / 2;

  for (var r = 0; r < ROWS; r++) {
    for (var c = 0; c < 4; c++) {
      var d = api.primitive("ellipse", PREFIX + "dot_" + r + "_" + c);
      var isCream = (c < 3);
      api.set(d, {
        "generator.radius": [R, R],
        "position.x": xCols[c],
        "position.y": startY - r * (R*2 + SMALL_GAP),
        "opacity": 0,
        "scale.x": 0.8, "scale.y": 0.8
      });
      api.setFill(d, true);
      api.set(d, { "material.materialColor": isCream ? CREAM : DARK });

      if (isCream) {
        api.keyframe(d, 0,  { "opacity": 0,   "scale.x": 0.8, "scale.y": 0.8 });
        api.keyframe(d, 17, { "opacity": 100, "scale.x": 1.0, "scale.y": 1.0 });
        api.magicEasing(d, "scale.x", 17, "EaseOut", "");
        api.magicEasing(d, "scale.y", 17, "EaseOut", "");
      } else {
        api.keyframe(d, 30, { "opacity": 0,   "scale.x": 0.8, "scale.y": 0.8 });
        api.keyframe(d, 45, { "opacity": 100, "scale.x": 1.0, "scale.y": 1.0 });
      }
    }
  }

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