TipsBlocks Web Technique Library

TB-0004

Reactive Shape Field

12種類の意味を持つUIシェイプが、カーソルとの距離・移動方向・拡大量・アニメーション深度に応じて個別に反応する近接インタラクションです。Vanilla JavaScriptとCSSだけで実装しています。

Cursor Effect UI Interaction Visual Effect Interactive Control Pointer Move Proximity
Interaction
フィールド内でマウスを動…
Runtime
Vanilla JavaScript / CSS

MOTION PREVIEW

近づくだけで、それぞれ違う動きが始まる。

同じルールで一括変形するのではなく、Loaderは回転、Cardは展開、Buttonは押下、Formは入力から送信、Scrollは実際のスクロールなど、12種類のシェイプがそれぞれ意味に沿ったアニメーションを持っています。

PREVIEW MEDIA Preview MP4 / Poster を登録すると、ここへ自動表示されます。

LIVE DEMO

距離・方向・大きさ・深度を、その場で変える。

フィールド内でマウスを動かすと、近くのシェイプが距離に応じて反応します。MOVE DIRECTIONでCENTER・AWAY・AROUNDを切り替えると、シェイプ本体の移動方向が変わります。RANGEは反応範囲、SIZEは拡大量、MOVEは移動距離、ANIMATION DEPTHは各シェイプ固有アニメーションのレイヤー数を調整します。

HOW TO INTERACT

Interactive Control

フィールド内でマウスを動かすと、近くのシェイプが距離に応じて反応します。MOVE DIRECTIONでCENTER・AWAY・AROUNDを切り替えると、シェイプ本体の移動方向が変わります。RANGEは反応範囲、SIZEは拡大量、MOVEは移動距離、ANIMATION DEPTHは各シェイプ固有アニメーションのレイヤー数を調整します。

ABOUT THIS BLOCK

Reactive Shape Fieldとは

Reactive Shape Fieldは、カーソルとの距離を基準に、12種類の意味を持つUIシェイプがそれぞれ異なる反応を見せる近接インタラクションです。

CURSOR、LOADER、CARD、MENU、MOTION、BUTTON、3D、TEXT、CANVAS、IMAGE、SCROLL、FORMを独立した素材として配置し、単純な一括拡大ではなく、各素材の役割に合わせた時間軸アニメーションを持たせています。

操作パネルでは、反応距離を変えるRANGE、拡大量を変えるSIZE、本体の移動量を変えるMOVE、固有アニメーションのレイヤー数を変えるANIMATION DEPTHを調整できます。MOVE DIRECTIONはCENTER・AWAY・AROUNDの3種類から選択でき、選択した方向は通常のポインター操作にも継続して反映されます。

ANIMATION DEPTHは速度を変える設定ではありません。SIMPLEでは素材ごとの基本動作、LAYEREDでは追加パーツの連動、RICHでは複数パーツが時間差で動くフルアニメーションへ段階的に変化します。

RANGEなどの設定を変更している間は専用プレビューへ切り替わり、通常のポインター操作とは分けて各設定の役割を確認できるようにしています。

Technology
CSS / HTML / JavaScript
Interaction
Interactive Control / Pointer Move / Proximity
External Library
None
Reduced Motion
Supported

HOW IT WORKS

Reactive Shape Fieldはどう動いているのか。

各シェイプの基準座標とポインター座標の距離から反応率を算出し、拡大・本体移動・固有アニメーションへ別々に適用します。方向制御とアニメーション深度を分離することで、移動方向を変えても各素材固有の動きが維持されます。

  1. 01

    ポインターと各シェイプの距離を測る

    各シェイプの基準位置と現在のポインター位置から距離を求め、RANGEで設定された半径に対して0〜1の反応率へ変換します。距離が近いほど反応率が高くなります。

  2. 02

    拡大量と本体移動を別々に計算する

    距離から得た反応率をSIZEとMOVEへ独立して適用します。拡大と移動を分離しているため、どちらか一方だけを強くすることもできます。

  3. 03

    CENTER・AWAY・AROUNDの方向ベクトルを作る

    CENTERではシェイプからポインターへ向かうベクトル、AWAYではその逆方向、AROUNDではポインター方向に直交する接線ベクトルを使います。AROUNDには外側への持ち上がりも加え、単なる横移動ではなく円弧状に回り込む動きへしています。

  4. 04

    12種類の素材に固有の時間軸アニメーションを持たせる

    Loaderの回転、Cardの展開、Buttonの押下、3Dの回転、Scrollのスクロール、Formの入力・送信・確認など、各素材の意味に沿った動作を個別に定義しています。

  5. 05

    ANIMATION DEPTHで動作レイヤーを追加する

    深度は速度ではなく、動くパーツの数を変えます。SIMPLEでは基本動作、LAYEREDでは二次的なパーツ連動、RICHでは複数パーツが時間差で動くフルアニメーションへ段階的に変化します。

  6. 06

    設定中だけ専用プレビューへ切り替える

    RANGE、SIZE、MOVE、ANIMATION DEPTHのスライダー操作中は、それぞれの役割が埋もれないように専用プレビューへ切り替えます。RANGEでは対象内のシェイプを強調し、対象外をフェードさせてACTIVE数も表示します。

CODE

近接反応と12種類の固有モーションのコードを見る。

HTMLでは各シェイプを意味ごとに構成し、CSS Custom Propertiesで反応状態を受け取り、JavaScriptで距離計算、CENTER・AWAY・AROUNDの方向ベクトル、設定プレビュー、SIMPLE・LAYERED・RICHのアニメーションレイヤーを制御しています。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
  <meta name="theme-color" content="#F3F1EB">
  <title>TB-0004 — Reactive Shape Field V11</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
<main class="runtime" id="runtime">
  <header class="runtime-head">
    <div class="brand">
      <span>TB-0004</span>
      <h1>Reactive Shape Field</h1>
    </div>

    <section class="controls" id="controls" aria-label="Interaction controls">
      <div class="direction-panel">
        <div class="direction-head">
          <b>MOVE DIRECTION</b>
          <output id="directionValue">AWAY</output>
        </div>

        <div class="direction-buttons" role="group" aria-label="Move direction">
          <button type="button" data-direction="center" aria-label="Move shapes toward pointer">
            <span class="direction-icon"><i class="dot left"></i><i class="arrow">→</i><i class="target"></i></span>
            <span>CENTER</span>
          </button>

          <button type="button" data-direction="away" class="active" aria-label="Move shapes away from pointer">
            <span class="direction-icon"><i class="target"></i><i class="arrow">→</i><i class="dot right"></i></span>
            <span>AWAY</span>
          </button>

          <button type="button" data-direction="around" aria-label="Move shapes around pointer">
            <span class="direction-icon"><i class="orbit">↻</i><i class="target"></i></span>
            <span>AROUND</span>
          </button>
        </div>
      </div>

      <label class="control">
        <span class="control-top"><b>RANGE</b><output id="rangeValue">330 PX</output></span>
        <input id="range" type="range" min="140" max="600" step="1" value="330">
        <span class="control-scale"><i>NEAR</i><i>FAR</i></span>
      </label>

      <label class="control">
        <span class="control-top"><b>SIZE</b><output id="sizeValue">2.6 X</output></span>
        <input id="size" type="range" min="1.1" max="4.0" step="0.1" value="2.6">
        <span class="control-scale"><i>SMALL</i><i>LARGE</i></span>
      </label>

      <label class="control">
        <span class="control-top"><b>MOVE</b><output id="moveValue">70</output></span>
        <input id="move" type="range" min="0" max="150" step="1" value="70">
        <span class="control-scale"><i>STILL</i><i>FAR</i></span>
      </label>

      <label class="control">
        <span class="control-top"><b>ANIMATION DEPTH</b><output id="animationDepthValue">LAYERED</output></span>
        <input id="animationDepth" type="range" min="0" max="100" step="1" value="60">
        <span class="control-scale"><i>SIMPLE</i><i>RICH</i></span>
      </label>
    </section>
    <div class="preview-badge" id="previewBadge" aria-live="polite">
      <span class="preview-badge-dot"></span>
      <b id="previewBadgeTitle">LIVE PREVIEW</b>
      <span id="previewBadgeText">Move through the field</span>
      <strong id="activeCount" class="active-count"></strong>
    </div>
  </header>

  <div class="radius-guide" id="radiusGuide" aria-hidden="true"><span></span></div>
  <div class="preview-anchor" id="previewAnchor" aria-hidden="true">
    <span class="anchor-ring"></span>
    <span class="anchor-dot"></span>
    <b>CONTROL POINT</b>
  </div>

  <section class="field" id="field" aria-label="Interactive shape field">

    <button class="shape cursor-shape" data-kind="cursor"
      style="--x:12;--y:25;--w:120px;--h:120px" aria-label="Cursor">
      <span class="shape-art cursor-art">
        <svg viewBox="0 0 120 120" aria-hidden="true">
          <defs>
            <linearGradient id="cursorGrad" x1="0" x2="1" y1="0" y2="1">
              <stop offset="0" stop-color="#FF705F"/>
              <stop offset="1" stop-color="#FFB24A"/>
            </linearGradient>
          </defs>
          <circle cx="84" cy="29" r="20" fill="#4D7CFE" opacity=".18"/>
          <path d="M18 12 L93 63 L60 68 L77 101 L56 111 L39 77 L18 96 Z"
                fill="url(#cursorGrad)" stroke="#141414" stroke-width="4" stroke-linejoin="round"/>
          <circle cx="88" cy="28" r="8" fill="#141414"/>
        </svg>
      </span>
      <span class="shape-label">CURSOR</span>
    </button>

    <button class="shape loader-shape" data-kind="loader"
      style="--x:80;--y:23;--w:124px;--h:124px" aria-label="Loader">
      <span class="shape-art loader-art">
        <span class="loader-ring"></span>
        <span class="loader-core"></span>
        <span class="loader-orbit"></span>
      </span>
      <span class="shape-label">LOADER</span>
    </button>

    <button class="shape card-shape" data-kind="card"
      style="--x:33;--y:30;--w:196px;--h:138px" aria-label="Card">
      <span class="shape-art card-art">
        <span class="card-layer layer-three"></span>
        <span class="card-layer layer-two"></span>
        <span class="card-face">
          <span class="card-avatar"></span>
          <span class="card-copy c1"></span>
          <span class="card-copy c2"></span>
          <span class="card-tag"></span>
          <span class="card-accent"></span>
        </span>
      </span>
      <span class="shape-label">CARD</span>
    </button>

    <button class="shape menu-shape" data-kind="menu"
      style="--x:64;--y:35;--w:158px;--h:120px" aria-label="Menu">
      <span class="shape-art menu-art">
        <span class="menu-plate">
          <i class="m1"></i><i class="m2"></i><i class="m3"></i>
          <b></b>
        </span>
      </span>
      <span class="shape-label">MENU</span>
    </button>

    <button class="shape motion-shape" data-kind="motion"
      style="--x:13;--y:52;--w:196px;--h:132px" aria-label="Motion">
      <span class="shape-art motion-art">
        <svg viewBox="0 0 210 140" aria-hidden="true">
          <defs>
            <linearGradient id="motionGrad" x1="0" x2="1">
              <stop offset="0" stop-color="#EF6351"/>
              <stop offset="1" stop-color="#F2C94C"/>
            </linearGradient>
          </defs>
          <path class="motion-main" d="M10 104 C42 22,92 18,196 62"/>
          <path class="motion-mid" d="M20 118 C62 62,116 66,194 94"/>
          <circle class="motion-node n1" cx="12" cy="103" r="9"/>
          <circle class="motion-node n2" cx="196" cy="62" r="13"/>
        </svg>
        <span class="motion-runner"></span>
      </span>
      <span class="shape-label">MOTION</span>
    </button>

    <button class="shape button-shape" data-kind="button"
      style="--x:46;--y:51;--w:200px;--h:100px" aria-label="Button">
      <span class="shape-art button-art">
        <span class="button-shadow"></span>
        <span class="button-face"><b>GO</b><i></i></span>
      </span>
      <span class="shape-label">BUTTON</span>
    </button>

    <button class="shape cube-shape" data-kind="three"
      style="--x:83;--y:51;--w:150px;--h:150px" aria-label="3D">
      <span class="shape-art cube-art">
        <span class="cube-scene">
          <span class="cube">
            <i class="face front"></i><i class="face back"></i>
            <i class="face right"></i><i class="face left"></i>
            <i class="face top"></i><i class="face bottom"></i>
          </span>
        </span>
      </span>
      <span class="shape-label">3D</span>
    </button>

    <button class="shape text-shape" data-kind="text"
      style="--x:27;--y:69;--w:190px;--h:126px" aria-label="Text">
      <span class="shape-art text-art">
        <span class="text-aa">Aa</span>
        <span class="text-lines"><i></i><i></i><i></i></span>
        <span class="text-caret"></span>
      </span>
      <span class="shape-label">TEXT</span>
    </button>

    <button class="shape canvas-shape" data-kind="canvas"
      style="--x:58;--y:70;--w:146px;--h:146px" aria-label="Canvas">
      <span class="shape-art canvas-art">
        <i></i><i></i><i></i><i></i>
        <i></i><i></i><i></i><i></i>
        <i></i><i></i><i></i><i></i>
        <i></i><i></i><i></i><i></i>
      </span>
      <span class="shape-label">CANVAS</span>
    </button>

    <button class="shape image-shape" data-kind="image"
      style="--x:10;--y:83;--w:170px;--h:132px" aria-label="Image">
      <span class="shape-art image-art">
        <span class="image-frame">
          <span class="sun"></span>
          <span class="cloud"></span>
          <span class="mountain mountain-a"></span>
          <span class="mountain mountain-b"></span>
          <span class="image-scan"></span>
        </span>
      </span>
      <span class="shape-label">IMAGE</span>
    </button>

    <button class="shape scroll-shape" data-kind="scroll"
      style="--x:84;--y:80;--w:112px;--h:160px" aria-label="Scroll">
      <span class="shape-art scroll-art">
        <span class="browser">
          <span class="browser-top"><i></i><i></i><i></i></span>
          <span class="browser-lines"><i></i><i></i><i></i><i></i></span>
          <span class="scroll-track"></span>
          <span class="scroll-thumb"></span>
          <span class="scroll-progress"></span>
        </span>
      </span>
      <span class="shape-label">SCROLL</span>
    </button>

    <button class="shape form-shape" data-kind="form"
      style="--x:48;--y:87;--w:220px;--h:116px" aria-label="Form">
      <span class="shape-art form-art">
        <span class="form-box">
          <span class="form-title"></span>
          <span class="input"><i></i><em></em></span>
          <span class="submit"><b></b></span>
          <span class="form-check"></span>
        </span>
      </span>
      <span class="shape-label">FORM</span>
    </button>
  </section>

  <footer class="runtime-foot">
    <span>MOVE POINTER THROUGH SHAPES</span>
    <b id="status">AWAY / READY</b>
    <span>ADJUST CONTROLS TO COMPARE</span>
  </footer>
</main>
<script src="./script.js"></script>
</body>
</html>
:root{
  --bg:#F3F1EB;
  --ink:#141414;
  --coral:#EF6351;
  --orange:#FFB24A;
  --yellow:#F2C94C;
  --blue:#4D7CFE;
  --green:#55B98A;
  --violet:#9A70FF;
}
*{box-sizing:border-box}
html,body{width:100%;height:100%;margin:0;overflow:hidden;background:var(--bg)}
body{
  font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
  color:var(--ink);touch-action:none;overscroll-behavior:none;
}
button,input{font:inherit}
.runtime{
  position:relative;width:100%;height:100%;min-height:100dvh;overflow:hidden;
  background:radial-gradient(circle at 50% 53%,rgba(255,255,255,.96) 0,rgba(255,255,255,.28) 31%,transparent 58%),var(--bg);
}
.runtime-head{
  position:absolute;z-index:100;top:18px;left:24px;right:24px;
  display:flex;align-items:flex-start;justify-content:space-between;gap:30px;pointer-events:none;
}
.brand{padding-top:4px}
.brand span{display:block;margin-bottom:4px;font-size:10px;font-weight:800;letter-spacing:.18em}
.brand h1{margin:0;font-size:clamp(22px,2vw,30px);line-height:.98;letter-spacing:-.045em;font-weight:720}

.controls{
  pointer-events:auto;
  width:min(760px,66vw);
  display:grid;
  grid-template-columns:1.55fr repeat(4,1fr);
  gap:10px;
  padding:11px 12px 10px;
  border:1px solid rgba(20,20,20,.14);
  background:rgba(243,241,235,.92);
  backdrop-filter:blur(15px);
  box-shadow:0 12px 35px rgba(20,20,20,.05);
}
.direction-panel{min-width:0}
.direction-head,.control-top{
  display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;
}
.direction-head b,.direction-head output,.control b,.control output{
  font-size:8px;line-height:1;letter-spacing:.11em;white-space:nowrap;
}
.direction-head b,.control b{font-weight:820}
.direction-head output,.control output{font-variant-numeric:tabular-nums;color:rgba(20,20,20,.52)}
.direction-buttons{display:grid;grid-template-columns:repeat(3,1fr);gap:5px}
.direction-buttons button{
  min-width:0;padding:6px 5px 5px;border:1px solid rgba(20,20,20,.18);background:transparent;color:var(--ink);
  font-size:7px;font-weight:820;letter-spacing:.06em;cursor:pointer;
}
.direction-buttons button.active{background:var(--ink);color:var(--bg);border-color:var(--ink)}
.direction-buttons button:hover{border-color:var(--ink)}

.direction-buttons button{
  display:grid;
  grid-template-rows:20px auto;
  align-items:center;
  justify-items:center;
  gap:2px;
  padding:5px 4px 4px;
}
.direction-icon{
  display:flex;
  align-items:center;
  justify-content:center;
  gap:5px;
  height:20px;
}
.direction-icon .target{
  width:14px;height:14px;border:2px solid currentColor;border-radius:50%;position:relative;display:inline-block;
}
.direction-icon .target::after{
  content:"";position:absolute;left:50%;top:50%;width:4px;height:4px;transform:translate(-50%,-50%);border-radius:50%;background:currentColor;
}
.direction-icon .dot{width:7px;height:7px;border-radius:50%;background:currentColor;display:inline-block}
.direction-icon .arrow,.direction-icon .orbit{font-style:normal;font-size:14px;line-height:1;font-weight:900}
.control-scale{
  display:flex;
  justify-content:space-between;
  margin-top:4px;
  pointer-events:none;
}
.control-scale i{
  font-style:normal;
  font-size:6px;
  font-weight:760;
  letter-spacing:.10em;
  color:rgba(20,20,20,.38);
}
.control.is-active .control-scale i{color:rgba(77,124,254,.78)}

.control{display:block;min-width:0}
.control input{
  width:100%;height:14px;margin:0;appearance:none;-webkit-appearance:none;background:transparent;cursor:ew-resize;
}
.control input::-webkit-slider-runnable-track{height:3px;background:rgba(20,20,20,.20);border-radius:4px}
.control input::-moz-range-track{height:3px;background:rgba(20,20,20,.20);border-radius:4px}
.control input::-webkit-slider-thumb{
  appearance:none;-webkit-appearance:none;width:14px;height:14px;margin-top:-5.5px;
  border:2px solid var(--bg);border-radius:50%;background:var(--ink);box-shadow:0 0 0 1px var(--ink);
}
.control input::-moz-range-thumb{
  width:11px;height:11px;border:2px solid var(--bg);border-radius:50%;background:var(--ink);box-shadow:0 0 0 1px var(--ink);
}
.control.is-active b,.control.is-active output{color:var(--blue)}
.control.is-active input::-webkit-slider-thumb{background:var(--blue);box-shadow:0 0 0 1px var(--blue)}

.field{position:absolute;z-index:10;inset:100px 0 34px}

.radius-guide{
  position:absolute;z-index:5;left:0;top:0;
  width:calc(var(--radius,330px)*2);height:calc(var(--radius,330px)*2);
  margin-left:calc(var(--radius,330px)*-1);margin-top:calc(var(--radius,330px)*-1);
  border:1px dashed rgba(77,124,254,.55);border-radius:50%;opacity:0;
  transform:translate3d(var(--gx,50vw),var(--gy,50vh),0);
  pointer-events:none;transition:opacity .14s ease;
}
.radius-guide.show{opacity:1}
.radius-guide span{
  position:absolute;left:50%;top:50%;width:10px;height:10px;transform:translate(-50%,-50%);
  border-radius:50%;background:var(--blue);box-shadow:0 0 0 6px rgba(77,124,254,.12)
}

.shape{
  --scale:1;--dx:0px;--dy:0px;--rot:0deg;--rx:0deg;--ry:0deg;
  --spread:0px;--shift:0px;--stretch:1;--activity:0;
  position:absolute;left:calc(var(--x)*1%);top:calc(var(--y)*1%);
  width:var(--w);height:var(--h);padding:0;border:0;background:none;color:var(--ink);cursor:pointer;outline:none;
  transform:translate(-50%,-50%) translate3d(var(--dx),var(--dy),0) scale(var(--scale)) rotate(var(--rot));
  transform-origin:50% 50%;will-change:transform;
}
.shape-art{position:absolute;inset:0;display:block;transform-style:preserve-3d}
.shape-label{
  position:absolute;left:50%;bottom:-21px;transform:translateX(-50%);
  padding:3px 6px 2px;border:1px solid rgba(20,20,20,.12);background:rgba(243,241,235,.78);
  font-size:9px;font-weight:820;line-height:1;letter-spacing:.12em;
  opacity:calc(.60 + var(--activity)*.40);white-space:nowrap;pointer-events:none;
}
.shape:focus-visible::after{content:"";position:absolute;inset:-12px;border:2px solid var(--blue);pointer-events:none}

/* Cursor */
.cursor-art{filter:drop-shadow(0 10px 14px rgba(20,20,20,.12))}
.cursor-art svg{width:100%;height:100%;overflow:visible}

/* Loader */
.loader-art{transform:rotate(var(--rot));filter:drop-shadow(0 9px 16px rgba(85,185,138,.18))}
.loader-ring{position:absolute;inset:9%;border:16px solid rgba(85,185,138,.18);border-top-color:var(--green);border-right-color:var(--green);border-bottom-color:var(--violet);border-radius:50%}
.loader-core{position:absolute;left:50%;top:50%;width:30%;aspect-ratio:1;transform:translate(-50%,-50%) scale(calc(.86 + var(--activity)*.28));border-radius:50%;background:var(--ink)}
.loader-orbit{position:absolute;right:1%;top:42%;width:18%;aspect-ratio:1;border-radius:50%;background:var(--yellow);border:3px solid var(--ink)}

/* Card */
.card-art{perspective:700px;transform:rotateX(var(--rx)) rotateY(var(--ry))}
.card-layer,.card-face{position:absolute;border:3px solid var(--ink);border-radius:12px}
.layer-three{inset:21px -8px -9px 24px;background:var(--violet);transform:translate(calc(var(--spread)*.65),calc(var(--spread)*.38))}
.layer-two{inset:11px -2px -4px 13px;background:var(--yellow);transform:translate(calc(var(--spread)*.35),calc(var(--spread)*.20))}
.card-face{inset:0;background:linear-gradient(135deg,#6F91FF,var(--blue));overflow:hidden;box-shadow:0 14px 25px rgba(77,124,254,.18)}
.card-avatar{position:absolute;left:14px;top:15px;width:36px;height:36px;border:3px solid var(--ink);border-radius:50%;background:var(--coral)}
.card-copy{position:absolute;left:63px;height:8px;background:var(--ink);border-radius:8px}
.c1{top:19px;width:98px}.c2{top:38px;width:67px;opacity:.54}
.card-tag{position:absolute;left:14px;bottom:14px;width:72px;height:25px;border:3px solid var(--ink);border-radius:6px;background:var(--bg)}

/* Menu */
.menu-art{filter:drop-shadow(0 10px 18px rgba(20,20,20,.09))}
.menu-plate{position:absolute;inset:6px;border:3px solid var(--ink);border-radius:16px;background:var(--ink);display:flex;flex-direction:column;justify-content:center;gap:calc(10px + var(--spread)*.30);padding:20px}
.menu-plate i{display:block;height:10px;border-radius:9px;background:var(--bg)}
.menu-plate .m1{width:86%;transform:translateX(calc(var(--shift)*-.25))}
.menu-plate .m2{width:62%;background:var(--yellow);transform:translateX(var(--shift))}
.menu-plate .m3{width:75%;background:var(--coral);transform:translateX(calc(var(--shift)*.45))}
.menu-plate b{position:absolute;right:15px;top:15px;width:13px;height:13px;border-radius:50%;background:var(--green)}

/* Motion */
.motion-art svg{width:100%;height:100%;overflow:visible}
.motion-art path{fill:none;stroke-linecap:round;transform-origin:10px 70px;transform:scaleX(var(--stretch))}
.motion-main{stroke:url(#motionGrad);stroke-width:17}.motion-mid{stroke:var(--ink);stroke-width:8;opacity:.9}
.motion-node{stroke:var(--ink);stroke-width:4}.motion-node.n1{fill:var(--blue)}.motion-node.n2{fill:var(--yellow)}

/* Button */
.button-shadow,.button-face{position:absolute;left:4px;right:4px;height:72px;border:3px solid var(--ink);border-radius:18px}
.button-shadow{top:18px;background:var(--ink);transform:translateY(calc(5px - var(--shift)*.08))}
.button-face{top:5px;background:linear-gradient(135deg,#FF7A67,var(--coral));display:flex;align-items:center;justify-content:center;gap:22px;transform:translateY(calc(var(--shift)*.22))}
.button-face b{font-size:28px;line-height:1;letter-spacing:.02em}
.button-face i{width:20px;height:20px;border-top:4px solid var(--ink);border-right:4px solid var(--ink);transform:rotate(45deg)}

/* Cube */
.cube-art{perspective:700px}.cube-scene{position:absolute;inset:0;display:grid;place-items:center;transform-style:preserve-3d}
.cube{position:relative;width:76px;height:76px;transform-style:preserve-3d;transform:rotateX(calc(-18deg + var(--rx))) rotateY(calc(29deg + var(--ry))) rotateZ(var(--rot))}
.face{position:absolute;inset:0;border:4px solid var(--ink);background:rgba(77,124,254,.14)}
.front{transform:translateZ(38px);background:rgba(77,124,254,.32)}.back{transform:rotateY(180deg) translateZ(38px)}
.right{transform:rotateY(90deg) translateZ(38px);background:rgba(242,201,76,.30)}.left{transform:rotateY(-90deg) translateZ(38px);background:rgba(85,185,138,.25)}
.top{transform:rotateX(90deg) translateZ(38px);background:rgba(239,99,81,.31)}.bottom{transform:rotateX(-90deg) translateZ(38px)}

/* Text */
.text-art{display:flex;align-items:center;gap:13px;padding:10px 8px}
.text-aa{font-family:Georgia,"Times New Roman",serif;font-size:72px;font-weight:700;line-height:.9;letter-spacing:-.08em;color:var(--coral);-webkit-text-stroke:2px var(--ink);transform:scaleX(var(--stretch));transform-origin:left center}
.text-lines{flex:1;display:flex;flex-direction:column;gap:10px}
.text-lines i{display:block;height:11px;border-radius:8px;background:var(--ink);transform:translateX(var(--shift))}
.text-lines i:nth-child(1){width:100%}.text-lines i:nth-child(2){width:74%;background:var(--blue)}.text-lines i:nth-child(3){width:88%;background:var(--yellow)}

/* Canvas */
.canvas-art{display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(4,1fr);gap:8px;padding:8px}
.canvas-art i{border:3px solid var(--ink);border-radius:4px;background:var(--green);transform:translate(calc(var(--gx,0)*var(--spread)),calc(var(--gy,0)*var(--spread))) rotate(calc(var(--gr,0)*var(--activity)*1deg))}
.canvas-art i:nth-child(2n){background:var(--yellow)}.canvas-art i:nth-child(3n){background:var(--blue)}.canvas-art i:nth-child(5n){background:var(--coral)}
.canvas-art i:nth-child(1){--gx:-.7;--gy:-.7;--gr:-14}.canvas-art i:nth-child(2){--gx:-.25;--gy:-.7;--gr:10}
.canvas-art i:nth-child(3){--gx:.25;--gy:-.7;--gr:-11}.canvas-art i:nth-child(4){--gx:.7;--gy:-.7;--gr:14}
.canvas-art i:nth-child(5){--gx:-.7;--gy:-.25;--gr:8}.canvas-art i:nth-child(6){--gx:-.25;--gy:-.25;--gr:-13}
.canvas-art i:nth-child(7){--gx:.25;--gy:-.25;--gr:10}.canvas-art i:nth-child(8){--gx:.7;--gy:-.25;--gr:-8}
.canvas-art i:nth-child(9){--gx:-.7;--gy:.25;--gr:-13}.canvas-art i:nth-child(10){--gx:-.25;--gy:.25;--gr:9}
.canvas-art i:nth-child(11){--gx:.25;--gy:.25;--gr:-11}.canvas-art i:nth-child(12){--gx:.7;--gy:.25;--gr:15}
.canvas-art i:nth-child(13){--gx:-.7;--gy:.7;--gr:12}.canvas-art i:nth-child(14){--gx:-.25;--gy:.7;--gr:-9}
.canvas-art i:nth-child(15){--gx:.25;--gy:.7;--gr:8}.canvas-art i:nth-child(16){--gx:.7;--gy:.7;--gr:-13}

/* Image */
.image-frame{position:absolute;inset:5px;overflow:hidden;border:3px solid var(--ink);border-radius:9px;background:#8BD3FF}
.sun{position:absolute;right:15px;top:13px;width:31px;height:31px;border:3px solid var(--ink);border-radius:50%;background:var(--yellow);transform:translateY(calc(var(--shift)*-.12))}
.mountain{position:absolute;border:3px solid var(--ink);transform-origin:center}
.mountain-a{left:-19px;bottom:-58px;width:126px;height:126px;transform:rotate(45deg) scale(calc(1 + var(--activity)*.10));background:var(--green)}
.mountain-b{right:-23px;bottom:-61px;width:111px;height:111px;transform:rotate(45deg) scale(calc(1 + var(--activity)*.16));background:var(--violet)}

/* Scroll */
.browser{position:absolute;inset:2px 8px;border:3px solid var(--ink);border-radius:10px;background:var(--bg);overflow:hidden}
.browser-top{position:absolute;left:0;right:0;top:0;height:25px;border-bottom:3px solid var(--ink);background:var(--yellow);display:flex;align-items:center;gap:5px;padding-left:7px}
.browser-top i{width:7px;height:7px;border:2px solid var(--ink);border-radius:50%;background:var(--coral)}
.browser-lines{position:absolute;left:10px;right:19px;top:38px;display:flex;flex-direction:column;gap:12px}
.browser-lines i{display:block;height:7px;background:var(--ink);border-radius:6px}
.browser-lines i:nth-child(2){width:72%;background:var(--blue)}.browser-lines i:nth-child(3){width:88%}.browser-lines i:nth-child(4){width:55%;background:var(--green)}
.scroll-track{position:absolute;right:7px;top:34px;bottom:8px;width:4px;background:rgba(20,20,20,.15)}
.scroll-thumb{position:absolute;right:4px;top:39px;width:10px;height:34px;border:2px solid var(--ink);border-radius:7px;background:var(--coral);transform:translateY(var(--shift))}

/* Form */
.form-box{position:absolute;inset:4px;border:3px solid var(--ink);border-radius:12px;background:var(--bg);box-shadow:9px 9px 0 var(--blue)}
.form-title{position:absolute;left:14px;top:13px;width:72px;height:10px;background:var(--ink);border-radius:7px}
.input{position:absolute;left:14px;right:60px;top:40px;height:48px;border:3px solid var(--ink);border-radius:8px;background:white}
.input i{position:absolute;left:10px;top:50%;height:7px;width:calc(25% + var(--activity)*55%);transform:translateY(-50%);background:var(--green);border-radius:6px}
.submit{position:absolute;right:12px;top:40px;width:40px;height:48px;border:3px solid var(--ink);border-radius:8px;background:var(--coral);transform:translateX(var(--shift))}
.submit b{position:absolute;left:10px;top:14px;width:13px;height:13px;border-top:4px solid var(--ink);border-right:4px solid var(--ink);transform:rotate(45deg)}

.runtime-foot{
  position:absolute;z-index:90;left:24px;right:24px;bottom:14px;display:flex;justify-content:space-between;align-items:center;
  color:rgba(20,20,20,.48);font-size:9px;font-weight:780;letter-spacing:.14em;pointer-events:none;
}
.runtime-foot b{color:var(--ink);font-weight:820}

@media (max-width:1050px){
  .runtime-head{display:block;left:14px;right:14px;top:12px}
  .brand{display:flex;align-items:baseline;gap:9px}
  .brand span{margin:0}.brand h1{font-size:20px}
  .controls{width:100%;margin-top:9px;grid-template-columns:1.45fr repeat(4,1fr);gap:7px;padding:8px}
  .field{inset:128px 0 28px}
  .shape{transform:translate(-50%,-50%) translate3d(var(--dx),var(--dy),0) scale(calc(var(--scale)*.80)) rotate(var(--rot))}
}
@media (max-width:720px){
  .controls{grid-template-columns:1fr 1fr;grid-template-areas:"behavior behavior" "reach scale" "push life"}
  .direction-panel{grid-area:behavior}.control:nth-of-type(1){grid-area:reach}.control:nth-of-type(2){grid-area:scale}.control:nth-of-type(3){grid-area:push}.control:nth-of-type(4){grid-area:life}
  .field{inset:184px 0 26px}
  .shape{transform:translate(-50%,-50%) translate3d(var(--dx),var(--dy),0) scale(calc(var(--scale)*.66)) rotate(var(--rot))}
  .shape-label{font-size:8px;bottom:-17px}
  .cursor-shape{--x:17!important;--y:18!important}.loader-shape{--x:79!important;--y:18!important}
  .card-shape{--x:34!important;--y:37!important}.menu-shape{display:none}.motion-shape{--x:77!important;--y:39!important}
  .button-shape{--x:21!important;--y:57!important}.cube-shape{--x:76!important;--y:59!important}.text-shape{display:none}
  .canvas-shape{--x:42!important;--y:74!important}.image-shape{display:none}.scroll-shape{--x:80!important;--y:80!important}.form-shape{--x:24!important;--y:91!important}
  .runtime-foot span{display:none}.runtime-foot{justify-content:center}
}


/* =========================
   V5 CONTROL PREVIEW SYSTEM
   ========================= */
.preview-badge{
  position:absolute;
  right:0;
  top:calc(100% + 8px);
  display:flex;
  align-items:center;
  gap:7px;
  padding:6px 9px 5px;
  border:1px solid rgba(20,20,20,.12);
  background:rgba(243,241,235,.88);
  backdrop-filter:blur(10px);
  font-size:7px;
  letter-spacing:.08em;
  pointer-events:none;
  opacity:.74;
  transition:opacity .16s ease,transform .16s ease;
}
.preview-badge.active{
  opacity:1;
  transform:translateY(2px);
}
.preview-badge-dot{
  width:7px;height:7px;border-radius:50%;background:var(--green);
  box-shadow:0 0 0 4px rgba(85,185,138,.12);
}
.preview-badge b{font-size:7px;letter-spacing:.11em}
.preview-badge span:last-child{color:rgba(20,20,20,.54)}

.preview-anchor{
  --ax:50vw;
  --ay:56vh;
  position:absolute;
  z-index:7;
  left:0;top:0;
  width:72px;height:72px;
  transform:translate3d(calc(var(--ax) - 36px),calc(var(--ay) - 36px),0);
  pointer-events:none;
  opacity:0;
  transition:opacity .15s ease;
}
.preview-anchor.show{opacity:1}
.anchor-ring{
  position:absolute;inset:8px;border:2px solid rgba(20,20,20,.58);border-radius:50%;
}
.anchor-ring::before,.anchor-ring::after{
  content:"";position:absolute;left:50%;top:50%;background:rgba(20,20,20,.58);transform:translate(-50%,-50%);
}
.anchor-ring::before{width:1px;height:62px}
.anchor-ring::after{height:1px;width:62px}
.anchor-dot{
  position:absolute;left:50%;top:50%;width:10px;height:10px;transform:translate(-50%,-50%);
  border-radius:50%;background:var(--coral);box-shadow:0 0 0 6px rgba(239,99,81,.16);
}
.preview-anchor b{
  position:absolute;left:50%;top:calc(100% + 4px);transform:translateX(-50%);
  font-size:7px;letter-spacing:.11em;white-space:nowrap;
}

/* Range preview: clearly mark which shapes are inside/outside */
.shape.range-inside .shape-art{
  filter:drop-shadow(0 0 0 rgba(0,0,0,0)) brightness(1.06);
}
.shape.range-inside::before{
  content:"";
  position:absolute;
  inset:-9px;
  border:2px solid rgba(77,124,254,.72);
  border-radius:12px;
  pointer-events:none;
}
.shape.range-outside{
  opacity:.28;
}
.shape.range-outside .shape-label{
  opacity:.26;
}

/* Whole-field tuning preview */
.runtime.preview-size .shape-label::after{
  content:" SIZE";
  color:var(--blue);
}
.runtime.preview-move .shape-label::after{
  content:" MOVE";
  color:var(--coral);
}
.runtime.preview-animation .shape-label::after{
  content:" MOTION";
  color:var(--green);
}

/* Make control sliders more explicit during tuning */
.control.is-active{
  background:rgba(77,124,254,.06);
  outline:1px solid rgba(77,124,254,.18);
  outline-offset:4px;
}


/* =========================
   V6 SHAPE MOTION ENHANCEMENT
   ========================= */

/* CARD: deeper layer separation + accent reveal */
.card-accent{
  position:absolute;
  right:14px;
  bottom:14px;
  width:42px;
  height:42px;
  border:3px solid var(--ink);
  border-radius:50%;
  background:var(--green);
  transform:
    translate(calc(var(--shift) * .42),calc(var(--shift) * -.28))
    scale(calc(.78 + var(--activity) * .52))
    rotate(calc(var(--rot) * .45));
}
.card-avatar{
  transform:
    translate(calc(var(--shift) * -.22),calc(var(--shift) * .18))
    scale(calc(1 + var(--activity) * .18));
}
.card-copy.c1{transform:translateX(calc(var(--shift) * .28))}
.card-copy.c2{transform:translateX(calc(var(--shift) * -.20))}
.card-tag{
  transform:
    translateY(calc(var(--shift) * -.18))
    scaleX(calc(1 + var(--activity) * .16));
}

/* MENU: each bar now fans out visibly */
.menu-plate .m1{
  transform:
    translateX(calc(var(--shift) * -.55))
    rotate(calc(var(--activity) * -5deg));
}
.menu-plate .m2{
  transform:
    translateX(var(--shift))
    scaleX(calc(1 + var(--activity) * .32));
}
.menu-plate .m3{
  transform:
    translateX(calc(var(--shift) * .72))
    rotate(calc(var(--activity) * 6deg));
}

/* MOTION: path stretch plus node travel */
.motion-node.n1{
  transform:
    translateX(calc(var(--shift) * -.55))
    scale(calc(1 + var(--activity) * .25));
}
.motion-node.n2{
  transform:
    translateX(calc(var(--shift) * .75))
    scale(calc(1 + var(--activity) * .42));
}

/* BUTTON: stronger press / lift */
.button-face{
  transform:
    translateY(calc(var(--shift) * .34))
    scale(calc(1 + var(--activity) * .10));
}
.button-shadow{
  transform:
    translateY(calc(8px - var(--shift) * .20))
    scaleX(calc(1 - var(--activity) * .06));
}

/* TEXT: obvious width change + caret travel */
.text-aa{
  transform:
    scaleX(var(--stretch))
    translateY(calc(var(--shift) * -.12))
    rotate(calc(var(--activity) * -2deg));
}
.text-lines i:nth-child(1){
  transform:translateX(calc(var(--shift) * .28)) scaleX(calc(1 + var(--activity) * .34));
  transform-origin:left center;
}
.text-lines i:nth-child(2){
  transform:translateX(calc(var(--shift) * -.22)) scaleX(calc(1 - var(--activity) * .14));
  transform-origin:left center;
}
.text-lines i:nth-child(3){
  transform:translateX(calc(var(--shift) * .42)) scaleX(calc(1 + var(--activity) * .18));
  transform-origin:left center;
}
.text-caret{
  position:absolute;
  right:8px;
  top:22px;
  width:6px;
  height:76px;
  background:var(--violet);
  border:2px solid var(--ink);
  transform:
    translateX(calc(var(--shift) * .55))
    scaleY(calc(.55 + var(--activity) * .62));
}

/* CANVAS: cells spread + rotate much more */
.canvas-art i{
  transform:
    translate(calc(var(--gx,0) * var(--spread) * 1.35),calc(var(--gy,0) * var(--spread) * 1.35))
    rotate(calc(var(--gr,0) * var(--activity) * 1.8deg))
    scale(calc(1 + var(--activity) * var(--gs, .18)));
}
.canvas-art i:nth-child(1),.canvas-art i:nth-child(6),.canvas-art i:nth-child(11),.canvas-art i:nth-child(16){--gs:.42}
.canvas-art i:nth-child(4),.canvas-art i:nth-child(13){--gs:.30}

/* IMAGE: strong internal parallax / scan */
.cloud{
  position:absolute;
  left:20px;
  top:18px;
  width:48px;
  height:20px;
  border:3px solid var(--ink);
  border-radius:14px;
  background:white;
  transform:translateX(calc(var(--shift) * -.38));
}
.cloud::before{
  content:"";
  position:absolute;
  width:22px;
  height:22px;
  left:7px;
  top:-12px;
  border:3px solid var(--ink);
  border-bottom:0;
  border-radius:50% 50% 0 0;
  background:white;
}
.image-scan{
  position:absolute;
  left:0;
  right:0;
  top:48%;
  height:5px;
  background:var(--coral);
  border-top:2px solid var(--ink);
  border-bottom:2px solid var(--ink);
  transform:
    translateY(calc((var(--activity) - .5) * 86px))
    scaleX(calc(.55 + var(--activity) * .55));
}
.sun{
  transform:
    translate(calc(var(--shift) * .24),calc(var(--shift) * -.28))
    scale(calc(1 + var(--activity) * .20));
}
.mountain-a{
  transform:
    rotate(45deg)
    translate(calc(var(--shift) * -.10),calc(var(--shift) * .04))
    scale(calc(1 + var(--activity) * .20));
}
.mountain-b{
  transform:
    rotate(45deg)
    translate(calc(var(--shift) * .12),calc(var(--shift) * -.05))
    scale(calc(1 + var(--activity) * .28));
}

/* SCROLL: thumb motion + content travel + progress fill */
.browser-lines{
  transform:translateY(calc(var(--shift) * -.55));
}
.scroll-progress{
  position:absolute;
  left:0;
  right:0;
  bottom:0;
  height:8px;
  background:linear-gradient(90deg,var(--blue),var(--green));
  transform-origin:left center;
  transform:scaleX(calc(.15 + var(--activity) * .85));
  border-top:2px solid var(--ink);
}
.scroll-thumb{
  transform:
    translateY(var(--shift))
    scaleY(calc(1 + var(--activity) * .35));
}

/* FORM: typing, button travel, check reveal */
.input i{
  width:calc(22% + var(--activity) * 64%);
}
.input em{
  position:absolute;
  right:10px;
  top:50%;
  width:5px;
  height:26px;
  transform:
    translateY(-50%)
    translateX(calc(var(--shift) * -.18))
    scaleY(calc(.55 + var(--activity) * .65));
  background:var(--violet);
}
.submit{
  transform:
    translateX(var(--shift))
    rotate(calc(var(--activity) * 5deg))
    scale(calc(1 + var(--activity) * .16));
}
.form-check{
  position:absolute;
  right:-18px;
  top:9px;
  width:28px;
  height:28px;
  border:3px solid var(--ink);
  border-radius:50%;
  background:var(--green);
  opacity:calc(.15 + var(--activity) * .85);
  transform:
    translate(calc(var(--shift) * .35),calc(var(--shift) * -.18))
    scale(calc(.55 + var(--activity) * .65));
}
.form-check::after{
  content:"";
  position:absolute;
  left:7px;
  top:5px;
  width:9px;
  height:5px;
  border-left:3px solid var(--ink);
  border-bottom:3px solid var(--ink);
  transform:rotate(-45deg);
}

/* CURSOR: more obvious directional life */
.cursor-art svg{
  transform:
    rotate(calc(var(--rot) * .28))
    scale(calc(1 + var(--activity) * .10));
}

/* CUBE: preserve as visual benchmark but make motion clearer */
.cube{
  transform:
    rotateX(calc(-18deg + var(--rx)))
    rotateY(calc(29deg + var(--ry)))
    rotateZ(var(--rot))
    scale(calc(1 + var(--activity) * .12));
}


/* =========================
   V7 DIRECTION-AWARE SHAPES
   ========================= */

/* CURSOR: point at / away / tangent */
.runtime[data-direction="center"] .cursor-art{
  transform:translateX(calc(var(--shift) * .14));
}
.runtime[data-direction="away"] .cursor-art{
  transform:translateX(calc(var(--shift) * -.18));
}
.runtime[data-direction="around"] .cursor-art{
  transform:translateY(calc(var(--shift) * .12));
}

/* LOADER: compress / expand / orbit */
.runtime[data-direction="center"] .loader-ring{
  transform:scale(calc(1 - var(--activity) * .20));
}
.runtime[data-direction="away"] .loader-ring{
  transform:scale(calc(1 + var(--activity) * .34));
}
.runtime[data-direction="around"] .loader-orbit{
  transform:
    rotate(calc(var(--activity) * 180deg))
    translateX(calc(10px + var(--activity) * 18px));
}

/* CARD */
.runtime[data-direction="center"] .card-layer{
  transform:translate(calc(var(--spread) * .18),calc(var(--spread) * .10));
}
.runtime[data-direction="away"] .layer-two{
  transform:translate(calc(var(--spread) * .55),calc(var(--spread) * .32));
}
.runtime[data-direction="away"] .layer-three{
  transform:translate(calc(var(--spread) * .88),calc(var(--spread) * .55));
}
.runtime[data-direction="around"] .card-face{
  transform:translateX(calc(var(--shift) * .18)) rotate(calc(var(--activity) * 5deg));
}
.runtime[data-direction="around"] .layer-two{
  transform:translate(calc(var(--shift) * -.18),calc(var(--spread) * .30)) rotate(calc(var(--activity) * -5deg));
}
.runtime[data-direction="around"] .layer-three{
  transform:translate(calc(var(--shift) * .24),calc(var(--spread) * .48)) rotate(calc(var(--activity) * 8deg));
}

/* MENU */
.runtime[data-direction="center"] .menu-plate .m1,
.runtime[data-direction="center"] .menu-plate .m2,
.runtime[data-direction="center"] .menu-plate .m3{
  transform:translateX(calc(var(--shift) * .22)) scaleX(calc(1 - var(--activity) * .20));
}
.runtime[data-direction="away"] .menu-plate .m1{transform:translateX(calc(var(--shift) * -.72))}
.runtime[data-direction="away"] .menu-plate .m2{transform:translateX(calc(var(--shift) * .66))}
.runtime[data-direction="away"] .menu-plate .m3{transform:translateX(calc(var(--shift) * -.40))}
.runtime[data-direction="around"] .menu-plate .m1{transform:translate(calc(var(--shift) * -.52),calc(var(--shift) * .10)) rotate(-8deg)}
.runtime[data-direction="around"] .menu-plate .m2{transform:translate(calc(var(--shift) * .18),calc(var(--shift) * -.12)) rotate(5deg)}
.runtime[data-direction="around"] .menu-plate .m3{transform:translate(calc(var(--shift) * .58),calc(var(--shift) * .08)) rotate(10deg)}

/* MOTION */
.runtime[data-direction="center"] .motion-art path{transform-origin:center;transform:scaleX(var(--stretch))}
.runtime[data-direction="away"] .motion-art path{transform-origin:left center;transform:scaleX(var(--stretch))}
.runtime[data-direction="around"] .motion-art svg{transform:rotate(calc(var(--activity) * 10deg))}

/* BUTTON */
.runtime[data-direction="center"] .button-face{transform:translateY(calc(var(--shift) * .24)) scale(calc(1 - var(--activity) * .08))}
.runtime[data-direction="away"] .button-face{transform:translateY(calc(var(--shift) * -.18)) scale(calc(1 + var(--activity) * .14))}
.runtime[data-direction="around"] .button-face{transform:translate(calc(var(--shift) * .18),calc(var(--shift) * -.08)) rotate(calc(var(--activity) * 10deg))}

/* TEXT */
.runtime[data-direction="center"] .text-lines{transform:translateX(calc(var(--shift) * .22))}
.runtime[data-direction="away"] .text-lines{transform:translateX(calc(var(--shift) * .38))}
.runtime[data-direction="around"] .text-lines{transform:translate(calc(var(--shift) * .25),calc(var(--shift) * -.12)) rotate(calc(var(--activity) * 5deg))}

/* CANVAS */
.runtime[data-direction="center"] .canvas-art i{
  transform:translate(calc(var(--gx,0) * var(--spread) * .72),calc(var(--gy,0) * var(--spread) * .72)) rotate(calc(var(--gr,0) * var(--activity) * .8deg));
}
.runtime[data-direction="away"] .canvas-art i{
  transform:translate(calc(var(--gx,0) * var(--spread) * 1.55),calc(var(--gy,0) * var(--spread) * 1.55)) rotate(calc(var(--gr,0) * var(--activity) * 2.1deg));
}
.runtime[data-direction="around"] .canvas-art{
  transform:rotate(calc(var(--activity) * 24deg));
}
.runtime[data-direction="around"] .canvas-art i{
  transform:translate(calc(var(--gy,0) * var(--spread) * -1.15),calc(var(--gx,0) * var(--spread) * 1.15)) rotate(calc(var(--gr,0) * var(--activity) * 2deg));
}

/* IMAGE */
.runtime[data-direction="center"] .sun{transform:translate(calc(var(--shift) * -.16),calc(var(--shift) * .14)) scale(calc(1 - var(--activity) * .10))}
.runtime[data-direction="away"] .sun{transform:translate(calc(var(--shift) * .34),calc(var(--shift) * -.32)) scale(calc(1 + var(--activity) * .24))}
.runtime[data-direction="around"] .sun{transform:translate(calc(var(--shift) * .40),calc(var(--shift) * -.10)) rotate(calc(var(--activity) * 30deg))}
.runtime[data-direction="around"] .cloud{transform:translate(calc(var(--shift) * -.44),calc(var(--shift) * .16))}

/* SCROLL */
.runtime[data-direction="center"] .browser-lines{transform:translateY(calc(var(--shift) * .62))}
.runtime[data-direction="away"] .browser-lines{transform:translateY(calc(var(--shift) * -.72))}
.runtime[data-direction="around"] .browser-lines{transform:translate(calc(var(--shift) * .28),calc(var(--shift) * -.48))}

/* FORM */
.runtime[data-direction="center"] .submit{transform:translateX(calc(var(--shift) * -.46)) scale(calc(1 - var(--activity) * .10))}
.runtime[data-direction="away"] .submit{transform:translateX(var(--shift)) scale(calc(1 + var(--activity) * .18))}
.runtime[data-direction="around"] .submit{transform:translate(calc(var(--shift) * .72),calc(var(--shift) * -.18)) rotate(calc(var(--activity) * 16deg))}
.runtime[data-direction="center"] .form-check{transform:translate(calc(var(--shift) * -.22),calc(var(--shift) * .12)) scale(calc(.55 + var(--activity) * .45))}
.runtime[data-direction="away"] .form-check{transform:translate(calc(var(--shift) * .46),calc(var(--shift) * -.22)) scale(calc(.55 + var(--activity) * .75))}
.runtime[data-direction="around"] .form-check{transform:translate(calc(var(--shift) * -.32),calc(var(--shift) * -.42)) rotate(calc(var(--activity) * 30deg)) scale(calc(.55 + var(--activity) * .70))}


/* =========================
   V8 TIME-BASED ANIMATION
   ========================= */

.shape{
  --cycle:0;
  --wave:0;
  --pulse:0;
  --type:0;
  --press:0;
  --check:0;
  --medium:0;
  --rich:0;
}

/* CURSOR: visible breathing / overshoot */
.cursor-art{
  transform:
    translateY(calc(var(--wave) * 5px))
    rotate(calc(var(--wave) * 4deg))
    scale(calc(1 + var(--pulse) * .08));
}
.cursor-dot{
  transform-origin:center;
  transform:scale(calc(.75 + var(--pulse) * .75));
}

/* LOADER remains the benchmark: true continuous rotation */
.loader-art{
  transform:rotate(var(--rot)) scale(calc(1 + var(--pulse) * .08));
}
.loader-core{
  transform:
    translate(-50%,-50%)
    scale(calc(.78 + var(--pulse) * .55));
}

/* CARD: rhythmic fan-open / close */
.card-art{
  transform:
    rotateX(var(--rx))
    rotateY(var(--ry))
    translateY(calc(var(--wave) * 5px));
}
.layer-two{
  transform:
    translate(calc(var(--spread) * .42),calc(var(--spread) * .22))
    rotate(calc(var(--wave) * 4deg));
}
.layer-three{
  transform:
    translate(calc(var(--spread) * .78),calc(var(--spread) * .46))
    rotate(calc(var(--wave) * 8deg));
}
.card-accent{
  transform:
    translate(calc(var(--shift) * .35),calc(var(--wave) * -8px))
    scale(calc(.72 + var(--pulse) * .58))
    rotate(calc(var(--cycle) * 360deg));
}

/* MENU: alternating bars are an obvious animation, not just a static state */
.menu-plate .m1{
  transform:translateX(calc(var(--shift) * -1));
}
.menu-plate .m2{
  transform:translateX(var(--shift));
}
.menu-plate .m3{
  transform:translateX(calc(var(--shift) * -.62));
}
.menu-plate b{
  transform:
    translate(calc(var(--wave) * 6px),calc(var(--wave) * -5px))
    scale(calc(.75 + var(--pulse) * .55));
}

/* MOTION: a real point visibly travels along the curve */
.motion-art{position:absolute;inset:0}
.motion-runner{
  position:absolute;
  left:0;
  top:0;
  width:18px;
  height:18px;
  margin:-9px 0 0 -9px;
  border:3px solid var(--ink);
  border-radius:50%;
  background:var(--yellow);
  offset-path:path("M 10 104 C 42 22, 92 18, 196 62");
  offset-distance:calc(var(--cycle) * 100%);
  offset-rotate:0deg;
  box-shadow:0 5px 12px rgba(20,20,20,.16);
  transform:scale(calc(.8 + var(--pulse) * .42));
}
.motion-node.n1{transform:scale(calc(.8 + var(--pulse) * .45))}
.motion-node.n2{transform:scale(calc(.9 + var(--pulse) * .62))}

/* BUTTON: repeated press → spring back */
.button-face{
  transform:
    translateY(calc(var(--press) * 22px))
    scale(calc(1 + var(--pulse) * .08));
}
.button-shadow{
  transform:
    translateY(calc(10px - var(--press) * 8px))
    scaleX(calc(1 - var(--press) * .08));
}
.button-face i{
  transform:
    rotate(45deg)
    translate(calc(var(--wave) * 3px),calc(var(--wave) * -3px));
}

/* 3D: truly continuous rotation */
.cube{
  transform:
    rotateX(calc(-18deg + var(--rx) + var(--wave) * 9deg))
    rotateY(calc(29deg + var(--ry) + var(--cycle) * 360deg))
    rotateZ(calc(var(--rot) + var(--wave) * 5deg))
    scale(calc(1 + var(--pulse) * .10));
}

/* TEXT: type rhythm + line wave */
.text-aa{
  transform:
    scaleX(var(--stretch))
    translateY(calc(var(--wave) * -7px))
    rotate(calc(var(--wave) * -3deg));
}
.text-lines i:nth-child(1){
  transform:translateX(calc(var(--wave) * 24px)) scaleX(calc(.82 + var(--pulse) * .38));
}
.text-lines i:nth-child(2){
  transform:translateX(calc(var(--wave) * -18px)) scaleX(calc(.72 + var(--pulse) * .34));
}
.text-lines i:nth-child(3){
  transform:translateX(calc(var(--wave) * 30px)) scaleX(calc(.78 + var(--pulse) * .42));
}
.text-caret{
  opacity:calc(.25 + var(--pulse) * .75);
  transform:
    translateX(calc(var(--shift) * .45))
    scaleY(calc(.45 + var(--pulse) * .72));
}

/* CANVAS: breathing scatter / gather loop */
.canvas-art{
  transform:rotate(calc(var(--wave) * 7deg));
}
.canvas-art i{
  transform:
    translate(calc(var(--gx,0) * var(--spread)),calc(var(--gy,0) * var(--spread)))
    rotate(calc(var(--gr,0) * var(--wave) * 1.6deg))
    scale(calc(.78 + var(--pulse) * var(--gs,.40)));
}

/* IMAGE: animated mini-scene */
.cloud{
  transform:
    translateX(calc(var(--wave) * 28px))
    translateY(calc(var(--pulse) * -5px));
}
.sun{
  transform:
    translate(calc(var(--wave) * -16px),calc(var(--pulse) * -12px))
    scale(calc(.85 + var(--pulse) * .35))
    rotate(calc(var(--cycle) * 120deg));
}
.image-scan{
  transform:
    translateY(calc(-48px + var(--cycle) * 96px))
    scaleX(calc(.55 + var(--pulse) * .50));
}
.mountain-a{
  transform:
    rotate(45deg)
    translate(calc(var(--wave) * -10px),calc(var(--wave) * 4px))
    scale(calc(1 + var(--pulse) * .12));
}
.mountain-b{
  transform:
    rotate(45deg)
    translate(calc(var(--wave) * 14px),calc(var(--wave) * -6px))
    scale(calc(1 + var(--pulse) * .18));
}

/* SCROLL: page visibly scrolls */
.browser-lines{
  transform:translateY(calc(var(--cycle) * -74px));
}
.scroll-thumb{
  transform:translateY(calc(var(--cycle) * 72px));
}
.scroll-progress{
  transform:scaleX(var(--cycle));
}

/* FORM: mini interaction sequence — type → submit → check → reset */
.input i{
  width:calc(18% + var(--type) * 68%);
}
.input em{
  opacity:calc(.2 + var(--type) * .8);
  transform:
    translateY(-50%)
    scaleY(calc(.5 + var(--pulse) * .7));
}
.submit{
  transform:
    translateX(calc(var(--shift) * .45))
    translateY(calc(var(--press) * 12px))
    scale(calc(1 - var(--press) * .16 + var(--pulse) * .05));
}
.form-check{
  opacity:var(--check);
  transform:
    translate(calc(8px + var(--check) * 16px),calc(-3px - var(--check) * 16px))
    scale(calc(.35 + var(--check) * .85))
    rotate(calc(var(--check) * 360deg));
}

/* Motion-speed preview: all labels clearly identify what's being tuned */
.runtime.preview-motionSpeed .shape-label::after{
  content:" SPEED";
  color:var(--green);
}


/* =========================
   V9 RANGE CLARITY + DEPTH
   ========================= */

.active-count{
  display:none;
  margin-left:4px;
  padding-left:8px;
  border-left:1px solid rgba(20,20,20,.14);
  font-size:7px;
  letter-spacing:.10em;
  color:var(--blue);
}
.preview-badge.range-mode .active-count{display:inline-block}

/* Make RANGE preview visually unmistakable */
.runtime.preview-range .shape{
  transition:opacity .12s ease;
}
.runtime.preview-range .shape.range-inside{
  opacity:1;
  z-index:20;
}
.runtime.preview-range .shape.range-outside{
  opacity:.12;
  filter:grayscale(1) saturate(0);
}
.runtime.preview-range .shape.range-inside .shape-art{
  filter:
    drop-shadow(0 0 0 rgba(0,0,0,0))
    drop-shadow(0 0 12px rgba(77,124,254,.18))
    brightness(1.05);
}
.runtime.preview-range .shape.range-inside::before{
  content:"";
  position:absolute;
  inset:-11px;
  border:3px solid rgba(77,124,254,.84);
  border-radius:14px;
  pointer-events:none;
  animation:rangePulse .8s ease-in-out infinite alternate;
}
.runtime.preview-range .shape.range-inside .shape-label{
  color:var(--blue);
  border-color:rgba(77,124,254,.58);
  background:rgba(243,241,235,.96);
  opacity:1;
}
.runtime.preview-range .shape.range-outside .shape-label{
  opacity:.10;
}
@keyframes rangePulse{
  from{transform:scale(.98);opacity:.62}
  to{transform:scale(1.02);opacity:1}
}

/* Stronger range circle */
.runtime.preview-range .radius-guide{
  border:2px solid rgba(77,124,254,.70);
  background:rgba(77,124,254,.035);
}
.runtime.preview-range .radius-guide::after{
  content:"ACTIVE RANGE";
  position:absolute;
  left:50%;
  top:7px;
  transform:translateX(-50%);
  padding:3px 6px;
  background:var(--bg);
  color:var(--blue);
  font-size:7px;
  font-weight:820;
  letter-spacing:.12em;
  white-space:nowrap;
}

/* Depth preview label */
.runtime.preview-animationDepth .shape-label::after{
  content:" DEPTH";
  color:var(--green);
}


/* =========================
   V10 TRUE ANIMATION DEPTH
   Depth adds layers; it does not speed up the base animation.
   ========================= */

/* LOADER
   SIMPLE: ring rotation
   LAYERED: core pulse
   RICH: orbiting satellite
*/
.loader-core{
  transform:
    translate(-50%,-50%)
    scale(calc(.86 + var(--pulse) * .34 * var(--medium)));
}
.loader-orbit{
  opacity:calc(.18 + var(--rich) * .82);
  transform-origin:-34px 50%;
  transform:
    rotate(calc(var(--cycle) * 360deg * var(--rich)))
    translateX(calc(var(--rich) * 14px))
    scale(calc(.65 + var(--rich) * .55));
}

/* CARD
   SIMPLE: tilt
   LAYERED: card fan
   RICH: internal choreography
*/
.card-accent{
  opacity:calc(.12 + var(--rich) * .88);
  transform:
    translate(calc(var(--shift) * .35 * var(--rich)),calc(var(--wave) * -8px * var(--rich)))
    scale(calc(.70 + var(--rich) * .45))
    rotate(calc(var(--cycle) * 360deg * var(--rich)));
}
.card-avatar{
  transform:
    translate(calc(var(--wave) * -9px * var(--rich)),calc(var(--pulse) * 7px * var(--rich)))
    scale(calc(1 + var(--rich) * .18));
}
.card-copy.c1{
  transform:translateX(calc(var(--wave) * 20px * var(--rich)));
}
.card-copy.c2{
  transform:translateX(calc(var(--wave) * -15px * var(--rich)));
}
.card-tag{
  transform:
    translateY(calc(var(--wave) * -8px * var(--rich)))
    scaleX(calc(1 + var(--rich) * .16));
}

/* MENU
   SIMPLE: primary bar moves
   LAYERED: all bars stagger
   RICH: status dot orbits/bounces
*/
.menu-plate .m1{
  transform:translateX(calc(var(--wave) * 24px));
}
.menu-plate .m2{
  transform:translateX(calc(var(--wave) * -38px * var(--medium)));
}
.menu-plate .m3{
  transform:translateX(calc(var(--wave) * 48px * var(--rich)));
}
.menu-plate b{
  transform:
    translate(calc(var(--wave) * 12px * var(--rich)),calc(var(--pulse) * -10px * var(--rich)))
    scale(calc(.82 + var(--rich) * .48));
}

/* MOTION
   SIMPLE: runner travels path
   LAYERED: curve breathes
   RICH: nodes pulse / second line becomes active
*/
.motion-main{
  transform:scaleX(calc(1 + (var(--stretch) - 1) * var(--medium)));
}
.motion-mid{
  opacity:calc(.18 + var(--rich) * .72);
  transform:
    scaleX(calc(1 + (var(--stretch) - 1) * var(--rich)))
    translateY(calc(var(--wave) * 8px * var(--rich)));
}
.motion-node.n1,
.motion-node.n2{
  transform:scale(calc(.82 + var(--pulse) * .55 * var(--rich)));
}

/* BUTTON
   SIMPLE: press
   LAYERED: spring
   RICH: arrow / face wobble
*/
.button-face{
  transform:
    translateY(calc(var(--press) * 22px))
    translateX(calc(var(--wave) * 7px * var(--medium)))
    rotate(calc(var(--wave) * 4deg * var(--rich)))
    scale(calc(1 + var(--pulse) * .06 * var(--medium)));
}
.button-shadow{
  transform:
    translateY(calc(10px - var(--press) * 8px))
    scaleX(calc(1 - var(--medium) * .07));
}
.button-face i{
  transform:
    rotate(45deg)
    translate(calc(var(--wave) * 7px * var(--rich)),calc(var(--wave) * -7px * var(--rich)));
}

/* 3D
   SIMPLE: Y rotation
   LAYERED: X tilt
   RICH: Z rotation + breathing
*/
.cube{
  transform:
    rotateX(calc(-18deg + var(--rx) * var(--medium)))
    rotateY(calc(29deg + var(--ry)))
    rotateZ(calc(var(--rot) * var(--rich)))
    scale(calc(1 + var(--pulse) * .10 * var(--rich)));
}

/* TEXT
   SIMPLE: Aa breathes
   LAYERED: lines animate
   RICH: caret / stagger
*/
.text-aa{
  transform:
    scaleX(var(--stretch))
    translateY(calc(var(--wave) * -5px))
    rotate(calc(var(--wave) * -3deg * var(--rich)));
}
.text-lines i:nth-child(1){
  transform:translateX(calc(var(--wave) * 22px * var(--medium)));
}
.text-lines i:nth-child(2){
  transform:translateX(calc(var(--wave) * -28px * var(--medium)));
}
.text-lines i:nth-child(3){
  transform:translateX(calc(var(--wave) * 38px * var(--rich)));
}
.text-caret{
  opacity:calc(.10 + var(--rich) * (.25 + var(--pulse) * .75));
  transform:
    translateX(calc(var(--wave) * 28px * var(--rich)))
    scaleY(calc(.45 + var(--pulse) * .70 * var(--rich)));
}

/* CANVAS
   SIMPLE: scatter/gather
   LAYERED: cells rotate
   RICH: whole matrix orbits
*/
.canvas-art{
  transform:rotate(calc(var(--cycle) * 28deg * var(--rich)));
}
.canvas-art i{
  transform:
    translate(calc(var(--gx,0) * var(--spread)),calc(var(--gy,0) * var(--spread)))
    rotate(calc(var(--gr,0) * var(--wave) * 1.5deg * var(--medium)))
    scale(calc(.82 + var(--pulse) * .28 + var(--rich) * .12));
}

/* IMAGE
   SIMPLE: cloud drift
   LAYERED: sun + mountains parallax
   RICH: scan line
*/
.cloud{
  transform:
    translateX(calc(var(--wave) * 30px))
    translateY(calc(var(--pulse) * -4px));
}
.sun{
  transform:
    translate(calc(var(--wave) * -18px * var(--medium)),calc(var(--pulse) * -12px * var(--medium)))
    scale(calc(.88 + var(--pulse) * .30 * var(--medium)))
    rotate(calc(var(--cycle) * 120deg * var(--rich)));
}
.mountain-a{
  transform:
    rotate(45deg)
    translate(calc(var(--wave) * -12px * var(--medium)),calc(var(--wave) * 5px * var(--medium)))
    scale(calc(1 + var(--pulse) * .12 * var(--medium)));
}
.mountain-b{
  transform:
    rotate(45deg)
    translate(calc(var(--wave) * 14px * var(--medium)),calc(var(--wave) * -6px * var(--medium)))
    scale(calc(1 + var(--pulse) * .18 * var(--medium)));
}
.image-scan{
  opacity:var(--rich);
  transform:
    translateY(calc(-48px + var(--cycle) * 96px))
    scaleX(calc(.55 + var(--rich) * .50));
}

/* SCROLL
   SIMPLE: content scrolls
   LAYERED: scrollbar tracks it
   RICH: progress line + subtle lateral motion
*/
.browser-lines{
  transform:
    translateY(calc(var(--cycle) * -74px))
    translateX(calc(var(--wave) * 9px * var(--rich)));
}
.scroll-thumb{
  transform:
    translateY(calc(var(--cycle) * 72px * var(--medium)))
    scaleY(calc(1 + var(--medium) * .26));
}
.scroll-progress{
  opacity:var(--rich);
  transform:scaleX(calc(var(--cycle) * var(--rich)));
}

/* FORM
   SIMPLE: type
   LAYERED: press submit
   RICH: confirmation check
*/
.input i{
  width:calc(18% + var(--type) * 68%);
}
.submit{
  transform:
    translateX(calc(var(--shift) * .36 * var(--medium)))
    translateY(calc(var(--press) * 12px))
    scale(calc(1 - var(--press) * .16 + var(--medium) * .04));
}
.form-check{
  opacity:calc(var(--check) * var(--rich));
  transform:
    translate(calc(8px + var(--check) * 16px),calc(-3px - var(--check) * 16px))
    scale(calc(.35 + var(--check) * .85))
    rotate(calc(var(--check) * 360deg));
}


/* =========================
   V11 POINTER DIRECTION EMPHASIS
   ========================= */

.shape{
  --dirLead:0;
  --arc:0;
}

.shape.direction-hot .shape-art{
  filter:drop-shadow(0 12px 18px rgba(20,20,20,.12));
}
.shape.direction-hot .shape-label{
  opacity:1;
}

.runtime[data-direction="center"] .shape.direction-hot::after,
.runtime[data-direction="away"] .shape.direction-hot::after{
  content:"";
  position:absolute;
  left:50%;
  top:50%;
  width:7px;
  height:7px;
  margin:-3.5px;
  border-radius:50%;
  background:rgba(20,20,20,.18);
  transform:scale(calc(.6 + var(--dirLead) * .7));
  pointer-events:none;
}

.runtime[data-direction="around"] .shape.direction-hot::after{
  content:"";
  position:absolute;
  left:50%;
  top:50%;
  width:48px;
  height:48px;
  margin:-24px;
  border:1px dashed rgba(20,20,20,.18);
  border-radius:50%;
  transform:
    rotate(calc(var(--arc) * 180deg))
    scale(calc(.78 + var(--dirLead) * .24));
  pointer-events:none;
}

.shape.direction-leading .card-accent,
.shape.direction-leading .card-avatar,
.shape.direction-leading .menu-plate b,
.shape.direction-leading .motion-node,
.shape.direction-leading .text-caret,
.shape.direction-leading .image-scan,
.shape.direction-leading .form-check{
  opacity:calc(.20 + var(--rich) * .45);
}
(() => {
  "use strict";

  const runtime = document.getElementById("runtime");
  const field = document.getElementById("field");
  const guide = document.getElementById("radiusGuide");
  const anchor = document.getElementById("previewAnchor");
  const status = document.getElementById("status");
  const previewBadge = document.getElementById("previewBadge");
  const previewBadgeTitle = document.getElementById("previewBadgeTitle");
  const previewBadgeText = document.getElementById("previewBadgeText");
  const activeCount = document.getElementById("activeCount");

  const directionValue = document.getElementById("directionValue");
  const directionButtons = [...document.querySelectorAll("[data-direction]")];
  const shapes = [...document.querySelectorAll(".shape")];

  const ui = {
    range: document.getElementById("range"),
    size: document.getElementById("size"),
    move: document.getElementById("move"),
    animationDepth: document.getElementById("animationDepth"),
    rangeValue: document.getElementById("rangeValue"),
    sizeValue: document.getElementById("sizeValue"),
    moveValue: document.getElementById("moveValue"),
    animationDepthValue: document.getElementById("animationDepthValue")
  };

  const state = {
    x: innerWidth*.5,
    y: innerHeight*.56,
    tx: innerWidth*.5,
    ty: innerHeight*.56,
    anchorX: innerWidth*.5,
    anchorY: innerHeight*.56,

    range:330,
    maxScale:2.6,
    move:70,
    animationDepth:.60,
    motionSpeed:1.55,

    direction:"away",
    tuning:false,
    tuningId:"",
    inside:true,

    directionDemo:null,
    directionDemoStart:0,
    directionDemoDuration:950,

    lastTime:performance.now(),
    now:performance.now()
  };

  const items = shapes.map((el,index) => ({
    el,
    kind:el.dataset.kind,
    cx:0,
    cy:0,
    baseX:0,
    baseY:0,
    p:0,
    scale:1,
    dx:0,
    dy:0,
    rot:0,
    rx:0,
    ry:0,
    spread:0,
    shift:0,
    stretch:1,
    impulse:0,
    spin:0,
    index,
    prevP:0,
    dirLead:0
  }));

  const clamp=(n,a,b)=>Math.max(a,Math.min(b,n));
  const lerp=(a,b,t)=>a+(b-a)*t;
  const smooth=t=>t*t*(3-2*t);
  const easeOut=t=>1-Math.pow(1-t,3);
  const easePulse=t=>Math.sin(Math.PI*clamp(t,0,1));

  function syncUI(){
    state.range = +ui.range.value;
    state.maxScale = +ui.size.value;
    state.move = +ui.move.value;
    state.animationDepth = +ui.animationDepth.value/100;

    ui.rangeValue.textContent = `${Math.round(state.range)} PX`;
    ui.sizeValue.textContent = `${state.maxScale.toFixed(1)} X`;
    ui.moveValue.textContent = `${Math.round(state.move)}`;
    ui.animationDepthValue.textContent =
      state.animationDepth < .34 ? "SIMPLE" :
      state.animationDepth < .67 ? "LAYERED" : "RICH";

    if(state.tuningId==="animationDepth"){
      previewBadgeText.textContent =
        state.animationDepth < .34 ? "Basic motion only" :
        state.animationDepth < .67 ? "Adds secondary moving parts" :
        "Full multi-part choreography";
    }

    guide.style.setProperty("--radius",`${state.range}px`);
  }

  function setBadge(title,text,active=true,rangeMode=false){
    previewBadgeTitle.textContent=title;
    previewBadgeText.textContent=text;
    previewBadge.classList.toggle("active",active);
    previewBadge.classList.toggle("range-mode",rangeMode);
    if(!rangeMode) activeCount.textContent="";
  }

  function setAnchor(x,y,show){
    anchor.style.setProperty("--ax",`${x}px`);
    anchor.style.setProperty("--ay",`${y}px`);
    anchor.classList.toggle("show",show);
  }

  function setGuide(){
    guide.style.setProperty("--gx",`${state.anchorX}px`);
    guide.style.setProperty("--gy",`${state.anchorY}px`);
  }

  function setDirection(name,withDemo=true){
    state.direction=name;
    runtime.dataset.direction=name;
    directionButtons.forEach(b=>b.classList.toggle("active",b.dataset.direction===name));
    directionValue.textContent=name.toUpperCase();
    status.textContent=`${name.toUpperCase()} / READY`;

    if(withDemo){
      state.directionDemo=name;
      state.directionDemoStart=performance.now();
      setBadge("MOVE DIRECTION",`${name.toUpperCase()} stays active — hover a shape to test it`,true);
      setAnchor(innerWidth*.5, innerHeight*.56, true);
    }
  }

  directionButtons.forEach(btn=>{
    btn.addEventListener("click",e=>{
      e.stopPropagation();
      setDirection(btn.dataset.direction,true);
    });
  });

  function clearRangeClasses(){
    items.forEach(item=>{
      item.el.classList.remove("range-inside","range-outside");
    });
  }

  function beginTune(input){
    state.tuning=true;
    state.tuningId=input.id;

    // While tuning, use a stable center point so ALL differences are visible.
    state.anchorX=innerWidth*.5;
    state.anchorY=innerHeight*.56;
    state.tx=state.anchorX;
    state.ty=state.anchorY;
    state.x=state.anchorX;
    state.y=state.anchorY;
    state.inside=true;

    input.closest(".control").classList.add("is-active");
    runtime.classList.add(`preview-${input.id}`);
    status.textContent=`TUNING ${input.id==="animationDepth"?"ANIMATION DEPTH":input.id.toUpperCase()}`;
    setAnchor(state.anchorX,state.anchorY,true);

    if(input.id==="range"){
      guide.classList.add("show");
      setGuide();
      setBadge("RANGE PREVIEW","Blue shapes are inside the current range",true,true);
    }else if(input.id==="size"){
      setBadge("SIZE PREVIEW","All shapes change size together",true);
    }else if(input.id==="move"){
      setBadge("MOVE PREVIEW",`${state.direction.toUpperCase()} from the center point`,true);
    }else if(input.id==="animationDepth"){
      setBadge("ANIMATION DEPTH","SIMPLE → LAYERED → RICH adds moving parts",true);
    }
  }

  function endTune(input){
    state.tuning=false;
    state.tuningId="";
    input.closest(".control").classList.remove("is-active");
    runtime.classList.remove(`preview-${input.id}`);
    guide.classList.remove("show");
    setAnchor(state.anchorX,state.anchorY,false);
    clearRangeClasses();
    activeCount.textContent="";
    setBadge("LIVE PREVIEW","Move through the field",false);
    status.textContent=`${state.direction.toUpperCase()} / READY`;
  }

  document.querySelectorAll(".control input").forEach(input=>{
    input.addEventListener("input",()=>{
      syncUI();
      if(input.id==="range") setGuide();
    });
    input.addEventListener("pointerdown",e=>{
      e.stopPropagation();
      beginTune(input);
    });
    input.addEventListener("pointerup",()=>endTune(input));
    input.addEventListener("pointercancel",()=>endTune(input));
    input.addEventListener("change",syncUI);
  });

  function measure(){
    const fr=field.getBoundingClientRect();
    items.forEach(item=>{
      const cs=getComputedStyle(item.el);
      const xp=parseFloat(cs.getPropertyValue("--x"))/100;
      const yp=parseFloat(cs.getPropertyValue("--y"))/100;
      item.baseX=fr.left+fr.width*xp;
      item.baseY=fr.top+fr.height*yp;
      item.cx=item.baseX;
      item.cy=item.baseY;
    });
  }

  let rr=0;
  window.addEventListener("resize",()=>{
    cancelAnimationFrame(rr);
    rr=requestAnimationFrame(measure);
  },{passive:true});

  field.addEventListener("pointermove",e=>{
    if(state.tuning || state.directionDemo) return;
    state.tx=state.anchorX=e.clientX;
    state.ty=state.anchorY=e.clientY;
    state.inside=true;
  },{passive:true});

  field.addEventListener("pointerenter",e=>{
    if(state.tuning || state.directionDemo) return;
    state.tx=state.anchorX=e.clientX;
    state.ty=state.anchorY=e.clientY;
    state.inside=true;
  },{passive:true});

  field.addEventListener("pointerleave",()=>{
    if(!state.tuning && !state.directionDemo) state.inside=false;
  });

  items.forEach(item=>{
    item.el.addEventListener("pointerenter",()=>{
      if(!state.tuning && !state.directionDemo){
        status.textContent=`${state.direction.toUpperCase()} / ${item.kind.toUpperCase()}`;
      }
    });
    item.el.addEventListener("pointerleave",()=>{
      if(!state.tuning && !state.directionDemo){
        status.textContent=`${state.direction.toUpperCase()} / READY`;
      }
    });
    item.el.addEventListener("pointerdown",e=>{
      e.stopPropagation();
      item.impulse=1;
      if(item.kind==="loader") item.spin+=240;
      status.textContent=`${item.kind.toUpperCase()} / ACTIVE`;
    });
    item.el.addEventListener("pointerup",()=>{
      if(!state.tuning && !state.directionDemo){
        status.textContent=`${state.direction.toUpperCase()} / ${item.kind.toUpperCase()}`;
      }
    });
  });

  function displacement(nx,ny,p,amountOverride=null,directionOverride=null){
    const amount=(amountOverride===null?state.move:amountOverride)*p;
    const dir=directionOverride || state.direction;

    if(dir==="away") return {x:-nx*amount,y:-ny*amount};
    if(dir==="center") return {x:nx*amount,y:ny*amount};
    return {x:-ny*amount,y:nx*amount};
  }

  function lifeTargets(item,p,depthOverride=null,directionOverride=null){
    const depth=depthOverride===null?state.animationDepth:depthOverride;
    const speed=state.motionSpeed;
    const D=directionOverride || state.direction;
    const t={
      rot:0,rx:0,ry:0,spread:0,shift:0,stretch:1,
      cycle:0,wave:0,pulse:0,type:0,press:0,check:0,
      medium:0,rich:0
    };

    const px=state.x-item.baseX;
    const py=state.y-item.baseY;
    const angle=Math.atan2(py,px)*180/Math.PI;

    // IMPORTANT: depth never changes the base animation speed.
    const seconds=state.now/1000;
    const phase=seconds*speed*(1.30 + item.index*.025) + item.index*.43;
    const cycle=((phase/(Math.PI*2))%1+1)%1;
    const wave=Math.sin(phase);
    const wave2=Math.sin(phase*1.65+.8);
    const pulse=(Math.sin(phase)+1)/2;
    const tri=1-Math.abs(cycle*2-1);

    // SIMPLE always keeps one clear base animation.
    // LAYERED and RICH progressively add additional moving parts.
    const medium=smooth(clamp((depth-.26)/.34,0,1));
    const rich=smooth(clamp((depth-.62)/.34,0,1));

    t.cycle=cycle;
    t.wave=wave*p;
    t.pulse=pulse*p;
    t.medium=medium;
    t.rich=rich;

    switch(item.kind){
      case "cursor":
        // SIMPLE: clearly points according to the selected direction.
        t.rot=(D==="center"?angle+7:D==="away"?angle+187:angle+97)*p;
        // LAYERED/RICH add overshoot without changing speed.
        t.shift=(D==="center"?14:D==="away"?-18:24)*p*(.35+medium*.65);
        t.rot+=wave*5*p*medium + wave2*5*p*rich;
        break;

      case "loader":
        // SIMPLE: ring rotates at one fixed speed.
        item.spin+=(2.2+p*8)*speed;
        t.rot=item.spin+item.impulse*180;
        // Extra layers are handled by core/orbit CSS using medium/rich.
        t.spread=(D==="center"?-10:D==="away"?16:10)*p*medium;
        break;

      case "card":
        // SIMPLE: pointer-aware tilt.
        if(D==="center"){
          t.ry=clamp(px/3.5,-40,40)*p;
          t.rx=clamp(-py/4.0,-34,34)*p;
        }else if(D==="away"){
          t.ry=clamp(-px/3.1,-46,46)*p;
          t.rx=clamp(py/3.5,-38,38)*p;
        }else{
          t.ry=36*p + wave*7*p;
          t.rx=16*p + wave2*5*p;
        }
        // LAYERED: fan the cards open/closed.
        t.spread=(D==="center"?24:D==="away"?72:52)*p*(.18+tri*.82)*medium;
        // RICH: internal parts choreograph.
        t.shift=(D==="center"?-24:D==="away"?34:48)*p*wave*rich;
        t.rot=(D==="around"?wave*7*p*rich:0);
        break;

      case "menu":
        // SIMPLE: first bar moves (CSS).
        t.shift=(D==="center"?-24:D==="away"?34:46)*p*wave;
        // LAYERED: vertical spacing grows.
        t.spread=(D==="center"?8:D==="away"?42:24)*p*(.35+pulse*.65)*medium;
        // RICH: whole menu gently rotates.
        t.rot=(D==="around"?wave*7*p*rich:0);
        break;

      case "motion":
        // SIMPLE: runner always travels the path (CSS from cycle).
        // LAYERED: the path breathes.
        if(D==="center"){
          t.stretch=1-.24*p*(.25+pulse*.75)*medium;
        }else if(D==="away"){
          t.stretch=1+.82*p*(.20+pulse*.80)*medium;
        }else{
          t.stretch=1+.36*p*(.25+pulse*.75)*medium;
        }
        // RICH: path itself drifts/rotates.
        t.shift=(D==="center"?-22:D==="away"?40:58)*p*wave*rich;
        t.rot=(D==="around"?wave*9*p*rich:0);
        break;

      case "button":
        // SIMPLE: rhythmic press.
        t.press=Math.pow(pulse,5)*p;
        // LAYERED: spring translation.
        t.shift=(D==="center"?-16:D==="away"?26:20)*p*wave*medium;
        // RICH: face wobble.
        t.rot=(D==="center"?-3:D==="away"?4:9)*p*wave*rich;
        break;

      case "three":
        // SIMPLE: continuous Y rotation.
        const spin=cycle*360;
        if(D==="center"){
          t.ry=(spin*.30 + clamp(px/4.2,-26,26))*p;
        }else if(D==="away"){
          t.ry=(-spin*.32 + clamp(-px/4.0,-30,30))*p;
        }else{
          t.ry=spin*.58*p;
        }
        // LAYERED: X-axis tilt.
        t.rx=(clamp(-py/5.0,-34,34)+wave*8)*p*medium;
        // RICH: Z-axis rotation.
        t.rot=(D==="around"?cycle*40:wave*8)*p*rich;
        break;

      case "text":
        // SIMPLE: headline width breathes.
        if(D==="center"){
          t.stretch=1-.20*p*(.35+pulse*.65);
        }else if(D==="away"){
          t.stretch=1+.46*p*(.25+pulse*.75);
        }else{
          t.stretch=1+.22*p*(.25+pulse*.75);
        }
        // LAYERED: lines stagger.
        t.shift=(D==="center"?-24:D==="away"?42:54)*p*wave*medium;
        // RICH: typographic rotation/caret.
        t.rot=(D==="around"?-wave*5*p*rich:0);
        break;

      case "canvas":
        // SIMPLE: cells scatter/gather.
        const breathe=.24+pulse*.76;
        t.spread=(D==="center"?-22:D==="away"?54:42)*p*breathe;
        // LAYERED: cell rotation.
        t.shift=(D==="around"?wave*32*p*medium:0);
        // RICH: full grid rotates.
        t.rot=(D==="center"?-4:D==="away"?6:cycle*28)*p*rich;
        break;

      case "image":
        // SIMPLE: cloud drift comes directly from wave.
        // LAYERED: sun/mountains parallax.
        t.shift=(D==="center"?-34:D==="away"?46:58)*p*wave*medium;
        // RICH: scan and slight scene rotation.
        t.rot=(D==="around"?wave*8*p*rich:0);
        break;

      case "scroll":
        // SIMPLE: page content scrolls via cycle.
        t.cycle=cycle*p;
        // LAYERED: scrollbar movement is enabled by medium in CSS.
        t.shift=(D==="center"?-24:D==="away"?32:26)*p*wave*medium;
        // RICH: lateral sway/progress.
        t.rot=(D==="around"?wave*6*p*rich:0);
        break;

      case "form":
        // SIMPLE: typing.
        const local=cycle;
        const typing=clamp(local/.42,0,1);
        // LAYERED: submit button presses.
        const press=(local>=.42 && local<.62)
          ? Math.sin(((local-.42)/.20)*Math.PI)*medium
          : 0;
        // RICH: confirmation check appears.
        const check=(local>=.58 && local<.88)
          ? clamp((local-.58)/.12,0,1)*clamp((.88-local)/.08,0,1)*rich
          : 0;

        t.type=typing*p;
        t.press=press*p;
        t.check=check*p;
        t.shift=(D==="center"?-20:D==="away"?28:34)*p*wave*medium;
        t.rot=(D==="around"?wave*7*p*rich:0);
        break;
    }

    if(item.impulse>0){
      t.shift+=item.impulse*18*p;
      t.rot+=item.impulse*7*p;
      t.press=Math.max(t.press,item.impulse*.8);
    }

    return t;
  }

  function distanceToControlPoint(item){
    const dx=state.x-item.baseX;
    const dy=state.y-item.baseY;
    return {dx,dy,dist:Math.hypot(dx,dy)};
  }

  function normalInteraction(item){
    const dx=state.x-item.baseX;
    const dy=state.y-item.baseY;
    const dist=Math.hypot(dx,dy);
    const p=clamp(1-dist/state.range,0,1);

    const len=Math.max(dist,1);
    const nx=dx/len;
    const ny=dy/len;

    // Strongest when the pointer newly enters the effective area.
    // This makes body direction readable before the internal animation takes over.
    const entry=Math.max(0,p-item.prevP);
    item.dirLead=Math.max(item.dirLead*.86,clamp(entry*4.8,0,1));
    if(p<.04) item.dirLead*=.72;
    item.prevP=p;

    const lead=item.dirLead;
    const eased=Math.pow(p,.42);
    const baseAmount=state.move*(1.65+lead*.55);

    let move;
    if(state.direction==="center"){
      // Pull directly toward the pointer.
      move={
        x:nx*baseAmount*eased,
        y:ny*baseAmount*eased
      };
    }else if(state.direction==="away"){
      // Push directly away from the pointer.
      move={
        x:-nx*baseAmount*eased,
        y:-ny*baseAmount*eased
      };
    }else{
      // Tangential movement + small outward lift = readable curved/orbital path.
      const tangentX=-ny;
      const tangentY=nx;
      const side=(item.index%2===0?1:-1);
      const arcAmount=baseAmount*1.18*eased;
      const radialLift=baseAmount*.28*eased*(.45+lead*.55);

      move={
        x:tangentX*arcAmount*side-nx*radialLift,
        y:tangentY*arcAmount*side-ny*radialLift
      };
    }

    // Scale and internal detail are slightly restrained while direction is leading.
    const scaleWeight=.36+(1-lead)*.18;
    const lifeP=p*(.58+(1-lead)*.42);

    return {
      p,
      lead,
      scale:1+(state.maxScale-1)*p*scaleWeight,
      move,
      life:lifeTargets(item,lifeP)
    };
  }

  function tuningInteraction(item){
    // Strong, explicit previews per control.
    if(state.tuningId==="size"){
      const p=.80;
      return {
        p,
        lead:0,
        scale:1+(state.maxScale-1)*p,
        move:{x:0,y:0},
        life:lifeTargets(item,0)
      };
    }

    if(state.tuningId==="move"){
      const dx=state.x-item.baseX;
      const dy=state.y-item.baseY;
      const dist=Math.hypot(dx,dy);
      const nx=dist>.001?dx/dist:0;
      const ny=dist>.001?dy/dist:0;
      const p=.92;
      return {
        p:.55,
        scale:1.10,
        move:displacement(nx,ny,p,state.move,state.direction),
        life:lifeTargets(item,0)
      };
    }

    if(state.tuningId==="animationDepth"){
      const p=.98;
      return {
        p,
        lead:0,
        scale:1.12,
        move:{x:0,y:0},
        life:lifeTargets(item,p,state.animationDepth)
      };
    }

    // RANGE preview
    const {dx,dy,dist}=distanceToControlPoint(item);
    const inside=dist<=state.range;
    item.el.classList.toggle("range-inside",inside);
    item.el.classList.toggle("range-outside",!inside);
    const p=inside ? .48 : 0;
    return {
      p,
      lead:0,
      scale:inside ? 1.16 : 1,
      move:{x:0,y:0},
      life:lifeTargets(item,0)
    };
  }

  function directionDemoInteraction(item,now){
    const t=clamp((now-state.directionDemoStart)/state.directionDemoDuration,0,1);
    const pulse=easePulse(t);

    const cx=innerWidth*.5;
    const cy=innerHeight*.56;
    const dx=cx-item.baseX;
    const dy=cy-item.baseY;
    const dist=Math.hypot(dx,dy);
    const nx=dist>.001?dx/dist:0;
    const ny=dist>.001?dy/dist:0;

    let move;
    if(state.directionDemo==="center"){
      move=displacement(nx,ny,pulse,90,"center");
    }else if(state.directionDemo==="away"){
      move=displacement(nx,ny,pulse,90,"away");
    }else{
      // Orbit: stronger tangent sweep plus a subtle radial breath
      move=displacement(nx,ny,pulse,110,"around");
    }

    return {
      p:pulse*.75,
      scale:1+pulse*.18,
      move,
      life:lifeTargets(item,pulse*.72,state.animationDepth,state.directionDemo)
    };
  }

  function apply(item,target,ease=.17){
    item.p=lerp(item.p,target.p,ease);
    item.scale=lerp(item.scale,target.scale+item.impulse*.16,ease);
    item.dx=lerp(item.dx,target.move.x,ease);
    item.dy=lerp(item.dy,target.move.y,ease);
    item.rot=lerp(item.rot,target.life.rot,ease);
    item.rx=lerp(item.rx,target.life.rx,ease);
    item.ry=lerp(item.ry,target.life.ry,ease);
    item.spread=lerp(item.spread,target.life.spread,ease);
    item.shift=lerp(item.shift,target.life.shift,ease);
    item.stretch=lerp(item.stretch,target.life.stretch,ease);
    item.impulse*=.875;

    const s=item.el.style;
    s.setProperty("--activity",item.p.toFixed(4));
    s.setProperty("--scale",item.scale.toFixed(4));
    s.setProperty("--dx",`${item.dx.toFixed(2)}px`);
    s.setProperty("--dy",`${item.dy.toFixed(2)}px`);
    s.setProperty("--rot",`${item.rot.toFixed(2)}deg`);
    s.setProperty("--rx",`${item.rx.toFixed(2)}deg`);
    s.setProperty("--ry",`${item.ry.toFixed(2)}deg`);
    s.setProperty("--spread",`${item.spread.toFixed(2)}px`);
    s.setProperty("--shift",`${item.shift.toFixed(2)}px`);
    s.setProperty("--stretch",item.stretch.toFixed(4));
    s.setProperty("--cycle",(target.life.cycle||0).toFixed(4));
    s.setProperty("--wave",(target.life.wave||0).toFixed(4));
    s.setProperty("--pulse",(target.life.pulse||0).toFixed(4));
    s.setProperty("--type",(target.life.type||0).toFixed(4));
    s.setProperty("--press",(target.life.press||0).toFixed(4));
    s.setProperty("--check",(target.life.check||0).toFixed(4));
    s.setProperty("--medium",(target.life.medium||0).toFixed(4));
    s.setProperty("--rich",(target.life.rich||0).toFixed(4));

    const dirLead=target.lead||0;
    s.setProperty("--dirLead",dirLead.toFixed(4));
    s.setProperty("--arc",((state.now/1000*.65)%1).toFixed(4));

    item.el.classList.toggle("direction-hot",!state.tuning && !state.directionDemo && target.p>.14);
    item.el.classList.toggle("direction-leading",!state.tuning && !state.directionDemo && dirLead>.10);
  }

  function render(now){
    const dt=clamp(now-state.lastTime,8,34);
    state.lastTime=now;
    state.now=now;

    // Direction demo owns the control point temporarily.
    if(state.directionDemo){
      state.x=state.tx=innerWidth*.5;
      state.y=state.ty=innerHeight*.56;

      const elapsed=now-state.directionDemoStart;
      if(elapsed>=state.directionDemoDuration){
        state.directionDemo=null;
        setAnchor(state.anchorX,state.anchorY,false);
        setBadge("LIVE PREVIEW","Move through the field",false);
        status.textContent=`${state.direction.toUpperCase()} / READY`;
      }
    }else if(state.tuning){
      state.x=state.tx=state.anchorX=innerWidth*.5;
      state.y=state.ty=state.anchorY=innerHeight*.56;
      state.inside=true;
    }else{
      state.x=lerp(state.x,state.tx,.18);
      state.y=lerp(state.y,state.ty,.18);
    }

    for(const item of items){
      let target;
      if(state.directionDemo){
        target=directionDemoInteraction(item,now);
      }else if(state.tuning){
        target=tuningInteraction(item);
      }else{
        clearRangeClasses();
        target=normalInteraction(item);
      }
      apply(item,target,state.tuning || state.directionDemo ? .20 : .16);
    }

    if(state.tuningId==="range"){
      const count=items.filter(i=>i.el.classList.contains("range-inside")).length;
      activeCount.textContent=`ACTIVE ${count} / ${items.length}`;
    }

    requestAnimationFrame(render);
  }

  syncUI();
  setDirection("away",false);
  setBadge("LIVE PREVIEW","Move through the field",false);

  requestAnimationFrame(()=>{
    measure();
    requestAnimationFrame(render);
  });
})();

USE CASE

近接インタラクションをWebデザインへどう活用できるか。

ポートフォリオやサービス紹介、機能一覧、実験的なメインビジュアルなど、複数の要素をただ並べるのではなく、閲覧者のポインター操作に合わせて関係性や個性を見せたい場面に利用できます。

Interactive Hero

Interactive Hero

サービスやプロダクトを複数の象徴的なUIパーツで見せ、ポインター操作に応じて画面全体へ反応を持たせるメインビジュアルとして利用できます。

Portfolio / Creative Website

Portfolio / Creative Website

制作会社やクリエイターのポートフォリオで、UI・モーション・インタラクションの表現力そのものを体験させるセクションとして利用できます。

Feature / Technology Showcase

Feature / Technology Showcase

複数の機能や技術カテゴリを、それぞれ意味の異なるシェイプとして配置し、静的なカード一覧よりも探索性のある見せ方へ発展させられます。

TECHNICAL NOTES

実装前に確認しておきたいこと。

多数のDOM要素を毎フレーム更新するため、実サイトへ組み込む場合は表示数、アニメーション量、画面幅、Pointer Events、Reduced Motionへの対応を用途に合わせて調整してください。

Browser

CSS Custom Properties、Pointer Events、requestAnimationFrameを使用します。現行の主要ブラウザを対象とし、公開前に実際の対象環境で操作感を確認してください。

Performance

CanvasやWebGLではなくDOM/CSSベースの実装です。12シェイプをrequestAnimationFrameで更新しています。実サイトで要素数を増やす場合は、表示数やdrop-shadowなどの描画負荷を確認してください。

Mobile

ポインター前提の操作が中心です。モバイルでは画面幅に合わせてレイアウトを縮小できますが、タッチ操作とページスクロールの競合を避けるため、実装先に応じて反応範囲や操作方法を調整してください。

Reduced Motion

実サイトへ組み込む場合はprefers-reduced-motionに応じて時間軸アニメーションや本体移動量を弱める、または停止する対応を推奨します。