|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset='utf-8' /> |
|
<title>Snap to Grid Relative</title> |
|
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js'></script> |
|
</head> |
|
<style> |
|
body { |
|
margin: 0; |
|
overflow: hidden; |
|
} |
|
|
|
#container > * { |
|
position: absolute; |
|
} |
|
|
|
.hidden { |
|
display: none; |
|
pointer-events: none; |
|
} |
|
|
|
</style> |
|
<body> |
|
<div id="container"> |
|
</div> |
|
<script> |
|
var width = window.innerWidth, |
|
height = window.innerHeight; |
|
|
|
var dpr = window.devicePixelRatio; |
|
|
|
var canvasGrid = d3.select('#container').append('canvas') |
|
.attr('id', 'grid') |
|
.attr('width', width * dpr) |
|
.attr('height', height * dpr) |
|
.style('width', width + 'px') |
|
.style('height', height + 'px'); |
|
|
|
var ctxGrid = canvasGrid.node().getContext('2d'); |
|
ctxGrid.scale(dpr, dpr); |
|
|
|
var canvasCircles = d3.select('#container').append('canvas') |
|
.attr('id', 'circles') |
|
.attr('width', width * dpr) |
|
.attr('height', height * dpr) |
|
.style('width', width + 'px') |
|
.style('height', height + 'px'); |
|
|
|
var ctxCircles = canvasCircles.node().getContext('2d'); |
|
ctxCircles.scale(dpr, dpr); |
|
|
|
var svg = d3.select('#container').append('svg') |
|
.attr('width', width) |
|
.attr('height', height) |
|
.classed('hidden', true); |
|
|
|
var g = svg.append('g') |
|
.attr('class', 'brush'); |
|
|
|
var style = { |
|
"grid": { |
|
"line-color": "#ccc", |
|
"line-width": 1 |
|
}, |
|
"circle": { |
|
"fill-color": "red", |
|
}, |
|
"selected": { |
|
"fill-color": "blue" |
|
} |
|
}; |
|
|
|
var random = Math.random; |
|
|
|
var circles = d3.range(100).map(function(i) { |
|
return { |
|
index: i, |
|
x: random() * width, |
|
y: random() * height |
|
} |
|
}); |
|
|
|
var grid = 20, |
|
radius = 8, |
|
spaceKey = false, |
|
selected = [], |
|
offsetx = 0, |
|
offsety = 0, |
|
transform = d3.zoomIdentity; |
|
|
|
var drag = d3.drag() |
|
.subject(dragsubject) |
|
.on('start', dragstarted) |
|
.on('drag', dragged) |
|
.on('end', dragended) |
|
.on('start.render drag.render end.render', renderCircles); |
|
|
|
var brush = d3.brush() |
|
.on('brush', brushed) |
|
.on('end', brushended); |
|
|
|
var zoom = d3.zoom() |
|
.scaleExtent([1, 1 << 10]) |
|
.on('zoom', zoomed); |
|
|
|
canvasCircles |
|
.call(drag) |
|
.call(zoom); |
|
|
|
renderGrid(); |
|
renderCircles(); |
|
|
|
document.addEventListener("keydown", keydown, false); |
|
|
|
function renderGrid() { |
|
ctxGrid.save(); |
|
ctxGrid.clearRect(0, 0, width, height); |
|
ctxGrid.translate(transform.x, transform.y); |
|
drawGrid(); |
|
ctxGrid.restore(); |
|
} |
|
|
|
function drawGrid() { |
|
ctxGrid.strokeStyle = style.grid["line-color"]; |
|
ctxGrid.lineWidth = style.grid["line-width"]; |
|
ctxGrid.beginPath(); |
|
|
|
for (let i = 0; i <= width / grid; i++) { |
|
ctxGrid.moveTo(i * grid - Math.floor(transform.x / grid) * grid, 0 - Math.ceil(transform.y / grid) * grid); |
|
ctxGrid.lineTo(i * grid - Math.floor(transform.x / grid) * grid, height - Math.floor(transform.y / grid) * grid); |
|
} |
|
|
|
for (let i = 0; i <= height / grid; i++) { |
|
ctxGrid.moveTo(0 - Math.ceil(transform.x / grid) * grid, i * grid - Math.floor(transform.y / grid) * grid); |
|
ctxGrid.lineTo(width - Math.floor(transform.x / grid) * grid, i * grid - Math.floor(transform.y / grid) * grid); |
|
} |
|
|
|
ctxGrid.stroke(); |
|
} |
|
|
|
function renderCircles() { |
|
ctxCircles.save(); |
|
ctxCircles.clearRect(0, 0, width, height); |
|
ctxCircles.translate(transform.x, transform.y); |
|
ctxCircles.scale(transform.k, transform.k); |
|
drawCircles(); |
|
ctxCircles.restore(); |
|
} |
|
|
|
function drawCircles() { |
|
ctxCircles.beginPath(); |
|
|
|
for (let i = 0, n = circles.length, circle; i < n; ++i) { |
|
circle = circles[i]; |
|
ctxCircles.beginPath(); |
|
ctxCircles.arc(circle.x, circle.y, radius / transform.k, 0, 2 * Math.PI); |
|
ctxCircles.fillStyle = (circle.active) ? style.selected["fill-color"] : style.circle["fill-color"]; |
|
ctxCircles.fill(); |
|
} |
|
} |
|
|
|
function dragsubject() { |
|
let x = transform.invertX(d3.event.x), |
|
y = transform.invertY(d3.event.y), |
|
dx, |
|
dy; |
|
|
|
for (let i = circles.length - 1, circle; i >= 0; --i) { |
|
circle = circles[i]; |
|
dx = x - circle.x; |
|
dy = y - circle.y; |
|
|
|
if (dx * dx + dy * dy < radius * radius / transform.k / transform.k) { |
|
circle.x = transform.applyX(circle.x); |
|
circle.y = transform.applyY(circle.y); |
|
return circle; |
|
} |
|
} |
|
} |
|
|
|
function dragstarted() { |
|
d3.event.subject.x = transform.invertX(d3.event.subject.x); |
|
d3.event.subject.y = transform.invertY(d3.event.subject.y); |
|
|
|
circles.splice(circles.indexOf(d3.event.subject), 1); |
|
circles.push(d3.event.subject); |
|
d3.event.subject.active = true; |
|
|
|
if (selected.map(d => d.index).indexOf(d3.event.subject.index) == -1) { |
|
selected.push(d3.event.subject); |
|
} |
|
} |
|
|
|
function dragged() { |
|
offsetx = transform.invertX(d3.event.x) - (d3.event.subject.x); |
|
offsety = transform.invertY(d3.event.y) - (d3.event.subject.y); |
|
|
|
selected.forEach(function(d) { |
|
d.x += offsetx; |
|
d.y += offsety; |
|
}); |
|
} |
|
|
|
function dragended() { |
|
let unit = grid / transform.k; |
|
|
|
selected.forEach(function(d) { |
|
d.x = Math.round(d.x / unit) * unit; |
|
d.y = Math.round(d.y / unit) * unit; |
|
d.active = false; |
|
}); |
|
|
|
d3.event.subject.active = false; |
|
selected = []; |
|
offsetx = 0; |
|
offsety = 0; |
|
} |
|
|
|
function brushed() { |
|
if (d3.event.selection == null) return; |
|
|
|
let extent = d3.event.selection; |
|
|
|
extent[0][0] = transform.invertX(extent[0][0]); |
|
extent[0][1] = transform.invertY(extent[0][1]); |
|
extent[1][0] = transform.invertX(extent[1][0]); |
|
extent[1][1] = transform.invertY(extent[1][1]); |
|
selected = []; |
|
|
|
for (let i = circles.length - 1, circle, x, y; i >= 0; --i) { |
|
circle = circles[i]; |
|
circle.active = false; |
|
if (circle.x >= extent[0][0] && circle.x <= extent[1][0]) { |
|
if (circle.y >= extent[0][1] && circle.y <= extent[1][1]) { |
|
circle.active = true; |
|
selected.push(circle); |
|
} |
|
} |
|
} |
|
|
|
renderCircles(); |
|
} |
|
|
|
function brushended() { |
|
if (d3.event.selection == null) return; |
|
|
|
d3.event.selection[0] = transform.apply(d3.event.selection[0]); |
|
d3.event.selection[1] = transform.apply(d3.event.selection[1]); |
|
} |
|
|
|
function keydown(e) { |
|
if (e.keyCode === 32) { //spacebar key |
|
d3.select('svg').classed('hidden', spaceKey); |
|
spaceKey = !spaceKey; |
|
|
|
if (spaceKey) { |
|
g.call(brush) |
|
} else { |
|
g.call(brush.move, null); |
|
} |
|
} |
|
} |
|
|
|
function zoomed() { |
|
transform = d3.event.transform; |
|
renderGrid(); |
|
renderCircles(); |
|
} |
|
|
|
</script> |
|
</body> |
|
</html> |