solitaire: let cards be dragged, not only tapped
Tapping stays exactly as it was. A press that travels more than a few pixels becomes a drag instead, and both paths go through the same pick() and accepts(), so what lights up and what a move means never depends on how you did it. The run follows the pointer as a copy while the real cards stay on the felt, faded, and letting go over nothing keeps the run in your hand with the targets still lit. Capture happens when the press becomes a drag, not on pointerdown: capturing early retargets the click that a tap ends with, and tapping quietly stops working.
This commit is contained in:
@@ -603,6 +603,7 @@
|
||||
// something else. Which means you never have to put a card down before choosing
|
||||
// a different one.
|
||||
root.querySelector(".pete-felt").addEventListener("click", function (e) {
|
||||
if (swallowClick) { swallowClick = false; return; } // that was the end of a drag
|
||||
if (busy || !board || board.phase !== "playing") return;
|
||||
if (e.target.closest("[data-stock]")) return; // the stock has its own handler
|
||||
|
||||
@@ -633,6 +634,143 @@
|
||||
}
|
||||
});
|
||||
|
||||
// ---- dragging --------------------------------------------------------------
|
||||
//
|
||||
// Tapping is the friendly way to play and it stays exactly as it was. Dragging
|
||||
// is the *other* way, and some hands want it: picking a run up and putting it
|
||||
// down is one gesture rather than two, and it's the one every physical deck of
|
||||
// cards has taught. The two share everything below the surface — a drag picks a
|
||||
// run up with the same pick() and asks the same accepts() — so what lights up
|
||||
// and what a move means never depends on how you did it.
|
||||
//
|
||||
// Nothing happens until the pointer has actually moved. Under the threshold this
|
||||
// is a tap and the click handler above gets it untouched; over it, the click that
|
||||
// browsers fire after a drag is swallowed so a drag never also counts as a tap.
|
||||
var SLOP = 6; // px of travel before a press becomes a drag
|
||||
|
||||
var drag = null; // {pile, idx, x0, y0, dx, dy, ghost, live}
|
||||
var swallowClick = false;
|
||||
|
||||
// ghost is the run in your hand, drawn once and moved with the pointer. It's a
|
||||
// copy: the real cards stay on the felt, faded, so you can see where you lifted
|
||||
// from and where you'd be putting it back.
|
||||
function ghostFor(cards, from) {
|
||||
var felt = getComputedStyle(root.querySelector(".pete-felt"));
|
||||
var g = document.createElement("div");
|
||||
g.className = "pete-drag";
|
||||
["--card-w", "--card-h", "--fan-up", "--fan-down"].forEach(function (v) {
|
||||
g.style.setProperty(v, felt.getPropertyValue(v));
|
||||
});
|
||||
|
||||
var col = document.createElement("div");
|
||||
col.className = "pete-col";
|
||||
cards.forEach(function (c) {
|
||||
var el = CARDS.el(c, { deal: false, tilt: false });
|
||||
col.appendChild(el);
|
||||
});
|
||||
g.appendChild(col);
|
||||
g.style.width = from.width + "px";
|
||||
document.body.appendChild(g);
|
||||
return g;
|
||||
}
|
||||
|
||||
function dragTo(x, y) {
|
||||
drag.ghost.style.transform =
|
||||
"translate3d(" + (x - drag.dx) + "px," + (y - drag.dy) + "px,0)";
|
||||
|
||||
// What's under the pointer, and would it take this? The ghost doesn't answer
|
||||
// to hit tests, so this sees the felt underneath it.
|
||||
var under = document.elementFromPoint(x, y);
|
||||
var pileEl = under && under.closest ? under.closest("[data-pile]") : null;
|
||||
var pile = pileEl ? pileEl.dataset.pile : null;
|
||||
if (pileEl && pileEl.classList.contains("pete-card")) {
|
||||
pileEl = pileEl.parentElement && pileEl.parentElement.closest("[data-pile]");
|
||||
}
|
||||
var ok = pile && pile !== drag.pile && accepts(pile, held.cards);
|
||||
|
||||
root.querySelectorAll('[data-over="1"]').forEach(function (el) { delete el.dataset.over; });
|
||||
if (ok && pileEl) pileEl.dataset.over = "1";
|
||||
drag.ghost.dataset.ok = ok ? "1" : "0";
|
||||
drag.target = ok ? pile : null;
|
||||
drag.targetEl = ok ? pileEl : null;
|
||||
}
|
||||
|
||||
function dragEnd(commit) {
|
||||
if (!drag) return;
|
||||
var d = drag;
|
||||
drag = null;
|
||||
root.querySelectorAll('[data-over="1"]').forEach(function (el) { delete el.dataset.over; });
|
||||
delete root.dataset.dragging;
|
||||
if (d.ghost) d.ghost.remove();
|
||||
if (!d.live) return; // never crossed the threshold: it was a tap
|
||||
|
||||
swallowClick = true;
|
||||
setTimeout(function () { swallowClick = false; }, 0); // in case no click follows
|
||||
|
||||
if (commit && d.target && held) {
|
||||
var move = { kind: "move", from: d.pile, to: d.target, count: held.count };
|
||||
drop();
|
||||
send(move);
|
||||
return;
|
||||
}
|
||||
// Dropped on nothing, or somewhere it doesn't go. The run goes back where it
|
||||
// came from and stays in your hand, so the drop targets are still lit and a
|
||||
// tap can finish what the drag started.
|
||||
if (commit && d.targetEl) nope(d.targetEl);
|
||||
}
|
||||
|
||||
root.querySelector(".pete-felt").addEventListener("pointerdown", function (e) {
|
||||
if (busy || !board || board.phase !== "playing") return;
|
||||
if (e.button !== 0 && e.pointerType === "mouse") return;
|
||||
var cardEl = e.target.closest('.pete-card[data-live="1"]');
|
||||
if (!cardEl) return;
|
||||
|
||||
var r = cardEl.getBoundingClientRect();
|
||||
drag = {
|
||||
pile: cardEl.dataset.pile,
|
||||
idx: parseInt(cardEl.dataset.idx, 10),
|
||||
x0: e.clientX,
|
||||
y0: e.clientY,
|
||||
dx: e.clientX - r.left,
|
||||
dy: e.clientY - r.top,
|
||||
width: r.width,
|
||||
live: false,
|
||||
target: null,
|
||||
targetEl: null,
|
||||
id: e.pointerId,
|
||||
el: cardEl,
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelector(".pete-felt").addEventListener("pointermove", function (e) {
|
||||
if (!drag || e.pointerId !== drag.id) return;
|
||||
|
||||
if (!drag.live) {
|
||||
if (Math.abs(e.clientX - drag.x0) < SLOP && Math.abs(e.clientY - drag.y0) < SLOP) return;
|
||||
// It's a drag. Pick the run up — the same pick a tap would have made — and
|
||||
// if it isn't liftable there's nothing to drag and this goes back to being
|
||||
// an ordinary press.
|
||||
pick(drag.pile, drag.idx);
|
||||
if (!held || held.pile !== drag.pile) { drag = null; return; }
|
||||
drag.live = true;
|
||||
drag.ghost = ghostFor(held.cards, drag);
|
||||
root.dataset.dragging = "1";
|
||||
// Capture only now that it's a drag, so the pointer stays ours even off the
|
||||
// felt. Doing it on the press instead would retarget the click that a *tap*
|
||||
// ends with, and tapping would quietly stop working.
|
||||
try { drag.el.setPointerCapture(e.pointerId); } catch (_) {}
|
||||
}
|
||||
e.preventDefault();
|
||||
dragTo(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
root.querySelector(".pete-felt").addEventListener("pointerup", function (e) {
|
||||
if (drag && e.pointerId === drag.id) dragEnd(true);
|
||||
});
|
||||
root.querySelector(".pete-felt").addEventListener("pointercancel", function (e) {
|
||||
if (drag && e.pointerId === drag.id) dragEnd(false);
|
||||
});
|
||||
|
||||
// Double-click sends a card home. It's the idiom every solitaire has used for
|
||||
// thirty years, and the alternative is asking the player which foundation — a
|
||||
// question with exactly one right answer.
|
||||
|
||||
Reference in New Issue
Block a user