rveciana - canvas-spline-editor

Canvas Spline Editor

Open raw page in new tab

Click on the example window to add points. You can move them by dragging, and remove them pressing supr or backspace.

This block is a Canvas version of the Spline Editor by Mike Bostock. To get all the parts together, I read these two nice tutorials: Working with D3.js and Canvas: When and How by [Irene Ros] and (https://bocoup.com/about/bocouper/irene-ros) and D3 and Canvas in 3 steps by lars verspohl. The first one covers the use of D3 with canvas maintaining the enter/update/remove pattern, and the second, the selection of the elements on the canvas.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.hiddenCanvas { display: none; }
</style>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var width = 960,
    height = 500;

var points = [];

var dragged = null,
    selected = null;

d3.select(window)
    .on("mousemove", mousemove)
    .on("mouseup", mouseup)
    .on("keydown", keydown);

var canvas = d3.select("body").append("canvas")
    .attr("width", width)
    .attr("height", height)
    .on("mousedown", mousedown);

var hiddenCanvas = d3.select("body")
  .append('canvas')
  .classed('hiddenCanvas', true) 
  .attr('width', width) 
  .attr('height', height);

var context = canvas.node().getContext("2d");
var hiddenContext = hiddenCanvas.node().getContext("2d");

var colourToNode = {};

var line = d3.line()
    .curve(d3.curveNatural)
    .context(context);
var line2 = d3.line()
    .curve(d3.curveNatural);

var detachedContainer = document.createElement("custom");
var dataContainer = d3.select(detachedContainer);

function redraw(){
    var circle = dataContainer.selectAll("custom.circle")
        .data(points, function(d) { return d; });

    circle.enter().append("custom")
        .classed("circle", true)
        .attr("r", 1e-6)
        .attr("cx", function(d) { return d[0]; })
        .attr("cy", function(d) { return d[1]; })
        .attr("r", 6.5)
        .attr('fillStyleHidden', function(d) { 
            var newColor =  genColor();
            colourToNode[newColor] = this;
            return newColor;
    	});
    
    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 drawCanvas(){
    context.clearRect(0,0,canvas.attr("width"),canvas.attr("height"));
    hiddenContext.clearRect(0,0,hiddenCanvas.attr("width"),hiddenCanvas.attr("height"));
    
    context.beginPath();
    context.lineWidth = 1.5;
    context.strokeStyle = "steelblue";
    line(points);
    context.stroke();

    var elements = dataContainer.selectAll("custom.circle");
    elements.each(function(d){
        var node = d3.select(this);
        context.beginPath();
        context.arc(node.attr("cx"), node.attr("cy"), node.attr("r"), 0, 2 * Math.PI, false);
        context.lineWidth = 1.5;
        if(!node.classed("selected")){
            context.strokeStyle = "rgba(70,130,180,0.7)";
            context.stroke();
        } else {
            context.fillStyle = "rgba(255,127,14,0.2)";
            context.fill();
            context.strokeStyle = "rgba(255,127,14,0.7)";
            context.stroke();
        }
        hiddenContext.beginPath();
        hiddenContext.arc(node.attr("cx"), node.attr("cy"), node.attr("r"), 0, 2 * Math.PI, false);
        hiddenContext.lineWidth = 1.5;
        hiddenContext.strokeStyle = node.attr("fillStyleHidden");
        hiddenContext.fillStyle = node.attr("fillStyleHidden");
        hiddenContext.fill();
        hiddenContext.stroke();
    });
    

}
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;
    }
  }
}


function mousedown() {
  var mousePos = d3.mouse(canvas.node());
  var col = hiddenContext.getImageData(mousePos[0], mousePos[1], 1, 1).data;
  if(col[3] == 255){
    var colKey = 'rgb(' + col[0] + ',' + col[1] + ',' + col[2] + ')';
    var nodeData = d3.select(colourToNode[colKey]);
    selected = dragged = nodeData.datum();
  } else {
    points.push(selected = dragged = d3.mouse(canvas.node()));
  }
  redraw();
  
}
function mousemove() {
  if (!dragged) return;
  var m = d3.mouse(canvas.node());
  dragged[0] = Math.max(0, Math.min(width, m[0]));
  dragged[1] = Math.max(0, Math.min(height, m[1]));
  redraw();
}

/*
  var t = d3.timer(function(elapsed) {
        if (elapsed > 1000) t.stop();
        drawCanvas();
    });*/

d3.timer(drawCanvas);

//https://medium.freecodecamp.org/d3-and-canvas-in-3-steps-8505c8b27444
var nextCol = 1;
function genColor(){ 
  
  var ret = [];
  if(nextCol < 16777215){ 
    
    ret.push(nextCol & 0xff); // R 
    ret.push((nextCol & 0xff00) >> 8); // G 
    ret.push((nextCol & 0xff0000) >> 16); // B
    nextCol += 1; 
  
  }
var col = "rgb(" + ret.join(',') + ")";
return col;
}
</script>