changeset 6:d3a25d39a6e2

implement closed x-splines
author Jordi Gutiérrez Hermoso <jordi@ecometrica.com>
date Sun, 26 Aug 2018 10:49:31 -0400
parents a9ffd0bcc292
children 35975411a2a1
files splines.html splines.js
diffstat 2 files changed, 73 insertions(+), 39 deletions(-) [+]
line wrap: on
line diff
--- a/splines.html
+++ b/splines.html
@@ -58,20 +58,23 @@
 
 </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>
+        <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()" />
+        <input name="toggleClosed"
+               type="button"
+               value="Toggle open/closed spline"
+               onclick="toggle_closed()" />
         <label for="interpolate">SVG spline type:</label>
         <select id="interpolate"></select><br>
     </form>
--- a/splines.js
+++ b/splines.js
@@ -5,6 +5,8 @@
     return [i * width / 5, 50 + Math.random() * (height - 100), 1];
 });
 
+// How many points to plot between nodes
+var tstep = 20;
 
 var dragged = null,
     selected = nodes[0];
@@ -63,13 +65,10 @@
 
 d3.select("#slider").call(slider);
 
+var closed_xspline = false;
 
-// 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 tvec = linspace(0, nodes.length-1, (nodes.length-1)*tstep + 1);
 
 var xspline = d3.svg.line()
     .x(function(d,i){return xspline_at_t(d, 0);})
@@ -82,7 +81,12 @@
 
 function redraw() {
     svg.select("path.line").attr("d", line);
-    svg.select("path.xspline").attr("d", xspline);
+    if (!closed_xspline) {
+        tvec = linspace(0, nodes.length-1, (nodes.length-1)*tstep + 1);
+    } else {
+        tvec = linspace(0, nodes.length, nodes.length*tstep + 1);
+    }
+    svg.select("path.xspline").datum(tvec).attr("d", xspline);
 
     var circle = svg.selectAll("circle")
         .data(nodes, function(d) { return d; });
@@ -127,8 +131,6 @@
     newcirc.push(1);
     slider.value(1);
     nodes.push(selected = dragged = newcirc);
-    tvec = linspace(0, nodes.length-1, nodes.length*tstep);
-    svg.select("path.xspline").datum(tvec);
     redraw();
 }
 
@@ -154,8 +156,6 @@
         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;
     }
@@ -184,25 +184,27 @@
 
 // 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;
+    var n = nodes.length;
+
+    if (! closed_xspline) {
+        // No blending functions outside the range
+        if(k >= n || k < 0) {
+            return 0;
+        }
     }
 
-    // Special case the endpoints
-    if ( (t == 0 && k == 0) || (k == nodes.length-1 && t == nodes.length-1)) {
-        return 1;
-    }
 
+    // Determine if we're doing the left or right side
     var s, tboundary;
-    // Determine if we're doing the left or right side
     if (t < k)
     {
-        s = nodes[k-1][2];
+        var leftk = (n + k - 1)%n;
+        s = nodes[leftk][2];
         tboundary = k - 1 - s;
     }
     else {
-        s = nodes[k+1][2];
+        var rightk = (n + k + 1)%n;
+        s = nodes[rightk][2];
         tboundary = k + 1 + s;
     }
 
@@ -248,20 +250,44 @@
 
 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 k1, k2, k3, k4;
+    var tfloor = Math.floor(t);
+    var tceil = Math.ceil(t);
+    if ( t == tfloor) {
+        // When exactly on a node, only immediate neighbours have
+        // influence
+        k1 = t - 1;
+        k2 = t;
+        k3 = t + 1;
+        k4 = null;
+    }
+    else {
+        k1 = tfloor - 1;
+        k2 = tfloor;
+        k3 = tceil;
+        k4 = tceil + 1;
+    }
+
+    var n = nodes.length;
+
+    // When indexing, it has to be mod n
+    var k1_idx = (n + k1) % n,
+        k2_idx = (n + k2) % n,
+        k3_idx = (n + k3) % n,
+        k4_idx = k4 ? (n + k4) % n : null;
 
     var f1t = fk(k1, t),
         f2t = fk(k2, t),
         f3t = fk(k3, t),
-        f4t = fk(k4, t);
+        f4t = k4 ? fk(k4, t) : 0;
 
     // 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 p1 = nodes[k1_idx],
+        p2 = nodes[k2_idx],
+        p3 = nodes[k3_idx],
+        p4 = k4 ? nodes[k4_idx] : [0, 0];
 
     var at_t = (f1t*p1[XorY] + f2t*p2[XorY] + f3t*p3[XorY] + f4t*p4[XorY])/denom;
 
@@ -288,3 +314,8 @@
     }
     redraw();
 }
+
+function toggle_closed() {
+    closed_xspline = !closed_xspline;
+    redraw();
+}