Architecture & Core Concept
The HTML5 Drag and Drop API has inconsistent mobile support. Pointer Events (pointerdown, pointermove, pointerup) offer 100% reliable cross-device dragging. When a draggable piece is pressed: 1. el.setPointerCapture(e.pointerId) locks touch tracking to the element 2. Track offset: dx = e.clientX - rect.left, dy = e.clientY - rect.top 3. On pointermove, update style.left and style.top 4. Check if pointer is inside any target bounding box using getBoundingClientRect() 5. On drop, verify whether data-key matches. If yes, snap into place!
Key Game Mechanics Taught:
- HTML5 Pointer Events vs legacy HTML5 Drag-and-Drop API
- setPointerCapture for tracking touches outside element boundaries
- Bounding box containment math (checking if x,y falls within target)
- Error bounce/shake CSS animations for wrong placements
- Procedural CSS confetti explosions on victory
Step-by-Step Implementation
Initialize Dragging with Pointer Events
Using setPointerCapture guarantees pointermove events will continue firing even if a fast swipe leaves the element boundaries.
let activeDrag = null;
function startDrag(e) {
const el = e.currentTarget;
if (el.classList.contains('matched')) return;
e.preventDefault();
const rect = el.getBoundingClientRect();
activeDrag = {
el,
key: el.dataset.key,
offsetX: e.clientX - rect.left,
offsetY: e.clientY - rect.top
};
el.classList.add('dragging');
el.setPointerCapture?.(e.pointerId);
el.addEventListener('pointermove', onDragMove);
el.addEventListener('pointerup', onDragEnd, { once: true });
}
Check Target Collision in Real-Time
On every pointermove, inspect target elements to highlight the one currently hovered underneath the finger.
function isInside(x, y, rect) {
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
}
function onDragMove(e) {
if (!activeDrag) return;
activeDrag.el.style.left = (e.clientX - activeDrag.offsetX) + 'px';
activeDrag.el.style.top = (e.clientY - activeDrag.offsetY) + 'px';
document.querySelectorAll('.target').forEach(target => {
const isHovered = isInside(e.clientX, e.clientY, target.getBoundingClientRect());
target.classList.toggle('highlighted', isHovered);
});
}
Validate Drop & Error Shake Animation
If dropped on the wrong target, play a quick keyframe shake animation and reset the piece to its original position.
function onDragEnd(e) {
if (!activeDrag) return;
const { el, key } = activeDrag;
const validTarget = [...document.querySelectorAll('.target')].find(t =>
isInside(e.clientX, e.clientY, t.getBoundingClientRect())
);
if (validTarget && validTarget.dataset.key === key) {
el.classList.add('matched');
validTarget.classList.add('done');
playSuccessSound();
} else {
// Wrong target: shake and return
el.animate([
{ transform: 'translateX(-8px)' },
{ transform: 'translateX(8px)' },
{ transform: 'none' }
], { duration: 260 });
}
activeDrag = null;
}
Complete Standalone Source Code
Here is the full, self-contained implementation. Save this as an index.html file on your computer and open it in any web browser to run your game immediately:
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Drag & Match Demo</title>
<style>
body { margin:0; background:#fdf4ff; display:flex; flex-direction:column; align-items:center; padding:30px; font-family:sans-serif; }
.container { display:flex; justify-content:space-between; width:500px; margin-top:40px; }
.pieces, .targets { display:flex; flex-direction:column; gap:16px; }
.piece { width:80px; height:80px; background:#a855f7; color:#fff; font-size:36px; display:flex; align-items:center; justify-content:center; border-radius:12px; cursor:grab; touch-action:none; user-select:none; }
.piece.dragging { position:fixed; pointer-events:none; z-index:999; }
.piece.matched { opacity:0.3; cursor:default; }
.target { width:120px; height:80px; border:3px dashed #d8b4fe; border-radius:12px; display:flex; align-items:center; justify-content:center; font-size:20px; color:#6b21a8; font-weight:bold; }
.target.highlighted { border-color:#a855f7; background:#fae8ff; }
.target.done { border-style:solid; background:#f3e8ff; }
</style></head><body>
<h2>Padan & Letak: Shape Match</h2>
<div class="container">
<div class="pieces">
<div class="piece" data-key="cat">๐ฑ</div>
<div class="piece" data-key="bird">๐ฆ</div>
<div class="piece" data-key="panda">๐ผ</div>
</div>
<div class="targets">
<div class="target" data-key="bird">BIRD</div>
<div class="target" data-key="cat">CAT</div>
<div class="target" data-key="panda">PANDA</div>
</div>
</div>
<script>
let drag = null;
document.querySelectorAll('.piece').forEach(p => {
p.onpointerdown = e => {
if(p.classList.contains('matched')) return;
const r = p.getBoundingClientRect();
drag = { el: p, key: p.dataset.key, dx: e.clientX - r.left, dy: e.clientY - r.top };
p.classList.add('dragging');
p.setPointerCapture(e.pointerId);
p.onpointermove = ev => {
p.style.left = (ev.clientX - drag.dx) + 'px';
p.style.top = (ev.clientY - drag.dy) + 'px';
document.querySelectorAll('.target').forEach(t => {
const tr = t.getBoundingClientRect();
t.classList.toggle('highlighted', ev.clientX >= tr.left && ev.clientX <= tr.right && ev.clientY >= tr.top && ev.clientY <= tr.bottom);
});
};
p.onpointerup = ev => {
p.onpointermove = null; p.classList.remove('dragging');
p.style.left = p.style.top = '';
const t = [...document.querySelectorAll('.target')].find(target => {
const tr = target.getBoundingClientRect();
return ev.clientX >= tr.left && ev.clientX <= tr.right && ev.clientY >= tr.top && ev.clientY <= tr.bottom;
});
document.querySelectorAll('.target').forEach(target => target.classList.remove('highlighted'));
if(t && t.dataset.key === drag.key) {
p.classList.add('matched'); t.classList.add('done');
}
drag = null;
};
};
});
</script>
</body></html>
Ideas to Expand Your Game
Now that you have the core mechanics running, consider adding:
- Local High Scores: Use
localStorage.setItem()andlocalStorage.getItem()to persist personal bests. - Visual Polish: Add screen shake on impacts by displacing
ctx.translate(shakeX, shakeY). - Mobile Touch Controls: Bind on-screen directional buttons using
onpointerdownandonpointerup.