Skip to content

Instantly share code, notes, and snippets.

@Redchards
Created March 13, 2016 22:38
Show Gist options
  • Select an option

  • Save Redchards/5e71e9d748c2417d4936 to your computer and use it in GitHub Desktop.

Select an option

Save Redchards/5e71e9d748c2417d4936 to your computer and use it in GitHub Desktop.
Dirty spline implementation in javascript
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="potato.js"></script>
</head>
<body onload="init();">
</body>
</html>
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