Skip to content

Instantly share code, notes, and snippets.

@Sphinxxxx
Last active September 5, 2017 23:08
Show Gist options
  • Save Sphinxxxx/e9d09af0ab467fb3d1e3116af6d92aea to your computer and use it in GitHub Desktop.
Save Sphinxxxx/e9d09af0ab467fb3d1e3116af6d92aea to your computer and use it in GitHub Desktop.
Drag Rotate
<script>
//WinPhone stuff..
Array.from = Array.from || function(list) { return Array.prototype.slice.call(list); };
//window.onerror = function(msg, url, line) { alert('Error: '+msg+'\nURL: '+url+'\nLine: '+line); };
</script>
<div class="box outer drag-move">
<button onclick="console.log('click')">Still clickable</button>
<textarea>Still focusable</textarea>
<div class="handle drag-rotate"></div>
<div class="handle drag-resize"></div>
<div class="box inner">
<ul contenteditable="true">
<li>Some editable (and selectable) paragraphs of text. Sed ut perspiciatis unde omnis iste natus</li>
<li>Error sit voluptatem accusantium doloremque laudantium, totam rem aperiam</li>
</ul>
<textarea>More editable text...</textarea>
<div class="handle drag-rotate"></div>
<div class="handle drag-resize"></div>
<div class="handle drag-move"></div>
<div class="box inner drag-move">
<div class="handle drag-rotate"></div>
<div class="handle drag-resize"></div>
</div>
</div>
<div class="box inner2 drag-move">
</div>
</div>
<div id="debug"></div>
function makeDraggable(box, handles) {
"use strict";
//console.clear();
box.style.position = 'absolute';
box.style.transformOrigin = 'center';
//Needed (along with offsetWidth/Height) to make the resizing calculations work:
box.style.boxSizing = 'border-box';
handles = handles || {};
const handleMove = handles.move || box,
handleRotate = handles.rotate,
handleResize = handles.resize;
handleMove.addEventListener('mousedown', dragStartMove);
handleMove.addEventListener('touchstart', dragStartMove);
if(handleRotate) {
handleRotate.addEventListener('mousedown', dragStartRotate);
handleRotate.addEventListener('touchstart', dragStartRotate);
}
if(handleResize) {
handleResize.addEventListener('mousedown', dragStartResize);
handleResize.addEventListener('touchstart', dragStartResize);
}
document.addEventListener('mousemove', dragMove);
document.addEventListener('touchmove', dragMove);
document.addEventListener('mouseup', dragEnd);
document.addEventListener('touchend', dragEnd);
//If the box itself isn't draggable, we must still stop event bubbling,
//or else interaction with the box (e.g. input elements) will drag a draggable parent:
if(handleMove !== box) {
box.addEventListener('mousedown', e => e.stopPropagation());
}
let mode, start;
function dragStartMove(e) {
//console.log('drag start move');
mode = 'move';
dragStart(e);
}
function dragStartRotate(e) {
//console.log('drag start rotate');
mode = 'rotate';
dragStart(e);
}
function dragStartResize(e) {
//console.log('drag start resize');
mode = 'resize';
dragStart(e);
}
function dragStart(e) {
//Leave box elements clickable since we don't know if this is a drag operation just yet:
// e.preventDefault();
//Only drag the topmost box (in case of nested boxes)
e.stopPropagation();
const clientRect = box.getBoundingClientRect(),
clientCenter = {
x: clientRect.left + clientRect.width/2,
y: clientRect.top + clientRect.height/2
},
rotSelf = (box.dataset.dragRotation ? Number(box.dataset.dragRotation) : 0);
//Traverse the DOM to find the total rotation on the box:
let rotParent = 0, elm = box.parentElement;
while(elm) {
if(elm.dataset.dragRotation) { rotParent += Number(elm.dataset.dragRotation) || 0; }
elm = elm.parentElement;
}
start = {
//Coordinates within the viewport.
//Useful to have absolute coordinates for rotated boxes nested inside other rotatec boxes:
client: {
center: clientCenter,
rotation: {
self: rotSelf,
accumParent: rotParent,
accumSelf: rotParent + rotSelf
},
//Top left corner, relative to the viewport:
topLeft: rotatePoint({
x: clientCenter.x - box.clientWidth/2,
y: clientCenter.y - box.clientHeight/2
}, rotParent + rotSelf, clientCenter),
mousePos: { x: e.clientX, y: e.clientY },
},
//The top/left coordinates used for absolute positioning,
//i.e. the bounding rect *before* any transforms like a previous rotation:
//https://stackoverflow.com/questions/27745438/how-to-compute-getboundingclientrect-without-considering-transforms
local: {
top: box.offsetTop,
left: box.offsetLeft,
//We need the entire size, including borders:
//https://stackoverflow.com/questions/30379924/understanding-offsetwidth-and-clientwidth-when-css-border-box-is-active
// width: box.clientWidth,
// height: box.clientHeight,
width: box.offsetWidth,
height: box.offsetHeight,
},
};
//debugPos(start.client.topLeft);
}
function dragMove(e) {
if(!mode) { return; }
e.preventDefault();
e.stopPropagation();
const css = box.style,
mousePoint = {
x: e.clientX,
y: e.clientY
};
if (mode === 'rotate') {
const boxPivot = start.client.center,
startRads = start.memoRads || (start.memoRads = calcRads(start.client.mousePos, boxPivot) - Number(box.dataset.dragRotation || 0)),
currRads = calcRads(mousePoint, boxPivot),
rotation = currRads - startRads;
box.dataset.dragRotation = rotation;
css.transform = "rotate(" + (rotation) + "rad)";
}
else if (mode === 'move') {
let mouseDiff = {
x: mousePoint.x - start.client.mousePos.x,
y: mousePoint.y - start.client.mousePos.y
};
var parentRotation = start.client.rotation.accumParent;
if(parentRotation) {
//If this is a box inside another rotated box, we must rotate the movement the other way:
mouseDiff = rotatePoint(mouseDiff, -parentRotation);
}
css.top = start.local.top + mouseDiff.y + "px";
css.left = start.local.left + mouseDiff.x + "px";
}
else if (mode === 'resize') {
const topLeft = start.client.topLeft,
rotation = start.client.rotation;
const localMousePoint = rotatePoint(mousePoint, -rotation.accumSelf, topLeft),
localMouseW = localMousePoint.x - topLeft.x,
localMouseH = localMousePoint.y - topLeft.y,
sizeOffset = start.memoSize || (start.memoSize = { width: start.local.width - localMouseW, height: start.local.height - localMouseH }),
newW = Math.max(0, localMouseW + sizeOffset.width),
newH = Math.max(0, localMouseH + sizeOffset.height);
css.width = newW + 'px';
css.height = newH + 'px';
//When we change the size, we also shift the box' pivot point (transform-origin: center;),
//so the top-left corner will move if there is a rotation on this box:
const newCenter = {
x: (newW - start.local.width)/2,
y: (newH - start.local.height)/2,
},
targetCenter = rotatePoint(newCenter, rotation.self);
css.top = start.local.top + (targetCenter.y-newCenter.y) + "px";
css.left = start.local.left + (targetCenter.x-newCenter.x) + "px";
}
//_stats.update();
}
function dragEnd(e) {
mode = '';
}
/* Geometry utils */
function calcRads(point, center) {
let radians = Math.PI/2 + Math.atan((center.x - point.x) / (point.y - center.y));
if ((point.y - center.y) < 0) { radians += Math.PI; }
return radians;
}
//Rotating a point about another point (2D)
//https://stackoverflow.com/a/32376643/1869660
function rotatePoint(point, radians, center) {
center = center || { x: 0, y: 0 };
const dx = point.x - center.x, dy = point.y - center.y,
sin = Math.sin(radians), cos = Math.cos(radians);
return {
x: (cos * dx) - (sin * dy) + center.x,
y: (sin * dx) + (cos * dy) + center.y
};
}
}
Array.from(document.querySelectorAll('.box')).forEach(box => {
const handles = Array.from(box.querySelectorAll('.handle')).filter(handle => handle.parentElement === box);
makeDraggable(box, {
rotate: handles[0],
resize: handles[1],
move: handles[2],
});
});
function debugPos(pos) {
var css = document.querySelector('#debug').style;
css.top = pos.y + 'px';
css.left = pos.x + 'px';
}
/*
//https://cdn.rawgit.com/mrdoob/stats.js/r17/build/stats.min.js
var _stats = new Stats();
_stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild( _stats.dom );
*/
body {
background: gainsboro;
#debug {
position: fixed;
width: 2px;
height: 2px;
background: red;
}
}
.box {
top: 60px;
left: 60px;
&.outer {
width: 600px;
height: 400px;
border: 2px dashed skyblue;
background: aliceblue;
}
&.inner {
width: 250px;
height: 180px;
border: 2px solid green;
background: rgba(lime, .5);
& ul {
list-style: none;
margin: 0;
padding: 0;
li {
text-indent: 1em;
margin-bottom: .5em;
}
}
}
&.inner2 {
top: 200px;
left: 500px;
width: 200px;
height: 150px;
border: 2px solid salmon;
background: rgba(salmon, .5);
}
}
.drag-move {
cursor: move;
}
.drag-rotate {
cursor: grab; //IE/Edge fallback
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='31' height='31' viewBox='0 0 31 31' %3E %3Cpath d='M5.5,15.5 a10,10 0 1,1 20,0 h3.5 l-5,5 -5,-5 h3.5 a5,5 0 1,0 -14,0 h3.5 l-5,5 -5,-5 z' stroke='black' stroke-width='1' fill='white' transform='rotate(45 15 15)' /%3E %3C/svg%3E") 15 15, grab;
}
.drag-resize {
cursor: nw-resize;
}
//Reset style for nested non-draggable boxes
:not(drag-move) {
cursor: auto;
}
.handle {
position: absolute;
box-sizing: border-box;
background: rgba(white, .5);
&::after {
box-sizing: border-box;
}
&.drag-move {
top:0; left:0;
width: 25px;
height: 25px;
margin: -15px;
overflow: hidden;
transform: rotate(45deg);
&::after {
content: '';
position: absolute;
display: block;
top:-6px; left:-6px; bottom:-6px; right:-6px;
border: 5px solid currentColor;
transform: rotate(45deg);
}
}
&.drag-rotate {
top:0; right:0;
width: 30px;
height: 30px;
margin: -15px;
border: 2px dashed;
border-color: currentColor currentColor transparent transparent;
border-radius: 100%;
}
&.drag-resize {
bottom:0; right:0;
width: 25px;
height: 25px;
margin: -10px;
border: 2px dashed;
border-color: transparent currentColor currentColor transparent;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment