changeset 5:a9ffd0bcc292

implement everything
author Jordi Gutiérrez Hermoso <jordi@ecometrica.com>
date Sun, 26 Aug 2018 00:19:57 -0400
parents 64bf53f0264d
children d3a25d39a6e2
files splines.html splines.js
diffstat 2 files changed, 188 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/splines.html
+++ b/splines.html
@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <head>
     <meta charset="utf-8">
-    <link rel="stylesheet" href="d3.slider.css" />  
+    <link rel="stylesheet" href="d3.slider.css" />
     <title>Spline Editor</title>
     <style>
      body {
@@ -30,6 +30,12 @@
          stroke-width: 1.5px;
      }
 
+     .xspline {
+         fill: none;
+         stroke: red;
+         stroke-width: 1.5px;
+     }
+
      circle {
          fill: #fff;
          fill-opacity: .2;
@@ -52,12 +58,27 @@
 
 </head>
 <body>
+    <input name="approximateButton"
+           type="button"
+           value="Approximate all"
+           onclick="approximate_all()" />
+    <input name="sharplyInterpolateButton"
+           type="button"
+           value="Sharply interpolate all"
+           onclick="sharply_interpolate_all()" />
+    <input name="interpolateButton"
+           type="button"
+           value="Interpolate all"
+           onclick="interpolate_all()" />
+
     <form>
-        <label for="interpolate">Interpolate:</label>
+        <label for="interpolate">SVG spline type:</label>
         <select id="interpolate"></select><br>
     </form>
     <div id="splines"></div>
     <div id="slider"></div>
 
+
+
     <script src="splines.js"></script>
 </body>
--- a/splines.js
+++ b/splines.js
@@ -1,21 +1,21 @@
 var width = 960,
     height = 500;
 
-var points = d3.range(1, 5).map(function(i) {
+var nodes = d3.range(1, 5).map(function(i) {
     return [i * width / 5, 50 + Math.random() * (height - 100), 1];
 });
 
+
 var dragged = null,
-    selected = points[0];
+    selected = nodes[0];
 
 var slider = d3.slider()
     .axis(true)
     .min(-1)
     .max(1)
+    .step(0.01)
     .value(1)
-    .on("slide", function(evt, value){
-        selected[2] = value;
-    });
+    .on("slide", schange);
 
 var line = d3.svg.line();
 
@@ -31,7 +31,7 @@
     .on("mousedown", mousedown);
 
 svg.append("path")
-    .datum(points)
+    .datum(nodes)
     .attr("class", "line")
     .call(redraw);
 
@@ -63,15 +63,33 @@
 
 d3.select("#slider").call(slider);
 
+
+// How many points to plot between nodes
+var tstep = 20;
+// Parameter space, "time", one "second" per node
+var tvec = linspace(0, nodes.length-1, nodes.length*tstep);
+
+var x = d3.scale.linear().range([0,width]).domain([0,tvec.length]);
+
+var xspline = d3.svg.line()
+    .x(function(d,i){return xspline_at_t(d, 0);})
+    .y(function(d,i){return xspline_at_t(d, 1);});
+
+svg.append('path')
+    .datum(tvec)
+    .attr('class','xspline')
+    .call(redraw);
+
 function redraw() {
-    svg.select("path").attr("d", line);
+    svg.select("path.line").attr("d", line);
+    svg.select("path.xspline").attr("d", xspline);
 
     var circle = svg.selectAll("circle")
-        .data(points, function(d) { return d; });
+        .data(nodes, function(d) { return d; });
 
     circle.enter().append("circle")
         .attr("r", 1e-6)
-        .on("mousedown", function(d, i) { 
+        .on("mousedown", function(d, i) {
             selected = dragged = d;
             slider.value(d[2]);
             redraw();
@@ -99,11 +117,18 @@
     redraw();
 }
 
+function schange(evt, value){
+    selected[2] = value;
+    svg.select("path.xspline").attr("d", xspline);
+}
+
 function mousedown() {
     var newcirc = d3.mouse(svg.node());
     newcirc.push(1);
     slider.value(1);
-    points.push(selected = dragged = newcirc);
+    nodes.push(selected = dragged = newcirc);
+    tvec = linspace(0, nodes.length-1, nodes.length*tstep);
+    svg.select("path.xspline").datum(tvec);
     redraw();
 }
 
@@ -126,13 +151,140 @@
     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;
+        var i = nodes.indexOf(selected);
+        nodes.splice(i, 1);
+        selected = nodes.length ? nodes[i > 0 ? i - 1 : 0] : null;
+        tvec = linspace(0, nodes.length-1, nodes.length*tstep);
+        svg.select("path.xspline").datum(tvec);
         redraw();
         break;
     }
     }
 }
 
+// Xspline stuff starts here
 
+function linspace(a, b, n) {
+    let stepsize = (b - a)/(n - 1);
+    return [...Array(n).keys()].map(x => a + x*stepsize);
+}
+
+// Basic polynomials
+function f(p, u) {
+    return u*u*u*(10-p + u*(2*p-15 + u*(6-p)));
+}
+
+function g(q, u) {
+    return u*(q + u*(2*q + u*((8 - 12*q) + u*(14*q - 11 + u*(4 - 5*q)))));
+}
+
+function h(q, u) {
+    return q*u*(1 + u*(2  - u*u*(2 + u)));
+}
+
+// Non-normalised blending function at node k
+function fk(k, t) {
+    // No blending functions outside the range
+    if(k >= nodes.length || k < 0) {
+        return 0;
+    }
+
+    // Special case the endpoints
+    if ( (t == 0 && k == 0) || (k == nodes.length-1 && t == nodes.length-1)) {
+        return 1;
+    }
+
+    var s, tboundary;
+    // Determine if we're doing the left or right side
+    if (t < k)
+    {
+        s = nodes[k-1][2];
+        tboundary = k - 1 - s;
+    }
+    else {
+        s = nodes[k+1][2];
+        tboundary = k + 1 + s;
+    }
+
+    // Approximation or sharp interpolation at this node
+    if (s >= 0) {
+        // Check if it's outside the support of the function
+        if ((t > k && t > tboundary) || (t < k && t < tboundary)) {
+            return 0;
+        }
+
+        var p = 2*(1+s)*(1+s);
+        return f(p, (t-tboundary)/(k-tboundary));
+    }
+
+    // Else, s < 0, and we're smoothly interpolating at this node
+    var q = -s/2;
+
+    //Outside of support
+    if (t < k-2) {
+        return 0;
+    }
+    //left negative
+    else if (t < k-1) {
+        return h(q, t - k + 1);
+    }
+    //left positive
+    else if (t < k) {
+        return g(q, t - k + 1);
+    }
+    //right positive
+    else if (t < k+1) {
+        return g(q, k + 1 -t);
+    }
+    // right negative
+    else if (t < k+ 2){
+        return h(q, k + 1 - t);
+    }
+
+    // Outside of support
+    return 0;
+}
+
+
+function xspline_at_t(t, XorY) {
+    // Determine which blending functions have influence at this point.
+    var k1 = Math.floor(t) - 1, k2 = Math.floor(t), k3 = Math.ceil(t), k4 = Math.ceil(t) + 1;
+
+    var f1t = fk(k1, t),
+        f2t = fk(k2, t),
+        f3t = fk(k3, t),
+        f4t = fk(k4, t);
+
+    // normalisation factor
+    var denom = f1t + f2t + f3t + f4t;
+
+    var p1 = k1 >= 0 ? nodes[k1] : [0, 0],
+        p2 = k2 >= 0 && k2 < nodes.length ? nodes[k2] : [0, 0],
+        p3 = k3 >= 0 && k3 < nodes.length ? nodes[k3] : [0, 0],
+        p4 = k4 < nodes.length ? nodes[k4] : [0, 0];
+
+    var at_t = (f1t*p1[XorY] + f2t*p2[XorY] + f3t*p3[XorY] + f4t*p4[XorY])/denom;
+
+    return at_t;
+}
+
+function approximate_all() {
+    for(var i = 0; i < nodes.length; i++) {
+        nodes[i][2] = 1;
+    }
+    redraw();
+}
+
+function sharply_interpolate_all() {
+    for(var i = 0; i < nodes.length; i++) {
+        nodes[i][2] = 0;
+    }
+    redraw();
+}
+
+function interpolate_all() {
+    for(var i = 0; i < nodes.length; i++) {
+        nodes[i][2] = -1;
+    }
+    redraw();
+}