17
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ドロップ対象を識別する基準のアイデア[TypeScript+React]

17
Last updated at Posted at 2023-12-23

0. 目次

  • 1 前提
    • 記事を読むにあたっての説明
    • 読み飛ばしてOK
    • 実装してGitHubに公開したので紹介
  • 2 基準
    • 各種基準について、特徴と実装例を画像とともに説明
    • 計算幾何学の活用(dnd-kitの衝突検出アルゴリズム)
  • 3 Tips
    • 実装して得た知見を紹介
    • 他ドラッグ実装方式の検討(dnd-kit、react-draggable等)
    • 再レンダリングを抑えた実装

1. 前提

1.1. 導入

本記事の目的

ドラッグ・アンド・ドロップ(以降: DnD)におけるドロップ対象の識別では、単純なマウスカーソルの重なり検知と異なり、多くの基準が考えられます。

本記事では、React+TypeScript環境でポインターイベントを使用し、ドロップ対象を識別する複数の基準を紹介します。
各基準のメリットや実装方法を詳しく探ることで、より洗練されたインタラクションの実現を目指します。

想定読者

  • この令和でDnDを一から実装する人
  • ユーザーのことを考えたDnDの実装方法を考えている人

1.2. 単語解説

DnD 図解
図2 DnD 図解

マウスカーソル

コンピューターの操作画面で入力位置を示すカーソルのひとつで、マウス操作に対応する、矢印の形をしたアイコンのことである。

タッチデバイスにおいては、マウスカーソルが存在しないことがあります。

ちなみに、マウスカーソルは左右対称ではありません。

この画像の出典がこのツイートで合っているか、ご存じの方がいらっしゃいましたら、ご教示ください。

ホットスポット

マウスカーソルの絵柄の中で、クリックの対象位置となる部分。
マウスカーソルの形状によって、ホットスポットの位置は異なる。
たとえば、矢印の形をしているマウスカーソルでは、矢印の先端がホットスポットとなる。

タッチデバイスにおいては、タッチ位置の中心がホットスポットとして扱われます1

ドラッグ要素

ドラッグ要素とは、ユーザーがマウスやタッチでドラッグし、移動させている要素のことを指します。

ドロップエリア

ドロップエリアとは、ドラッグ要素をドロップできる領域のことを指します。

1.3. 実装例について

今回の実装では、複数のドロップ対象を一意には識別していません。
ただし、優先順位を設けることで、一意に識別できます。
優先順位のアイデアとしては以下が考えられます:

  • それぞれのドロップ対象の中心点のうち、最もドラッグ要素の中心点との距離が短い
  • それぞれのドロップ対象の左上の点のうち、最も座標が左上

また、いずれも pointermove のハンドラ内に実装しています。
ただし、requestAnimationFrameを使用したほうが動作が軽量になる場合があります。

コードは弊社の GitHub に配置しており、 GitHub Pages から確認できます。

2. 基準

2.1. ポインターの座標

特徴

  • 最も簡単
  • PCではマウスカーソルがあるため直感的に思える
  • SPなどマウスカーソルのないタッチデバイスでは、指でタッチ箇所が隠れて見づらい
基準:ポインターの座標
図3 基準:ポインターの座標

実装例

// locClient(ホットスポット座標) が dropRect(ドロップ対象) の内側にあるか判定
return (
  locClient.x >= dropRect.left &&
  locClient.x <= dropRect.right &&
  locClient.y >= dropRect.top &&
  locClient.y <= dropRect.bottom
);

2.2. ドラッグ要素の中心座標

特徴

  • 主体がマウスや指ではなくドラッグ要素になる
  • ホットスポットがドロップ対象の中に入ってなくても良い
基準:ドラッグ要素の中心座標
図4 基準:ドラッグ要素の中心座標

実装例

// centerX,Y(ドラッグ要素の中心座標) が dropRect(ドロップ対象) の内側にあるか判定
const centerX = locRect.x + sizRect.width / 2;
const centerY = locRect.y + sizRect.height / 2;
return (
  locScroll.x + dropRect.left <= centerX &&
  locScroll.x + dropRect.right >= centerX &&
  locScroll.y + dropRect.top <= centerY &&
  locScroll.y + dropRect.bottom >= centerY
);

2.3. 重なっている面積の割合

特徴

  • 複数の識別に向く
  • 包含判定の算出が困難
    • 単純な四角形でない場合は交差オブザーバーの使用を検討

基準:重なっている面積の割合 (25%)
図5 基準:重なっている面積の割合 (25%)

実装例 (25%)

// dropArea(ドロップ対象の面積) を算出
const dropArea = dropRect.width * dropRect.height;
// overlapWidth(包含する横幅) を算出
const overlapWidth =
  Math.min(locRect.x + sizRect.width, locScroll.x + dropRect.right) -
  Math.max(locRect.x, locScroll.x + dropRect.left);
// 包含しない場合は終了
if (overlapWidth <= 0) return false;
// overlapHeight(包含する縦幅) を算出
const overlapHeight =
  Math.min(locRect.y + sizRect.height, locScroll.y + dropRect.bottom) -
  Math.max(locRect.y, locScroll.y + dropRect.top);
// 包含しない場合は終了
if (overlapHeight <= 0) return false;
// 包含面積がドロップ対象の面積の25%以上なら包含判定
return overlapWidth * overlapHeight >= dropArea * 0.25;

2.4. ドラッグ要素内のいずれかの点

特徴

  • 実装方法はドラッグ要素の中心座標と概ね同じ
  • ドラッグ要素の形状によっては直感的になる
基準:ドラッグ要素内のいずれかの点 (中上)
図6 基準:ドラッグ要素内のいずれかの点 (中上)

実装例 (中上)

// 中上の点がドロップ対象に含まれるか判定
const centerX = locRect.x + sizRect.width / 2;
const topY = locRect.y;
return (
  locScroll.x + dropRect.left <= centerX &&
  locScroll.x + dropRect.right >= centerX &&
  locScroll.y + dropRect.top <= topY &&
  locScroll.y + dropRect.bottom >= topY
);

2.5. ドロップ対象の中心とドラッグ要素の中心の距離

特徴

  • 小さい要素でも広い範囲を見ることができる
  • 光っているドラッグ要素などで出番有?
  • 割りと実装が面倒
基準:ドロップ対象の中心とドラッグ要素の中心の距離
図7 基準:ドロップ対象の中心とドラッグ要素の中心の距離

実装例

// ドラッグ要素の中心座標を算出
const dragCenterX = locRect.x + sizRect.width / 2;
const dragCenterY = locRect.y + sizRect.height / 2;
// ドロップ対象の中心座標を算出
const dropCenterX = locScroll.x + dropRect.x + dropRect.width / 2;
const dropCenterY = locScroll.y + dropRect.y + dropRect.height / 2;
// 2点の距離を算出
const distance =
  (dragCenterX - dropCenterX) ** 2 + (dragCenterY - dropCenterY) ** 2;
// 2点の距離が 2^7 px なら包含判定
return distance <= 16384; // 2^14

2.6. 速度から移動先を予測

特徴

  • ドラッグの動き自体へ反応させたい場合に使用
  • ゲームやインタラクティブなコンテンツでの活用
基準:速度から移動先を予測
図8 基準:速度から移動先を予測 (赤矢印線はドラッグ要素の移動方向)

実装例

// 次に予測する座標がドロップ対象に含まれるか判定
const nextDragCenterX = locNext.x + sizRect.width / 2;
const nextDragCenterY = locNext.y + sizRect.height / 2;
return (
  locScroll.x + dropRect.left <= nextDragCenterX &&
  locScroll.x + dropRect.right >= nextDragCenterX &&
  locScroll.y + dropRect.top <= nextDragCenterY &&
  locScroll.y + dropRect.bottom >= nextDragCenterY
);

2.7. 計算幾何学の活用

本記事で紹介した各基準は、計算幾何学における交差判定や包含判定に基づいています。
dnd-kitはこれらの判定をライブラリとして提供しており、以下の衝突検出アルゴリズムが組み込まれています2

アルゴリズム 判定方法
closestCenter ドラッグ要素とドロップ対象の中心点間の距離で判定
closestCorners ドラッグ要素とドロップ対象の四隅間の距離で判定
rectIntersection 矩形の重なり(交差)で判定
pointerWithin ポインター位置がドロップ対象の範囲内にあるかで判定

これらのアルゴリズムは合成が可能で、たとえばpointerWithinで衝突がなければrectIntersectionにフォールバックする構成も実現できます。
また、カスタムアルゴリズムを実装して独自の判定ロジックを組み込むこともできます。

3. Tips

3.1. 他ドラッグ実装方式の検討

先行研究

HTML5 Drag and Drop API

タッチデバイスでうまくいきませんでした。

SortableJS

タッチデバイスでも同様の操作感で使用できました。
※ react-sortablejs を使用しています。

dnd-kit

現在のReact DnDライブラリで最も推奨される選択肢です3
フック型API(useDraggableuseDroppable)を提供し、リスト・グリッド・ツリー・ネストなど多様なレイアウトに対応しています。
衝突検出アルゴリズムのカスタマイズも可能です(2.7節参照)。

react-draggable

シンプルなドラッグ専用ライブラリです4
CSS Transformで要素を移動させる仕組みで、ドロップ対象の判定機能は持っていません。
本記事のテーマであるドロップ領域の識別とは直接関係しませんが、単純なドラッグUIの実装には有用です。

react-rnd

リサイズとドラッグを同時に実現するコンポーネントです5
ウィンドウやパネルなど、サイズ変更可能なUI要素の実装に適しています。

react-beautiful-dnd

Atlassianが開発したDnDライブラリですが、2024年に非推奨が宣言され、2025年8月にリポジトリがアーカイブされました6
コミュニティフォークの@hello-pangea/dndがReact 18以降に対応した後継として利用できます7
新規プロジェクトではdnd-kitの使用を推奨します。

3.2. タッチデバイス用の対応まとめ

ドラッグ操作とスクロール操作の競合回避

以下を設定するとスクロール操作やピンチ操作などを無効化できます。

.draggable {
  touch-action: none;
}

iOS のメニューを無効化

以下の設定で、iOSデバイスにおける長押し時のコールアウトを無効化できます。

body {
  -webkit-touch-callout: none;
}

長押し時の ハイライトカラーを無効化

以下を設定すると iOS および Android で長押し時のハイライトカラーを無効化できます。

body {
  -webkit-tap-highlight-color: rgba(0 0 0 / 0%);
}

3.3. パッシブモード

touchmove イベントのリスナーは、デフォルトでパッシブモードになっています。
これをオーバーライドするには、イベントリスナーを以下のように追加します。

element.addEventListener("touchmove", handleTouchMove, { passive: false });

3.4. 再レンダリングを抑えた実装

DnDではpointermoveイベントが高頻度で発火するため、ドラッグ位置をReactのstateで管理すると不要な再レンダリングが発生します。
useRefでDOM操作を直接行う方法や、requestAnimationFrameでレンダリングを間引く方法が有効です。

脚注

  1. https://developer.mozilla.org/en-US/docs/Web/API/Touch

  2. https://docs.dndkit.com/api-documentation/context-provider/collision-detection-algorithms

  3. https://dndkit.com/

  4. https://github.com/react-grid-layout/react-draggable

  5. https://github.com/bokuweb/react-rnd

  6. https://github.com/atlassian/react-beautiful-dnd

  7. https://github.com/hello-pangea/dnd

17
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?