Skip to content

Instantly share code, notes, and snippets.

@yonicd
Last active March 9, 2017 12:37
Show Gist options
  • Save yonicd/4bc59fca901388ebe4905bdb19af1567 to your computer and use it in GitHub Desktop.
Save yonicd/4bc59fca901388ebe4905bdb19af1567 to your computer and use it in GitHub Desktop.
spline editor with animation
license: mit
<!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;
top: 20px;
left: 80px;
}
rect {
fill: none;
pointer-events: all;
}
ellipse {
fill: steelblue;
stroke: steelblue;
stroke-width: 1px;
}
circle,
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
circle {
fill: #fff;
fill-opacity: .2;
cursor: move;
}
.selected {
fill: #ff7f0e;
stroke: #ff7f0e;
}
.tooltip {
position: absolute;
width: 200px;
height: 28px;
pointer-events: none;
}
.axis {
shape-rendering: crispEdges;
}
.x.axis line,
.x.axis path {
fill: none;
stroke: #000;
}
.y.axis line,
.y.axis path {
fill: none;
stroke: #000;
}
</style>
<form>
<br>
<select id="interpolate"></select>
</form>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var x= {n:3,width:600,height:500,anim:1,loop:1};
var margin={top:20,right:20,bottom:30,left:40};
var width = x.width-margin.left-margin.right;
var height = x.height-margin.top-margin.bottom;
var data = {x:[1,2,3], y:[Math.random(),Math.random(),Math.random()]}
var axisName=Object.keys(data);
var points = d3.range(0, x.n).map(function(i) {
return [data[axisName[0]][i], data[axisName[1]][i]];
});
var pointsFloat = points[0];
// setup x
var xValue = function(d) { return d[0];}, // data -> value
xScale = d3.scale.linear()
.domain([d3.min(points, xValue)-1, d3.max(points, xValue)+1])
.range([ 0, width ]),
xMap = function(d) { return xScale(xValue(d));}, // data -> display
xAxis = d3.svg.axis().scale(xScale).orient("bottom");
// setup y
var yValue = function(d) { return d[1];}, // data -> value
yScale = d3.scale.linear()
.domain([d3.min(points, yValue)-1, d3.max(points, yValue)+1])
.range([ height, 0 ]), // value -> display
yMap = function(d) { return yScale(yValue(d));}, // data -> display
yAxis = d3.svg.axis().scale(yScale).orient("left");
//setup inverse scales
var xScaleInv = d3.scale.linear()
.range([d3.min(points, xValue)-1, d3.max(points, xValue)+1])
.domain([ 0, width ]),
yScaleInv = d3.scale.linear()
.range([d3.min(points, yValue)-1, d3.max(points, yValue)+1])
.domain([ height, 0 ]),
xMapInv = function(d) { return xScaleInv(d);},
yMapInv = function(d) { return yScaleInv(d);};
// add tooltip
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// add line
var dragged = null,
selected = points[0];
var line = d3.svg.line()
.x(function(d) { return xMap(d); })
.y(function(d) { return yMap(d); });
//append svg to body
var svg = d3.select('body').append("svg")
.attr("width", width+margin.left+margin.right)
.attr("height", height+margin.top+margin.bottom).append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("tabindex", 1);
// append rect
svg.append("rect")
.attr("width", width)
.attr("height", height)
.on("mousedown", mousedown);
// append line
svg.append("path")
.datum(points)
.attr("class", "line")
.call(redraw);
// map mouse actions
d3.select(window)
.on("mousemove", mousemove)
.on("mouseup", mouseup)
.on("keydown", keydown);
//populate form with interpolation options
d3.select("#interpolate")
.on("change", change)
.selectAll("option")
.data([
"linear",
"step-before",
"step-after",
"basis",
"basis-open",
"basis-closed",
"cardinal",
"cardinal-open",
"cardinal-closed",
"monotone"
])
.enter().append("option")
.attr("value", function(d) { return d; })
.text(function(d) { return d; });
svg.node().focus();
// add x-axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text(axisName[0]);
// add y-axis
svg.append("g")
.attr("class", "y axis")
.attr('transform', 'translate(0,0)')
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text(axisName[1]);
// function that redraws path on mouse clicks and change to form
function redraw() {
var pauseValues = {
lastT: 0,
currentT: 0
};
var path=svg.select("path").attr("d", line);
var circle = svg.selectAll("circle").data(points);
circle.enter().append("circle")
.attr("cx", xMap)
.attr("cy", yMap);
circle.on("mouseover", function(d) {
tooltip.transition()
.duration(200)
.style("opacity", 0.9);
tooltip.html("(" + d3.round(xValue(d),1) + ", " + d3.round(yValue(d),1) + ")")
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY) + "px");
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
circle.classed("selected", function(d) { return d === selected; })
.attr("cx", xMap)
.attr("cy", yMap);
circle.exit().remove();
if (d3.event) {
d3.event.preventDefault();
d3.event.stopPropagation();
}
circle.on("mousedown", function(d) { selected = dragged = d; redraw(); })
.transition()
.duration(750)
.ease("elastic")
.attr("r", 2);
function transition() {
svg.selectAll('ellipse').transition()
.duration(duration - (duration * pauseValues.lastT))
.attrTween("transform", translateAlong(svg.selectAll('path').node()))
.each("end", function(){
pauseValues = {
lastT: 0,
currentT: 0
};
pathpoints={x:[],y:[]};
if(x.loop==1) transition();
});
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
t += pauseValues.lastT;
var p = path.getPointAtLength(t * l);
pauseValues.currentT = t;
if(typeof(Shiny) !== "undefined"){
Shiny.onInputChange(el.id + "_update",{".pathData": JSON.stringify(pathpoints)});
}
return "translate(" + p.x + "," + p.y + ")";
};
};
}
if(x.anim==1){
var pause =0;
var duration=10000;
var circleBig = svg.append("ellipse")
.attr("rx", 5)
.attr("ry", 5)
.attr('transform','translate('+ xMap(pointsFloat) + ',' + yMap(pointsFloat) +')');
pauseButton(svg,width -margin.right-10 , margin.top-15);
//transition();
}
function pauseButton(svg,x, y){
var Pbutton = svg.append("g")
.attr("transform", "translate("+ x +","+ y +")")
.attr('id', 'PbtnId' );
Pbutton
.append("rect")
.attr("width", 25)
.attr("height", 25)
.attr("rx", 6)
.style("fill", "steelblue");
Pbutton
.append("path")
.attr('id', 'pathId' )
.attr("d", "M5 5 L5 20 L20 13 Z")
.style("fill", "white");
Pbutton
.on("mousedown", function() {
d3.select(this).select("rect")
.style("fill","white")
.transition().style("fill","steelblue");
pause++;
if(pause%2==0){
svg.selectAll("ellipse").transition().duration(0);
setTimeout(function() {
pauseValues.lastT=pauseValues.currentT;
}, 100);
Pbutton.select("path").attr("d", "M5 5 L5 20 L20 13 Z");
}else{
Pbutton.select("path").attr("d", "M5 5 L10 5 L10 20 L5 20 M15 5 L20 5 L20 20 L15 20 Z");
transition();
}
});
}
}
//update line with changes to form selection
function change() {
line
.tension(0)
.interpolate(this.value);
svg.selectAll("ellipse").remove();
redraw();
}
// mouse click down
function mousedown() {
points.push(selected = dragged = d3.mouse(svg.node()));
var m = d3.mouse(svg.node());
dragged[0] = xMapInv(Math.max(0, Math.min(width, m[0])));
dragged[1] = yMapInv(Math.max(0, Math.min(height, m[1])));
svg.selectAll("ellipse").remove();
redraw();
}
// mouse drag
function mousemove() {
if (!dragged) return;
var m = d3.mouse(svg.node());
dragged[0] = xMapInv(Math.max(0, Math.min(width, m[0])));
dragged[1] = yMapInv(Math.max(0, Math.min(height, m[1])));
svg.selectAll("ellipse").remove();
redraw();
}
// mouse release
function mouseup() {
if (!dragged) return;
mousemove();
dragged = null;
}
//key press events
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;
svg.selectAll("ellipse").remove();
redraw();
break;
}
}
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment