window["Bezier Section Handlers"] = { "introduction": { handler: (function() { return { drawQuadratic: function(api) { var curve = api.getDefaultQuadratic(); api.setCurve(curve); }, drawCubic: function(api) { var curve = api.getDefaultCubic(); api.setCurve(curve); }, drawCurve: function(api, curve) { api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); } }; }()) }, "whatis": { handler: (function() { return { setup: function(api) { api.setPanelCount(3); var curve = api.getDefaultQuadratic(); api.setCurve(curve); api.step = 25; }, draw: function(api, curve) { var dim = api.getPanelWidth(), pts = curve.points, p1 = pts[0], p2=pts[1], p3 = pts[2], p1e, p2e, m, t, i, offset = {x:0, y:0}, d,v,tvp; api.reset(); api.setColor("black"); api.setFill("black"); api.drawSkeleton(curve, offset); api.text("First linear interpolation at "+api.step+"% steps", {x:5, y:15}, offset); offset.x += dim; api.drawLine({x:0, y:0}, {x:0, y:this.dim}, offset); api.drawSkeleton(curve, offset); api.text("Second interpolation at "+api.step+"% steps", {x:5, y:15}, offset); offset.x += dim; api.drawLine({x:0, y:0}, {x:0, y:this.dim}, offset); api.drawSkeleton(curve, offset); api.text("Curve points generated this way", {x:5, y:15}, offset); api.setColor("lightgrey"); for(t=1,d=20,v,tvp; t0; i -= api.step) { t = i/100; if (t>1) continue; api.setRandomColor(); p1e = { x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y) }; p2e = { x: p2.x + t * (p3.x - p2.x), y: p2.y + t * (p3.y - p2.y) }; m = { x: p1e.x + t * (p2e.x - p1e.x), y: p1e.y + t * (p2e.y - p1e.y) }; offset = {x:0, y:0}; api.drawCircle(p1e,3, offset); api.drawCircle(p2e,3, offset); api.setWeight(0.5); api.drawLine(p1e, p2e, offset); api.setWeight(1.5); api.drawLine(p1, p1e, offset); api.drawLine(p2, p2e, offset); api.setWeight(1); offset.x += dim; api.drawCircle(p1e,3, offset); api.drawCircle(p2e,3, offset); api.setWeight(0.5); api.drawLine(p1e, p2e, offset); api.setWeight(1.5); api.drawLine(p1e, m, offset); api.setWeight(1); api.drawCircle(m,3,offset); offset.x += dim; api.drawCircle(m,3,offset); api.text(i+"%, or t = " + api.utils.round(t,2), {x: m.x + 10 + offset.x, y: m.y + 10 + offset.y}); } }, values: { "38": 1, // up arrow "40": -1 // down arrow }, onKeyDown: function(e, api) { var v = this.values[e.keyCode]; if(v) { e.preventDefault(); api.step += v; if (api.step < 1) { api.step = 1; } } } }; }()) }, "explanation": { handler: (function() { return { statics: { keyHandlingOptions: { propName: "step", values: { "38": 0.1, // up arrow "40": -0.1 // down arrow }, controller: function(api) { if (api.step < 0.1) { api.step = 0.1; } } } }, setup: function(api) { api.step = 5; }, draw: function(api, curve) { var dim = api.getPanelWidth(), w = dim, h = dim, w2 = w/2, h2 = h/2, w4 = w2/2, h4 = h2/2; api.reset(); api.setColor("black"); api.drawLine({x:0,y:h2},{x:w,y:h2}); api.drawLine({x:w2,y:0},{x:w2,y:h}); var offset = {x:w2, y:h2}; for(var t=0, p; t<=api.step; t+=0.1) { p = { x: w4 * Math.cos(t), y: h4 * Math.sin(t) }; api.drawPoint(p, offset); var modulo = t % 1; if(modulo<0.05 || modulo> 0.95) { api.text("t = " + Math.round(t), { x: offset.x + 1.25 * w4 * Math.cos(t) - 10, y: offset.y + 1.25 * h4 * Math.sin(t) + 5 }); api.drawCircle(p, 2, offset); } } } }; }()), withKeys: true }, "control": { handler: (function() { return { drawCubic: function(api) { var curve = api.getDefaultCubic(); api.setCurve(curve); }, drawCurve: function(api, curve) { api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); }, drawFunction: function(api, label, where, generator) { api.setRandomColor(); api.drawFunction(generator); api.setFill(api.getColor()); if (label) api.text(label, where); }, drawLerpBox: function(api, dim, pad, p) { api.noColor(); api.setFill("rgba(0,0,100,0.2)"); var p1 = {x: p.x-5, y:pad}, p2 = {x:p.x + 5, y:dim}; api.drawRect(p1, p2); api.setColor("black"); }, drawLerpPoint: function(api, tf, pad, fwh, p) { p.y = pad + tf*fwh; api.drawCircle(p, 3); api.setFill("black"); api.text(((tf*10000)|0)/100 + "%", {x:p.x+10, y:p.y+4}); api.noFill(); }, drawQuadraticLerp: function(api) { api.reset(); var dim = api.getPanelWidth(), pad = 20, fwh = dim - pad*2; api.drawAxes(pad, "t",0,1, "S","0%","100%"); var p = api.hover; if (p && p.x >= pad && p.x <= dim-pad) { this.drawLerpBox(api, dim, pad, p); var t = (p.x-pad)/fwh; this.drawLerpPoint(api, (1-t)*(1-t), pad, fwh, p); this.drawLerpPoint(api, 2*(1-t)*(t), pad, fwh, p); this.drawLerpPoint(api, (t)*(t), pad, fwh, p); } this.drawFunction(api, "first term", {x: pad*2, y: fwh}, function(t) { return { x: pad + t * fwh, y: pad + fwh * (1-t) * (1-t) }; }); this.drawFunction(api, "second term", {x: dim/2 - 1.5*pad, y: dim/2 + pad}, function(t) { return { x: pad + t * fwh, y: pad + fwh * 2 * (1-t) * (t) }; }); this.drawFunction(api, "third term", {x: fwh - pad*2.5, y: fwh}, function(t) { return { x: pad + t * fwh, y: pad + fwh * (t) * (t) }; }); }, drawCubicLerp: function(api) { api.reset(); var dim = api.getPanelWidth(), pad = 20, fwh = dim - pad*2; api.drawAxes(pad, "t",0,1, "S","0%","100%"); var p = api.hover; if (p && p.x >= pad && p.x <= dim-pad) { this.drawLerpBox(api, dim, pad, p); var t = (p.x-pad)/fwh; this.drawLerpPoint(api, (1-t)*(1-t)*(1-t), pad, fwh, p); this.drawLerpPoint(api, 3*(1-t)*(1-t)*(t), pad, fwh, p); this.drawLerpPoint(api, 3*(1-t)*(t)*(t), pad, fwh, p); this.drawLerpPoint(api, (t)*(t)*(t), pad, fwh, p); } this.drawFunction(api, "first term", {x: pad*2, y: fwh}, function(t) { return { x: pad + t * fwh, y: pad + fwh * (1-t) * (1-t) * (1-t) }; }); this.drawFunction(api, "second term", {x: dim/2 - 4*pad, y: dim/2 }, function(t) { return { x: pad + t * fwh, y: pad + fwh * 3 * (1-t) * (1-t) * (t) }; }); this.drawFunction(api, "third term", {x: dim/2 + 2*pad, y: dim/2}, function(t) { return { x: pad + t * fwh, y: pad + fwh * 3 * (1-t) * (t) * (t) }; }); this.drawFunction(api, "fourth term", {x: fwh - pad*2.5, y: fwh}, function(t) { return { x: pad + t * fwh, y: pad + fwh * (t) * (t) * (t) }; }); }, draw15thLerp: function(api) { api.reset(); var dim = api.getPanelWidth(), pad = 20, fwh = dim - pad*2; api.drawAxes(pad, "t",0,1, "S","0%","100%"); var factors = [1,15,105,455,1365,3003,5005,6435,6435,5005,3003,1365,455,105,15,1]; var p = api.hover, n; if (p && p.x >= pad && p.x <= dim-pad) { this.drawLerpBox(api, dim, pad, p); for(n=0; n<=15; n++) { var t = (p.x-pad)/fwh, tf = factors[n] * Math.pow(1-t, 15-n) * Math.pow(t, n); this.drawLerpPoint(api, tf, pad, fwh, p); } } for(n=0; n<=15; n++) { var label = false, position = false; if (n===0) { label = "first term"; position = {x: pad + 5, y: fwh}; } if (n===15) { label = "last term"; position = {x: dim - 3.5*pad, y: fwh}; } this.drawFunction(api, label, position, function(t) { return { x: pad + t * fwh, y: pad + fwh * factors[n] * Math.pow(1-t, 15-n) * Math.pow(t, n) }; }); } } }; }()) }, "weightcontrol": { handler: (function() { var ratios; return { drawCubic: function(api) { var curve = new api.Bezier( 120, 160, 35, 200, 220, 260, 220, 40 ); api.setCurve(curve); }, drawCurve: function(api, curve) { api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); }, setRatio: function(api, values) { ratios = values; this.update(api); }, changeRatio: function(api, value, pos) { ratios[pos] = parseFloat(value) || 0.00001; this.update(api); }, update: function(api) { api.curve.setRatios(ratios.slice()); this.drawCurve(api, api.curve); }, // ====== drawConic(api) { api.setSize(1.25 * api.getPanelWidth(), api.getPanelHeight()); var vectorOffset = { x: 2.5/5 * api.getPanelWidth(), y: 4/5 * api.getPanelHeight() }; var prj = this.prj = p => api.project(p, vectorOffset); var r = this.r = 60; var h = this.h = 160; this.cone = []; this.density = 50; for(var i=0, e=this.density; i<=e; i++) { let cx = r * Math.cos(Math.PI * 2 * i / e); let cy = r * Math.sin(Math.PI * 2 * i / e); this.cone.push( {x:cx, y:cy, z:0}, {x:0, y:0, z:h} ); } this.cone = this.cone.map(p => prj(p)); }, drawCone(api) { api.reset(); var i, c = this.cone; var center = this.prj({x:0, y:0, z:0}); api.setColor("blue"); api.drawLine(center, this.prj({x:400, y:0, z:0})); api.drawLine(center, this.prj({x:0, y:400, z:0})); api.drawLine(center, this.prj({x:0, y:0, z:400})); api.setColor("red"); api.drawLine(center, this.prj({x:-400, y:0, z:0})); api.drawLine(center, this.prj({x:0, y:-400, z:0})); api.drawLine(center, this.prj({x:0, y:0, z:-400})); // cone api.setColor("rgba(200,100,200,0.6)"); for(i=0; i [p.x]); var nx = multiply(V, x); var y = pts.map(p => [p.y]); var ny = multiply(V, y); var npts = nx.map((x,i) => { return { x: x[0], y: ny[i][0] }; }); return new api.Bezier(npts); }, getInitialState: function() { return { order: 0 }; }, setup: function(api) { var points = []; var w = api.getPanelWidth(), h = api.getPanelHeight(); for (var i=0; i<10; i++) { points.push({ x: w/2 + (Math.random() * 20) + Math.cos(Math.PI*2 * i/10) * (w/2 - 40), y: h/2 + (Math.random() * 20) + Math.sin(Math.PI*2 * i/10) * (h/2 - 40) }); } var curve = new api.Bezier(points); api.setCurve(curve); }, draw: function(api, curve) { api.reset(); var pts = curve.points; this.setState({ order: pts.length }); var p0 = pts[0]; // we can't "just draw" this curve, since it'll be an arbitrary order, // And the canvas only does 2nd and 3rd - we use de Casteljau's algorithm: for(var t=0; t<=1; t+=0.01) { var q = JSON.parse(JSON.stringify(pts)); while(q.length > 1) { for (var i=0; i { if(p===p0) return; api.setColor("#DDD"); api.drawLine(p0,p); api.setColor("black"); api.drawCircle(p,3); p0 = p; }); }, getOrder: function() { var order = this.state.order; if (order%10 === 1 && order !== 11) { order += "st"; } else if (order%10 === 2 && order !== 12) { order += "nd"; } else if (order%10 === 3 && order !== 13) { order += "rd"; } else { order += "th"; } return order; }, onMouseMove: function(evt, api) { api.redraw(); } }; return Reordering; }()), withKeys: true }, "pointvectors": { handler: (function() { return { setupQuadratic: function(api) { var curve = api.getDefaultQuadratic(); api.setCurve(curve); }, setupCubic: function(api) { var curve = api.getDefaultCubic(); api.setCurve(curve); }, draw: function(api, curve) { api.reset(); api.drawSkeleton(curve); var i,t,p,tg,n,m,nd=20; for(i=0; i<=10; i++) { t = i/10.0; p = curve.get(t); tg = curve.derivative(t); m = Math.sqrt(tg.x*tg.x + tg.y*tg.y); tg = {x:tg.x/m, y:tg.y/m}; n = curve.normal(t); api.setColor("blue"); api.drawLine(p, {x:p.x+tg.x*nd, y:p.y+tg.y*nd}); api.setColor("red"); api.drawLine(p, {x:p.x+n.x*nd, y:p.y+n.y*nd}); api.setColor("black"); api.drawCircle(p,3); } } }; }()) }, "pointvectors3d": { handler: (function() { var vectorOffset; var normalsOffset; var SHADOW_ALPHA = 0.2; var SHOW_PROJECTIONS = true; function normalize(v) { var d = Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z); return { x:v.x/d, y:v.y/d, z:v.z/d }; } function vdot(v1, v2) { return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; } function vscale(v1, s) { return { x: s * v1.x, y: s * v1.y, z: s * v1.z }; } function vplus(v1, v2) { return { x: v1.x + v2.x, y: v1.y + v2.y, z: v1.z + v2.z }; } function vminus(v1, v2) { return { x: v1.x - v2.x, y: v1.y - v2.y, z: v1.z - v2.z }; } function vcross(v1, v2) { return { x: v1.y * v2.z - v1.z * v2.y, y: v1.z * v2.x - v1.x * v2.z, z: v1.x * v2.y - v1.y * v2.x }; } function vlerp(t, v1, v2) { return { x: (1-t)*v1.x + t*v2.x, y: (1-t)*v1.y + t*v2.y, z: (1-t)*v1.z + t*v2.z }; } return { setup: function(api) { vectorOffset = { x: 2 * api.getPanelWidth() / 5, y: 4 * api.getPanelHeight() / 5 }; api.setSize(1.25 * api.getPanelWidth(),api.getPanelHeight()); }, drawCube: function(api) { var prj = p => api.project(p, vectorOffset); var cube = [ {x:0, y:0, z:0}, {x:200,y:0, z:0}, {x:200,y:200,z:0}, {x:0, y:200,z:0}, {x:0, y:0, z:200}, {x:200,y:0, z:200}, {x:200,y:200,z:200}, {x:0, y:200,z:200} ].map(p => prj(p)); // "most of the cube" api.setColor("grey"); api.drawLine(cube[1], cube[2]); api.drawLine(cube[2], cube[3]); api.drawLine(cube[1], cube[5]); api.drawLine(cube[2], cube[6]); api.drawLine(cube[3], cube[7]); api.drawLine(cube[4], cube[5]); api.drawLine(cube[5], cube[6]); api.drawLine(cube[6], cube[7]); api.drawLine(cube[7], cube[4]); // x axis api.setColor("blue"); api.drawLine(cube[0], cube[1]); // y axis api.setColor("red"); api.drawLine(cube[3], cube[0]); // z axis api.setColor("green"); api.drawLine(cube[0], cube[4]); }, drawCurve(api, curvepoints, project) { var prj = p => api.project(p, vectorOffset), curve2d = curvepoints.map(p => prj(p)), points; if (project) { // projections api.setColor(`rgba(0,0,0,${SHADOW_ALPHA})`); api.drawCurve({ points: curvepoints.map(p => api.projectXY(p, vectorOffset)) }); api.drawCurve({ points: curvepoints.map(p => api.projectYZ(p, vectorOffset)) }); api.drawCurve({ points: curvepoints.map(p => api.projectXZ(p, vectorOffset)) }); } // control lines api.setColor("#333"); api.drawLine(curve2d[0], curve2d[1]); api.drawCircle(curve2d[1], 3); api.drawCircle(curve2d[2], 3); api.drawLine(curve2d[2], curve2d[3]); // main curve api.setColor("black"); api.drawCircle(curve2d[0], 3); api.drawCircle(curve2d[3], 3); var curve = new api.Bezier(curve2d); api.drawCurve({ points: curve2d }); }, getFrenetVectors: function(t, curve, d1curve) { var o = curve.get(t), // get the normalized tangent dt = d1curve.get(t), // and then let's work in the change in tangent ddt = d1curve.derivative(t), b = normalize(vplus(dt, ddt)), // compute the normalized axis of rotation r = normalize(vcross(b, dt)), // compute the normal n = normalize(vcross(r, dt)); return { o, dt, r, n }; }, lerpVectors(t, v1, v2) { var v = {}; ['o', 'dt', 'r', 'n'].forEach(p => { v[p] = vlerp(t, v1[p], v2[p]); }); return v; }, generateRMF: function(curve, d1curve) { var frames = [], step = 0.05; frames.push(this.getFrenetVectors(0, curve, d1curve)); for(var t0=0; t0<=1; t0+=step) { var x0 = frames.slice(-1)[0], t1 = t0 + step, x1 = { o: curve.get(t1), dt: d1curve.get(t1) }, v1 = vminus(x1.o, x0.o), c1 = vdot(v1, v1), riL = vminus(x0.r, vscale(v1, 2/c1 * vdot(v1, x0.r))), tiL = vminus(x0.dt, vscale(v1, 2/c1 * vdot(v1, x0.dt))), v2 = vminus(x1.dt, tiL), c2 = vdot(v2, v2); x1.r = vminus(riL, vscale(v2, 2/c2 * vdot(v2, riL))); x1.n = vcross(x1.r, x1.dt); frames.push(x1); } return frames; }, getRMF: function(t, curve, d1curve) { if (!this.rmf_LUT) { this.rmf_LUT = this.generateRMF(curve, d1curve); } // find integer index var l = this.rmf_LUT.length; var i = t * l; if (i != (i|0)) { // no intenger index: interpolate values? i = (i|0); if (i===l-1) return this.rmf_LUT[i-1]; var j = i + 1, ti = i/l, tj = j/l; t = (t - ti) / (tj - ti); return this.lerpVectors(t, this.rmf_LUT[i], this.rmf_LUT[j]); } return this.rmf_LUT[i]; }, drawVector: function(api, from, to, len, r,g,b) { var prj = p => api.project(p, vectorOffset); to = normalize(to); to = { x: from.x + len * to.x, y: from.y + len * to.y, z: from.z + len * to.z }; api.setColor(`rgba(${r},${g},${b},1)`); // draw the actual vector api.drawLine(prj(from), prj(to)); }, drawFrenetVectors: function(api) { api.reset(); var prj = p => api.project(p, vectorOffset); this.drawCube(api); var curvepoints = [ {x:120,y:0,z:0}, {x:120,y:220,z:0}, {x:30,y:0,z:30}, {x:0,y:0,z:200} ]; this.drawCurve(api, curvepoints, SHOW_PROJECTIONS); // let's mark t var curve = new api.Bezier(curvepoints); var d1curve = new api.Bezier(curve.dpoints[0]); var t = Math.max(api.hover.x? api.hover.x / api.getPanelWidth() : 0, 0); var mt = curve.get(t); api.drawCircle(prj(mt), 3); // draw the tangent, rotational axis, and normal var vectors = this.getFrenetVectors(t, curve, d1curve); this.drawVector(api, mt, vectors.dt, 40, 0,200,0); this.drawVector(api, mt, vectors.r, 40, 0,0,200); this.drawVector(api, mt, vectors.n, 40, 200,0,0); }, drawRMFNormals: function(api) { api.reset(); var prj = p => api.project(p, vectorOffset); this.drawCube(api); var curvepoints = [ {x:120,y:0,z:0}, {x:120,y:220,z:0}, {x:30,y:0,z:30}, {x:0,y:0,z:200} ]; this.drawCurve(api, curvepoints, SHOW_PROJECTIONS); // let's mark t var curve = new api.Bezier(curvepoints); var d1curve = new api.Bezier(curve.dpoints[0]); var t = Math.max(api.hover.x? api.hover.x / api.getPanelWidth() : 0, 0); var mt = curve.get(t); api.drawCircle(prj(mt), 3); // draw the tangent, rotational axis, and normal var vectors = this.getRMF(t, curve, d1curve); this.drawVector(api, mt, vectors.dt, 40, 0,200,0); this.drawVector(api, mt, vectors.r, 40, 0,0,200); this.drawVector(api, mt, vectors.n, 40, 200,0,0); } }; }()) }, "components": { handler: (function() { return { setupQuadratic: function(api) { var curve = api.getDefaultQuadratic(); curve.points[2].x = 210; api.setCurve(curve); }, setupCubic: function(api) { var curve = api.getDefaultCubic(); api.setCurve(curve); }, draw: function(api, curve) { api.setPanelCount(3); api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); var tf = curve.order, pad = 20, pts = curve.points, w = api.getPanelWidth(), wp = w - 2 * pad, h = api.getPanelHeight(), offset = { x: w, y: 0 }; var x_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:wp*t/tf, y:p.x}; }); api.drawLine({x:0,y:0}, {x:0,y:h}, offset); api.drawAxes(pad, "t",0,1, "x",0,w, offset); offset.x += pad; api.drawCurve(new api.Bezier(x_pts), offset); offset.x += w-pad; var y_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:wp*t/tf, y:p.y}; }); api.drawLine({x:0,y:0}, {x:0,y:h}, offset); api.drawAxes(pad, "t",0,1, "y",0,w, offset); offset.x += pad; api.drawCurve(new api.Bezier(y_pts), offset); } }; }()) }, "extremities": { handler: (function() { return { setupQuadratic: function(api) { var curve = api.getDefaultQuadratic(); curve.points[2].x = 210; api.setCurve(curve); }, setupCubic: function(api) { var curve = api.getDefaultCubic(); api.setCurve(curve); }, draw: function(api, curve) { api.setPanelCount(3); api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); var tf = curve.order + 1, pad = 20, pts = curve.points, w = api.getPanelWidth(), h = api.getPanelHeight(), offset = { x: w, y: 0 }; var x_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.x}; }); api.setColor("black"); api.drawLine({x:0,y:0}, {x:0,y:h}, offset); api.drawAxes(pad, "t",0,1, "x",0,w, offset); offset.x += pad; var xcurve = new api.Bezier(x_pts); api.drawCurve(xcurve, offset); api.setColor("red"); xcurve.extrema().y.forEach(t => { var p = xcurve.get(t); api.drawCircle(p, 3, offset); }); offset.x += w-pad; var y_pts = JSON.parse(JSON.stringify(pts)).map((p,t) => { return {x:w*t/tf, y:p.y}; }); api.setColor("black"); api.drawLine({x:0,y:0}, {x:0,y:h}, offset); api.drawAxes(pad, "t",0,1, "y",0,w, offset); offset.x += pad; var ycurve = new api.Bezier(y_pts); api.drawCurve(ycurve, offset); api.setColor("red"); ycurve.extrema().y.forEach(t => { var p = ycurve.get(t); api.drawCircle(p, 3, offset); }); } }; }()) }, "boundingbox": { handler: (function() { return { setupQuadratic: function(api) { var curve = api.getDefaultQuadratic(); api.setCurve(curve); }, setupCubic: function(api) { var curve = api.getDefaultCubic(); api.setCurve(curve); }, draw: function(api, curve) { api.reset(); api.setColor("#00FF00"); api.drawbbox(curve.bbox()); api.setColor("black"); api.drawSkeleton(curve); api.drawCurve(curve); api.setColor("red"); curve.extrema().values.forEach(t => { api.drawCircle(curve.get(t), 3); }); } }; }()) }, "aligning": { handler: (function() { return { /** * Setup function for a default quadratic curve. */ setupQuadratic: function(api) { var curve = api.getDefaultQuadratic(); api.setCurve(curve); }, /** * Setup function for a default cubic curve. */ setupCubic: function(api) { var curve = api.getDefaultCubic(); api.setCurve(curve); }, /** * A coordinate rotation function that rotates and * translates the curve, such that the first coordinate * of the curve is (0,0) and the last coordinate is (..., 0) */ align: function(points, line) { var tx = line.p1.x, ty = line.p1.y, // The atan2 function is so important to computing // that most CPUs have a dedicated implementation // at the hardware level for it. a = -Math.atan2(line.p2.y-ty, line.p2.x-tx), cos = Math.cos, sin = Math.sin, d = function(v) { return { x: (v.x-tx)*cos(a) - (v.y-ty)*sin(a), y: (v.x-tx)*sin(a) + (v.y-ty)*cos(a) }; }; return points.map(d); }, /** * Draw a curve and its aligned counterpart * side by side across two panels. */ draw: function(api, curve) { api.setPanelCount(2); api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); var pts = curve.points; var line = {p1: pts[0], p2: pts[pts.length-1]}; var apts = this.align(pts, line); var aligned = new api.Bezier(apts); var w = api.getPanelWidth(); var h = api.getPanelHeight(); var offset = {x:w, y:0}; api.setColor("black"); api.drawLine({x:0,y:0}, {x:0,y:h}, offset); offset.x += w/4; offset.y += h/2; api.setColor("grey"); api.drawLine({x:0,y:-h/2}, {x:0,y:h/2}, offset); api.drawLine({x:-w/4,y:0}, {x:w,y:0}, offset); api.setFill("grey"); api.setColor("black"); api.drawSkeleton(aligned, offset); api.drawCurve(aligned, offset); } }; }()) }, "tightbounds": { handler: (function() { return { setupQuadratic: function(api) { var curve = api.getDefaultQuadratic(); api.setCurve(curve); }, setupCubic: function(api) { var curve = api.getDefaultCubic(); api.setCurve(curve); }, align: function(points, line) { var tx = line.p1.x, ty = line.p1.y, a = -Math.atan2(line.p2.y-ty, line.p2.x-tx), cos = Math.cos, sin = Math.sin, d = function(v) { return { x: (v.x-tx)*cos(a) - (v.y-ty)*sin(a), y: (v.x-tx)*sin(a) + (v.y-ty)*cos(a), a: a }; }; return points.map(d); }, // FIXME: I'm not satisfied with needing to turn a bbox[] into a point[], // this needs a bezier.js solution, really, with a call curve.tightbbox() transpose: function(points, angle, offset) { var tx = offset.x, ty = offset.y, cos = Math.cos, sin = Math.sin, v = [points.x.min, points.y.min, points.x.max, points.y.max]; return [ {x: v[0], y: v[1] }, {x: v[2], y: v[1] }, {x: v[2], y: v[3] }, {x: v[0], y: v[3] } ].map(p => { var x=p.x, y=p.y; return { x: x*cos(angle) - y*sin(angle) + tx, y: x*sin(angle) + y*cos(angle) + ty }; }); }, draw: function(api, curve) { api.reset(); var pts = curve.points; var line = {p1: pts[0], p2: pts[pts.length-1]}; var apts = this.align(pts, line); var angle = -apts[0].a; var aligned = new api.Bezier(apts); var bbox = aligned.bbox(); var tpts = this.transpose(bbox, angle, pts[0]); api.setColor("#00FF00"); api.drawLine(tpts[0], tpts[1]); api.drawLine(tpts[1], tpts[2]); api.drawLine(tpts[2], tpts[3]); api.drawLine(tpts[3], tpts[0]); api.setColor("black"); api.drawSkeleton(curve); api.drawCurve(curve); } }; }()) }, "inflections": { handler: (function() { return { setupCubic: function(api) { var curve = new api.Bezier(135,25, 25, 135, 215,75, 215,240); api.setCurve(curve); }, draw: function(api, curve) { api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); api.setColor("red"); curve.inflections().forEach(function(t) { api.drawCircle(curve.get(t), 5); }); } }; }()) }, "canonical": { handler: (function() { return { setup: function(api) { var curve = api.getDefaultCubic(); api.setCurve(curve); api.reset(); api._map_loaded = false; }, draw: function(api, curve) { var w = 400, h = w, unit = this.unit, center = {x:w/2, y:h/2}; api.setSize(w,h); api.setPanelCount(2); api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); api.offset.x += 400; if (api._map_loaded) { api.image(api._map_image); } else { setTimeout(( function() { this.drawBase(api, curve); this.draw(api, curve); } ).bind(this), 100); } api.drawLine({x:0,y:0}, {x:0, y:h}); var npts = [ {x:0, y: 0}, {x:0, y: unit}, {x:unit, y: unit}, this.forwardTransform(curve.points, unit) ]; var canonical = new api.Bezier(npts); api.setColor("blue"); api.drawCurve(canonical, center); api.drawCircle(npts[3], 3, center); }, forwardTransform: function(pts, s) { s = s || 1; var p1 = pts[0], p2 = pts[1], p3 = pts[2], p4 = pts[3]; var xn = -p1.x + p4.x - (-p1.x+p2.x)*(-p1.y+p4.y)/(-p1.y+p2.y); var xd = -p1.x + p3.x - (-p1.x+p2.x)*(-p1.y+p3.y)/(-p1.y+p2.y); var np4x = s*xn/xd; var yt1 = s*(-p1.y+p4.y) / (-p1.y+p2.y); var yt2 = s - (s*(-p1.y+p3.y)/(-p1.y+p2.y)); var yp = yt2 * xn / xd; var np4y = yt1 + yp; return {x:np4x, y:np4y}; }, drawBase: function(api, curve) { api.reset(); var w = 400, h = w, unit = this.unit = w/5, center = {x:w/2, y:h/2}; api.setSize(w,h); // axes + gridlines api.setColor("lightgrey"); for(var x=0; x-10) { pts.push({x:unit*px, y:unit*py}); api.drawLine({x:unit*px, y:unit*py}, {x:unit*x, y:unit*y}, center); } px = x; py = y; } pts.push({x:unit*px, y:unit*py}); api.text("Curve form has cusp →", {x:w/2-unit*2, y: h/2+unit/2.5}); // loop/arch transition boundary, elliptical section api.setColor("#FF00FF"); api.setFill(api.getColor()); var sqrt = Math.sqrt; for (x=1; x>=0; x-=0.005) { pts.push({x:unit*px, y:unit*py}); y = 0.5 * (sqrt(3) * sqrt(4*x - x*x) - x); api.drawLine({x:unit*px, y:unit*py}, {x:unit*x, y:unit*y}, center); px = x; py = y; } pts.push({x:unit*px, y:unit*py}); api.text("← Curve forms a loop at t = 1", {x:w/2+unit/4, y: h/2+unit/1.5}); // loop/arch transition boundary, parabolic section api.setColor("#3300FF"); api.setFill(api.getColor()); for (x=0; x>-w; x-=0.01) { pts.push({x:unit*px, y:unit*py}); y = (-x*x + 3*x)/3; api.drawLine({x:unit*px, y:unit*py}, {x:unit*x, y:unit*y}, center); px = x; py = y; } pts.push({x:unit*px, y:unit*py}); api.text("← Curve forms a loop at t = 0", {x:w/2-unit+10, y: h/2-unit*1.25}); // shape fill api.setColor("transparent"); api.setFill("rgba(255,120,100,0.2)"); api.drawPath(pts, center); pts = [{x:-w/2,y:unit}, {x:w/2,y:unit}, {x:w/2,y:h}, {x:-w/2,y:h}]; api.setFill("rgba(0,200,0,0.2)"); api.drawPath(pts, center); // further labels api.setColor("black"); api.setFill(api.getColor()); api.text("← Curve form has one inflection →", {x:w/2 - unit, y: h/2 + unit*1.75}); api.text("← Plain curve ↕", {x:w/2 + unit/2, y: h/6}); api.text("↕ Double inflection", {x:10, y: h/2 - 10}); api._map_image = api.toImage(); api._map_loaded = true; } }; }()) }, "yforx": { handler: (function() { var sketch = { getCurve: api => { if (!sketch.curve) { sketch.curve = new api.Bezier(20, 250, 30, 20, 200, 250, 250, 20); } return sketch.curve; }, onMouseMove: function(evt, api) { api.redraw(); }, tforx: { setup: function(api) { api.setPanelCount(2); api.setCurve(sketch.getCurve(api)); }, draw: function(api, curve) { api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); let w = api.defaultWidth; let h = api.defaultHeight; let bbox = curve.bbox(); let x = api.mx; if (bbox.x.min < x && x < bbox.x.max) { api.setColor("red"); api.drawLine({ x: x, y: 0 }, { x: x, y: h }); api.text(`x=${x | 0}`, { x: x + 5, y: h - 30 }); } api.setColor("black"); api.drawLine({ x: w, y: 0 }, { x: w, y: h }); api.setOffset({ x: w, y: 0 }); // draw x = t(x) api.drawLine({x:0,y:h-20}, {x:w, y:h-20}); api.text('0', {x:10,y:h-10}); api.text('⅓', {x:10 + (w-10)/3,y:h-10}); api.text('⅔', {x:10 + 2*(w-10)/3,y:h-10}); api.text('1', {x:w-10,y:h-10}); let p, s = { x: 0, y: h - curve.get(0).x }; for (let step = 0.05, t = step; t < 1 + step; t += step) { p = {x: t * w, y: h - curve.get(t).x }; api.drawLine(s, p); s = p; } api.setColor("black"); api.text("↑\nx", {x:10,y:h/2}); api.text("t →", {x:w/2,y:h-10}); if (bbox.x.min < x && x < bbox.x.max) { api.setColor("red"); api.drawLine({ x: 0, y: h-x }, { x: w, y: h-x }); } } }, yforx: { setup: function(api) { api.setCurve(sketch.getCurve(api)); }, draw: function(api, curve) { api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); let w = api.defaultWidth; let h = api.defaultHeight; let bbox = curve.bbox(); let x = api.mx; if (bbox.x.min < x && x < bbox.x.max) { api.setColor("red"); // The root finder is based on normal x/y coordinates, // so we can "trick" it by giving it "t" values as x // values, and "x" values as y values. Since it won't // even look at the x dimension, we can also just leave it. let roots = api.utils.roots(curve.points.map(v => { return { x: v.x, y: v.x-x}; })); roots = roots.filter(t => t>=0 && t<=1.0); let t = roots[0]; let p = curve.get(t); api.drawLine({ x: p.x, y: p.y }, { x: p.x, y: h }); api.drawLine({ x: p.x, y: p.y }, { x: 0, y: p.y }); api.text(`y=${p.y|0}`, { x: p.x/2, y: p.y - 5 }); api.text(`x=${p.x|0}`, { x: x + 5, y: h - (h-p.y)/2 }); api.text(`t=${((t*100)|0)/100}`, { x: x + 15, y: p.y }); } } } }; return sketch; }()) }, "arclength": { handler: (function() { var sin = Math.sin; var tau = Math.PI*2; return { /** * Set up a sinusoid generating function, * which we'll use to draw the "progressively * better looking" integral approximations. */ setup: function(api) { var w = api.getPanelWidth(); var h = api.getPanelHeight(); var generator; if (!this.generator) { generator = ((v,scale) => { scale = scale || 1; return { x: v*w/tau, y: scale * sin(v) }; }); generator.start = 0; generator.end = tau; generator.step = 0.1; generator.scale = h/3; this.generator = generator; } }, /** * Draw the generator's sine function: */ drawSine: function(api, dheight) { var w = api.getPanelWidth(); var h = api.getPanelHeight(); var generator = this.generator; generator.dheight = dheight; api.setColor("black"); api.drawLine({x:0,y:h/2}, {x:w,y:h/2}); api.drawFunction(generator, {x:0, y:h/2}); }, /** * Draw the sliced between the sine curve and * the x-axis, with a variable number of steps so * we can show the approximation becoming better * and better as we increase the step count. */ drawSlices: function(api, steps) { var w = api.getPanelWidth(); var h = api.getPanelHeight(); var f = w/tau; var area = 0; var c = steps <= 25 ? 1 : 0; api.reset(); api.setColor("transparent"); api.setFill("rgba(150,150,255, 0.4)"); for (var step=tau/steps, i=step/2, v, p1, p2; i (p.x += d/2)); c.points.forEach(p => (p.x += 3*d/2)); // And "fake" a master curve that we'll never draw, but which // will allow us to move interact with the curve points. api.setCurve({ points: q.points.concat(c.points) }); }, updateCurves(api, curve) { // update the quadratic and cubic curves by grabbing // whatever the points in our "fake" master curve are let q = this.q; q.points = curve.points.slice(0,3); q.update(); let c = this.c; c.points = curve.points.slice(3,7); c.update(); }, drawCurvature(api, curve, omni) { api.drawSkeleton(curve); api.drawCurve(curve); var s, t, p, n, c, ox, oy; for( s=0; s<256; s++) { // Draw the curvature as a coloured line at the // current point, along the normal. api.setColor('rgba(255,127,'+s+',0.6)'); t = s/255; p = curve.get(t); n = curve.normal(t); c = curve.curvature(t); ox = c.k * n.x; oy = c.k * n.y; api.drawLine(p, { x: p.x + ox, y: p.y + oy }); // And if requested, also draw it along the anti-normal. if (omni) { api.setColor('rgba('+s+',127,255,0.6)'); api.drawLine(p, { x: p.x - ox, y: p.y - oy }); } } }, proxyDraw: function(api, curve, omni) { api.reset(); this.updateCurves(api, curve); [this.q, this.c].forEach(curve => this.drawCurvature(api, curve, omni)); }, draw: function(api, curve) { this.proxyDraw(api, curve); }, drawOmni: function(api, curve) { this.proxyDraw(api, curve, true); } }; }()) }, "tracing": { handler: (function() { return { statics: { keyHandlingOptions: { propName: "steps", values: { "38": 1, // up arrow "40": -1 // down arrow }, controller: function(api) { if (api.steps < 1) { api.steps = 1; } } } }, setup: function(api) { var curve = api.getDefaultCubic(); api.setCurve(curve); api.steps = 8; }, generate: function(api, curve, offset, pad, fwh) { offset.x += pad; offset.y += pad; var len = curve.length(); var pts = [{x:0, y:0, d:0}]; for(var v=1, t, d; v<=100; v++) { t = v/100; d = curve.split(t).left.length(); pts.push({ x: api.utils.map(t, 0,1, 0,fwh), y: api.utils.map(d, 0,len, 0,fwh), d: d, t: t }); } return pts; }, draw: function(api, curve, offset) { api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); var len = curve.length(); var w = api.getPanelWidth(); var h = api.getPanelHeight(); var pad = 20; var fwh = w - 2*pad; offset.x += w; api.drawLine({x:0,y:0}, {x:0,y:h}, offset); api.drawAxes(pad, "t",0,1, "d",0,len, offset); return this.generate(api, curve, offset, pad, fwh); }, plotOnly: function(api, curve) { api.setPanelCount(2); var offset = {x:0, y:0}; var pts = this.draw(api, curve, offset); for(var i=0; i target) { p--; break; } } if(p<0) p=0; if(p===pts.length) p=pts.length-1; ts.push(pts[p]); } for(i=0; i { var pt = { x: api.utils.map(p.t,0,1,0,fwh), y: 0 }; var pd = { x: 0, y: api.utils.map(p.d,0,len,0,fwh) }; api.setColor("black"); api.drawCircle(pt, 3, offset); api.drawCircle(pd, 3, offset); api.setColor("lightgrey"); api.drawLine(pt, {x:pt.x, y:pd.y}, offset); api.drawLine(pd, {x:pt.x, y:pd.y}, offset); }); offset = {x:2*w, y:0}; api.drawLine({x:0,y:0}, {x:0,y:h}, offset); var idx=0, colors = ["rgb(240,0,200)", "rgb(0,40,200)"]; api.setColor(colors[idx]); var p0 = curve.get(pts[0].t), p1; api.drawCircle(curve.get(0), 4, offset); for (i=1, p1; i { api.drawSkeleton(curve); api.setColor("black"); if (p) { var pts = curve.points, mx = min(pts[0].x, pts[1].x), my = min(pts[0].y, pts[1].y), Mx = max(pts[0].x, pts[1].x), My = max(pts[0].y, pts[1].y); if (mx <= p.x && my <= p.y && Mx >= p.x && My >= p.y) { api.setColor("#00FF00"); mark++; } } api.drawCurve(curve); }); if (p) { api.setColor(mark < 2 ? "red" : "#00FF00"); api.drawCircle(p, 3); } }, setupQuadratic: function(api) { var curve1 = api.getDefaultQuadratic(); var curve2 = new api.Bezier([15,250,220,20]); api.setCurve(curve1, curve2); }, setupCubic: function(api) { var curve1 = new api.Bezier([100,240, 30,60, 210,230, 160,30]); var curve2 = new api.Bezier([25,260, 230,20]); api.setCurve(curve1, curve2); }, draw: function(api, curves) { api.reset(); curves.forEach(curve => { api.drawSkeleton(curve); api.drawCurve(curve); }); var utils = api.utils; var line = { p1: curves[1].points[0], p2: curves[1].points[1] }; var acpts = utils.align(curves[0].points, line); var nB = new api.Bezier(acpts); var roots = utils.roots(nB.points); roots.forEach(t => { var p = curves[0].get(t); api.drawCircle(p, 3); api.text("t = " + t, {x: p.x + 5, y: p.y + 10}); }); } }; }()) }, "curveintersection": { handler: (function() { var abs = Math.abs; return { setup: function(api) { this.api = api; api.setPanelCount(3); var curve1 = new api.Bezier(10,100,90,30,40,140,220,220); var curve2 = new api.Bezier(5,150,180,20,80,250,210,190); api.setCurve(curve1, curve2); this.pairReset(); }, pairReset: function() { this.prevstep = 0; this.step = 0; }, draw: function(api, curves) { api.reset(); var offset = {x:0, y:0}; curves.forEach(curve => { api.drawSkeleton(curve); api.drawCurve(curve); }); // next panel: iterations var w = api.getPanelWidth(); var h = api.getPanelHeight(); offset.x += w; api.drawLine({x:0,y:0}, {x:0,y:h}, offset); if (this.step === 0) { this.pairs = [{c1: curves[0], c2: curves[1]}]; } if(this.step !== this.prevstep) { var pairs = this.pairs; this.pairs = []; this.finals = []; pairs.forEach(p => { if(p.c1.length() < 0.6 && p.c2.length() < 0.6) { return this.finals.push(p); } var s1 = p.c1.split(0.5); api.setColor("black"); api.drawCurve(p.c1, offset); api.setColor("red"); api.drawbbox(s1.left.bbox(), offset); api.drawbbox(s1.right.bbox(), offset); var s2 = p.c2.split(0.5); api.setColor("black"); api.drawCurve(p.c2, offset); api.setColor("blue"); api.drawbbox(s2.left.bbox(), offset); api.drawbbox(s2.right.bbox(), offset); if (s1.left.overlaps(s2.left)) { this.pairs.push({c1: s1.left, c2: s2.left}); } if (s1.left.overlaps(s2.right)) { this.pairs.push({c1: s1.left, c2: s2.right}); } if (s1.right.overlaps(s2.left)) { this.pairs.push({c1: s1.right, c2: s2.left}); } if (s1.right.overlaps(s2.right)) { this.pairs.push({c1: s1.right, c2: s2.right}); } }); this.prevstep = this.step; } else { this.pairs.forEach(p => { api.setColor("black"); api.drawCurve(p.c1, offset); api.drawCurve(p.c2, offset); api.setColor("red"); api.drawbbox(p.c1.bbox(), offset); api.setColor("blue"); api.drawbbox(p.c2.bbox(), offset); }); } if (this.pairs.length === 0) { this.pairReset(); this.draw(api, curves); } // next panel: results offset.x += w; api.setColor("black"); api.drawLine({x:0,y:0}, {x:0,y:h}, offset); // get intersections as coordinates var results = curves[0].intersects(curves[1]).map(s => { var tvals = s.split('/').map(v => parseFloat(v)); return {t1: tvals[0], t2: tvals[1]}; }); // filter out likely duplicates var curr = results[0], _, i, same = ((a,b) => abs(a.t1-b.t1) < 0.01 && abs(a.t2-b.t2) < 0.01); for(i=1; i { api.drawCircle(curves[0].get(tvals.t1), 3, offset); }); }, stepUp: function() { this.step++; this.api.redraw(); } }; }()) }, "abc": { handler: (function() { return { // ============== first sketch set ===================== /** * The entry point for the quadratic curve example */ setupQuadratic: function(api) { var curve = api.getDefaultQuadratic(); curve.points[0].y -= 10; api.setCurve(curve); }, /** * The entry point for the cubic curve example */ setupCubic: function(api) { var curve = api.getDefaultCubic(); curve.points[2].y -= 20; api.setCurve(curve); api.lut = curve.getLUT(100); }, /** * When someone clicks a graphic, find the associated * on-curve t value and redraw with that new knowledge. */ onClick: function(evt, api) { api.t = api.curve.on({x: evt.offsetX, y: evt.offsetY},7); if (api.t < 0.05 || api.t > 0.95) api.t = false; api.redraw(); }, /** * The master draw function for the "projection" sketches */ draw: function(api, curve) { // draw the basic curve and curve control points api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); api.setColor("black"); if (!api.t) return; // draw the user-clicked on-curve point api.drawCircle(api.curve.get(api.t),3); api.setColor("lightgrey"); var utils = api.utils; // find the A/B/C values as described in the section text var hull = api.drawHull(curve, api.t); var A, B, C; if(hull.length === 6) { A = curve.points[1]; B = hull[5]; C = utils.lli4(A, B, curve.points[0], curve.points[2]); api.setColor("lightgrey"); api.drawLine(curve.points[0], curve.points[2]); } else if(hull.length === 10) { A = hull[5]; B = hull[9]; C = utils.lli4(A, B, curve.points[0], curve.points[3]); api.setColor("lightgrey"); api.drawLine(curve.points[0], curve.points[3]); } // show the lines between the A/B/C values api.setColor("#00FF00"); api.drawLine(A,B); api.setColor("red"); api.drawLine(B,C); api.setColor("black"); api.drawCircle(C,3); // with their associated labels api.setFill("black"); api.text("A", {x:10 + A.x, y: A.y}); api.text("B (t = " + api.utils.round(api.t,2) + ")", {x:10 + B.x, y: B.y}); api.text("C", {x:10 + C.x, y: C.y}); // and show the distance ratio, which we see does not change irrespective of whether A/B/C change. var d1 = utils.dist(A, B); var d2 = utils.dist(B, C); var ratio = d1/d2; var h = api.getPanelHeight(); api.text("d1 (A-B): " + utils.round(d1,2) + ", d2 (B-C): "+ utils.round(d2,2) + ", ratio (d1/d2): " + utils.round(ratio,4), {x:10, y:h-7}); }, // ============== second sketch set ===================== /** * on mouse move, fix the t value for drawing based on the * cursor position over the sketch. All the way on the left * is t=0, all the way on the right is t=1, with a linear * interpolation for anything in between. */ setCT: function(evt,api) { api.t = evt.offsetX / api.getPanelWidth(); }, /** * Draw the quadratic C(t) values */ drawQCT: function(api) { api.u = api.u || function(t) { var top = (t-1) * (t-1), bottom = 2*t*t - 2*t + 1; return top/bottom; }; this.drawCTgraph(api); }, /** * Draw the cubic C(t) values */ drawCCT: function(api) { api.u = api.u || function(t) { var top = (1-t) * (1-t) * (1-t), bottom = t*t*t + top; return top/bottom; }; this.drawCTgraph(api); }, /** * Draw a C(t) curve */ drawCTgraph: function(api) { api.reset(); var w = api.getPanelWidth(); var pad = 20; var fwh = w - 2*pad; // draw some axes api.setColor("black"); api.drawAxes(pad, "t",0,1, "u",0,1); // draw the C(t) function using an // indirection function that takes a // t value and spits out the C(t) value // as a point coordinate. api.setColor("blue"); var uPoint = function(t) { var value = api.u(t), res = { x: pad + t*fwh, y: pad + value*fwh }; return res; }; api.drawFunction(uPoint); // if the cursor is (or was ever) over this // graphic, draw the "crosshair" that pinpoints // where in the function the associated t/C(t) // coordinate is. if (api.t) { var v = api.u(api.t), v1 = api.utils.round(v,3), v2 = api.utils.round(1-v,3), up = uPoint(api.t); api.drawLine({x:up.x,y:pad}, up); api.drawLine({x:pad,y:up.y}, up); api.drawCircle(up,3); // with some handy text that shows the actual computed values api.setFill("blue"); api.text(" t = " + api.utils.round(api.t,3), {x:up.x+10, y:up.y-7}); api.text("u(t) = " + api.utils.round(v,3), {x:up.x+10, y:up.y+7}); api.setFill("black"); api.text("C = "+v1+" * start + "+v2+" * end", {x:w/2 - pad, y:pad+fwh}); } } }; }()) }, "moulding": { handler: (function() { var abs = Math.abs; return { setupQuadratic: function(api) { api.setPanelCount(3); var curve = api.getDefaultQuadratic(); curve.points[2].x -= 30; api.setCurve(curve); }, setupCubic: function(api) { api.setPanelCount(3); var curve = new api.Bezier([100,230, 30,160, 200,50, 210,160]); curve.points[2].y -= 20; api.setCurve(curve); api.lut = curve.getLUT(100); }, saveCurve: function(evt, api) { if (!api.t) return; if (!api.newcurve) return; api.setCurve(api.newcurve); api.t = false; api.redraw(); }, findTValue: function(evt, api) { var t = api.curve.on({x: evt.offsetX, y: evt.offsetY},7); if (t < 0.05 || t > 0.95) return false; return t; }, markQB: function(evt, api) { api.t = this.findTValue(evt, api); if(api.t) { var t = api.t, t2 = 2*t, top = t2*t - t2, bottom = top + 1, ratio = abs(top/bottom), curve = api.curve, A = api.A = curve.points[1], B = api.B = curve.get(t); api.C = api.utils.lli4(A, B, curve.points[0], curve.points[2]); api.ratio = ratio; this.dragQB(evt, api); } }, markCB: function(evt, api) { api.t = this.findTValue(evt, api); if(api.t) { var t = api.t, mt = (1-t), t3 = t*t*t, mt3 = mt*mt*mt, bottom = t3 + mt3, top = bottom - 1, ratio = abs(top/bottom), curve = api.curve, hull = curve.hull(t), A = api.A = hull[5], B = api.B = curve.get(t); api.db = curve.derivative(t); api.C = api.utils.lli4(A, B, curve.points[0], curve.points[3]); api.ratio = ratio; this.dragCB(evt, api); } }, drag: function(evt, api) { if (!api.t) return; var newB = api.newB = { x: evt.offsetX, y: evt.offsetY }; // now that we know A, B, C and the AB:BC ratio, we can compute the new A' based on the desired B' api.newA = { x: newB.x - (api.C.x - newB.x) / api.ratio, y: newB.y - (api.C.y - newB.y) / api.ratio }; }, dragQB: function(evt, api) { if (!api.t) return; this.drag(evt, api); api.update = [api.newA]; }, dragCB: function(evt, api) { if (!api.t) return; this.drag(evt,api); // preserve struts for B when repositioning var curve = api.curve, hull = curve.hull(api.t), B = api.B, Bl = hull[7], Br = hull[8], dbl = { x: Bl.x - B.x, y: Bl.y - B.y }, dbr = { x: Br.x - B.x, y: Br.y - B.y }, pts = curve.points, // find new point on s--c1 p1 = {x: api.newB.x + dbl.x, y: api.newB.y + dbl.y}, sc1 = { x: api.newA.x - (api.newA.x - p1.x)/(1-api.t), y: api.newA.y - (api.newA.y - p1.y)/(1-api.t) }, // find new point on c2--e p2 = {x: api.newB.x + dbr.x, y: api.newB.y + dbr.y}, sc2 = { x: api.newA.x + (p2.x - api.newA.x)/(api.t), y: api.newA.y + (p2.y - api.newA.y)/(api.t) }, // construct new c1` based on the fact that s--sc1 is s--c1 * t nc1 = { x: pts[0].x + (sc1.x - pts[0].x)/(api.t), y: pts[0].y + (sc1.y - pts[0].y)/(api.t) }, // construct new c2` based on the fact that e--sc2 is e--c2 * (1-t) nc2 = { x: pts[3].x - (pts[3].x - sc2.x)/(1-api.t), y: pts[3].y - (pts[3].y - sc2.y)/(1-api.t) }; api.p1 = p1; api.p2 = p2; api.sc1 = sc1; api.sc2 = sc2; api.nc1 = nc1; api.nc2 = nc2; api.update = [nc1, nc2]; }, drawMould: function(api, curve) { api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); var w = api.getPanelWidth(), h = api.getPanelHeight(), offset = {x:w, y:0}, round = api.utils.round; api.setColor("black"); api.drawLine({x:0,y:0},{x:0,y:h}, offset); api.drawLine({x:w,y:0},{x:w,y:h}, offset); if (api.t && api.update) { api.drawCircle(curve.get(api.t),3); api.npts = [curve.points[0]].concat(api.update).concat([curve.points.slice(-1)[0]]); api.newcurve = new api.Bezier(api.npts); api.setColor("lightgrey"); api.drawCurve(api.newcurve); var newhull = api.drawHull(api.newcurve, api.t, offset); api.drawLine(api.npts[0], api.npts.slice(-1)[0], offset); api.drawLine(api.newA, api.newB, offset); api.setColor("grey"); api.drawCircle(api.newA, 3, offset); api.setColor("blue"); api.drawCircle(api.B, 3, offset); api.drawCircle(api.C, 3, offset); api.drawCircle(api.newB, 3, offset); api.drawLine(api.B, api.C, offset); api.drawLine(api.newB, api.C, offset); api.setFill("black"); api.text("A'", api.newA, {x:offset.x + 7, y:offset.y + 1}); api.text("start", curve.get(0), {x:offset.x + 7, y:offset.y + 1}); api.text("end", curve.get(1), {x:offset.x + 7, y:offset.y + 1}); api.setFill("blue"); api.text("B'", api.newB, {x:offset.x + 7, y:offset.y + 1}); api.text("B, at t = "+round(api.t,2), api.B, {x:offset.x + 7, y:offset.y + 1}); api.text("C", api.C, {x:offset.x + 7, y:offset.y + 1}); if(curve.order === 3) { var hull = curve.hull(api.t); api.drawLine(hull[7], hull[8], offset); api.drawLine(newhull[7], newhull[8], offset); api.drawCircle(newhull[7], 3, offset); api.drawCircle(newhull[8], 3, offset); api.text("e1", newhull[7], {x:offset.x + 7, y:offset.y + 1}); api.text("e2", newhull[8], {x:offset.x + 7, y:offset.y + 1}); } offset.x += w; api.setColor("lightgrey"); api.drawSkeleton(api.newcurve, offset); api.setColor("black"); api.drawCurve(api.newcurve, offset); } else { offset.x += w; api.drawCurve(curve, offset); } } }; }()) }, "pointcurves": { handler: (function() { var abs = Math.abs; return { setup: function(api) { api.lpts = [ {x:56, y:153}, {x:144,y:83}, {x:188,y:185} ]; }, onClick: function(evt, api) { if (api.lpts.length==3) { api.lpts = []; } api.lpts.push({ x: evt.offsetX, y: evt.offsetY }); api.redraw(); }, getQRatio: function(t) { var t2 = 2*t, top = t2*t - t2, bottom = top + 1; return abs(top/bottom); }, getCRatio: function(t) { var mt = (1-t), t3 = t*t*t, mt3 = mt*mt*mt, bottom = t3 + mt3, top = bottom - 1; return abs(top/bottom); }, drawQuadratic: function(api, curve) { var labels = ["start","t=0.5","end"]; api.reset(); api.setColor("lightblue"); api.drawGrid(10,10); api.setFill("black"); api.setColor("black"); api.lpts.forEach((p,i) => { api.drawCircle(p,3); api.text(labels[i], p, {x:5, y:2}); }); if(api.lpts.length === 3) { var S = api.lpts[0], E = api.lpts[2], B = api.lpts[1], C = { x: (S.x + E.x)/2, y: (S.y + E.y)/2 }; api.setColor("blue"); api.drawLine(S, E); api.drawLine(B, C); api.drawCircle(C, 3); var ratio = this.getQRatio(0.5), A = { x: B.x + (B.x-C.x)/ratio, y: B.y + (B.y-C.y)/ratio }; curve = new api.Bezier([S, A, E]); api.setColor("lightgrey"); api.drawLine(A, B); api.drawLine(A, S); api.drawLine(A, E); api.setColor("black"); api.drawCircle(A, 1); api.drawCurve(curve); } }, drawCubic: function(api, curve) { var labels = ["start","t=0.5","end"]; api.reset(); api.setFill("black"); api.setColor("black"); api.lpts.forEach((p,i) => { api.drawCircle(p,3); api.text(labels[i], p, {x:5, y:2}); }); api.setColor("lightblue"); api.drawGrid(10,10); if(api.lpts.length === 3) { var S = api.lpts[0], E = api.lpts[2], B = api.lpts[1], C = { x: (S.x + E.x)/2, y: (S.y + E.y)/2 }; api.setColor("blue"); api.drawLine(S, E); api.drawLine(B, C); api.drawCircle(C, 1); var ratio = this.getCRatio(0.5), A = { x: B.x + (B.x-C.x)/ratio, y: B.y + (B.y-C.y)/ratio }, selen = api.utils.dist(S,E), bclen_min = selen/8, bclen = api.utils.dist(B,C), aesthetics = 4, be12dist = bclen_min + bclen/aesthetics, bx = be12dist * (E.x-S.x)/selen, by = be12dist * (E.y-S.y)/selen, e1 = { x: B.x - bx, y: B.y - by }, e2 = { x: B.x + bx, y: B.y + by }, v1 = { x: A.x + (e1.x-A.x)*2, y: A.y + (e1.y-A.y)*2 }, v2 = { x: A.x + (e2.x-A.x)*2, y: A.y + (e2.y-A.y)*2 }, nc1 = { x: S.x + (v1.x-S.x)*2, y: S.y + (v1.y-S.y)*2 }, nc2 = { x: E.x + (v2.x-E.x)*2, y: E.y + (v2.y-E.y)*2 }; curve = new api.Bezier([S, nc1, nc2, E]); api.drawLine(e1, e2); api.setColor("lightgrey"); api.drawLine(A, C); api.drawLine(A, v1); api.drawLine(A, v2); api.drawLine(S, nc1); api.drawLine(E, nc2); api.drawLine(nc1, nc2); api.setColor("black"); api.drawCircle(A, 1); api.drawCircle(nc1, 1); api.drawCircle(nc2, 1); api.drawCurve(curve); } } }; }()) }, "curvefitting": { handler: (function() { var fit = require('../../../lib/curve-fitter.js'); return { setup: function(api) { this.api = api; this.reset(); }, reset: function() { this.points = []; this.curveset = false; this.mode = 0; if (this.api) { let api = this.api; api.setCurve(false); api.reset(); api.redraw(); } }, toggle: function() { if (this.api) { this.customTimeValues = false; this.mode = (this.mode + 1) % fit.modes.length; this.fitCurve(this.api); this.api.redraw(); } }, draw: function(api, curve) { api.setPanelCount(1); api.reset(); api.setColor('lightgrey'); api.drawGrid(10,10); api.setColor('black'); if (!this.curveset && this.points.length > 2) { curve = this.fitCurve(api); } if (curve) { api.drawCurve(curve); api.drawSkeleton(curve); } api.drawPoints(this.points); if (!this.customTimeValues) { api.setFill(0); api.text("using "+fit.modes[this.mode]+" t values", {x: 5, y: 10}); } }, processTimeUpdate(sliderid, timeValues) { var api = this.api; this.customTimeValues = true; this.fitCurve(api, timeValues); api.redraw(); }, fitCurve(api, timeValues) { let bestFitData = fit(this.points, timeValues || this.mode), x = bestFitData.C.x, y = bestFitData.C.y, bpoints = []; x.forEach((r,i) => { bpoints.push({ x: r[0], y: y[i][0] }); }); var curve = new api.Bezier(bpoints); api.setCurve(curve); this.curveset = true; this.sliders.setOptions(bestFitData.S); return curve; }, onClick: function(evt, api) { this.curveset = false; this.points.push({x: api.mx, y: api.my }); api.redraw(); } }; }()) }, "catmullmoulding": { handler: (function() { return { statics: { keyHandlingOptions: { propName: "distance", values: { "38": 1, // up arrow "40": -1 // down arrow } } }, setup: function(api) { api.setPanelCount(3); api.lpts = [ {x:56, y:153}, {x:144,y:83}, {x:188,y:185} ]; api.distance = 0; }, convert: function(p1, p2, p3, p4) { var t = 0.5; return [ p2, { x: p2.x + (p3.x-p1.x)/(6*t), y: p2.y + (p3.y-p1.y)/(6*t) }, { x: p3.x - (p4.x-p2.x)/(6*t), y: p3.y - (p4.y-p2.y)/(6*t) }, p3 ]; }, draw: function(api) { api.reset(); api.setColor("lightblue"); api.drawGrid(10,10); var pts = api.lpts; api.setColor("black"); api.setFill("black"); pts.forEach((p,pos) => { api.drawCircle(p, 3); api.text("point "+(pos+1), p, {x:10, y:7}); }); var w = api.getPanelWidth(); var h = api.getPanelHeight(); var offset = {x:w, y:0}; api.setColor("lightblue"); api.drawGrid(10,10,offset); api.setColor("black"); api.drawLine({x:0,y:0}, {x:0,y:h}, offset); pts.forEach((p,pos) => { api.drawCircle(p, 3, offset); }); var p1 = pts[0], p2 = pts[1], p3 = pts[2]; var dx = p3.x - p1.x, dy = p3.y - p1.y, m = Math.sqrt(dx*dx + dy*dy); dx /= m; dy /= m; api.drawLine(p1, p3, offset); var p0 = { x: p1.x + (p3.x - p2.x) - api.distance * dx, y: p1.y + (p3.y - p2.y) - api.distance * dy }; var p4 = { x: p1.x + (p3.x - p2.x) + api.distance * dx, y: p1.y + (p3.y - p2.y) + api.distance * dy }; var center = api.utils.lli4(p1,p3,p2,{ x: (p0.x + p4.x)/2, y: (p0.y + p4.y)/2 }); api.setColor("blue"); api.drawCircle(center, 3, offset); api.drawLine(pts[1],center, offset); api.setColor("#666"); api.drawLine(center, p0, offset); api.drawLine(center, p4, offset); api.setFill("blue"); api.text("p0", p0, {x:-20 + offset.x, y:offset.y + 2}); api.text("p4", p4, {x:+10 + offset.x, y:offset.y + 2}); // virtual point p0 api.setColor("red"); api.drawCircle(p0, 3, offset); api.drawLine(p2, p0, offset); api.drawLine(p1, { x: p1.x + (p2.x - p0.x)/5, y: p1.y + (p2.y - p0.y)/5 }, offset); // virtual point p4 api.setColor("#00FF00"); api.drawCircle(p4, 3, offset); api.drawLine(p2, p4, offset); api.drawLine(p3, { x: p3.x + (p4.x - p2.x)/5, y: p3.y + (p4.y - p2.y)/5 }, offset); // Catmull-Rom curve for p0-p1-p2-p3-p4 var c1 = new api.Bezier(this.convert(p0,p1,p2,p3)), c2 = new api.Bezier(this.convert(p1,p2,p3,p4)); api.setColor("lightgrey"); api.drawCurve(c1, offset); api.drawCurve(c2, offset); offset.x += w; api.setColor("lightblue"); api.drawGrid(10,10,offset); api.setColor("black"); api.drawLine({x:0,y:0}, {x:0,y:h}, offset); api.drawCurve(c1, offset); api.drawCurve(c2, offset); api.drawPoints(c1.points, offset); api.drawPoints(c2.points, offset); api.setColor("lightgrey"); api.drawLine(c1.points[0], c1.points[1], offset); api.drawLine(c1.points[2], c2.points[1], offset); api.drawLine(c2.points[2], c2.points[3], offset); } }; }()), withKeys: true }, "polybezier": { handler: (function() { var atan2 = Math.atan2, sqrt = Math.sqrt, sin = Math.sin, cos = Math.cos; return { setupQuadratic: function(api) { var w = api.getPanelWidth(), h = api.getPanelHeight(), cx = w/2, cy = h/2, pad = 40, pts = [ // first curve: {x:cx,y:pad}, {x:w-pad,y:pad}, {x:w-pad,y:cy}, // subsequent curve {x:w-pad,y:h-pad}, {x:cx,y:h-pad}, // subsequent curve {x:pad,y:h-pad}, {x:pad,y:cy}, // final curve control point {x:pad,y:pad} ]; api.lpts = pts; }, setupCubic: function(api) { var w = api.getPanelWidth(), h = api.getPanelHeight(), cx = w/2, cy = h/2, pad = 40, r = (w - 2*pad)/2, k = 0.55228, kr = k*r, pts = [ // first curve: {x:cx,y:pad}, {x:cx+kr,y:pad}, {x:w-pad,y:cy-kr}, {x:w-pad,y:cy}, // subsequent curve {x:w-pad,y:cy+kr}, {x:cx+kr,y:h-pad}, {x:cx,y:h-pad}, // subsequent curve {x:cx-kr,y:h-pad}, {x:pad,y:cy+kr}, {x:pad,y:cy}, // final curve control point {x:pad,y:cy-kr}, {x:cx-kr,y:pad} ]; api.lpts = pts; }, movePointsQuadraticLD: function(api, i) { // ...we need to move _everything_ var anchor, fixed, toMove; for(var p=1; p<4; p++) { anchor = i + (2*p - 2) + api.lpts.length; anchor = api.lpts[anchor % api.lpts.length]; fixed = i + (2*p - 1); fixed = api.lpts[fixed % api.lpts.length]; toMove = i + 2*p; toMove = api.lpts[toMove % api.lpts.length]; toMove.x = fixed.x + (fixed.x - anchor.x); toMove.y = fixed.y + (fixed.y - anchor.y); } // then, the furthest point cannot be computed properly! toMove = i + 6; toMove = api.lpts[toMove % api.lpts.length]; api.problem = toMove; }, movePointsCubicLD: function(api, i) { var toMove, fixed; if (i%3 === 1) { fixed = i-1; fixed += (fixed < 0) ? api.lpts.length : 0; toMove = i-2; toMove += (toMove < 0) ? api.lpts.length : 0; } else { fixed = (i+1) % api.lpts.length; toMove = (i+2) % api.lpts.length; } fixed = api.lpts[fixed]; toMove = api.lpts[toMove]; toMove.x = fixed.x + (fixed.x - api.mp.x); toMove.y = fixed.y + (fixed.y - api.mp.y); }, linkDerivatives: function(evt, api) { if (api.mp) { var quad = api.lpts.length === 8; var i = api.mp_idx; if (quad) { if (i%2 !== 0) { this.movePointsQuadraticLD(api, i); } } else { if(i%3 !== 0) { this.movePointsCubicLD(api, i); } } } }, movePointsQuadraticDirOnly: function(api, i) { // ...we need to move _everything_ ...again var anchor, fixed, toMove; // move left and right [-1,1].forEach(v => { anchor = api.mp; fixed = i + v + api.lpts.length; fixed = api.lpts[fixed % api.lpts.length]; toMove = i + 2*v + api.lpts.length; toMove = api.lpts[toMove % api.lpts.length]; var a = atan2(fixed.y - anchor.y, fixed.x - anchor.x), dx = toMove.x - fixed.x, dy = toMove.y - fixed.y, d = sqrt(dx*dx + dy*dy); toMove.x = fixed.x + d*cos(a); toMove.y = fixed.y + d*sin(a); }); // then, the furthest point cannot be computed properly! toMove = i + 4; toMove = api.lpts[toMove % api.lpts.length]; api.problem = toMove; }, movePointsCubicDirOnly: function(api, i) { var toMove, fixed; if (i%3 === 1) { fixed = i-1; fixed += (fixed < 0) ? api.lpts.length : 0; toMove = i-2; toMove += (toMove < 0) ? api.lpts.length : 0; } else { fixed = (i+1) % api.lpts.length; toMove = (i+2) % api.lpts.length; } fixed = api.lpts[fixed]; toMove = api.lpts[toMove]; var a = atan2(fixed.y - api.mp.y, fixed.x - api.mp.x), dx = toMove.x - fixed.x, dy = toMove.y - fixed.y, d = sqrt(dx*dx + dy*dy); toMove.x = fixed.x + d*cos(a); toMove.y = fixed.y + d*sin(a); }, linkDirection: function(evt, api) { if (api.mp) { var quad = api.lpts.length === 8; var i = api.mp_idx; if (quad) { if(i%2 !== 0) { this.movePointsQuadraticDirOnly(api, i); } } else { if(i%3 !== 0) { this.movePointsCubicDirOnly(api, i); } } } }, bufferPoints: function(evt, api) { api.bpts = JSON.parse(JSON.stringify(api.lpts)); }, moveQuadraticPoint: function(api, i) { this.moveCubicPoint(api,i); // then move the other control points [-1,1].forEach(v => { var anchor = i - v + api.lpts.length; anchor = api.lpts[anchor % api.lpts.length]; var fixed = i - 2*v + api.lpts.length; fixed = api.lpts[fixed % api.lpts.length]; var toMove = i - 3*v + api.lpts.length; toMove = api.lpts[toMove % api.lpts.length]; var a = atan2(fixed.y - anchor.y, fixed.x - anchor.x), dx = toMove.x - fixed.x, dy = toMove.y - fixed.y, d = sqrt(dx*dx + dy*dy); toMove.x = fixed.x + d*cos(a); toMove.y = fixed.y + d*sin(a); }); // then signal a problem var toMove = i + 4; toMove = api.lpts[toMove % api.lpts.length]; api.problem = toMove; }, moveCubicPoint: function(api, i) { var op = api.bpts[i], np = api.lpts[i], dx = np.x - op.x, dy = np.y - op.y, len = api.lpts.length, l = i-1+len, r = i+1, // original left and right ol = api.bpts[l % len], or = api.bpts[r % len], // current left and right nl = api.lpts[l % len], nr = api.lpts[r % len]; // update current left nl.x = ol.x + dx; nl.y = ol.y + dy; // update current right nr.x = or.x + dx; nr.y = or.y + dy; return {x:dx, y:dy}; }, modelCurve: function(evt, api) { if (api.mp) { var quad = api.lpts.length === 8; var i = api.mp_idx; if (quad) { if (i%2 !== 0) { this.movePointsQuadraticDirOnly(api, i); } else { this.moveQuadraticPoint(api, i); } } else { if(i%3 !== 0) { this.movePointsCubicDirOnly(api, i); } else { this.moveCubicPoint(api, i); } } } }, draw: function(api, curves) { api.reset(); var pts = api.lpts; var quad = pts.length === 8; var c1 = quad ? new api.Bezier(pts[0],pts[1],pts[2]) : new api.Bezier(pts[0],pts[1],pts[2],pts[3]); api.drawSkeleton(c1, false, true); api.drawCurve(c1); var c2 = quad ? new api.Bezier(pts[2],pts[3],pts[4]) : new api.Bezier(pts[3],pts[4],pts[5],pts[6]); api.drawSkeleton(c2, false, true); api.drawCurve(c2); var c3 = quad ? new api.Bezier(pts[4],pts[5],pts[6]) : new api.Bezier(pts[6],pts[7],pts[8],pts[9]); api.drawSkeleton(c3, false, true); api.drawCurve(c3); var c4 = quad ? new api.Bezier(pts[6],pts[7],pts[0]) : new api.Bezier(pts[9],pts[10],pts[11],pts[0]); api.drawSkeleton(c4, false, true); api.drawCurve(c4); if (api.problem) { api.setColor("red"); api.drawCircle(api.problem,5); } } }; }()) }, "shapes": { handler: (function() { var modes; return { getInitialState: function() { modes = this.modes = ["unite","intersect","exclude","subtract"]; return { mode: modes[0] }; }, setMode: function(mode) { this.setState({ mode: mode }); }, formPath: function(api, mx, my, w, h) { mx = mx || 0; my = my || 0; var unit = 30; var unit2 = unit/2; w = w || 8 * unit; h = h || 4 * unit; var w2 = w/2; var h2 = h/2; var ow3 = w2/3; var oh3 = h2/3; var Paper = api.Paper; var Path = Paper.Path; var Point = Paper.Point; var path = new Path(); path.moveTo( new Point(mx-w2 + unit*2, my-h2) ); path.cubicCurveTo( new Point(mx-w2 + unit2, my-h2 + unit2), new Point(mx-w2 + unit2, my+h2 - unit2), new Point(mx-w2 + unit*2, my+h2) ); path.cubicCurveTo( new Point(mx-ow3, my+oh3), new Point(mx+ow3, my+oh3), new Point(mx+w2 - unit*2, my+h2) ); path.cubicCurveTo( new Point(mx+w2 - unit2, my+h2 - unit2), new Point(mx+w2 - unit2, my-h2 + unit2), new Point(mx+w2 - unit*2, my-h2) ); path.cubicCurveTo( new Point(mx+ow3, my-oh3), new Point(mx-ow3, my-oh3), new Point(mx-w2 + unit*2, my-h2) ); path.closePath(true); path.strokeColor = "rgb(100,100,255)"; return path; }, setup: function(api) { var dim = api.getPanelWidth(); var pad = 40; var cx = dim/2; var cy = dim/2; api.c1 = this.formPath(api, cx, cy); cx += pad; cy += pad; api.c2 = this.formPath(api, cx, cy); this.state.mode = modes[0]; }, onMouseMove: function(evt, api) { var cx = evt.offsetX; var cy = evt.offsetY; api.c2.position = {x:cx, y:cy}; }, draw: function(api) { if (api.c3) { api.c3.remove(); } var c1 = api.c1, c2 = api.c2, fn = c1[this.state.mode].bind(c1), c3 = api.c3 = fn(c2); c3.strokeColor = "red"; c3.fillColor = "rgba(255,100,100,0.4)"; api.Paper.view.draw(); } }; }()) }, "projections": { handler: (function() { return { setup: function(api) { api.setSize(320,320); var curve = new api.Bezier([ {x:248,y:188}, {x:218,y:294}, {x:45,y:290}, {x:12,y:236}, {x:14,y:82}, {x:186,y:177}, {x:221,y:90}, {x:18,y:156}, {x:34,y:57}, {x:198,y:18} ]); api.setCurve(curve); api._lut = curve.getLUT(); }, findClosest: function(LUT, p, dist) { var i, end = LUT.length, d, dd = dist(LUT[0],p), f = 0; for(i=1; i { api.setRandomColor(); api.drawCurve(c); api.drawCircle(c.points[0], 1); }); var last = reduced.slice(-1)[0]; api.drawPoint(last.points[3] || last.points[2]); api.setColor("red"); var offset = curve.offset(api.distance); offset.forEach(c => { api.drawPoint(c.points[0]); api.drawCurve(c); }); last = offset.slice(-1)[0]; api.drawPoint(last.points[3] || last.points[2]); api.setColor("blue"); offset = curve.offset(-api.distance); offset.forEach(c => { api.drawPoint(c.points[0]); api.drawCurve(c); }); last = offset.slice(-1)[0]; api.drawPoint(last.points[3] || last.points[2]); } }; }()), withKeys: true }, "graduatedoffset": { handler: (function() { return { statics: { keyHandlingOptions: { propName: "distance", values: { "38": 1, // up arrow "40": -1 // down arrow } } }, setup: function(api, curve) { api.setCurve(curve); api.distance = 20; }, setupQuadratic: function(api) { var curve = api.getDefaultQuadratic(); this.setup(api, curve); }, setupCubic: function(api) { var curve = api.getDefaultCubic(); this.setup(api, curve); }, draw: function(api, curve) { api.reset(); api.drawSkeleton(curve); api.drawCurve(curve); api.setColor("blue"); var outline = curve.outline(0,0,api.distance,api.distance); outline.curves.forEach(c => api.drawCurve(c)); } }; }()), withKeys: true }, "circles": { handler: (function() { var sin = Math.sin, cos = Math.cos; return { setup: function(api) { api.w = api.getPanelWidth(); api.h = api.getPanelHeight(); api.pad = 20; api.r = api.w/2 - api.pad; api.mousePt = false; api.angle = 0; var spt = { x: api.w-api.pad, y: api.h/2 }; api.setCurve(new api.Bezier(spt, spt, spt)); }, draw: function(api, curve) { api.reset(); api.setColor("lightgrey"); api.drawGrid(1,1); api.setColor("red"); api.drawCircle({x:api.w/2,y:api.h/2},api.r); api.setColor("transparent"); api.setFill("rgba(100,255,100,0.4)"); var p = { x: api.w/2, y: api.h/2, r: api.r, s: api.angle < 0 ? api.angle : 0, e: api.angle < 0 ? 0 : api.angle }; api.drawArc(p); api.setColor("black"); api.drawSkeleton(curve); api.drawCurve(curve); }, onMouseMove: function(evt, api) { var x = evt.offsetX - api.w/2, y = evt.offsetY - api.h/2; var angle = Math.atan2(y,x); var pts = api.curve.points; // new control var r = api.r, b = (cos(angle) - 1) / sin(angle); pts[1] = { x: api.w/2 + r * (cos(angle) - b * sin(angle)), y: api.w/2 + r * (sin(angle) + b * cos(angle)) }; // new endpoint pts[2] = { x: api.w/2 + api.r * cos(angle), y: api.w/2 + api.r * sin(angle) }; api.setCurve(new api.Bezier(pts)); api.angle = angle; } }; }()) }, "circles_cubic": { handler: (function() { var sin = Math.sin, cos = Math.cos, tan = Math.tan; return { setup: function(api) { api.setSize(400,400); api.w = api.getPanelWidth(); api.h = api.getPanelHeight(); api.pad = 80; api.r = api.w/2 - api.pad; api.mousePt = false; api.angle = 0; var spt = { x: api.w-api.pad, y: api.h/2 }; api.setCurve(new api.Bezier(spt, spt, spt, spt)); }, guessCurve: function(S, B, E) { var C = { x: (S.x + E.x)/2, y: (S.y + E.y)/2 }, A = { x: B.x + (B.x-C.x)/3, // cubic ratio at t=0.5 is 1/3 y: B.y + (B.y-C.y)/3 }, bx = (E.x-S.x)/4, by = (E.y-S.y)/4, e1 = { x: B.x - bx, y: B.y - by }, e2 = { x: B.x + bx, y: B.y + by }, v1 = { x: A.x + (e1.x-A.x)*2, y: A.y + (e1.y-A.y)*2 }, v2 = { x: A.x + (e2.x-A.x)*2, y: A.y + (e2.y-A.y)*2 }, nc1 = { x: S.x + (v1.x-S.x)*2, y: S.y + (v1.y-S.y)*2 }, nc2 = { x: E.x + (v2.x-E.x)*2, y: E.y + (v2.y-E.y)*2 }; return [nc1, nc2]; }, draw: function(api, curve) { api.reset(); api.setColor("lightgrey"); api.drawGrid(1,1); api.setColor("rgba(255,0,0,0.4)"); api.drawCircle({x:api.w/2,y:api.h/2},api.r); api.setColor("transparent"); api.setFill("rgba(100,255,100,0.4)"); var p = { x: api.w/2, y: api.h/2, r: api.r, s: api.angle < 0 ? api.angle : 0, e: api.angle < 0 ? 0 : api.angle }; api.drawArc(p); // guessed curve var B = { x: api.w/2 + api.r * cos(api.angle/2), y: api.w/2 + api.r * sin(api.angle/2) }; var S = curve.points[0], E = curve.points[3], nc = this.guessCurve(S,B,E); var guess = new api.Bezier([S, nc[0], nc[1], E]); api.setColor("rgb(140,140,255)"); api.drawLine(guess.points[0], guess.points[1]); api.drawLine(guess.points[1], guess.points[2]); api.drawLine(guess.points[2], guess.points[3]); api.setColor("blue"); api.drawCurve(guess); api.drawCircle(guess.points[1], 3); api.drawCircle(guess.points[2], 3); // real curve api.drawSkeleton(curve); api.setColor("black"); api.drawLine(curve.points[1], curve.points[2]); api.drawCurve(curve); }, onMouseMove: function(evt, api) { var x = evt.offsetX - api.w/2, y = evt.offsetY - api.h/2; if (x>api.w/2) return; var angle = Math.atan2(y,x); if (angle < 0) { angle = 2*Math.PI + angle; } var pts = api.curve.points; // new control 1 var r = api.r, f = (4 * tan(angle/4)) /3; pts[1] = { x: api.w/2 + r, y: api.w/2 + r * f }; // new control 2 pts[2] = { x: api.w/2 + api.r * (cos(angle) + f*sin(angle)), y: api.w/2 + api.r * (sin(angle) - f*cos(angle)) }; // new endpoint pts[3] = { x: api.w/2 + api.r * cos(angle), y: api.w/2 + api.r * sin(angle) }; api.setCurve(new api.Bezier(pts)); api.angle = angle; }, drawCircle: function(api) { api.setSize(325,325); api.reset(); var w = api.getPanelWidth(), h = api.getPanelHeight(), pad = 60, r = w/2 - pad, k = 0.55228, offset = {x: -pad/2, y:-pad/4}; var curve = new api.Bezier([ {x:w/2 + r, y:h/2}, {x:w/2 + r, y:h/2 + k*r}, {x:w/2 + k*r, y:h/2 + r}, {x:w/2, y:h/2 + r} ]); api.setColor("lightgrey"); api.drawLine({x:0,y:h/2}, {x:w+pad,y:h/2}, offset); api.drawLine({x:w/2,y:0}, {x:w/2,y:h+pad}, offset); var pts = curve.points; api.setColor("red"); api.drawPoint(pts[0], offset); api.drawPoint(pts[1], offset); api.drawPoint(pts[2], offset); api.drawPoint(pts[3], offset); api.drawCurve(curve, offset); api.setColor("rgb(255,160,160)"); api.drawLine(pts[0],pts[1],offset); api.drawLine(pts[1],pts[2],offset); api.drawLine(pts[2],pts[3],offset); api.setFill("red"); api.text((pts[0].x - w/2) + "," + (pts[0].y - h/2), {x: pts[0].x + 7, y: pts[0].y + 3}, offset); api.text((pts[1].x - w/2) + "," + (pts[1].y - h/2), {x: pts[1].x + 7, y: pts[1].y + 3}, offset); api.text((pts[2].x - w/2) + "," + (pts[2].y - h/2), {x: pts[2].x + 7, y: pts[2].y + 7}, offset); api.text((pts[3].x - w/2) + "," + (pts[3].y - h/2), {x: pts[3].x, y: pts[3].y + 13}, offset); pts.forEach(p => { p.x = -(p.x - w); }); api.setColor("blue"); api.drawCurve(curve, offset); api.drawLine(pts[2],pts[3],offset); api.drawPoint(pts[2],offset); api.setFill("blue"); api.text("reflected", {x: pts[2].x - pad/2, y: pts[2].y + 13}, offset); api.setColor("rgb(200,200,255)"); api.drawLine(pts[1],pts[0],offset); api.drawPoint(pts[1],offset); pts.forEach(p => { p.y = -(p.y - h); }); api.setColor("green"); api.drawCurve(curve, offset); pts.forEach(p => { p.x = -(p.x - w); }); api.setColor("purple"); api.drawCurve(curve, offset); api.drawLine(pts[1],pts[0],offset); api.drawPoint(pts[1],offset); api.setFill("purple"); api.text("reflected", {x: pts[1].x + 10, y: pts[1].y + 3}, offset); api.setColor("rgb(200,200,255)"); api.drawLine(pts[2],pts[3],offset); api.drawPoint(pts[2],offset); api.setColor("black"); api.setFill("black"); api.drawLine({x:w/2, y:h/2}, {x:w/2 + r -2, y:h/2}, offset); api.drawLine({x:w/2, y:h/2}, {x:w/2, y:h/2 + r -2}, offset); api.text("r = " + r, {x:w/2 + r/3, y:h/2 + 10}, offset); } }; }()) }, "arcapproximation": { handler: (function() { var atan2 = Math.atan2, PI = Math.PI, TAU = 2*PI, cos = Math.cos, sin = Math.sin; return { // These are functions that can be called "From the page", // rather than being internal to the sketch. This is useful // for making on-page controls hook into the sketch code. statics: { keyHandlingOptions: { propName: "error", values: { "38": 0.1, // up arrow "40": -0.1 // down arrow }, controller: function(api) { if (api.error < 0.1) { api.error = 0.1; } } } }, /** * Setup up a skeleton curve that, when using its * points for a B-spline, can form a circle. */ setupCircle: function(api) { var curve = new api.Bezier(70,70, 140,40, 240,130); api.setCurve(curve); }, /** * Set up the default quadratic curve. */ setupQuadratic: function(api) { var curve = api.getDefaultQuadratic(); api.setCurve(curve); }, /** * Set up the default cubic curve. */ setupCubic: function(api) { var curve = api.getDefaultCubic(); api.setCurve(curve); api.error = 0.5; }, /** * Given three points, find the (only!) circle * that passes through all three points, based * on the fact that the perpendiculars of the * chords between the points all cross each * other at the center of that circle. */ getCCenter: function(api, p1, p2, p3) { // deltas var dx1 = (p2.x - p1.x), dy1 = (p2.y - p1.y), dx2 = (p3.x - p2.x), dy2 = (p3.y - p2.y); // perpendiculars (quarter circle turned) var dx1p = dx1 * cos(PI/2) - dy1 * sin(PI/2), dy1p = dx1 * sin(PI/2) + dy1 * cos(PI/2), dx2p = dx2 * cos(PI/2) - dy2 * sin(PI/2), dy2p = dx2 * sin(PI/2) + dy2 * cos(PI/2); // chord midpoints var mx1 = (p1.x + p2.x)/2, my1 = (p1.y + p2.y)/2, mx2 = (p2.x + p3.x)/2, my2 = (p2.y + p3.y)/2; // midpoint offsets var mx1n = mx1 + dx1p, my1n = my1 + dy1p, mx2n = mx2 + dx2p, my2n = my2 + dy2p; // intersection of these lines: var i = api.utils.lli8(mx1,my1,mx1n,my1n, mx2,my2,mx2n,my2n); var r = api.utils.dist(i,p1); // arc start/end values, over mid point var s = atan2(p1.y - i.y, p1.x - i.x), m = atan2(p2.y - i.y, p2.x - i.x), e = atan2(p3.y - i.y, p3.x - i.x); // determine arc direction (cw/ccw correction) var __; if (sm || m>e) { s += TAU; } if (s>e) { __=e; e=s; s=__; } } else { if (e api.drawCircle(p,3)); // chords and perpendicular lines var m; api.setColor("blue"); api.drawLine(pts[0], pts[1]); m = {x: (pts[0].x + pts[1].x)/2, y: (pts[0].y + pts[1].y)/2}; api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)}); api.setColor("red"); api.drawLine(pts[1], pts[2]); m = {x: (pts[1].x + pts[2].x)/2, y: (pts[1].y + pts[2].y)/2}; api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)}); api.setColor("green"); api.drawLine(pts[2], pts[0]); m = {x: (pts[2].x + pts[0].x)/2, y: (pts[2].y + pts[0].y)/2}; api.drawLine(m, {x:C.x+(C.x-m.x), y:C.y+(C.y-m.y)}); // center api.setColor("black"); api.drawPoint(C); api.setFill("black"); api.text("Intersection point", C, {x:-25, y:10}); }, /** * Draw a single arc being fit to a Bezier curve, * to show off the general application. */ drawSingleArc: function(api, curve) { api.reset(); var arcs = curve.arcs(api.error); api.drawSkeleton(curve); api.drawCurve(curve); var a = arcs[0]; api.setColor("red"); api.setFill("rgba(255,0,0,0.2)"); api.debug = true; api.drawArc(a); api.setFill("black"); api.text("Arc approximation with total error " + api.utils.round(api.error,1), {x:10, y:15}); }, /** * Draw an arc approximation for an entire Bezier curve. */ drawArcs: function(api, curve) { api.reset(); var arcs = curve.arcs(api.error); api.drawSkeleton(curve); api.drawCurve(curve); arcs.forEach(a => { api.setRandomColor(0.3); api.setFill(api.getColor()); api.drawArc(a); }); api.setFill("black"); api.text("Arc approximation with total error " + api.utils.round(api.error,1) + " per arc segment", {x:10, y:15}); } }; }()), withKeys: true }, "bsplines": { handler: (function() { return { basicSketch: require('./basic-sketch'), interpolationGraph: require('./interpolation-graph'), uniformBSpline: require('./uniform-bspline'), centerCutBSpline: require('./center-cut-bspline'), openUniformBSpline: require('./open-uniform-bspline'), rationalUniformBSpline: require('./rational-uniform-bspline'), bindKnots: function(owner, knots, ref) { this.refs[ref].bindKnots(owner, knots); }, bindWeights: function(owner, weights, closed, ref) { this.refs[ref].bindWeights(owner, weights, closed); } }; }()) }, "comments": { handler: (function() { /** * We REALLY don't want disqus to load unless the user * is actually looking at the comments section, because it * tacks on 2.5+ MB in network transfers... */ return { componentDidMount() { if (typeof document === "undefined") { return this.silence(); } this.heading = document.getElementById(this.props.page); document.addEventListener("scroll", this.scrollHandler, {passive:true}); }, scrollHandler(evt) { var bbox = this.heading.getBoundingClientRect(); var top = bbox.top; var limit = window.innerHeight; if (top {}; }, unlisten() { document.removeEventListener("scroll", this.scrollHandler); } }; }()) } };