GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

ドラッグ ハンドル 9種 — キャンバスの自由変形ツールキット

9種のドラッグ ハンドルのうち、1つは回転ハンドルを弧を描いて引くと選択ボックスの中心を軸にオブジェクトごと回転し、1つは散らばったアイコンをワンクリックでグリッドへ一斉に整列させ、1つは方向キーを押し続けるほど移動幅が徐々に大きくなります。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

このツールキットは、1枚のキャンバス上で9種類の操作をまとめて確認できるように作られています。ドラッグ移動・四隅ハンドルのサイズ調整・回転・二本指タッチ・背景グリッドへの吸着・ワンクリック整列・前後の重なり順変更・矢印キーの微調整・PNGプレビュー書き出しです。9つとも自前で描いたCSS・SVG図形だけで構成され、外部の写真・ロゴ・人物素材は一切使っていません。各パターンは名前の付いた小さな計算関数が動かしており、その中身はこの記事のコード欄でそのまま確認できます。

デモ

ハンドル不要でオブジェクトごとドラッグ移動
新しいタブで開く

01 · ハンドル不要でオブジェクトごとドラッグ移動 — 計測 累積面積 11.417% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

applyTransformonPointerMoveeaseOutCubic

上のパターン01はハンドルを持ちません。オブジェクトの本体を押してそのまま引けば、離した位置で止まります。残り8種はそれぞれのコードブロックの隣に置いています。

9つのパターンの解剖 — どこで計算の一層が回るか

# パターン 累積面積 平均強度 動いたフレーム
01 オブジェクトごとドラッグ 11.42% 126.0 23中14
02 四隅ハンドルのサイズ調整 14.39% 75.6 23中16
03 回転ハンドル 9.46% 86.4 23中19
04 二本指の拡大・回転 9.61% 82.8 23中23
05 グリッドへの吸着 6.41% 136.0 23中10
06 一括整列 14.91% 85.8 23中8
07 前後の重なり順変更 6.11% 88.4 23中9
08 矢印キーの微調整 1.34% 158.8 23中15
09 PNGプレビュー書き出し 10.89% 47.5 23中23

画面をもっとも広く塗り替えるのは一括整列(06)、もっとも狭いのは矢印キーの微調整(08)です。08は面積こそ最小ですが平均強度158.8は9種の中で最も高く、1ピクセル単位の動きが局所的に強く現れていることを示します。逆にもっとも強度が低いのは09のPNGプレビュー書き出しで47.5にとどまります。動いたフレーム数を見ると04と09がそろって23中23で毎フレーム変化しており、06はわずか23中8で完了します。

ポインターを追い続けて変形する系統 — 01・03・02

パターン01・02・03は、いずれも1本のポインターをpointermoveのたびに追跡し続け、そのつど1つの変形値を計算し直します。01のapplyTransformは枠の端でclampした平行移動量を求め、02のapplyScaleはcornerAnchorRectで反対側の角を基準点にしながらMIN_SCALE・MAX_SCALEの範囲にクランプし、03のapplyRotateはangleFromPointerが返すatan2の角度をmod360で包んで回転に変換します。どれもpointerupを待たず、ドラッグの最中にその場で変形が反映されます。

2つの入力から幾何を組み立てる系統 — 04・05・06

パターン04・05・06は、DOMに触れる前に複数の生の座標から目標値を計算しています。04のcomputeFromFingersはシミュレートした2つのポインター位置から距離比と角度差を同時に求めます。05のnearestGridは生の座標をCELL_PXの倍数に丸め、その距離をCORNER_SNAP_RADIUS_PXと比較します。06のgridTargetは散らばった各チップに行・列のセルを割り当て、animateToがeaseOutCubicで滑らかに近づけます。

1回ごとに状態を確定させる系統 — 07・08・09

パターン07・08・09は連続したポインター軌跡を追わず、操作1回につき1つの離散的な状態変化だけを確定させます。07のswapNeighborはorder配列の中でidを1つずらし、その配列の位置をそのままz-indexに割り当て直します。08のcomputeStepは押している時間を加速倍率に変換し、moveByがキーリピートのたびに一定量ずつ移動を確定します。09のgeneratePreviewはroundedRectPathで選択範囲を描き直し、アニメーションループを介さずcanvas.toDataURLで一気にエンコードします。

パターン別コード

以下のコードはすべて、各デモの実際のスクリプトからそのまま切り出したものです。省略も書き直しもしていません。

01ハンドル不要でオブジェクトごとドラッグ移動

選択したオブジェクトの本体のどこを押して引いても、境界の選択ボックスごとポインターに沿って動き、離した位置でそのまま止まります。キャンバス上のステッカーやアイコンの配置を動かすときに向いています。

ハンドル不要でオブジェクトごとドラッグ移動
新しいタブで開く

01 · ハンドル不要でオブジェクトごとドラッグ移動 — 計測 累積面積 11.417% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

applyTransformonPointerMoveeaseOutCubic
  function onPointerDown(e) {
    human();
    dragging = true;
    el.setPointerCapture(e.pointerId);
    startX = e.clientX; startY = e.clientY;
    originTx = tx; originTy = ty;
    el.classList.add('isDragging');
  }
  function onPointerMove(e) {
    if (!dragging) return;
    var dx = e.clientX - startX;
    var dy = e.clientY - startY;
    applyTransform(clamp(originTx + dx, -110, 110), clamp(originTy + dy, -55, 55));
  }
  function onPointerUp(e) {
    if (!dragging) return;
    dragging = false;
    el.classList.remove('isDragging');
    try { el.releasePointerCapture(e.pointerId); } catch (err) {}
  }

02四隅ハンドルで行う比率固定のサイズ調整

選択ボックスの四隅に小さな正方形のドラッグ ハンドル アイコンが付いており、そのうち1つを引くと反対側の角を基点に縦横比を保ったまま拡大・縮小します。250のpanel-corner-resize-gripはレイアウトパネル自体の幅と高さを変えるグリップですが、こちらはパネルではなくキャンバス上のオブジェクト1つに付いた境界ハンドルで、まったく別の実演です。ムードボードや名刺案で画像オブジェクトのサイズを合わせるときに使います。

四隅ハンドルで行う比率固定のサイズ調整
新しいタブで開く

02 · 四隅ハンドルで行う比率固定のサイズ調整 — 計測 累積面積 14.389% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

applyScalecornerAnchorRecthopScale
  function applyScale(corner, s) {
    scale = clamp(s, MIN_SCALE, MAX_SCALE);
    subject.style.transformOrigin = ORIGIN_BY_CORNER[corner];
    subject.style.transform = 'scale(' + scale.toFixed(3) + ')';
    rp.textContent = Math.round(scale * 100);
    handles.forEach(function (h) { h.setAttribute('aria-valuenow', String(Math.round(scale * 100))); });
  }

  function human() {
    if (scene.classList.contains('is-demo')) {
      scene.classList.remove('is-demo');
      timers.forEach(function (t) { clearTimeout(t); clearInterval(t); });
      timers = [];
    }
  }

  function cornerAnchorRect(corner) {
    var r = frame.getBoundingClientRect();
    switch (corner) {
      case 'tl': return { x: r.right, y: r.bottom };
      case 'tr': return { x: r.left, y: r.bottom };
      case 'bl': return { x: r.right, y: r.top };
      default: return { x: r.left, y: r.top };
    }
  }

03選択ボックス上部から伸びる回転ハンドル

選択ボックス上辺中央から短い線が伸び、その先に丸い回転ハンドルが付いています。このハンドルを円軌道で引くと、選択ボックスの中心を軸にその角度だけオブジェクトが回転します。ステッカーやスタンプ風のオブジェクトを斜めに配置したいときに向いています。

選択ボックス上部から伸びる回転ハンドル
新しいタブで開く

03 · 選択ボックス上部から伸びる回転ハンドル — 計測 累積面積 9.46% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

applyRotateangleFromPointermod360
  function applyRotate(deg) {
    angle = mod360(deg);
    rotator.style.transform = 'rotate(' + angle.toFixed(1) + 'deg)';
    rd.textContent = Math.round(angle);
    handle.setAttribute('aria-valuenow', String(Math.round(angle)));
  }

  function human() {
    if (scene.classList.contains('is-demo')) {
      scene.classList.remove('is-demo');
      timers.forEach(function (t) { clearTimeout(t); clearInterval(t); });
      timers = [];
    }
  }

  // 回転ハンドル = atan2 角度 → rotate。フレーム中心(回転しない固定要素)からポインターまでのベクトルで
  // 絶対角度を毎フレーム求め、ROTATE_SNAP_DEG の倍数に丸めてスナップする。
  function angleFromPointer(e) {
    var r = frame.getBoundingClientRect();
    var cx = r.left + r.width / 2;
    var cy = r.top + r.height / 2;
    var raw = Math.atan2(e.clientY - cy, e.clientX - cx) * 180 / Math.PI;
    return mod360(raw + 90);
  }

04二本指で選択オブジェクトだけ拡大・回転

タッチスクリーン上でオブジェクトが1つ選択された状態で、その上に置いた二本指を広げたりつまんだりすると、そのオブジェクトだけが拡大・縮小し、同時に二本指をひねれば同じジェスチャーの中に回転も重なって起こります。247のgesture-pinch-zoom-panは画面全体を拡大・移動するジェスチャーですが、こちらは選択された1つのオブジェクトにのみ適用される別の実演で、モバイルでコラージュのオブジェクトを指先で調整するときに向いています。

二本指で選択オブジェクトだけ拡大・回転
新しいタブで開く

04 · 二本指で選択オブジェクトだけ拡大・回転 — 計測 累積面積 9.606% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

computeFromFingersplaceFingerbindFinger
  function placeFinger(el, pos) {
    var r = frame.getBoundingClientRect();
    el.style.left = (pos.x * r.width) + 'px';
    el.style.top = (pos.y * r.height) + 'px';
  }

  // 2つのポインター間の距離・角度 → scale・rotate。実際の二本指の代わりに2つのポインターイベント(finger A・B)を
  // それぞれシミュレートし、その間の距離比・角度差を subjectShape にそのまま反映する。
  function computeFromFingers() {
    var r = frame.getBoundingClientRect();
    var ax = posA.x * r.width, ay = posA.y * r.height;
    var bx = posB.x * r.width, by = posB.y * r.height;
    var d = Math.hypot(bx - ax, by - ay);
    var a = Math.atan2(by - ay, bx - ax) * 180 / Math.PI;
    if (baseDist === null) { baseDist = d; baseAngle = a; }
    var scale = clamp(d / baseDist, MIN_SCALE, MAX_SCALE);
    var rotate = a - baseAngle;
    subject.style.transform = 'translate(-50%,-50%) rotate(' + rotate.toFixed(1) + 'deg) scale(' + scale.toFixed(3) + ')';
    rp.textContent = Math.round(scale * 100);
    rr.textContent = Math.round(rotate);
    placeFinger(fingerA, posA);
    placeFinger(fingerB, posB);
  }

05移動中に背景グリッドへ吸い付くスナップ

オブジェクトをドラッグしている間、端が背景グリッドの交点に一定距離まで近づくと、指を離す前にその交点へ引き寄せられて吸い付きます。250のpanel-snap-guidesは他のパネルの端に合わせて現れる動的な整列線ですが、こちらは他のオブジェクトではなく固定された背景グリッド1種類だけに反応するグリッド スナップで、まったく別の実演です。複数のアイコンを均等な間隔で並べたいときに使います。

移動中に背景グリッドへ吸い付くスナップ
新しいタブで開く

05 · 移動中に背景グリッドへ吸い付くスナップ — 計測 累積面積 6.406% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

nearestGridapplyPositionhopTo
  // オブジェクトの角(左上)を最も近い格子の交点(CELL_PX の倍数)と比較し、その距離が
  // CORNER_SNAP_RADIUS_PX 以内なら指を離す前に交点へスナップする — 他のオブジェクトではなく
  // 固定された背景グリッド1種類にのみ吸い付く(250のpanel-snap-guidesとは異なる実演)。
  function nearestGrid(v) { return Math.round(v / CELL_PX) * CELL_PX; }

  function applyPosition(rawX, rawY) {
    var nx = nearestGrid(rawX);
    var ny = nearestGrid(rawY);
    var d = Math.hypot(rawX - nx, rawY - ny);
    var snapped = d <= CORNER_SNAP_RADIUS_PX;
    tx = clamp(snapped ? nx : rawX, 0, frameW - 44);
    ty = clamp(snapped ? ny : rawY, 0, frameH - 32);
    draggable.style.transform = 'matrix(1,0,0,1,' + tx.toFixed(1) + ',' + ty.toFixed(1) + ')';
    if (snapped) {
      guideDot.style.transform = 'matrix(1,0,0,1,' + nx.toFixed(1) + ',' + ny.toFixed(1) + ')';
      guideDot.classList.add('isActive');
    } else {
      guideDot.classList.remove('isActive');
    }
    rs.textContent = snapped ? 'snap' : 'free';
    draggable.classList.toggle('isSnapped', snapped);
  }

06散らばったオブジェクトを一括でグリッド整列

ボタンを1つ押すと、画面のあちこちに散らばっていたオブジェクトがそれぞれの位置から同時に動き出し、行と列のそろったグリッドの位置へ滑るように移動して整列します。ムードボードに散らしたアイコンを発表直前にまとめて整理したいときに向いています。

散らばったオブジェクトを一括でグリッド整列
新しいタブで開く

06 · 散らばったオブジェクトを一括でグリッド整列 — 計測 累積面積 14.908% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

gridTargetanimateTotoggleArrange
  function animateTo(targets, onDone) {
    var from = current.map(function (p) { return { x: p.x, y: p.y }; });
    var start = performance.now();
    function step(now) {
      var p = Math.min(1, (now - start) / HOP_MS);
      var e = easeOutCubic(p);
      for (var i = 0; i < chips.length; i++) {
        current[i].x = from[i].x + (targets[i].x - from[i].x) * e;
        current[i].y = from[i].y + (targets[i].y - from[i].y) * e;
      }
      paint();
      if (p < 1) requestAnimationFrame(step);
      else if (onDone) onDone();
    }
    requestAnimationFrame(step);
  }

  function toggleArrange() {
    arranged = !arranged;
    var targets = arranged ? chips.map(function (_, i) { return gridTarget(i); }) : SCATTER;
    status.textContent = arranged ? '整列済み' : '散らばり';
    btn.setAttribute('aria-pressed', String(arranged));
    animateTo(targets);
  }

07重なったオブジェクトを一段ずつ前後させる順序調整

重なりの下にあるオブジェクトを選んで前面へ移動を押すと、それまで覆っていた他のオブジェクトの上へ即座に入れ替わり、重なり順が画面上ですぐに変わります。カードの裏に隠れたステッカーを前面に出したいときに向いています。

重なったオブジェクトを一段ずつ前後させる順序調整
新しいタブで開く

07 · 重なったオブジェクトを一段ずつ前後させる順序調整 — 計測 累積面積 6.113% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

swapNeighborbringToFrontpopCard
  // 配列の並べ替え → z-index: 選択した id を order 配列内で1つ動かし、その配列の位置をそのまま
  // z-index に割り当て直す。スタイル値を直接選ぶのではなく、配列操作の結果として z が決まる。
  function swapNeighbor(dir) {
    var idx = order.indexOf(selectedId);
    var next = idx + dir;
    if (next < 0 || next >= order.length) return;
    var tmp = order[idx];
    order[idx] = order[next];
    order[next] = tmp;
    applyZ();
    popCard(cards[selectedId]);
  }

  function bringToFront(id) {
    var idx = order.indexOf(id);
    order.splice(idx, 1);
    order.push(id);
    selectedId = id;
    applyZ();
    popCard(cards[selectedId]);
  }

08矢印キーで1ピクセルずつ微調整移動

オブジェクトを選択した状態で矢印キーを1回押すたびに、その方向へごく小さな一定距離だけ動きます。押しっぱなしにすると最初の数回は同じ間隔で進んだあと、移動幅が徐々に大きくなっていきます。ずれたオブジェクトをピクセル単位で合わせたいときに使います。

矢印キーで1ピクセルずつ微調整移動
新しいタブで開く

08 · 矢印キーで1ピクセルずつ微調整移動 — 計測 累積面積 1.344% · 動いたフレーム 15/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

computeStepmoveBysimulateHold
  // 押している時間(elapsed) → 加速倍率。押した直後は1倍、ACCEL_WINDOW_MS ごとに1倍ずつ増えて
  // MAX_MULT で止まる — キーを長く押し続けるほど(ブラウザのネイティブキーリピートがこの関数を呼び続ける)遠くまで進む。
  function computeStep(elapsedMs) {
    var mult = 1 + Math.min(Math.floor(elapsedMs / ACCEL_WINDOW_MS), MAX_MULT - 1);
    return { step: Math.max(1, Math.round(KEYBOARD_NUDGE_PX * mult)), mult: mult };
  }

  function moveBy(dx, dy, mult) {
    tx = clamp(tx + dx, 0, frameW - 44);
    ty = clamp(ty + dy, 0, frameH - 32);
    draggable.style.transform = 'matrix(1,0,0,1,' + tx.toFixed(0) + ',' + ty.toFixed(0) + ')';
    rm.textContent = String(mult);
  }

09選択範囲だけを切り出してPNGに書き出すパネル

ボタンを押すと選択ボックスで囲んだ範囲だけをキャンバスに描き直してPNGデータにエンコードし、その結果を書き出しパネルのプレビュー画像としてすぐに表示します。実際のファイル保存やダウンロードは行わない、プレビューまでのシミュレーションです。作った組み合わせをプロフィール画像やバナーとして保存したい場面を想定しています。

選択範囲だけを切り出してPNGに書き出すパネル
新しいタブで開く

09 · 選択範囲だけを切り出してPNGに書き出すパネル — 計測 累積面積 10.889% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

roundedRectPathgeneratePreviewautoPreviewLoop
  // オブジェクトを canvas.toDataURL で書き出す: 画面上の subjectShape と同じ回転・拡大率を ctx の変換で
  // そのまま再現し(対称な算術)、roundRect パスをグラデーションで塗って PNG データURLを作る。
  // 実際のファイル保存・ダウンロード・クリップボード書き込みは行わない — プレビュー画像の生成までをシミュレートする。
  function generatePreview() {
    var w = canvas.width, h = canvas.height;
    ctx.clearRect(0, 0, w, h);
    ctx.save();
    ctx.translate(w / 2, h / 2);
    ctx.rotate(angle * Math.PI / 180);
    ctx.scale(scale, scale);
    var grad = ctx.createLinearGradient(-34, -34, 34, 34);
    grad.addColorStop(0, FILL_A);
    grad.addColorStop(1, FILL_B);
    ctx.fillStyle = grad;
    roundedRectPath(ctx, -34, -34, 68, 68, 14);
    ctx.fill();
    ctx.restore();
    img.src = canvas.toDataURL('image/png');
    status.textContent = '生成済み';
  }

値がUIを変える場所 — スナップ半径・回転スナップ角度・ナッジ加速

05 — CORNER_SNAP_RADIUS_PX

この定数は、引いている角が格子の交点からどれだけ近づけばスナップに切り替わるかをピクセル単位で決めます。値が小さいと自由に動かせる範囲が広く保たれ、大きくすると遠くからでも吸い寄せられる磁石のように働きます。

03 — ROTATE_SNAP_DEG

angleFromPointerが返す角度は、applyRotateに渡される前にすべてこの値の倍数へ丸められます。間隔が広いとカチカチと段階的に決まる回転になり、狭めるとより細かい中間角度まで止められるようになります。

08 — KEYBOARD_NUDGE_PX

computeStepは、この基本移動量にそのときの加速倍率を掛けてキーリピートのたびに返します。値を大きくすると、加速が始まる前の最初の1回目の押下から移動幅が大きく変わります。

どこで壊れるか — アクセシビリティ・reduced-motion・キーボードフォーカス

パターン04の二本指ハンドルはどちらもrole="slider"で、HTML内には静的なaria-valuenow="50"が書かれています。ところが02・03のハンドルが変化のたびに自分のapply関数の中で.setAttribute('aria-valuenow', value)を呼び直しているのに対し、04のスクリプトはページが読み込まれたあと一度もこの属性を更新しません。ピンチ操作でオブジェクトを実際にどれだけ拡大・回転させても、スクリーンリーダーはどちらの指ハンドルに触れても最初の値をそのまま読み上げ続けます。

reduced-motionで失われるもの

  • 01 — 待機中にウェイポイント間を自動で跳ねる動きが止まります。実際のポインタードラッグはそのままオブジェクトを動かします。
  • 02 — デモ自身が自動で繰り返すリサイズ演出が止まります。角のハンドルを引く操作はそのまま即座に反映されます。
  • 03 — ダイヤルを自動で一周する演出が止まります。回転ハンドルを引く操作はそのまま即座に反映されます。
  • 04 — シミュレートされたピンチ・回転ループが止まります。実際の二本指入力は遅延なくそのまま反映されます。
  • 05 — 自動でドラッグして吸着させるデモ演出が止まります。手動でのドラッグは変わらず同じようにグリッドへ吸い付きます。
  • 06 — toggleArrange自体にアニメーション以外の時間依存処理はないため、クリックすれば滑らかなイージングなしで整列が即座に切り替わります。
  • 07 — 前面へ・背面へを自動で繰り返す演出が止まります。カードをクリックする操作はそのまま即座に順序を変えます。
  • 08 — どちらの設定でも変わりません。パターン08はそもそもrequestAnimationFrameを一度も呼んでおらず、キー入力による移動量は設定にかかわらず同じです。
  • 09 — 自動プレビュー更新(AUTO_PREVIEW_MS)が止まります。書き出しボタンを押す操作はそのまま即座にPNGプレビューを描き直します。

9つのパターンすべてのマークアップ・スクリプト・トークン化されたSCSSは下のzipに入っており、パスワード8eh278nqで開くことができます。

3つのバリエーションとアクセシビリティ

バリエーション 変えた値 感触
コーナースナップの磁力半径を広げる 05 CORNER_SNAP_RADIUS_PX 6 → 16 格子の交点からより離れた位置でもスナップが働くようになり、自由に動かせる範囲が狭まってより簡単に吸い付くようになる。
回転ハンドルの角度スナップ間隔を狭める 03 ROTATE_SNAP_DEG 15 → 5 ハンドルを回すたびに跳ぶ角度が小さくなり、より細かく精密な回転調整が可能になる。
キーボードナッジの移動量を増やす 08 KEYBOARD_NUDGE_PX 1 → 8 最初の1回から大きく動くようになり、1ピクセル単位の精密合わせよりも素早い大幅移動向けの挙動になる。

9つのデモのうち、スライダーのように振る舞うハンドル(02・03・04)はrole="slider"とaria-valuemin・aria-valuemax・aria-valuenowを持ち、07の並べ替えカードはrole="button"とtabindex="0"を持つのですべてのカードがキーボードだけで到達でき、06・07・09のステータス表示はaria-live="polite"の領域に入っているため、フォーカスを移さなくても「整列済み」や並べ替え結果、「生成済み」がスクリーンリーダーに読み上げられます。

FAQ

パターン04の二本指ジェスチャーは、ブラウザ標準のピンチズームと同じ動きですか

いいえ。このピンチズームは選択したキャンバスオブジェクトだけに閉じており、computeFromFingersがシミュレートした2つのポインター位置から計算しています。ページ全体のズーム率やスクロール位置には一切触れないため、画面全体を動かす247のgesture-pinch-zoom-panとは別の実演です。

パターン09を押すと、本当にPNGファイルが保存されるのですか

いいえ。generatePreviewはcanvas.toDataURL('image/png')を呼び、その結果をプレビュー用の<img>に渡しているだけです。プレビュー画像を表示するところまでのシミュレーションで、ディスクへの保存やダウンロード、クリップボードへのコピーは一切行われません。

パターン08の矢印キー移動は、なぜ押し続けると速くなるのですか

computeStepが押している時間をMAX_MULTを上限とする加速倍率に変換しているため、同じ矢印キーでもネイティブのキーリピートが続くほど1回あたりの移動量が増えていきます。遠くまで一気に動かしたあと、最後は押しっぱなしではなく1回ずつ押してピクセル単位に合わせる、という使い分けができます。

出典: Pointer events · CSS transform · touch-action · HTMLCanvasElement.toDataURL · keydown event

関連記事: 画面分割 css レイアウト9選 — ドラッグで幅が変わる · 画像 編集 javascript のトリミングUI 9選 · スマホ ジェスチャー操作 9選 — 指で本当に動くUI

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。