Skip to content

Instantly share code, notes, and snippets.

@olliecheng
Created April 25, 2020 08:22
Show Gist options
  • Save olliecheng/12c2f159b532875780be0193adbea6f8 to your computer and use it in GitHub Desktop.
Save olliecheng/12c2f159b532875780be0193adbea6f8 to your computer and use it in GitHub Desktop.
drag and drop with collision: https://codepen.io/denosawr/pen/vYNxVPL
<div id="container">
<div id="draggable">
<!-- #draggable is the div inside which things can be dragged -->
<div class="block" id="one">
Drag me!
</div>
<div class="block" id="two">
Crash!
</div>
</div>
</div>
const DEBUG = true;
if (!DEBUG) {
console.debug = () => {};
}
/**
* A class which stores coordinates, and allows you to
* apply primitive operations to them.
*/
class Coords {
/**
* Create a Coord.
*
* Constructor form 1
* @param {Event} - a mouse event such as mousemove
*
* Constructor form 2
* @param {number} x-coordinate (distance from left)
* @param {number} y-coordinate (distance from top)
*/
constructor (a, b) {
if (b === undefined) {
// form (event, null)
this.x = a.clientX;
this.y = a.clientY;
} else {
this.x = a;
this.y = b;
}
}
/**
* Reposition a DOM element so it's top left corner is
* at the stored coordinates.
* @param {element} target - the element to be repositioned
* @warning Ensure the element is absolutely or relatively positioned
*/
repositionElement (target) {
target.style.left = this.x + "px";
target.style.top = this.y + "px";
}
/**
* Add the values of an offset Coords object
* @param {Coords} offsetCoords - the Coords object to add
*/
add (offsetCoords) {
this.x += offsetCoords.x;
this.y += offsetCoords.y;
}
/**
* Transform (scale) the values of the Coords object by a factor
* @param {number} factor - the scaling value
*/
transform (factor) {
this.x *= factor;
this.y *= factor;
}
/**
* Subtract the values of an offset Coords object
* @param {Coords} offsetCoords - the Coords object to subtract
*/
subtract (offsetCoords) {
this.x -= offsetCoords.x;
this.y -= offsetCoords.y;
}
/**
* Return a new copy of a coords object
* @param {Coords} coords - the Coords object to copy
* @param {number} [factor=1] - the transform factor (see transform())
*/
static copy(coords, factor) {
factor = factor ? factor : 1;
const newCoords = new Coords(coords.x, coords.y);
newCoords.transform(factor);
return newCoords;
}
/**
* Return an array representation of the Coords object
* @return {array} [x, y]
*/
toArray () {
return [this.x, this.y];
}
}
/**
* A bounding rectangle which supports collision detection.
*/
class BoundingRect {
/**
* Create a BoundingRect.
*
* Constructor form 1
* @param {element} - the mask for the bounding rectangle size and position
*
* Constructor form 2
* @param {number} x - the x-position of the rectangle
* @param {number} y - the y-position of the rectangle
* @param {element} - the mask for the bounding rectangle size (but not position)
*
* Constructor form 3
* @param {number} x - the x-position of the rectangle
* @param {number} y - the y-position of the rectangle
* @param {number} width - the width of the rectangle
* @param {number} height - the height of the rectangle
*/
constructor (a, b, c, d) {
if (b === undefined) {
// form (element, null, null, null)
const elementBoundingRect = a.getBoundingClientRect();
this.x = elementBoundingRect.x;
this.y = elementBoundingRect.y;
this.width = elementBoundingRect.width;
this.height = elementBoundingRect.height;
} else if (d === undefined) {
// form (x, y, element, null)
const elementBoundingRect = c.getBoundingClientRect();
this.x = a;
this.y = b;
this.width = elementBoundingRect.width;
this.height = elementBoundingRect.height;
} else {
// form (x, y, width, height)
this.x = a;
this.y = b;
this.width = c;
this.height = d;
}
}
/**
* Determine if the BoundingRect has collided with another BoundingRect
* @params {BoundingRect} rect - the bounding rectangle to check collision with
* @return {bool} whether collision has occurred or not
*/
collidesWith (rect) {
if (this.x < rect.x + rect.width &&
this.x + this.width > rect.x &&
this.y < rect.y + rect.height &&
this.y + rect.height > rect.y ) {
return true;
}
return false;
}
/**
* Determine if the BoundingRect is fully contained by another (presumably larger) BoundingRect.
* @params {BoundingRect} rect - the bounding rectangle to check containment with
* @return {bool} whether containment has occurred or not
*/
isFullyContainedBy (largeRect) {
if (this.x > largeRect.x &&
this.x + this.width < largeRect.x + largeRect.width &&
this.y > largeRect.y &&
this.y + this.height < largeRect.y + largeRect.height ) {
return true;
}
return false;
}
/**
* Return a Coords representation of the coordinates of the top-left point of the BoundingRect.
* @return {bool} whether collision has occurred or not
*/
getCoords () {
return new Coords(this.x, this.y);
}
}
/**
* An object representing collision data of the draggable blocks.
*/
class Block {
/**
* Create a Block.
*
* @param {element} element - DOM element for the Block
* @param {number} id - the block ID
*/
constructor (element, id) {
this.element = element;
this.id = id;
this.bounds = new BoundingRect(element);
this.collisions = new Set();
}
/**
* Retrieves the ID of a block, given a DOM element.
*
* @param {element} - DOM element for the Block
* @param {array} blocksList - a list of blocks which contains the block
* @returns {number} either the block ID or -1 if not found.
*/
static getBlockIDFromElement(element, blocksList) {
for (let block of blocksList) {
if (block.element == element) {
return parseInt(block.id);
}
}
return -1
}
}
window.onload = function() {
// first, get position of master draggable element
const draggableElement = document.querySelector("#draggable");
const draggableBoundingRect = new BoundingRect(draggableElement);
// initialise lists of blocks
const blocksList = document.querySelectorAll(".block");
const blocks = {
// blockID: new Block(),
};
// mouse click and drag state
let dragging = false;
let dragState = {
target: null,
targetOffset: new Coords(0, 0)
}
// colour the blocks red if collision
function updateBlockColours() {
for (let [id, block] of Object.entries(blocks)) {
if (block.collisions.size > 0) {
block.element.style.background = "lightsalmon"; // collision!
} else {
block.element.style.background = ""; // restore to default
}
}
}
// check a moved block to see if it has collided
function checkForCollisions(movedBlockID, movedBoundingBox) {
// warning: may be laggier with more and more blocks
movedBlockID = parseInt(movedBlockID);
const movedBlock = blocks[movedBlockID];
for (let [id, block] of Object.entries(blocks)) {
if (id == movedBlockID) {
continue;
}
if (movedBoundingBox.collidesWith(block.bounds)) {
block.collisions.add(movedBlockID);
movedBlock.collisions.add(block.id);
} else {
block.collisions.delete(movedBlockID);
movedBlock.collisions.delete(block.id);
}
}
updateBlockColours();
}
function handleMouseMove(event) {
if (!dragging) {
return;
}
const target = dragState.target;
const targetBoundingRect = new BoundingRect(target);
const absoluteCoords = new Coords(event);
absoluteCoords.subtract(dragState.targetOffset);
// calculate if it will fit
const newBoundingBox = new BoundingRect(
...absoluteCoords.toArray(),
target
);
// if outside purple box
if (!newBoundingBox.isFullyContainedBy(draggableBoundingRect)) {
return;
}
const blockID = Block.getBlockIDFromElement(target, Object.values(blocks))
checkForCollisions(blockID, newBoundingBox);
// update element position
const relativeCoords = Coords.copy(absoluteCoords);
relativeCoords.subtract(draggableBoundingRect);
relativeCoords.repositionElement(target);
blocks[blockID].bounds = new BoundingRect(target);
}
function handleMouseDown(event) {
dragging = true;
const target = event.target;
const targetBoundingRect = new BoundingRect(target);
dragState.target = target;
dragState.targetOffset = new Coords(event);
dragState.targetOffset.subtract(targetBoundingRect);
handleMouseMove(event);
}
function handleMouseUp(event) {
dragging = false;
}
// add mouse event listeners
for (let [index, blockElement] of blocksList.entries()) {
blockElement.style["z-index"] = index;
blockElement.addEventListener("mousedown", handleMouseDown);
blocks[index] = new Block(blockElement, index);
}
document.body.addEventListener("mousemove", handleMouseMove);
document.body.addEventListener("mouseup", handleMouseUp);
// check for initial collisions
for (let [id, block] of Object.entries(blocks)) {
checkForCollisions(id, new BoundingRect(block.element));
}
}
#container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* centre inside div */
display: flex;
justify-content: center;
align-items: center;
}
#draggable {
width: 600px;
height: 600px;
background: #a687d4;
/* Need this for position: absolute to work in child elements */
position: relative;
}
.block {
height: 80px;
width: 80px;
/* centre text */
text-align: center;
line-height: 80px;
/* Exact positioning, to be manipulated in JS */
position: absolute;
user-select: none;
}
#one {
left: 60px;
top: 60px;
background: white;
}
#two {
left: 130px;
top: 130px;
background: lightblue;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment