LoginSignup
14
0

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

Last updated at Posted at 2023-12-23

0. 目次

  • 1 前提
    • 記事を読むにあたっての説明
    • 読み飛ばしてOK
    • 実装してGitHubに公開したので紹介
  • 2 基準
    • 各種基準について、特徴と実装例を画像とともに説明
    • 社内勉強会で計算幾何学との関連をご教示いただいたので調査中
  • 3 Tips
    • 実装して得た知見を紹介
    • ポインターイベント以外による実装方法について調査中

1. 前提

1.1. 導入

本記事の目的

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

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

想定読者

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

1.2. 単語解説

DnD 図解
図2 DnD 図解

マウスカーソル

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

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

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

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

ホットスポット

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

タッチデバイスにおいては、圧力などによって計算された、力の中心地がホットスポットの扱いになります[要出典]。

ドラッグ要素

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

ドロップエリア

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

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. [調査中] 計算幾何学の活用

計算幾何学における交差判定や包含判定について調査中
React-Three-Fiber を使用すると良さそう?

3. Tips

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

先行研究

HTML5 Drag and Drop API

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

SortableJS

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

[調査中] dnd-kit

調査中

[調査中] react-draggable

調査中

[調査中] react-rnd

調査中

[調査中] react-beautiful-dnd

調査中

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. 再レンダリングを抑えた実装

14
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
14
0