|
| 1 | + |
| 2 | +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> |
| 3 | +<html> |
| 4 | +<head> |
| 5 | + <title>Spline Interpolation</title> |
| 6 | + <meta content="Scaled Innovation, the personal site of Robin W. Spencer"> |
| 7 | + <meta content="javascript, design, analytics, canvas, algorithms, data mining"> |
| 8 | + <meta name="author" content="Robin W. Spencer" /> |
| 9 | + <meta name="date" content="2010-07-22" /> |
| 10 | +<style type="text/css"> |
| 11 | +body{ |
| 12 | + margin:24px; |
| 13 | + font:100% sans-serif; |
| 14 | + color:#999; |
| 15 | + background:#eee; |
| 16 | + position:relative; |
| 17 | +} |
| 18 | +#canvasFrame{ |
| 19 | + position:relative; |
| 20 | + margin:12px 0 0 0; |
| 21 | + padding:12px; |
| 22 | + background:#fff; |
| 23 | + -webkit-box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.5); |
| 24 | + -moz-box-shadow:2px 2px 8px rgba(0, 0, 0, 0.5); |
| 25 | + -webkit-border-radius: 16px; |
| 26 | + -moz-border-radius: 16px; |
| 27 | +} |
| 28 | +h1{ |
| 29 | + text-shadow:1px 1px 1px #000; |
| 30 | +} |
| 31 | +a:link,a:visited,a:active{ |
| 32 | + text-decoration:none; |
| 33 | + color:#aa3; |
| 34 | +} |
| 35 | +a:hover{ |
| 36 | + text-decoration:none; |
| 37 | + color:#ff5; |
| 38 | +} |
| 39 | +input.text{ |
| 40 | + width:36px; |
| 41 | + margin:0 2em 0 0; |
| 42 | + text-align:center; |
| 43 | + background:#fff; |
| 44 | + border:1px solid #ccc; |
| 45 | +} |
| 46 | +.button{ |
| 47 | + margin:0 0 0 1em; |
| 48 | +} |
| 49 | +</style> |
| 50 | +<script type="text/javascript"> |
| 51 | +/* |
| 52 | + Copyright 2010 by Robin W. Spencer |
| 53 | +
|
| 54 | + This program is free software: you can redistribute it and/or modify |
| 55 | + it under the terms of the GNU General Public License as published by |
| 56 | + the Free Software Foundation, either version 3 of the License, or |
| 57 | + (at your option) any later version. |
| 58 | +
|
| 59 | + This program is distributed in the hope that it will be useful, |
| 60 | + but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 61 | + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 62 | + GNU General Public License for more details. |
| 63 | +
|
| 64 | + You can find a copy of the GNU General Public License |
| 65 | + at http://www.gnu.org/licenses/. |
| 66 | +
|
| 67 | +*/ |
| 68 | + |
| 69 | +function HSVtoRGB(h,s,v,opacity){ |
| 70 | + // inputs h=hue=0-360, s=saturation=0-1, v=value=0-1 |
| 71 | + // algorithm from Wikipedia on HSV conversion |
| 72 | + var toHex=function(decimalValue,places){ |
| 73 | + if(places == undefined || isNaN(places)) places = 2; |
| 74 | + var hex = new Array("0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"); |
| 75 | + var next = 0; |
| 76 | + var hexidecimal = ""; |
| 77 | + decimalValue=Math.floor(decimalValue); |
| 78 | + while(decimalValue > 0){ |
| 79 | + next = decimalValue % 16; |
| 80 | + decimalValue = Math.floor((decimalValue - next)/16); |
| 81 | + hexidecimal = hex[next] + hexidecimal; |
| 82 | + } |
| 83 | + while (hexidecimal.length<places){ |
| 84 | + hexidecimal = "0"+hexidecimal; |
| 85 | + } |
| 86 | + return hexidecimal; |
| 87 | + } |
| 88 | + var hi=Math.floor(h/60)%6; |
| 89 | + var f=h/60-Math.floor(h/60); |
| 90 | + var p=v*(1-s); |
| 91 | + var q=v*(1-f*s); |
| 92 | + var t=v*(1-(1-f)*s); |
| 93 | + var r=v; // case hi==0 below |
| 94 | + var g=t; |
| 95 | + var b=p; |
| 96 | + switch(hi){ |
| 97 | + case 1:r=q;g=v;b=p;break; |
| 98 | + case 2:r=p;g=v;b=t;break; |
| 99 | + case 3:r=p;g=q;b=v;break; |
| 100 | + case 4:r=t;g=p;b=v;break; |
| 101 | + case 5:r=v;g=p;b=q;break; |
| 102 | + } |
| 103 | + // At this point r,g,b are in 0...1 range. Now convert into rgba or #FFFFFF notation |
| 104 | + if(opacity){ |
| 105 | + return "rgba("+Math.round(255*r)+","+Math.round(255*g)+","+Math.round(255*b)+","+opacity+")"; |
| 106 | + }else{ |
| 107 | + return "#"+toHex(r*255)+toHex(g*255)+toHex(b*255); |
| 108 | + } |
| 109 | +} |
| 110 | +function hexToCanvasColor(hexColor,opacity){ |
| 111 | + // Convert #AA77CC to rbga() format for Firefox |
| 112 | + opacity=opacity || "1.0"; |
| 113 | + hexColor=hexColor.replace("#",""); |
| 114 | + var r=parseInt(hexColor.substring(0,2),16); |
| 115 | + var g=parseInt(hexColor.substring(2,4),16); |
| 116 | + var b=parseInt(hexColor.substring(4,6),16); |
| 117 | + return "rgba("+r+","+g+","+b+","+opacity+")"; |
| 118 | +} |
| 119 | +function drawPoint(ctx,x,y,r,color){ |
| 120 | + ctx.save(); |
| 121 | + ctx.beginPath(); |
| 122 | + ctx.lineWidth=1; |
| 123 | + ctx.fillStyle=hexToCanvasColor(color,1); |
| 124 | + ctx.arc(x,y,r,0.0,2*Math.PI,false); |
| 125 | + ctx.closePath(); |
| 126 | + ctx.stroke(); |
| 127 | + ctx.fill(); |
| 128 | + ctx.restore(); |
| 129 | +} |
| 130 | +function getControlPoints(x0,y0,x1,y1,x2,y2,t){ |
| 131 | + // x0,y0,x1,y1 are the coordinates of the end (knot) pts of this segment |
| 132 | + // x2,y2 is the next knot -- not connected here but needed to calculate p2 |
| 133 | + // p1 is the control point calculated here, from x1 back toward x0. |
| 134 | + // p2 is the next control point, calculated here and returned to become the |
| 135 | + // next segment's p1. |
| 136 | + // t is the 'tension' which controls how far the control points spread. |
| 137 | + |
| 138 | + // Scaling factors: distances from this knot to the previous and following knots. |
| 139 | + var d01=Math.sqrt(Math.pow(x1-x0,2)+Math.pow(y1-y0,2)); |
| 140 | + var d12=Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2)); |
| 141 | + |
| 142 | + var fa=t*d01/(d01+d12); |
| 143 | + var fb=t-fa; |
| 144 | + |
| 145 | + var p1x=x1+fa*(x0-x2); |
| 146 | + var p1y=y1+fa*(y0-y2); |
| 147 | + |
| 148 | + var p2x=x1-fb*(x0-x2); |
| 149 | + var p2y=y1-fb*(y0-y2); |
| 150 | + |
| 151 | + return [p1x,p1y,p2x,p2y] |
| 152 | +} |
| 153 | +function drawControlLine(ctx,x,y,px,py){ |
| 154 | + // Only for demo purposes: show the control line and control points. |
| 155 | + ctx.save(); |
| 156 | + ctx.beginPath(); |
| 157 | + ctx.lineWidth=1; |
| 158 | + ctx.strokeStyle="rgba(0,0,0,0.3)"; |
| 159 | + ctx.moveTo(x,y); |
| 160 | + ctx.lineTo(px,py); |
| 161 | + ctx.closePath(); |
| 162 | + ctx.stroke(); |
| 163 | + drawPoint(ctx,px,py,1.5,"#000000"); |
| 164 | + ctx.restore(); |
| 165 | +} |
| 166 | +function drawSpline(ctx,pts,t,closed){ |
| 167 | + showDetails=document.getElementById('details').checked; |
| 168 | + ctx.lineWidth=4; |
| 169 | + ctx.save(); |
| 170 | + var cp=[]; // array of control points, as x0,y0,x1,y1,... |
| 171 | + var n=pts.length; |
| 172 | + |
| 173 | + if(closed){ |
| 174 | + // Append and prepend knots and control points to close the curve |
| 175 | + pts.push(pts[0],pts[1],pts[2],pts[3]); |
| 176 | + pts.unshift(pts[n-1]); |
| 177 | + pts.unshift(pts[n-1]); |
| 178 | + for(var i=0;i<n;i+=2){ |
| 179 | + cp=cp.concat(getControlPoints(pts[i],pts[i+1],pts[i+2],pts[i+3],pts[i+4],pts[i+5],t)); |
| 180 | + } |
| 181 | + cp=cp.concat(cp[0],cp[1]); |
| 182 | + for(var i=2;i<n+2;i+=2){ |
| 183 | + var color=HSVtoRGB(Math.floor(240*(i-2)/(n-2)),0.8,0.8); |
| 184 | + if(!showDetails){color="#555555"} |
| 185 | + ctx.strokeStyle=hexToCanvasColor(color,0.75); |
| 186 | + ctx.beginPath(); |
| 187 | + ctx.moveTo(pts[i],pts[i+1]); |
| 188 | + ctx.bezierCurveTo(cp[2*i-2],cp[2*i-1],cp[2*i],cp[2*i+1],pts[i+2],pts[i+3]); |
| 189 | + ctx.stroke(); |
| 190 | + ctx.closePath(); |
| 191 | + if(showDetails){ |
| 192 | + drawControlLine(ctx,pts[i],pts[i+1],cp[2*i-2],cp[2*i-1]); |
| 193 | + drawControlLine(ctx,pts[i+2],pts[i+3],cp[2*i],cp[2*i+1]); |
| 194 | + } |
| 195 | + } |
| 196 | + }else{ |
| 197 | + // Draw an open curve, not connected at the ends |
| 198 | + for(var i=0;i<n-4;i+=2){ |
| 199 | + cp=cp.concat(getControlPoints(pts[i],pts[i+1],pts[i+2],pts[i+3],pts[i+4],pts[i+5],t)); |
| 200 | + } |
| 201 | + for(var i=2;i<pts.length-5;i+=2){ |
| 202 | + var color=HSVtoRGB(Math.floor(240*(i-2)/(n-2)),0.8,0.8); |
| 203 | + if(!showDetails){color="#555555"} |
| 204 | + ctx.strokeStyle=hexToCanvasColor(color,0.75); |
| 205 | + ctx.beginPath(); |
| 206 | + ctx.moveTo(pts[i],pts[i+1]); |
| 207 | + ctx.bezierCurveTo(cp[2*i-2],cp[2*i-1],cp[2*i],cp[2*i+1],pts[i+2],pts[i+3]); |
| 208 | + ctx.stroke(); |
| 209 | + ctx.closePath(); |
| 210 | + if(showDetails){ |
| 211 | + drawControlLine(ctx,pts[i],pts[i+1],cp[2*i-2],cp[2*i-1]); |
| 212 | + drawControlLine(ctx,pts[i+2],pts[i+3],cp[2*i],cp[2*i+1]); |
| 213 | + } |
| 214 | + } |
| 215 | + // For open curves the first and last arcs are simple quadratics. |
| 216 | + var color=HSVtoRGB(40,0.4,0.4); // brown |
| 217 | + if(!showDetails){color="#555555"} |
| 218 | + ctx.strokeStyle=hexToCanvasColor(color,0.75); |
| 219 | + ctx.beginPath(); |
| 220 | + ctx.moveTo(pts[0],pts[1]); |
| 221 | + ctx.quadraticCurveTo(cp[0],cp[1],pts[2],pts[3]); |
| 222 | + ctx.stroke(); |
| 223 | + ctx.closePath(); |
| 224 | + |
| 225 | + var color=HSVtoRGB(240,0.8,0.8); // indigo |
| 226 | + if(!showDetails){color="#555555"} |
| 227 | + ctx.strokeStyle=hexToCanvasColor(color,0.75); |
| 228 | + ctx.beginPath(); |
| 229 | + ctx.moveTo(pts[n-2],pts[n-1]); |
| 230 | + ctx.quadraticCurveTo(cp[2*n-10],cp[2*n-9],pts[n-4],pts[n-3]); |
| 231 | + ctx.stroke(); |
| 232 | + ctx.closePath(); |
| 233 | + if(showDetails){ |
| 234 | + drawControlLine(ctx,pts[2],pts[3],cp[0],cp[1]); |
| 235 | + drawControlLine(ctx,pts[n-4],pts[n-3],cp[2*n-10],cp[2*n-9]); |
| 236 | + } |
| 237 | + } |
| 238 | + ctx.restore(); |
| 239 | + |
| 240 | + if(showDetails){ // Draw the knot points. |
| 241 | + for(var i=0;i<n;i+=2){ |
| 242 | + drawPoint(ctx,pts[i],pts[i+1],2.5,"#ffff00"); |
| 243 | + } |
| 244 | + } |
| 245 | +} |
| 246 | + |
| 247 | +function main(t){ |
| 248 | + var e=document.getElementById("canvas1"); |
| 249 | + e.width=600; |
| 250 | + e.height=550; |
| 251 | + e.parentNode.style.width=e.width+"px"; // The div around the canvas element should fit snugly. |
| 252 | + var ctx=e.getContext('2d'); |
| 253 | + if(!ctx){return} |
| 254 | + ctx.clearRect(0,0,e.width,e.height); |
| 255 | + ctx.scale(1.5,1.5); |
| 256 | + // Drawing a spline takes one call. The points are an array [x0,y0,x1,y1,...], |
| 257 | + // the tension is t (typically 0.33 to 0.5), and true/false tells whether to |
| 258 | + // connect the endpoints of the data to make a closed curve. |
| 259 | + drawSpline(ctx,[20,50,100,100,150,50,200,150,250,50,300,70,310,130,380,30],t,false); |
| 260 | + drawSpline(ctx,[50,200,150,200,150,300,50,300],t,true); |
| 261 | + drawSpline(ctx,[260,240,360,240,310,340],t,true); |
| 262 | + // Update the passive display of tension t. |
| 263 | + document.getElementById("t").value=Math.round(1000*t)/1000; |
| 264 | +} |
| 265 | +</script> |
| 266 | +</head> |
| 267 | +<body onload="main(0.5);"> |
| 268 | +<div style="position:absolute;top:0px;right:12px;text-align:right;"> |
| 269 | +back to <a href='http://scaledinnovation.com'>Scaled Innovation</a><br/><br/> |
| 270 | +<a href="aboutSplines.html">about the geometry</a> |
| 271 | +</div> |
| 272 | +<h1>Spline Interpolation</h1> |
| 273 | +<div id='gFrame'> |
| 274 | +tension t <input disabled="disabled" class="text" id='t' type='text' value='0.5'/></td> |
| 275 | +<input id='details' type='checkbox' checked='checked' value='true'/>show details |
| 276 | +<input class="button" id='animate' style='margin:0 0 0 2em' type='button' onclick='multiAnimate()' value='animate'/> |
| 277 | +<input class="button" type='button' onclick='main(0)' value='t = 0'/> |
| 278 | +<input class="button" type='button' onclick='main(1/5)' value='t = 1/5'/> |
| 279 | +<input class="button" type='button' onclick='main(1/2)' value='t = 1/2'/> |
| 280 | +<input class="button" type='button' onclick='main(1)' value='t = 1'/> |
| 281 | +<input class="button" type='button' onclick='main(2)' value='t = 2'/> |
| 282 | +<input class="button" type='button' onclick='main(-1)' value='t = -1'/> |
| 283 | + |
| 284 | +<div id="canvasFrame"> |
| 285 | +<canvas id='canvas1'> |
| 286 | +<div style="padding:72px;text-align:center;font:18pt bold serif"> |
| 287 | + This page requires a modern HTML5-compliant browser, such as Safari, Firefox, Chrome, or Opera.<br/> |
| 288 | + <img src="splineDefault.png" width="300" height="225" border="0"> |
| 289 | +</div> |
| 290 | +</canvas> |
| 291 | +</div> |
| 292 | +</div> |
| 293 | +</body> |
| 294 | +</html> |
0 commit comments