Last active
April 23, 2021 21:59
-
-
Save tobyshooters/12b097dbf68b7b3093578d38f7b45579 to your computer and use it in GitHub Desktop.
infinite, multimedia canvas
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
<style> | |
html, body { | |
margin: 0px; | |
background-color: #fdf6e3; | |
font-family: monospace; | |
} | |
#canvas { | |
height: 100%; | |
width: 100%; | |
} | |
.draggableItem { | |
position: absolute; | |
user-select: none; | |
user-drag: none; | |
-webkit-user-drag: none; | |
-moz-user-drag: none; | |
} | |
</style> | |
<div id="canvas"></div> | |
<script> | |
const getPosition = (elem) => { | |
// This doesn't work with textarea... | |
// const rect = elem.getBoundingClientRect() | |
// return [rect.x, rect.y]; | |
const left = parseInt(elem.style.left.slice(0, -2)); | |
const top = parseInt(elem.style.top.slice(0, -2)); | |
return [left, top]; | |
} | |
const setPosition = (elem, x, y) => { | |
elem.style.top = `${y}px`; | |
elem.style.left = `${x}px`; | |
} | |
const movePosition = (elem, dx, dy) => { | |
const [x, y] = getPosition(elem); | |
setPosition(elem, x + dx, y + dy); | |
} | |
const MAX_Z_INDEX = 99999; | |
const decrementZ = (elem) => { | |
elem.style.zIndex = (elem.style.zIndex || MAX_Z_INDEX) - 1; | |
} | |
const popToFront = (elem, otherElements) => { | |
otherElements.map(decrementZ); | |
elem.style.zIndex = MAX_Z_INDEX; | |
} | |
const makeElementDraggable = (elem, otherElements) => { | |
// References: | |
// 1. htmldom.dev/make-a-draggable-element | |
// 2. developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events | |
// e.target also would work. However, if the cursor ever escapes from | |
// the boundary of the object (e.g. fast movement) then the target | |
// switches to whatever is beneath it. Binding `elem` is better. | |
let x, y; | |
const mouseDown = (e) => { | |
if (e.shiftKey || e.metaKey) { | |
e.preventDefault(); | |
e.stopPropagation(); // Don't call parent's mouseDown | |
x = e.clientX; | |
y = e.clientY; | |
popToFront(elem, otherElements); | |
document.addEventListener('mousemove', mouseMove); | |
document.addEventListener('mouseup', mouseUp); | |
} | |
} | |
const mouseMove = (e) => { | |
const dx = e.clientX - x; | |
const dy = e.clientY - y; | |
if (e.shiftKey) { | |
movePosition(elem, dx, dy); | |
} else if (e.metaKey) { | |
elem.width = parseInt(elem.width) + dx; | |
elem.height = parseInt(elem.height) + dy; | |
} | |
x = e.clientX; | |
y = e.clientY; | |
} | |
const mouseUp = (e) => { | |
document.removeEventListener('mousemove', mouseMove); | |
document.removeEventListener('mouseup', mouseUp); | |
} | |
elem.addEventListener('mousedown', mouseDown); | |
elem.addEventListener("dblclick", (e) => { | |
e.preventDefault(); | |
e.stopPropagation(); | |
if (e.metaKey) { elem.remove(); } | |
}) | |
} | |
const makeBackgroundDraggable = (canvas, elements) => { | |
let x, y; | |
const mouseDown = (e) => { | |
if (e.shiftKey) { | |
e.preventDefault(); | |
e.stopPropagation(); | |
x = e.clientX; | |
y = e.clientY; | |
document.addEventListener('mousemove', mouseMove); | |
document.addEventListener('mouseup', mouseUp); | |
} | |
} | |
const mouseMove = (e) => { | |
const dx = e.clientX - x; | |
const dy = e.clientY - y; | |
elements.map(elem => movePosition(elem, dx, dy)) | |
x = e.clientX; | |
y = e.clientY; | |
} | |
const mouseUp = () => { | |
document.removeEventListener('mousemove', mouseMove); | |
document.removeEventListener('mouseup', mouseUp); | |
} | |
canvas.addEventListener('mousedown', mouseDown); | |
} | |
const canvas = document.getElementById("canvas"); | |
const elements = []; | |
makeBackgroundDraggable(canvas, elements); | |
const addToCanvas = (elem) => { | |
elem.classList.add("draggableItem"); | |
makeElementDraggable(elem, elements); | |
// NOTE: pushing to `elements` makes item move with background. | |
// Only works due to Javascript's lexical scope. Since the original | |
// draggable functions reference `elements`, pushing to the array | |
// is sufficient to make all other elements aware of it. | |
elements.push(elem); | |
canvas.appendChild(elem); | |
popToFront(elem, otherElements); | |
} | |
canvas.addEventListener('dblclick', (e) => { | |
var textInput = document.createElement("textarea"); | |
setPosition(textInput, e.clientX, e.clientY); | |
textInput.style.width = 200; | |
textInput.style.outline = 'none'; | |
textInput.style.backgroundColor = 'rgba(0, 0, 0, 0)'; | |
textInput.style.borderStyle = 'dashed'; | |
textInput.style.borderWidth = '1px'; | |
textInput.style.padding = '8px'; | |
textInput.style.fontSize = '18px'; | |
addToCanvas(textInput); | |
}) | |
canvas.addEventListener('dragover', e => e.preventDefault()); | |
canvas.addEventListener('drop', (e) => { | |
e.preventDefault(); | |
if (e.dataTransfer.types[0] !== "Files") return; | |
const file = e.dataTransfer.items[0].getAsFile(); | |
const reader = new FileReader(); | |
reader.onload = () => { | |
if (["image/png", "image/jpg"].includes(file.type)) { | |
var elem = document.createElement('img'); | |
} else if (["video/mp4", "video/quicktime"].includes(file.type)) { | |
var elem = document.createElement('video'); | |
elem.controls = true; | |
} else if (["audio/mpeg"].includes(file.type)) { | |
var elem = document.createElement('audio'); | |
elem.controls = true; | |
} else if (["application/pdf"].includes(file.type)) { | |
var elem = document.createElement('embed'); | |
elem.type = "application/pdf"; | |
elem.height = 1.29 * 400; | |
} | |
elem.src = reader.result; | |
elem.width = 400; | |
setPosition(elem, e.clientX, e.clientY); | |
addToCanvas(elem); | |
} | |
reader.readAsDataURL(file); | |
}); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment