├── README.md ├── assets └── caterpillar.png ├── index.html ├── package.json └── src ├── draggable.css ├── draggable.js ├── index.js └── styles.css /README.md: -------------------------------------------------------------------------------- 1 | # draggable.js 2 | Created with CodeSandbox 3 | -------------------------------------------------------------------------------- /assets/caterpillar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxBittker/draggable.js/bd5d39f36477bb54c579eda663e56e1265aad88c/assets/caterpillar.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | collage template with draggable.js 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 |

I love eating leaves

13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "draggablejs-template-2021", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "@babel/core": "7.2.0", 13 | "parcel-bundler": "^1.6.1" 14 | }, 15 | "keywords": [] 16 | } -------------------------------------------------------------------------------- /src/draggable.css: -------------------------------------------------------------------------------- 1 | .draggable { 2 | position: absolute; 3 | transform: translate(-50%, -50%); 4 | /* filter: drop-shadow(1px 1px 3px white); */ 5 | display: block; 6 | user-select: none; 7 | margin: 0; 8 | top: 50%; 9 | left: 50%; 10 | touch-action: manipulation; 11 | } 12 | 13 | .handle-container { 14 | border: 1px solid rgba(0, 0, 0, 0.15); 15 | position: absolute; 16 | cursor: nwse-resize; 17 | z-index: 10000; 18 | display: block; 19 | user-select: none; 20 | margin: 0; 21 | pointer-events: none; 22 | } 23 | .resize-handle { 24 | position: absolute; 25 | width: 12px; 26 | height: 12px; 27 | border: 1px solid black; 28 | box-sizing: border-box; 29 | z-index: 1000000; 30 | background-color: white; 31 | transform: translate(-50%, -50%); 32 | display: block; 33 | user-select: none; 34 | margin: 0; 35 | pointer-events: all; 36 | right: -12px; 37 | bottom: -12px; 38 | border-radius: 100%; 39 | cursor: grabbing; 40 | } 41 | -------------------------------------------------------------------------------- /src/draggable.js: -------------------------------------------------------------------------------- 1 | import "./draggable.css"; 2 | // you don't need to edit this file 3 | // (sorry it's so messy) 4 | 5 | let dragStartCallback = function (element, x, y, scale, rotation) { 6 | // the default drag callback does nothing 7 | }; 8 | let dragMoveCallback = function (element, x, y, scale, rotation) { 9 | // the default drag callback does nothing 10 | }; 11 | let dragEndCallback = function (element, x, y, scale, rotation) { 12 | // the default drag callback does nothing 13 | }; 14 | 15 | function setDragStartCallback(callback) { 16 | if (typeof callback === "function") { 17 | dragStartCallback = callback; 18 | } else { 19 | throw new Error("drag callback must be a function!"); 20 | } 21 | } 22 | function setDragMoveCallback(callback) { 23 | if (typeof callback === "function") { 24 | dragMoveCallback = callback; 25 | } else { 26 | throw new Error("drag callback must be a function!"); 27 | } 28 | } 29 | function setDragEndCallback(callback) { 30 | if (typeof callback === "function") { 31 | dragEndCallback = callback; 32 | } else { 33 | throw new Error("drag callback must be a function!"); 34 | } 35 | } 36 | let activeElement = null; 37 | let mousedown = false; 38 | let handle_state = false; 39 | let offset_x; 40 | let offset_y; 41 | 42 | window.last_z = 1; 43 | let initialDistance; 44 | let initialScale; 45 | let initialWidth; 46 | let initialHeight; 47 | let initialAngle; 48 | 49 | let resizeHandle = document.createElement("div"); 50 | let handleContainer = document.createElement("div"); 51 | resizeHandle.classList.add("resize-handle"); 52 | handleContainer.classList.add("handle-container"); 53 | handleContainer.appendChild(resizeHandle); 54 | document.body.appendChild(handleContainer); 55 | 56 | var isTouchDevice = "ontouchstart" in document.documentElement; 57 | // this moves the outline rectangle to match the current element 58 | function updateHandleContainer() { 59 | if (!activeElement) { 60 | handleContainer.style.left = "-1000px"; 61 | return; 62 | } 63 | let styles = window.getComputedStyle(activeElement); 64 | let scale = getCurrentScale(activeElement); 65 | let rotate = getCurrentRotation(activeElement); 66 | handleContainer.style.left = styles.left; 67 | handleContainer.style.top = styles.top; 68 | handleContainer.style.width = parseFloat(styles.width) * scale + "px"; 69 | handleContainer.style.height = parseFloat(styles.height) * scale + "px"; 70 | handleContainer.style.transform = ` 71 | translate(-50%,-50%) 72 | rotate(${rotate * (180 / Math.PI)}deg)`; 73 | } 74 | 75 | function startAction(ev, isMouse) { 76 | let touches = Array.from(ev.touches); 77 | let firstTouch = touches[0]; 78 | if (firstTouch.target.classList.contains("resize-handle")) { 79 | ev.preventDefault(); 80 | initialScale = getCurrentScale(activeElement); 81 | initialAngle = getCurrentRotation(activeElement); 82 | let styles = window.getComputedStyle(activeElement); 83 | dragStartCallback( 84 | activeElement, 85 | parseFloat(styles.left), 86 | parseFloat(styles.top), 87 | initialScale, 88 | initialAngle 89 | ); 90 | } 91 | if (firstTouch.target.classList.contains("draggable")) { 92 | if (firstTouch.target.tagName === "IMG") { 93 | ev.preventDefault(); 94 | } 95 | let selectedElement = checkImageCoord(firstTouch.target, ev); 96 | if (!selectedElement || !selectedElement.classList.contains("draggable")) { 97 | return; 98 | } 99 | activeElement = selectedElement; 100 | 101 | let bounds = selectedElement.getBoundingClientRect(); 102 | if (isMouse) { 103 | updateHandleContainer(); 104 | } 105 | offset_x = firstTouch.clientX - bounds.left; 106 | offset_y = firstTouch.clientY - bounds.top; 107 | activeElement.style.zIndex = window.last_z; 108 | window.last_z++; 109 | initialWidth = bounds.width; 110 | initialHeight = bounds.height; 111 | initialScale = getCurrentScale(activeElement); 112 | initialAngle = getCurrentRotation(activeElement); 113 | 114 | let secondTouch = touches[1]; 115 | if (secondTouch) { 116 | let p1 = { x: firstTouch.clientX, y: firstTouch.clientY }; 117 | let p2 = { x: secondTouch.clientX, y: secondTouch.clientY }; 118 | let pDifference = sub(p1, p2); 119 | let pMid = add(p1, scale(pDifference, 0.5)); 120 | 121 | initialDistance = distance(p1, p2); 122 | initialAngle = angle(pDifference) - getCurrentRotation(selectedElement); 123 | offset_x = pMid.x - bounds.left; 124 | offset_y = pMid.y - bounds.top; 125 | } 126 | let styles = window.getComputedStyle(activeElement); 127 | 128 | dragStartCallback( 129 | activeElement, 130 | parseFloat(styles.left), 131 | parseFloat(styles.top), 132 | initialScale, 133 | initialAngle 134 | ); 135 | } 136 | } 137 | document.body.addEventListener("touchstart", function (ev) { 138 | if (activeElement) { 139 | handleContainer.style.left = "-1000px"; 140 | } 141 | startAction(ev, false); 142 | }); 143 | 144 | document.body.addEventListener("mousedown", function (ev) { 145 | if (isTouchDevice) { 146 | return; 147 | } 148 | 149 | if (ev.target.classList.contains("resize-handle") && activeElement) { 150 | let styles = window.getComputedStyle(activeElement); 151 | let scale = getCurrentScale(activeElement); 152 | let rotate = getCurrentRotation(activeElement); 153 | let size = { 154 | x: parseFloat(styles.width) * scale, 155 | y: parseFloat(styles.height) * scale 156 | }; 157 | 158 | handleContainer.style.transform = ` 159 | translate(-50%,-50%) 160 | rotate(${rotate * (180 / Math.PI)}deg)`; 161 | 162 | initialDistance = magnitude(size); 163 | 164 | handle_state = "resize"; 165 | } else { 166 | handle_state = false; 167 | if (activeElement) { 168 | handleContainer.style.left = "-1000px"; 169 | activeElement = false; 170 | } 171 | } 172 | 173 | mousedown = true; 174 | 175 | ev.touches = [ev]; 176 | startAction(ev, true); 177 | }); 178 | document.body.addEventListener("touchend", function (ev) { 179 | if (activeElement) { 180 | handleContainer.style.left = "-1000px"; 181 | let styles = window.getComputedStyle(activeElement); 182 | dragEndCallback( 183 | activeElement, 184 | parseFloat(styles.left), 185 | parseFloat(styles.top), 186 | getCurrentScale(activeElement), 187 | getCurrentRotation(activeElement) 188 | ); 189 | } 190 | 191 | activeElement = null; 192 | }); 193 | document.body.addEventListener("mouseup", function (ev) { 194 | mousedown = false; 195 | handle_state = false; 196 | 197 | if (!activeElement) return; 198 | initialScale = getCurrentScale(activeElement); 199 | initialAngle = getCurrentRotation(activeElement); 200 | let styles = window.getComputedStyle(activeElement); 201 | dragEndCallback( 202 | activeElement, 203 | parseFloat(styles.left), 204 | parseFloat(styles.top), 205 | getCurrentScale(activeElement), 206 | getCurrentRotation(activeElement) 207 | ); 208 | }); 209 | 210 | function moveAction(ev, isMouse) { 211 | if (!activeElement) { 212 | return; 213 | } 214 | 215 | let touches = Array.from(ev.touches); 216 | let firstTouch = touches[0]; 217 | 218 | let x = firstTouch.clientX - offset_x + initialWidth / 2; 219 | let y = firstTouch.clientY - offset_y + initialHeight / 2; 220 | 221 | let newScale = initialScale; 222 | let newAngle = initialAngle; 223 | 224 | let secondTouch = touches[1]; 225 | if (secondTouch) { 226 | let p1 = { x: firstTouch.clientX, y: firstTouch.clientY }; 227 | let p2 = { x: secondTouch.clientX, y: secondTouch.clientY }; 228 | let pDifference = sub(p1, p2); 229 | let pMid = add(p1, scale(pDifference, 0.5)); 230 | 231 | let newDistance = distance(p1, p2); 232 | newAngle = angle(pDifference) - initialAngle; 233 | newScale = initialScale * (newDistance / initialDistance); 234 | x = pMid.x - offset_x + initialWidth / 2; 235 | y = pMid.y - offset_y + initialHeight / 2; 236 | } 237 | 238 | if (handle_state === "resize") { 239 | let b = activeElement.getBoundingClientRect(); 240 | let p1 = { x: firstTouch.clientX, y: firstTouch.clientY }; 241 | let center = { x: b.left + b.width / 2, y: b.top + b.height / 2 }; 242 | let p2 = add(center, sub(p1, center)); 243 | let newDistance = distance(p1, p2); 244 | 245 | newScale = initialScale * (newDistance / initialDistance); 246 | 247 | let pDifference = sub(p1, p2); 248 | let handleAngle = angle(pDifference); 249 | 250 | let styles = window.getComputedStyle(activeElement); 251 | let w = parseFloat(styles.width); 252 | let h = parseFloat(styles.height); 253 | let a = Math.atan2(h, w) + Math.PI; 254 | 255 | newAngle = handleAngle - a; 256 | } else if (!handle_state) { 257 | activeElement.style.left = x + "px"; 258 | activeElement.style.top = y + "px"; 259 | } 260 | 261 | activeElement.style.transform = ` 262 | translate(-50%,-50%) 263 | scale(${newScale}) 264 | rotate(${newAngle * (180 / Math.PI)}deg)`; 265 | 266 | dragMoveCallback(activeElement, x, y, newScale, newAngle); 267 | 268 | if (isMouse) { 269 | try { 270 | updateHandleContainer(); 271 | } catch (e) { 272 | console.log(e); 273 | } 274 | } 275 | } 276 | document.body.addEventListener("mousemove", function (ev) { 277 | ev.touches = [ev]; 278 | 279 | if (mousedown) { 280 | moveAction(ev, true); 281 | } 282 | }); 283 | 284 | document.body.addEventListener( 285 | "touchmove", 286 | function (ev) { 287 | ev.preventDefault(); 288 | moveAction(ev); 289 | }, 290 | { passive: false } 291 | ); 292 | let canvas = document.createElement("canvas"); 293 | let ctx = canvas.getContext("2d"); 294 | // document.body.appendChild(ctx.canvas); // used for debugging 295 | 296 | // this function checks if a pixel location in an image is opaque 297 | // if it's not, it attemps to find the next image below it until 298 | // it finds one 299 | function checkImageCoord(img_element, event) { 300 | // non-image elements are always considered opaque 301 | if (img_element.tagName !== "IMG") { 302 | return img_element; 303 | } 304 | img_element.crossOrigin = "anonymous"; 305 | let touches = Array.from(event.touches); 306 | let firstTouch = touches[0]; 307 | 308 | // Get click coordinates 309 | let x = firstTouch.clientX; 310 | let y = firstTouch.clientY; 311 | let w = (ctx.canvas.width = window.innerWidth); 312 | let h = (ctx.canvas.height = window.innerHeight); 313 | 314 | ctx.clearRect(0, 0, w, h); 315 | 316 | let scale = getCurrentScale(img_element); 317 | let rotation = getCurrentRotation(img_element); 318 | 319 | let styles = window.getComputedStyle(img_element); 320 | ctx.translate(parseFloat(styles.left), parseFloat(styles.top)); 321 | ctx.scale(scale, scale); 322 | ctx.rotate(rotation); 323 | 324 | ctx.drawImage( 325 | img_element, 326 | -img_element.width / 2, 327 | -img_element.height / 2, 328 | img_element.width, 329 | img_element.height 330 | ); 331 | ctx.resetTransform(); 332 | let alpha = 1; 333 | try { 334 | alpha = ctx.getImageData(x, y, 1, 1).data[3]; // [0]R [1]G [2]B [3]A 335 | if (!img_element.complete) { 336 | alpha = 1; 337 | } 338 | } catch (e) { 339 | console.warn(`add crossorigin="anonymous" to your img`); 340 | } 341 | // If pixel is transparent, then retrieve the element underneath 342 | // and trigger it's click event 343 | if (alpha === 0) { 344 | img_element.style.pointerEvents = "none"; 345 | let nextTarget = document.elementFromPoint( 346 | firstTouch.clientX, 347 | firstTouch.clientY 348 | ); 349 | let nextEl = null; 350 | if (nextTarget.classList.contains("draggable")) { 351 | nextEl = checkImageCoord(nextTarget, event); 352 | } 353 | img_element.style.pointerEvents = "auto"; 354 | return nextEl; 355 | } else { 356 | //image is opaque at location 357 | return img_element; 358 | } 359 | } 360 | 361 | function getTransform(el) { 362 | try { 363 | let st = window.getComputedStyle(el, null); 364 | let tr = 365 | st.getPropertyValue("-webkit-transform") || 366 | st.getPropertyValue("-moz-transform") || 367 | st.getPropertyValue("-ms-transform") || 368 | st.getPropertyValue("-o-transform") || 369 | st.getPropertyValue("transform") || 370 | "FAIL"; 371 | 372 | return tr.split("(")[1].split(")")[0].split(","); 373 | } catch (e) { 374 | console.log(e); 375 | return [0, 0, 0, 0]; 376 | } 377 | } 378 | function getCurrentScale(el) { 379 | let values = getTransform(el); 380 | 381 | return Math.sqrt(values[0] * values[0] + values[1] * values[1]); 382 | } 383 | 384 | function getCurrentRotation(el) { 385 | let values = getTransform(el); 386 | 387 | return Math.atan2(values[1], values[0]); 388 | } 389 | 390 | function add(a, b) { 391 | return { x: a.x + b.x, y: a.y + b.y }; 392 | } 393 | 394 | function sub(a, b) { 395 | return { x: b.x - a.x, y: b.y - a.y }; 396 | } 397 | 398 | function scale(a, s) { 399 | return { x: a.x * s, y: a.y * s }; 400 | } 401 | function magnitude(a) { 402 | return Math.sqrt(Math.pow(a.x, 2) + Math.pow(a.y, 2)); 403 | } 404 | function angle(b) { 405 | return Math.atan2(b.y, b.x); //radians 406 | } 407 | 408 | function distance(a, b) { 409 | return magnitude(sub(a, b)); 410 | } 411 | 412 | export { setDragStartCallback, setDragMoveCallback, setDragEndCallback }; 413 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "./styles.css"; 2 | import { 3 | setDragStartCallback, 4 | setDragMoveCallback, 5 | setDragEndCallback 6 | } from "./draggable.js"; 7 | 8 | // If you want to attach extra logic to the drag motions, you can use these callbacks: 9 | // they are not required for the homework! 10 | setDragStartCallback(function (element, x, y, scale, angle) { 11 | // console.log(element) 12 | }); 13 | setDragMoveCallback(function (element, x, y, scale, angle) { 14 | // console.log(element) 15 | }); 16 | setDragEndCallback(function (element, x, y, scale, angle) { 17 | // console.log(element) 18 | }); 19 | 20 | // your code here 21 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | height: 100vh; 4 | overflow: hidden; 5 | margin: 0; 6 | } 7 | 8 | body, 9 | html { 10 | height: -webkit-fill-available; 11 | } 12 | .draggable { 13 | /* filter: drop-shadow(1px 1px 3px white); */ 14 | } 15 | 16 | .caterpillar { 17 | width: 200px; 18 | top: 60%; 19 | } 20 | --------------------------------------------------------------------------------