Created
April 25, 2020 08:22
-
-
Save olliecheng/12c2f159b532875780be0193adbea6f8 to your computer and use it in GitHub Desktop.
drag and drop with collision: https://codepen.io/denosawr/pen/vYNxVPL
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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