This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<title>Spline Editor</title> | |
<style> | |
body { | |
font: 13px sans-serif; | |
position: relative; | |
width: 960px; | |
height: 500px; | |
} | |
form { | |
position: absolute; | |
bottom: 10px; | |
left: 10px; | |
} | |
rect { | |
fill: none; | |
pointer-events: all; | |
} | |
circle, | |
.line { | |
fill: none; | |
stroke: steelblue; | |
stroke-width: 1.5px; | |
} | |
.offsetpath { | |
fill: none; | |
stroke: aquamarine; | |
stroke-width: 1.5px; | |
} | |
.offsetline { | |
fill: none; | |
stroke: aquamarine; | |
stroke-width: 1.5px; | |
} | |
.cp { | |
stroke: red; | |
} | |
.offset { | |
stroke: green; | |
} | |
.o1 { | |
stroke: chartreuse; | |
} | |
.o2 { | |
stroke: deeppink; | |
} | |
.int { | |
stroke: darkblue; | |
} | |
.loffset { | |
stroke: aquamarine; | |
} | |
circle { | |
fill: #fff; | |
fill-opacity: .2; | |
cursor: move; | |
} | |
.selected { | |
fill: #ff7f0e; | |
stroke: #ff7f0e; | |
} | |
</style> | |
<form> | |
<label for="interpolate">Interpolate:</label> | |
<select id="interpolate"></select><br> | |
</form> | |
<script src="//d3js.org/d3.v3.min.js"></script> | |
<script src="//d3js.org/d3-path.v0.1.js"></script> | |
<script src="//d3js.org/d3-shape.v0.2.js"></script> | |
<script> | |
var width = 960, | |
height = 500; | |
var points = d3.range(1, 5).map(function(i) { | |
return [i * width / 5, 50 + Math.random() * (height - 100)]; | |
}); | |
var dragged = null, | |
selected = points[0]; | |
var line = d3_shape.line().curve(d3_shape.basis); | |
var svg = d3.select("body").append("svg") | |
.attr("width", width) | |
.attr("height", height) | |
.attr("tabindex", 1); | |
svg.append("rect") | |
.attr("width", width) | |
.attr("height", height) | |
.on("mousedown", mousedown); | |
svg.append("path") | |
.datum(points) | |
.attr("class", "line") | |
.call(redraw); | |
d3.select(window) | |
.on("mousemove", mousemove) | |
.on("mouseup", mouseup) | |
.on("keydown", keydown); | |
d3.select("#interpolate") | |
.on("change", change) | |
.selectAll("option") | |
.data([ | |
"basis", | |
"basisClosed", | |
"basisOpen", | |
"bundle", | |
"cardinal", | |
"cardinalClosed", | |
"cardinalOpen", | |
"catmullRom", | |
"catmullRomClosed", | |
"catmullRomOpen", | |
"linear", | |
"linearClosed", | |
"monotone", | |
"natural", | |
"step", | |
"stepAfter", | |
"stepBefore", | |
]) | |
.enter().append("option") | |
.attr("value", function(d) { return d; }) | |
.text(function(d) { return d; }); | |
svg.node().focus(); | |
// return a normal vector for two points | |
function getNormal(x1, y1, x2, y2, n) { | |
var dx = x2 - x1, | |
dy = y2 - y1, | |
dist = Math.sqrt(dx*dx + dy*dy), | |
normdx = dx / dist, | |
normdy = dy / dist; | |
return [n * normdy, n * -normdx]; | |
} | |
// return the distance between two points | |
function dist(x1, y1, x2, y2) { | |
return Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2)); | |
} | |
// return the intersection point of two lines | |
function intersection(x1, y1, x2, y2, x3, y3, x4, y4) { | |
// sometimes this is already an intersection, and we hit bugs if it is. | |
// Also, if x2,y2 and x3,y3 are very nearly coincident, we hit numerical | |
// instability so just pick x2,y2 arbitrarily | |
if ((x2 == x3 && y2 == y3) || dist(x2, y2, x3, y3) < 1e-5) { | |
return [x2, y2]; | |
} | |
d = (x1-x2)*(y3-y4) - (y1-y2)*(x3-x4); | |
if (d == 0) throw "slope of zero"; | |
xi = ((x3-x4)*(x1*y2-y1*x2)-(x1-x2)*(x3*y4-y3*x4))/d; | |
yi = ((y3-y4)*(x1*y2-y1*x2)-(y1-y2)*(x3*y4-y3*x4))/d; | |
return [xi,yi]; | |
} | |
function circle(r, cx, cy, klass) { | |
svg.append("circle") | |
.attr("class", klass) | |
.attr("r", r) | |
.attr("cx", cx) | |
.attr("cy", cy); | |
} | |
function aline(x1, y1, x2, y2, klass) { | |
svg.append("line") | |
.attr("class", klass) | |
.attr("x1", x1) | |
.attr("y1", y1) | |
.attr("x2", x2) | |
.attr("y2", y2) | |
.attr("stroke", "black") | |
.attr("stroke-width", "1.5"); | |
} | |
function handleCurve(start, curve, n) { | |
var r = 3, | |
x0 = start[0], | |
y0 = start[1], | |
x1 = curve[1], | |
y1 = curve[2], | |
x2 = curve[3], | |
y2 = curve[4], | |
x3 = curve[5], | |
y3 = curve[6], | |
norm1 = getNormal(x0, y0, x1, y1, n), | |
norm2 = getNormal(x1, y1, x2, y2, n), | |
norm3 = getNormal(x2, y2, x3, y3, n), | |
o0x = x0 + norm1[0], | |
o0y = y0 + norm1[1], | |
o1x = x1 + norm1[0], | |
o1y = y1 + norm1[1], | |
o2x = x1 + norm2[0], | |
o2y = y1 + norm2[1], | |
o3x = x2 + norm2[0], | |
o3y = y2 + norm2[1], | |
o4x = x2 + norm3[0], | |
o4y = y2 + norm3[1], | |
o5x = x3 + norm3[0], | |
o5y = y3 + norm3[1], | |
i = intersection(o0x, o0y, o1x, o1y, o2x, o2y, o3x, o3y), | |
j = intersection(o2x, o2y, o3x, o3y, o4x, o4y, o5x, o5y), | |
d = "C" + [i[0], i[1], j[0], j[1], o5x, o5y].join(","); | |
return [[o0x, o0y], [x3, y3], d]; | |
} | |
function handleLine(start, line, n) { | |
var r = 3, | |
x1 = start[0], | |
y1 = start[1], | |
x2 = line[1], | |
y2 = line[2], | |
norm = getNormal(x1, y1, x2, y2, n), | |
o1x = x1 + norm[0], | |
o1y = y1 + norm[1], | |
o2x = x2 + norm[0], | |
o2y = y2 + norm[1], | |
cmd = "L" + [o2x,o2y].join(","); | |
//circle(r, o1x, o1y, "loffset"); | |
//circle(r, o2x, o2y, "loffset"); | |
//aline(x1, y1, x2, y2, "cpline"); | |
return [[o1x, o1y], [x2, y2], cmd]; | |
} | |
function draw_offset(line, n) { | |
// Grab all commands. This does not at all match the full spec of path commands | |
// (see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d), but it | |
// does seem to match d3's commands. Maybe. | |
var commands = line.match(/[MCL][\d\.\,\s]+/g).map(function(s) { | |
return [s.substring(0,1)].concat( | |
s.substring(1).split(",").map(parseFloat)); | |
}); | |
if (commands[0][0] != "M") { throw "expecting moveto"; } | |
var curpt = [commands[0][1], commands[0][2]]; | |
var data = undefined; | |
var d = ""; | |
for (var i=1; i < commands.length; i += 1) { | |
if (commands[i][0] == "C") { | |
data = handleCurve(curpt, commands[i], n); | |
} | |
if (commands[i][0] == "L") { | |
data = handleLine(curpt, commands[i], n); | |
} | |
// if we're on the first command, use its first point as the moveto | |
if (i == 1) { | |
d = "M" + data[0][0] + "," + data[0][1]; | |
} | |
// then append the command it returns | |
d += data[2]; | |
// and save the point where it ended | |
curpt = data[1]; | |
} | |
return d; | |
} | |
function redraw() { | |
d3.selectAll(".cp,.cpline,.offset,.loffset,.offsetpath").remove() | |
var line_svg = line(svg.select("path").datum()), | |
a = draw_offset(line_svg, 20), | |
b = draw_offset(line_svg, 40), | |
c = draw_offset(line_svg, 60); | |
svg.append("path") | |
.attr("class", "offsetpath") | |
.attr("d", a); | |
svg.append("path") | |
.attr("class", "offsetpath") | |
.attr("d", b); | |
svg.append("path") | |
.attr("class", "offsetpath") | |
.attr("d", c); | |
svg.select(".line").attr("d", line); | |
var circle = svg.selectAll(".handle") | |
.data(points, function(d) { return d; }); | |
circle.enter().append("circle") | |
.attr("class", "handle") | |
.attr("r", 1e-6) | |
.on("mousedown", function(d) { selected = dragged = d; redraw(); }) | |
.transition() | |
.duration(750) | |
.ease("elastic") | |
.attr("r", 6.5); | |
circle | |
.classed("selected", function(d) { return d === selected; }) | |
.attr("cx", function(d) { return d[0]; }) | |
.attr("cy", function(d) { return d[1]; }); | |
circle.exit().remove(); | |
if (d3.event) { | |
d3.event.preventDefault(); | |
d3.event.stopPropagation(); | |
} | |
} | |
function change() { | |
// old d3 | |
//line.interpolate(this.value); | |
line.curve(d3_shape[this.value]); | |
redraw(); | |
} | |
function mousedown() { | |
points.push(selected = dragged = d3.mouse(svg.node())); | |
redraw(); | |
} | |
function mousemove() { | |
if (!dragged) return; | |
var m = d3.mouse(svg.node()); | |
dragged[0] = Math.max(0, Math.min(width, m[0])); | |
dragged[1] = Math.max(0, Math.min(height, m[1])); | |
redraw(); | |
} | |
function mouseup() { | |
if (!dragged) return; | |
mousemove(); | |
dragged = null; | |
} | |
function keydown() { | |
if (!selected) return; | |
switch (d3.event.keyCode) { | |
case 8: // backspace | |
case 46: { // delete | |
var i = points.indexOf(selected); | |
points.splice(i, 1); | |
selected = points.length ? points[i > 0 ? i - 1 : 0] : null; | |
redraw(); | |
break; | |
} | |
} | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment