]> git.cworth.org Git - lmno.games/commitdiff
Add support for dragging tiles while claiming
authorCarl Worth <cworth@cworth.org>
Sun, 8 Mar 2026 13:20:25 +0000 (09:20 -0400)
committerCarl Worth <cworth@cworth.org>
Sun, 8 Mar 2026 13:20:25 +0000 (09:20 -0400)
This can be either to drag tiles to the rack (to claim them) or to
just rearrange the tiles (as can already be done outside of
claiming). A 5-pixel threshold of movement will trigger a drag
operation. Movement less than that will be considered a click and will
claim the letter.

anagrams/anagrams.jsx

index 0d2397da4aa239112c5709d3f6d0102fa286e3d7..14b4d054f0cebbea3f0302cdc23970c66e651894 100644 (file)
@@ -596,23 +596,43 @@ class Game extends React.Component {
    * Center tile dragging (client-side rearrangement)  *
    *****************************************************/
 
+  _drop_on_rack(clientX, clientY, tile) {
+    if (!this.state.claim_active) return false;
+    const rack = document.querySelector(".claim-rack");
+    if (!rack) return false;
+    const r = rack.getBoundingClientRect();
+    if (clientX >= r.left && clientX <= r.right
+        && clientY >= r.top && clientY <= r.bottom) {
+      this.take_letter(tile);
+      return true;
+    }
+    return false;
+  }
+
   on_center_mouse_down(e, tile) {
-    /* Only allow rearranging when not in claim mode. */
-    if (this.state.claim_active) return;
     if (e.button !== 0) return;
 
     const pool = e.target.closest(".center-pool");
     if (!pool) return;
 
     const rect = pool.getBoundingClientRect();
+    const startX = e.clientX;
+    const startY = e.clientY;
     this._drag_offset = {
       x: e.clientX - (this.state.tile_positions[tile.id].x / 100 * rect.width),
       y: e.clientY - (this.state.tile_positions[tile.id].y / 100 * rect.height)
     };
     this._drag_pool_rect = rect;
-    this.setState({ dragging_center_tile: tile.id });
+    let dragging = false;
 
     const onMove = (me) => {
+      const dx = me.clientX - startX;
+      const dy = me.clientY - startY;
+      if (!dragging) {
+        if (dx * dx + dy * dy < 25) return;
+        dragging = true;
+        this.setState({ dragging_center_tile: tile.id });
+      }
       const x = ((me.clientX - this._drag_offset.x) / rect.width) * 100;
       const y = ((me.clientY - this._drag_offset.y) / rect.height) * 100;
       this.setState(prev => ({
@@ -626,10 +646,15 @@ class Game extends React.Component {
       }));
     };
 
-    const onUp = () => {
+    const onUp = (me) => {
       document.removeEventListener("mousemove", onMove);
       document.removeEventListener("mouseup", onUp);
-      this.setState({ dragging_center_tile: null });
+      if (dragging) {
+        this.setState({ dragging_center_tile: null });
+        this._drop_on_rack(me.clientX, me.clientY, tile);
+      } else if (this.state.claim_active && !this.state.revealing[tile.id]) {
+        this.take_letter(tile);
+      }
     };
 
     document.addEventListener("mousemove", onMove);
@@ -638,7 +663,6 @@ class Game extends React.Component {
   }
 
   on_center_touch_start(e, tile) {
-    if (this.state.claim_active) return;
     if (e.touches.length !== 1) return;
 
     const pool = e.target.closest(".center-pool");
@@ -646,29 +670,38 @@ class Game extends React.Component {
 
     const touch = e.touches[0];
     const rect = pool.getBoundingClientRect();
+    this._drag_start = { x: touch.clientX, y: touch.clientY };
     this._drag_offset = {
       x: touch.clientX - (this.state.tile_positions[tile.id].x / 100 * rect.width),
       y: touch.clientY - (this.state.tile_positions[tile.id].y / 100 * rect.height)
     };
     this._drag_pool_rect = rect;
-    this._touch_moved = false;
-    this.setState({ dragging_center_tile: tile.id });
+    this._drag_dragging = false;
+    this._drag_tile = tile;
 
     e.preventDefault();
   }
 
   on_center_touch_move(e) {
-    if (this.state.dragging_center_tile === null) return;
+    if (!this._drag_tile) return;
     const touch = e.touches[0];
+    this._drag_last_touch = { x: touch.clientX, y: touch.clientY };
     const rect = this._drag_pool_rect;
-    this._touch_moved = true;
+
+    if (!this._drag_dragging) {
+      const dx = touch.clientX - this._drag_start.x;
+      const dy = touch.clientY - this._drag_start.y;
+      if (dx * dx + dy * dy < 25) return;
+      this._drag_dragging = true;
+      this.setState({ dragging_center_tile: this._drag_tile.id });
+    }
 
     const x = ((touch.clientX - this._drag_offset.x) / rect.width) * 100;
     const y = ((touch.clientY - this._drag_offset.y) / rect.height) * 100;
     this.setState(prev => ({
       tile_positions: {
         ...prev.tile_positions,
-        [this.state.dragging_center_tile]: {
+        [this._drag_tile.id]: {
           x: Math.max(0, Math.min(90, x)),
           y: Math.max(0, Math.min(85, y))
         }
@@ -678,8 +711,21 @@ class Game extends React.Component {
   }
 
   on_center_touch_end(e) {
-    if (this.state.dragging_center_tile === null) return;
-    this.setState({ dragging_center_tile: null });
+    const tile = this._drag_tile;
+    if (!tile) return;
+
+    if (this._drag_dragging) {
+      this.setState({ dragging_center_tile: null });
+      /* Use last known touch position for drop detection. */
+      if (this._drag_last_touch) {
+        this._drop_on_rack(this._drag_last_touch.x, this._drag_last_touch.y, tile);
+      }
+    } else if (this.state.claim_active && !this.state.revealing[tile.id]) {
+      this.take_letter(tile);
+    }
+
+    this._drag_tile = null;
+    this._drag_dragging = false;
   }
 
   /*****************************************************
@@ -791,8 +837,7 @@ class Game extends React.Component {
             left: pos.x + "%",
             top: pos.y + "%",
             zIndex: is_dragging ? 10 : 1,
-            cursor: claim_active && !is_revealing ? "pointer" :
-                    !claim_active && !is_revealing ? "grab" : "default"
+            cursor: claim_active ? "pointer" : "grab"
           };
 
           let className = "";
@@ -804,12 +849,8 @@ class Game extends React.Component {
             <div key={tile.id}
                  className={"tile" + (className ? " " + className : "")}
                  style={style}
-                 onMouseDown={!claim_active && !is_revealing
-                   ? (e) => this.on_center_mouse_down(e, tile) : null}
-                 onTouchStart={!claim_active && !is_revealing
-                   ? (e) => this.on_center_touch_start(e, tile) : null}
-                 onClick={claim_active && !is_revealing
-                   ? () => this.take_letter(tile) : null}>
+                 onMouseDown={(e) => this.on_center_mouse_down(e, tile)}
+                 onTouchStart={(e) => this.on_center_touch_start(e, tile)}>
               {is_revealing
                 ? <span style={{opacity: rev.opacity}}>{rev.seconds}</span>
                 : tile.letter}