Skip to content

Instantly share code, notes, and snippets.

@evanjmg
Last active February 19, 2017 16:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save evanjmg/c2ffda2df9748e56425194eb5e6ea878 to your computer and use it in GitHub Desktop.
Save evanjmg/c2ffda2df9748e56425194eb5e6ea878 to your computer and use it in GitHub Desktop.
Drag, Pan, Snap To Grid example
license: gpl-3.0

In this example, one can drag, drop, rotate, and create squares that snap to grid. They can also pan and zoom. This covers a lot of important features for building user interfaces based on grid. It also includes collision detection.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.axis path {
display: none;
}
.axis line {
stroke-opacity: 0.3;
shape-rendering: crispEdges;
}
svg,
#chart-container {
width: 100%;
height: 100vh;
display: block;
}
rect {
-webkit-transform-origin: 50% 50% 0;
-moz-transform-origin: 50% 50% 0;
-o-transform-origin: 50% 50% 0;
transform-origin: 50% 50% 0;
}
button {
position: absolute;
top: 20px;
left: 20px;
}
.rectButton {
position: absolute;
top: 20px;
left: 180px;
}
button.rotate {
position: absolute;
top: 20px;
left: 100px;
}
p {
display: block;
position: absolute;
top: 20px;
right: 20px;
}
</style>
<button onclick="rotate()" class="rotate">
Rotate
</button>
<div id="chart-container">
</div>
<button class="rectButton" onclick="addRect()">ADD REC</button>
<button class="reset" onclick="resetted()">Reset</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.js"></script>
<script>
var containerStyle = document.querySelector('#chart-container').getBoundingClientRect();
var svg = null,
width = containerStyle.width,
height = containerStyle.height,
cubeResolution = 50,
previousDraggedPosition,
selected = null,
selectedElements = [],
draggedSvg = null,
view = null,
itemContainer = null,
backdropContainer = null,
item = null,
gX = null,
gY = null,
currentTransform = null;
var zoom = d3.zoom()
.scaleExtent([0.5, 5])
.translateExtent([
[-width * 2, -height * 2],
[width * 2, height * 2]
])
.on("zoom", zoomed);
var xScale = d3.scaleLinear()
.domain([-width / 2, width / 2])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([-height / 2, height / 2])
.range([height, 0]);
var xAxis = d3.axisBottom(xScale)
.ticks((width + 2) / (height + 2) * 10)
.tickSize(height)
.tickPadding(8 - height);
var yAxis = d3.axisRight(yScale)
.ticks(10)
.tickSize(width)
.tickPadding(8 - width);
var points = d3.range(10).map(() => {
return {
x: snapToGrid(Math.random() * 500, cubeResolution),
y: snapToGrid(Math.random() * 500, cubeResolution)
};
});
var slider = d3.select("body").append("p").append("input")
.datum({})
.attr("type", "range")
.attr("value", 1)
.attr("min", zoom.scaleExtent()[0])
.attr("max", zoom.scaleExtent()[1])
.attr("step", (zoom.scaleExtent()[1] - zoom.scaleExtent()[0]) / 100)
.on("input", slided);
function draw() {
svg = d3.select("#chart-container").append('svg');
view = svg.append("g")
.attr("class", "view")
if (currentTransform) view.attr('transform', currentTransform);
itemContainer = view.selectAll("g").attr("class", "itemContainer")
.data(points).enter().append('g')
.attr("transform", () => 'translate(' + xScale(0) + ',' + yScale(0) + ')')
.append('g')
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
backdropContainer = view
.append('g')
.attr('transform', function() {
return 'translate(' + xScale(0) + ',' + yScale(0) + ')';
});
backdrop = backdropContainer
.lower()
.append('rect')
.attr('x', -width * 2)
.attr('y', -height * 2)
.attr('class', 'table-backdrop')
.attr('height', height * 3)
.attr('width', width * 3)
.attr('opacity', '0');
backdrop.on('mousedown', function() {
var mouse = d3.mouse(this);
if (draggedSvg && svg) {
newItem(snapToGrid(mouse[0] - cubeResolution - 10, cubeResolution), snapToGrid(mouse[1] - cubeResolution - 10, cubeResolution));
}
}).on('mousemove', function() {
var mouse = d3.mouse(this);
if (draggedSvg) {
var x = mouse[0] - cubeResolution - 10;
var y = mouse[1] - cubeResolution - 10;
draggedSvg.attr('x', x);
draggedSvg.attr('y', y);
}
});
item = itemContainer.append('rect').attr('class', 'table-graphic')
.attr('x', d => d.x)
.attr('y', d => d.y)
.attr('data-rotation', 0)
.attr('width', cubeResolution)
.attr('height', cubeResolution)
.attr('fill', 'blue')
.on('click', function() {
console.log('clicked');
selected = this.parentNode;
})
gX = svg.append("g")
.attr("class", "axis axis--x")
.call(xAxis);
gY = svg.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
svg.call(zoom)
.on("wheel.zoom", null)
.on('dblclick.zoom', null);
}
draw();
function addRect() {
draggedSvg = backdropContainer.append('rect').attr('width', cubeResolution).attr('height', cubeResolution);
}
d3.select("button")
.on("click", resetted);
function newItem(x, y, width, height) {
points.push({
x: x,
y: y
});
clearDrawing();
draw();
}
function clearDrawing() {
if (draggedSvg) draggedSvg.remove();
draggedSvg = null;
newElementData = null;
if (svg) {
svg.on('mousedown', null);
view.exit().remove();
svg.remove();
svg = null;
}
}
function dragstarted(d) {
var el = d3.select(this);
savePreviousDragPoint(el);
el.raise().classed("dragging", true);
}
function savePreviousDragPoint(el) {
var elBox = el.nodes()[0].getBBox();
if (!el.nodes()[0].classList.contains('dragging')) {
previousDraggedPosition = {
x: elBox.x,
y: elBox.y
};
}
}
function rotate(d) {
if (selected) {
var el = d3.select(selected).select('.table-graphic');
var transVal = parseInt(el.attr('data-rotation'), 10);
var newDegree = transVal && transVal < 360 ? transVal + 45 : 45;
el.attr('data-rotation', newDegree);
return el.attr('transform', () => {
var center = getCenter(el.attr('x'), el.attr('y'), cubeResolution, cubeResolution);
return "rotate(" + newDegree + "," + center.x + ',' + center.y + ")";
});
} else {
return false;
}
}
function getCenter(x, y, w, h) {
return {
x: parseInt(x, 10) + parseInt(w, 10) / 2,
y: parseInt(y, 10) + parseInt(h) / 2
}
};
function collide(d, element) {
node = d.nodes()[0];
nodeBox = node.getBBox();
nodeLeft = nodeBox.x;
nodeRight = nodeBox.x + nodeBox.width;
nodeTop = nodeBox.y;
nodeBottom = nodeBox.y + nodeBox.height;
var objects = d3.selectAll(".table-graphic");
objects.nodes().forEach(object => {
if (object !== node) {
otherBox = object.getBBox();
otherLeft = otherBox.x;
otherRight = otherBox.x + otherBox.width;
otherTop = otherBox.y;
otherBottom = otherBox.y + otherBox.height;
collideHoriz = nodeLeft < otherRight && nodeRight > otherLeft;
collideVert = nodeTop < otherBottom && nodeBottom > otherTop;
if (collideHoriz && collideVert) {
console.log('collide');
d3.select(node).style('fill', () => "tomato");
d3.select(object).style('fill', () => "tomato");
setTimeout(() => {
d3.select(object).style('fill', () => "blue")
d3.select(node).style('fill', () => "blue")
}, 1000);
if (previousDraggedPosition) {
d3.select(node).attr('x', () => previousDraggedPosition.x);
d3.select(node).attr('y', () => previousDraggedPosition.y);
}
} else {
d3.select(object).style('fill', () => "blue");
d3.select(node).style('fill', () => "blue");
}
} else {
element.style('fill', () => 'blue');
}
});
}
function dragged(d) {
selected = this;
var el = d3.select(this).select('.table-graphic').attr("x", (d) => snapToGrid(d3.event.x, cubeResolution)).attr("y", () => snapToGrid(d3.event.y, cubeResolution))
var center = getCenter(el.attr('x'), el.attr('y'), cubeResolution, cubeResolution);
el.attr('transform', () => {
return "rotate(" + el.attr('data-rotation') + "," + center.x + ',' + center.y + ")";
})
el.call(collide, el);
}
function findAndUpdate(oldPt, newPt) {
for (var i = 0; i < points.length; i++) {
if (points[i].x === oldPt.x && points[i].y === oldPt.y) {
points[i] = {
x: newPt.x,
y: newPt.y
};
return points[i];
}
}
}
function coorNum(pt) {
return {
x: parseInt(pt.x, 10),
y: parseInt(pt.y, 10)
};
}
function dragended(d) {
d3.select(this).classed("dragging", false);
var newEl = d3.select(this).select('.table-graphic');
newPt = {
x: newEl.attr('x'),
y: newEl.attr('y')
};
pt = findAndUpdate(coorNum(previousDraggedPosition), coorNum(newPt));
if (pt) {
previousDraggedPosition = pt
};
}
function slided(d) {
zoom.scaleTo(svg, d3.select(this).property("value"));
}
function snapToGrid(p, r) {
return Math.round(p / r) * r;
}
function zoomed() {
currentTransform = d3.event.transform;
view.attr("transform", currentTransform);
gX.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
gY.call(yAxis.scale(d3.event.transform.rescaleY(yScale)));
slider.property("value", d3.event.scale);
}
function resetted() {
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity);
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment