├── 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 |
--------------------------------------------------------------------------------