Created
March 13, 2016 22:38
-
-
Save Redchards/5e71e9d748c2417d4936 to your computer and use it in GitHub Desktop.
Dirty spline implementation in javascript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <script src="potato.js"></script> | |
| </head> | |
| <body onload="init();"> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| var graphZone; | |
| var ctx; | |
| var axes; | |
| var container; | |
| var points = new Array(); | |
| var pointsRef = new Array(); | |
| var graphZoneBaseHeight = 200; | |
| var graphZoneBaseWidth = 600; | |
| function assert(cond, msg) { | |
| if(!cond) { | |
| msg = msg || "Assert failed"; | |
| if(typeof Error !== "undefined") { | |
| throw new Error(msg); | |
| } | |
| throw message; | |
| } | |
| } | |
| function ProxyArray() { | |
| //Object.setPrototypeOf(this, Array.prototype); | |
| this.innerArray = new Array(); | |
| this.innerArray.__proto__ = ProxyArray.prototype; | |
| return new Proxy(this, | |
| { | |
| set : function(target, property, value) { | |
| target.innerArray[property] = value; | |
| console.log("Set"); | |
| } | |
| }) | |
| } | |
| ProxyArray.prototype = new Array; | |
| function onHandlersClick() { | |
| alert("clicked !"); | |
| } | |
| function init() { | |
| //graphZone = document.getElementById("graph"); | |
| //borders = document.createElement("div"); | |
| //borders.style.border = "1px dotted grey"; | |
| //handlers.addEventListener("click", onHandlersClick); | |
| //borders.style.width = graphZoneBaseWidth + "px"; | |
| //borders.style.height = graphZoneBaseHeight + "px"; | |
| //borders.style.position = "relative"; | |
| //document.body.appendChild(borders); | |
| container = document.createElement("div"); | |
| document.body.appendChild(container); | |
| graphZone = document.createElement("canvas"); | |
| graphZone.setAttribute("id", "graph"); | |
| graphZone.setAttribute("width", graphZoneBaseWidth); | |
| graphZone.setAttribute("height", graphZoneBaseHeight); | |
| graphZone.style.top = "0px"; | |
| graphZone.style.left = "0px"; | |
| graphZone.style.position = "absolute"; | |
| graphZone.style.border = "1px dotted grey"; | |
| graphZone.style.float = "left"; | |
| container.appendChild(graphZone); | |
| //<canvas id="graph" width="500" height="200"></canvas> | |
| //console.log(handlers.offsetHeight); | |
| ctx = graphZone.getContext("2d"); | |
| ctx.strokeStyle = "black"; | |
| var topLeftHandler = createHandler(); | |
| topLeftHandler.style.top = "0"; | |
| topLeftHandler.style.left = "0"; | |
| container.appendChild(topLeftHandler); | |
| initGraph(); | |
| //plot(Math.cos, -5, 5, 100); | |
| //plot(Math.sin, -5, 5, 100); | |
| //plot(Math.exp, -5, 5, 100); | |
| var addPointButton = document.createElement("div"); | |
| addPointButton.style.width = "100px"; | |
| addPointButton.style.height = "20px"; | |
| addPointButton.style.border = "1px solid black"; | |
| addPointButton.style.float = "right"; | |
| addPointButton.addEventListener("click", addPointButtonClick); | |
| addPointButton.style.cursor = "pointer"; | |
| addPointButton.innerHTML = "Add point"; | |
| addPointButton.style.textAlign = "center"; | |
| document.body.appendChild(addPointButton); | |
| addPoint(1, 1); | |
| addPoint(1.35, 1.25); | |
| addPoint(2.36, -1.3); | |
| addPoint(3.56, -1.7); | |
| addPoint(4.56, -1.7); | |
| addPoint(4.96, -1.0); | |
| addPoint(5.96, -1.45); | |
| addPoint(-5.96, -1.45); | |
| /*ctx.moveTo(axes.origin.x, axes.origin.y); | |
| ctx.lineTo(axes.origin.x -1*axes.scale.x, axes.origin.y + 1*axes.scale.y); | |
| ctx.moveTo(axes.origin.x, axes.origin.y); | |
| ctx.lineTo(axes.origin.x +1*axes.scale.x, axes.origin.y + 1*axes.scale.y); | |
| ctx.moveTo(axes.origin.x, axes.origin.y - 1*axes.scale.y); | |
| ctx.lineTo(axes.origin.x -1*axes.scale.x, axes.origin.y - 1*axes.scale.y); | |
| ctx.moveTo(axes.origin.x, axes.origin.y - 1*axes.scale.y); | |
| ctx.lineTo(axes.origin.x +1*axes.scale.x, axes.origin.y - 1*axes.scale.y); | |
| ctx.moveTo(axes.origin.x + 0.5*axes.scale.x, axes.origin.y - 1.5*axes.scale.y); | |
| ctx.arc(axes.origin.x, axes.origin.y - 1.5*axes.scale.y, 1*axes.scale.y/2, 0, 2 * Math.PI, false); | |
| ctx.stroke();*/ | |
| //console.log(computeSecondSplineDerivatives(points)); | |
| drawSplinesFrom(pointsRef); | |
| } | |
| function initGraph() { | |
| drawAxes(); | |
| drawGrid({x:2, y:2}); | |
| } | |
| function plot(func, from, to, samples) { | |
| //console.log(from, to); | |
| assert(from < to, "'from' should be greater than 'to' !"); | |
| var interval = (to - from); | |
| var beginPosition = axes.origin.x + from*axes.scale.x + 0.5; | |
| var endPosition = axes.origin.x + to*axes.scale.x + 0.5; | |
| var scale = interval / samples; | |
| ctx.beginPath(); | |
| ctx.moveTo(beginPosition, axes.origin.y - func(from)*axes.scale.y); | |
| //console.log("From : " + from); | |
| for(var i = scale; i < interval; i += scale) { | |
| ctx.lineTo(beginPosition + i*axes.scale.x, axes.origin.y - func(from + i)*axes.scale.y - 0.5); | |
| //console.log((beginPosition) + " : " + (from + i) + " : " + func(from + i)); | |
| } | |
| ctx.lineTo(endPosition, axes.origin.y - func(to)*axes.scale.y - 0.5); | |
| ctx.stroke(); | |
| } | |
| function drawAxes() { | |
| axes = new Object(); | |
| axes.origin = new Object(); | |
| axes.scale = new Object(); | |
| axes.origin.x = graphZone.width * 0.5; | |
| axes.origin.y = graphZone.height * 0.5; | |
| axes.scale.x = 50; | |
| axes.scale.y = 50; | |
| ctx.beginPath(); | |
| ctx.moveTo(0.5, axes.origin.y + 0.5); | |
| ctx.lineTo(graphZone.width + 0.5, axes.origin.y + 0.5); | |
| ctx.moveTo(axes.origin.x + 0.5, -0.5); | |
| ctx.lineTo(axes.origin.x + 0.5, graphZone.height - 0.5); | |
| ctx.stroke(); | |
| } | |
| function drawGrid(resolution) { | |
| var oldStrokeStyle = new String(ctx.strokeStyle); | |
| var oldLineWidth = new Number(ctx.lineWidth); | |
| ctx.strokeStyle = "grey"; | |
| ctx.lineWidth = 0.5; | |
| ctx.beginPath(); | |
| var beginXValue = (axes.scale.x / resolution.x); | |
| var beginYValue = (axes.scale.y / resolution.y); | |
| var tmp = parseInt(graphZone.width)/axes.scale.x; | |
| var floored = Math.floor(tmp); | |
| var diffX = (tmp - floored); | |
| //console.log(diffX); | |
| // = parseInt(graphZone.; | |
| if(tmp != floored) { | |
| // DO adjustment | |
| beginXValue = diffX; | |
| //console.log(beginXValue); | |
| } | |
| tmp = parseInt(graphZone.height)/axes.scale.y; | |
| floored = Math.floor(tmp); | |
| var diffY = (tmp - floored); | |
| //console.log(diffY); | |
| if(tmp != floored) { | |
| beginYValue = diffY; | |
| //console.log(beginYValue); | |
| } | |
| for(var i = beginXValue + 0.5; i < graphZone.width; i += axes.scale.x/resolution.x) { | |
| ctx.moveTo(i, 0); | |
| ctx.lineTo(i, graphZone.height); | |
| } | |
| for(var i = Math.floor(beginYValue) - 0.5; i < graphZone.height; i += axes.scale.y/resolution.y) { | |
| ctx.moveTo(0, i); | |
| ctx.lineTo(graphZone.width, i); | |
| } | |
| ctx.stroke(); | |
| ctx.strokeStyle = oldStrokeStyle; | |
| ctx.lineWidth = oldLineWidth; | |
| } | |
| function onHandlerDrag(e) { | |
| console.log("drag"); | |
| } | |
| function log2DPosition(x, y) { | |
| console.log(x + " : " + y); | |
| } | |
| function startMove(e) { | |
| e = e || window.event; | |
| var origPosX = e.clientX; | |
| var origPosY = e.clientY; | |
| log2DPosition(origPosX, origPosY); | |
| var target = e.target; | |
| var parent = target.parentElement; | |
| var parentOrigHeight = parseInt(graphZone.height); | |
| var parentOrigWidth = parseInt(graphZone.width); | |
| var parentOrigLeft = (parseInt(graphZone.style.left) || 0); | |
| var parentOrigTop = (parseInt(graphZone.style.top) || 0 ); | |
| var topPosition = parseInt(target.style.top.replace("px", "")) | |
| || parseInt(target.style.bottom.replace("px", "")) | |
| || 0; | |
| var leftPosition = parseInt(target.style.left.replace("px", "")) | |
| || parseInt(target.style.right.replace("px", "")) | |
| || 0; | |
| document.onmousemove = function(e) { | |
| e = e || window.event; | |
| var posX = e.clientX; | |
| var posY = e.clientY; | |
| var diffX = posX - origPosX; | |
| var diffY = posY - origPosY; | |
| targetX = diffX + leftPosition; | |
| targetY = diffY + topPosition; | |
| doMove(target, targetX, targetY); | |
| //log2DPosition(parentOrigWidth - diffX + "px", parentOrigHeight - diffY); | |
| graphZone.width = parentOrigWidth - diffX; | |
| graphZone.height = parentOrigHeight - diffY; | |
| log2DPosition(parentOrigWidth) | |
| graphZone.style.left = parentOrigLeft + diffX + "px"; | |
| graphZone.style.top = parentOrigTop + diffY + "px"; | |
| //console.log("Top : " + parent.style.top); | |
| } | |
| //console.log("left : " + leftPosition + "\ntop : " + topPosition); | |
| } | |
| function stopMove() { | |
| console.log("Unhook event"); | |
| document.onmousemove = function(){} | |
| initGraph(); | |
| } | |
| function doMove(e, x, y) { | |
| e.style.left = x + "px"; | |
| e.style.top = y + "px"; | |
| } | |
| function startMovePoint(e) { | |
| e = e || window.event; | |
| var origPosX = e.clientX; | |
| var origPosY = e.clientY; | |
| var target = e.target; | |
| var point = target; | |
| var origPointX = point.x; | |
| var origPointY = point.y; | |
| var topPosition = parseInt(target.style.top.replace("px", "")) || 0; | |
| var leftPosition = parseInt(target.style.left.replace("px", "")) || 0; | |
| document.onmousemove = function(e) { | |
| e = e || window.event; | |
| var diffX = e.clientX - origPosX; | |
| var diffY = e.clientY - origPosY; | |
| target.style.top = topPosition + diffY + "px"; | |
| target.style.left = leftPosition + diffX + "px"; | |
| point.x = origPointX + diffX/axes.scale.x; | |
| point.y = origPointY - diffY/axes.scale.y; | |
| clearCanvas(); | |
| initGraph(); | |
| pointsRef.sort(comparePoints); | |
| drawSplinesFrom(pointsRef); | |
| } | |
| document.onmouseup = stopMovePoint; | |
| } | |
| function stopMovePoint(e) { | |
| document.onmousemove = function() {} | |
| } | |
| function clearCanvas() { | |
| ctx.clearRect(0, 0, graphZone.width, graphZone.height); | |
| } | |
| function createHandler() { | |
| var handler = document.createElement("div"); | |
| handler.style.border = "1px solid black"; | |
| handler.style.position = "absolute"; | |
| handler.style.height = "5px"; | |
| handler.style.width = "5px"; | |
| handler.addEventListener('mousedown', startMove); | |
| handler.addEventListener('mouseup', stopMove); | |
| handler.setAttribute("draggable", "true"); | |
| return handler; | |
| } | |
| function addPoint(x, y) { | |
| var point = document.createElement("div"); | |
| point.style.top = axes.origin.y - y*axes.scale.y - 3 + "px"; | |
| point.style.left = axes.origin.x + x*axes.scale.x - 3 + "px"; | |
| console.log(point.style.left); | |
| point.style.border = "1px solid black"; | |
| point.style.borderRadius = "100%"; | |
| point.style.height = "6px"; | |
| point.style.width = "6px"; | |
| point.style.position = "absolute"; | |
| point.addEventListener("mousedown", startMovePoint); | |
| point.style.cursor = "all-scroll"; | |
| //point.id = points.length || "0"; | |
| point.x = x; | |
| point.y = y; | |
| pointsRef.push(point); | |
| container.appendChild(point); | |
| points.push({x:x, y:y, htmlElement:point}); | |
| pointsRef.sort(comparePoints); | |
| return point; | |
| } | |
| function comparePoints(p1, p2) { | |
| return p1.x > p2.x; | |
| /*if((greater && (parseInt(p1.id) < parseInt(p2.id))) || (!greater && (parseInt(p1.id) > parseInt(p2.id)))) { | |
| var tmp = new String(p1.htmlElement.id); | |
| p1.htmlElement.id = new String(p2.htmlElement.id); | |
| p2.htmlElement.id = tmp; | |
| } | |
| return greater;*/ | |
| } | |
| function addPointButtonClick(e) { | |
| x = e.clientX/axes.scale.x - axes.origin.x/axes.scale.x ; | |
| y = axes.origin.y/axes.scale.y - e.clientY/axes.scale.y; | |
| log2DPosition(e.clientX, y) | |
| var newPoint = addPoint(x, y); | |
| startMovePoint({target: newPoint, clientX: e.clientX, clientY: e.clientY}); | |
| } | |
| function drawSplinesFrom(points) { | |
| // Should check if sorted first. | |
| // points.sort(comparePoints); | |
| var curvatures = computeSecondSplineDerivatives(points); | |
| for(var i = 0; i < points.length - 1; ++i) { | |
| plot(function(x) { | |
| var hi = (points[i + 1].x - points[i].x); | |
| var A = (curvatures[i + 1] - curvatures[i])/(6*hi); | |
| var B = (curvatures[i]/2); | |
| var C = ((-(hi)*curvatures[i + 1])/6) - (hi)*curvatures[i]/3 + (points[i + 1].y - points[i].y)/(hi); | |
| var xxi = (x - points[i].x); | |
| var res = points[i].y + (xxi)*(C + (xxi)*(B + (xxi)*A)); | |
| return res; | |
| }, points[i].x, points[i + 1].x, 200); | |
| } | |
| } | |
| // Solving the tridiagonal system using Thomas algorithm. | |
| function computeSecondSplineDerivatives(points) { | |
| var a = new Array(), | |
| b = new Array(), | |
| c = new Array(), | |
| d = new Array(); | |
| var results = new Array(); | |
| // Add check if there is only one point. | |
| var point2 = points[2]; | |
| var x2 = (point2 && point2.x) || 0; | |
| var y2 = (point2 && point2.y) || 0; | |
| var b0 = 2 * (x2 - points[0].x) | |
| c[0] = (x2 - points[1].x)/ b0; | |
| d[0] = 6 * (((y2 - points[1].y) / (x2 - points[1].x)) | |
| - ((points[1].y - points[0].y) / (points[1].x - points[0].x))) / b0; | |
| for(var i = 2; i < points.length - 1; ++i) { | |
| var hi = (points[i+1].x - points[i].x); | |
| var him = (points[i].x - points[i - 1].x); | |
| var ai = points[i].x - points[i - 1].x; | |
| var bi = 2*(hi + him); | |
| c[i - 1] = (hi / (bi - ai*c[i - 2])); | |
| d[i - 1] = (6*(((points[i + 1].y - points[i].y) / (hi)) | |
| - ((points[i].y - points[i - 1].y) / (him))) - ai*d[i - 2]) / (bi - ai*c[i - 2]); | |
| } | |
| results[0] = 0; | |
| results[points.length - 1] = 0; | |
| results[points.length - 2] = d[points.length - 3] || 0; | |
| for(var i = points.length - 3; i > 0; --i) { | |
| //console.log(i); | |
| results[i] = d[i-1] - c[i-1]*(results[i + 1]); | |
| } | |
| return results; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment