Skip to content

Instantly share code, notes, and snippets.

@llimllib
Last active August 10, 2016 17:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save llimllib/8d7d571c3233e5cf711f6a6c1e28b5fb to your computer and use it in GitHub Desktop.
Save llimllib/8d7d571c3233e5cf711f6a6c1e28b5fb to your computer and use it in GitHub Desktop.
<!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