HTML5 + Javascript + CSS
So I did it long time ago (the last modified date of the file was 9/21/2012). Glad that I kept it. Unfortunately it only supports 4 control points in its current state, but I'm working on it.
EDIT: Although the UI only supports 4 control points, the underlying function (animateConstruction
) does support an arbitrary number of control points. Though I would not suggest doing it for more than 15 since the code is VERY inefficient. (I tried with 25 and had to kill the tab using Task Manager) If this counts as a valid submission I am not planning on revising the code.
NOTE: I was a naive hobbyist back then. The code is wrong on so many levels (including lack of semicolons and use of eval
).
To Use
Save the code as a .html file and open it in Google Chrome
Code
<html>
<head>
<style>
span.h{
display: inline-block;
text-align: center;
text-decoration: underline;
font: bold 1em Arial;
}
input[type="color"]{
-webkit-appearance: button-bevel;
vertical-align: -7px;
width: 21px;
height: 27px;
}
input[type="color"][disabled]{background: #FFF}
td{position:relative; padding:1px; text-align:center}
table[class] td{text-align:left}
td.t{padding:1px 5px; width:46px;}
table input[type="checkbox"]{visibility:hidden}
tr:hover input[type="checkbox"]{visibility:visible}
</style>
<script type='text/javascript'>
function Bezier(c){
if(c.length==2) return function(t){return [c[0][0]+t*(c[1][0]-c[0][0]),c[0][1]+t*(c[1][1]-c[0][1])]}
else return function(t){return Bezier([Bezier(c.slice(0,-1))(t),Bezier(c.slice(1))(t)])(t)}
}
function Bezier2(f1,f2){
return function(t){return Bezier([f1(t),f2(t)])(t)}
}
//============================================
var c = null
var settings = {'guide':{'show':[true,true,true,true], 'color':['#EEEEEE','#00FF00','#0000FF','#FF00FF'], 'width':[10,1,1,1]}, 'curve':{'show':[true,true,true,true], 'color':['#EEEEEE','#00FF00','#0000FF','#FF00FF'], 'width':[10,3,3,3]}, 'main':{'show':true, 'color':'#FF0000', 'width':10}, 'sample': 100, 'steps':200, 'stepTime':10, 'mode':'Bezier', 'coords':[[0,500],[125,450],[125,0],[500,0]]}
var itv = 0
window.addEventListener('load',function(){
c = $('c').getContext('2d')
c.lineCap = 'round'
c.lineJoin = 'round'
draw(settings.coords,1)
},true)
function get(k,i){
var t = settings
if(k.constructor == Array) k.forEach(function(e){t = t[e]})
return t.length>i ? t[i] : t.slice(-1)[0]
}
function frame(coords){
c.strokeStyle = settings.curve.color[0]
c.lineWidth = settings.guide.width[0]
c.beginPath()
c.moveTo.apply(c,coords[0])
coords.slice(1).forEach(function(e){c.lineTo.apply(c,e)})
c.stroke()
}
function transf(c){
var t = []
c.forEach(function(e){t.push([e[0]+5,e[1]+5])})
return t
}
//============================================
function drawBezier(coords,t){
if(t===undefined) t = 1
coords = transf(coords)
c.clearRect(0,0,510,510)
frame(coords)
c.beginPath()
c.strokeStyle = settings.main.color
c.lineWidth = settings.main.width
c.moveTo.apply(c,coords[0])
for(var i=0;i<=t*settings.sample;i++) c.lineTo.apply(c,Bezier(coords)(i/settings.sample))
c.stroke()
}
function animateBezier(coords){
var s = settings.steps
var cur = ($('t').value==1 ? ($('t').value=$('T').innerHTML=(0).toFixed(3))*1 : $('t').value*s)+1
var b = drawBezier(coords,$('t').value*1)
itv = setInterval(function(){
$("T").innerHTML = ($("t").value = cur/s).toFixed(3)
drawBezier(coords,cur++/s,b)
if(cur>s) clearInterval(itv)
},settings.stepTime)
}
//============================================
function drawBezier2(coords,t){
if(t===undefined) t = 1
c.beginPath()
c.strokeStyle = get(['curve','color'],coords.length-1)
c.lineWidth = get(['curve','width'],coords.length-1)
c.moveTo.apply(c,coords[0])
for(var i=0;i<=t*100;i++) c.lineTo.apply(c,Bezier(coords)(i/100))
c.stroke()
}
function drawConstruction(coords,t,B){
coords = transf(coords)
if(t===undefined) t = 0.5
var b = B===undefined ? [[]] : B
coords.forEach(function(e){b[0].push(function(t){return e})})
c.clearRect(0,0,510,510)
frame(coords)
for(var i=1;i<coords.length;i++){
if(B===undefined) b.push([])
with(c){
for(var j=0;j<coords.length-i;j++){
if(B===undefined) b[i].push(Bezier2(b[i-1][j],b[i-1][j+1]))
if(i!=coords.length-1 && get(['curve','show'],i-1) || i==coords.length-1 && settings.main.show){
strokeStyle = i==coords.length-1?settings.main.color:get(['curve','color'],i-1)
lineWidth = i==coords.length-1?settings.main.width:get(['curve','width'],i-1)
beginPath()
moveTo.apply(c,b[i][j](0))
for(var k=0;k<=t*settings.sample;k++) lineTo.apply(c,b[i][j](k/settings.sample))
stroke()
}
if(i && i!=coords.length-1 && get(['guide','show'],i)){
strokeStyle = i==coords.length-1?settings.main.color:get(['guide','color'],i)
lineWidth = i==coords.length-1?settings.main.width:get(['guide','width'],i)
beginPath()
if(i!=coords.length-1) arc.apply(c,b[i][j](t).concat([settings.curve.width[0]/2,0,2*Math.PI]))
stroke()
}
}
if(i && i!=coords.length-1 && get(['guide','show'],i)){
beginPath()
moveTo.apply(c,b[i][0](t))
for(var j=1;j<coords.length-i;j++) lineTo.apply(c,b[i][j](t))
stroke()
}
}
}
return b
}
function animateConstruction(coords){
var s = settings.steps
var cur = ($('t').value==1 ? ($('t').value=$('T').innerHTML=(0).toFixed(3))*1 : $('t').value*s)+1
var b = drawConstruction(coords,$('t').value*1)
itv = setInterval(function(){
$("T").innerHTML = ($("t").value = cur/s).toFixed(3)
drawConstruction(coords,cur++/s,b)
if(cur>s) clearInterval(itv)
},settings.stepTime)
}
//============================================
function draw(coords,t){clearInterval(itv); return window['draw'+settings.mode](coords,t)}
function animate(coords){clearInterval(itv); return window['animate'+settings.mode](coords);}
//============================================
function $(id){return document.getElementById(id)}
function v(o,p){
for(var i in o){
var k = (p||[]).concat([i]).join('-')
var t
if((t = o[i].constructor) == Object || t == Array) v(o[i],[k])
else if(t = $(k)){
if(t.type=='checkbox') t.checked = o[i]
else if(t.type=='radio'){
for(var j=0, t=document.getElementsByName(t.name); j<t.length; j++) if(t[j].value == o[i]){
t[j].checked = true
break
}
}else t.value = o[i]
}else if(t = $((i==0?'x':'y') + p[0].slice(-1))) t.value = o[i]
}
}
document.addEventListener('load',function(){
v(settings)
$('t').setAttribute('step',1/settings.steps)
var t = document.getElementsByTagName('input')
for(i=0;i<t.length;i++) t[i].addEventListener('change',function(){
var t
if((t=this.id.split('-')).length > 1){
var t1 = function(T){
var t = 'settings'
T.forEach(function(e){t += '[' + (isNaN(e)?'"'+e+'"':e) +']'})
eval(t + '=' + (this.type=='text'?this.value:(this.type=='checkbox'?this.checked:'"'+this.value+'"')))
$(T.join('-')).value = this.value
}
t1.call(this,t)
if(t[0]=='curve' && t[1]=='color' && $('u').checked==true) t1.call(this,['guide'].concat(t.slice(1)))
}else if(this.id == 'u'){
for(i=0;t=$('guide-color-'+i);i++){
t.disabled = this.checked
t.value = settings.guide.color[i] = this.checked?settings.curve.color[i]:t.value
}
}else if(this.id == 't'){
$('T').innerHTML = (this.value*1).toFixed(3)
draw(settings.coords,this.value*1)
}else if(t = /([xy])(\d+)/.exec(this.id)) settings.coords[t[2]*1][t[1]=='x'?0:1] = this.value*1
else settings[this.id] = this.value
if(this.id == 'steps') $("t").setAttribute("step",1/settings.steps)
},true)
},true)
</script>
</head>
<body>
<canvas style='float:left' width='510' height='510' id='c'>
</canvas>
<div style='padding-left:550px; font-family:Arial'>
<span class='h' style='width:123px'>Control Points</span><br />
(<input type='text' id='x0' size='3' maxlength='3' />,<input type='text' id='y0' size='3' maxlength='3' />)<br />
(<input type='text' id='x1' size='3' maxlength='3' />,<input type='text' id='y1' size='3' maxlength='3' />)<br />
(<input type='text' id='x2' size='3' maxlength='3' />,<input type='text' id='y2' size='3' maxlength='3' />)<br />
(<input type='text' id='x3' size='3' maxlength='3' />,<input type='text' id='y3' size='3' maxlength='3' />)<br /><br />
<span class='h' style='width:200px'>Appearance</span><br />
<span style='font-weight:bold'>Guide lines</span><br />
<input type='checkbox' checked='checked' id='u' onchange='' /> Use curve colors<br />
<table style='border-collapse:collapse'>
<tr><td><input type='checkbox' id='guide-show-0' /></td><td><input type='color' id='guide-color-0' disabled='disabled' /></td><td class='t'>Frame</td><td><input type='text' id='guide-width-0' size='2' maxlength='2' /></td></tr>
<tr><td><input type='checkbox' id='guide-show-1' /></td><td><input type='color' id='guide-color-1' disabled='disabled' /></td><td class='t'>1</td><td><input type='text' id='guide-width-1' size='2' maxlength='2' /></td></tr>
<tr><td><input type='checkbox' id='guide-show-2' /></td><td><input type='color' id='guide-color-2' disabled='disabled' /></td><td class='t'>2</td><td><input type='text' id='guide-width-2' size='2' maxlength='2' /></td></tr>
<tr><td><input type='checkbox' id='guide-show-3' /></td><td><input type='color' id='guide-color-3' disabled='disabled' /></td><td class='t'>3</td><td><input type='text' id='guide-width-3' size='2' maxlength='2' /></td></tr>
</table>
<span style='font-weight:bold'>Curves</span>
<table style='border-collapse:collapse'>
<tr><td><input type='checkbox' id='curve-show-0' /></td><td><input type='color' id='curve-color-0' /></td><td class='t'>1</td><td><input type='text' id='curve-width-0' size='2' maxlength='2' /></td></td></tr>
<tr><td><input type='checkbox' id='curve-show-1' /></td><td><input type='color' id='curve-color-1' /></td><td class='t'>2</td><td><input type='text' id='curve-width-1' size='2' maxlength='2' /></td></td></tr>
<tr><td><input type='checkbox' id='curve-show-2' /></td><td><input type='color' id='curve-color-2' /></td><td class='t'>3</td><td><input type='text' id='curve-width-2' size='2' maxlength='2' /></td></td></tr>
<tr><td><input type='checkbox' id='curve-show-3' /></td><td><input type='color' id='curve-color-3' /></td><td class='t'>4</td><td><input type='text' id='curve-width-3' size='2' maxlength='2' /></td></td></tr>
<tr><td><input type='checkbox' id='main-show' /></td><td><input type='color' id='main-color' /></td><td class='t'>Main</td><td><input type='text' id='main-width' size='2' maxlength='2' /></td></td></tr>
</table><br />
<span class='h' style='width:300px'>Graphing & Animation</span><br />
<table class>
<tr><td>Sample points:</td><td><input type='text' id='sample' /></td></tr>
<tr><td>Animation steps:</td><td><input type='text' id='steps' /></td></tr>
<tr><td>Step time:</td><td><input type='text' id='stepTime' />ms</td></tr>
</table>
<div style='position:absolute; top:526px; left:8px; width:510px; height:100px;'>
<input type='range' id='t' max='1' min='0' style='width:450px' value='1' /> <span id='T' style='vertical-align: 6px'>1.000</span><br />
<input type='button' onclick='draw(settings.coords,$("t").value*1)' value='Draw' /><input type='button' onclick='animate(settings.coords)' value='Animate' />
<input type='radio' id='mode' name='mode' value='Bezier' />Basic Mode <input type='radio' id='mode' name='mode' value='Construction' />Construction Mode
</div>
</body>
</html>