Rotating canvas about axis problems - javascript

I am using canvas 3d to draw a 3d graph in which i can plot points such as (1,5,4), (-8,6,-2) etc.So i am able to draw in all positive and negative x,y and z axis.I also have rotation effect by using arrow keys.
Instructions for rotation:
The z-axis extends out from the center of the screen.
To rotate about the x-axis, press the up/down arrow keys.
To rotate about the y-axis, press the left/right arrow keys.
To rotate about the z-axis, press the ctrl+left/ctrl+down arrow keys.
I can plot the point by specifying points in the text field i provided.
Now the problem is that for example if i plot(5,5,2) it will plot properly.But if i rotate x axis first and then y axis then point will be plotted properly. The problem comes if i rotate y-axis first and then x-axis.the point will be wrongly plotted.
Easy method to find the problem i encountered:
This can be easily find out if you go on plotting the same point repeatedly.The point should be plotted above the same point so that only single point is visible.But in my case the same point( for ex(5,5,2) is drawn at different place in canvas while rotating.This problem only comes if i rotate y-axis first and then x-axis or if i rotate z axis first and then y-axis. So what is the mistake i have done in coding.I am new to this canvas 3d and java script.So please help.
<html>
<head>
<script src="http://code.jquery.com/jquery-latest.min.js"></script>
<title>Canvas Surface Rotation</title>
<style>
body {
text-align: center;
}
canvas {
border: 1px solid black;
}
</style>
<script>
var p1;
var p2;
var p3;
var p4;
var p5;
var p6;
var xangle=0;
var yangle=0;
var zangle=0;
var constants = {
canvasWidth: 600, // In pixels.
canvasHeight: 600, // In pixels.
leftArrow: 37,
upArrow: 38,
rightArrow: 39,
downArrow: 40,
xMin: -10, // These four max/min values define a square on the xy-plane that the surface will be plotted over.
xMax: 10,
yMin: -10,
yMax: 10,
xDelta: 0.06, // Make smaller for more surface points.
yDelta: 0.06, // Make smaller for more surface points.
colorMap: ["#000080"], // There are eleven possible "vertical" color values for the surface, based on the last row of http://www.cs.siena.edu/~lederman/truck/AdvanceDesignTrucks/html_color_chart.gif
pointWidth: 2, // The size of a rendered surface point (i.e., rectangle width and height) in pixels.
dTheta: 0.05, // The angle delta, in radians, by which to rotate the surface per key press.
surfaceScale: 24 // An empirically derived constant that makes the surface a good size for the given canvas size.
};
// These are constants too but I've removed them from the above constants literal to ease typing and improve clarity.
var X = 0;
var Y = 1;
var Z = 2;
// -----------------------------------------------------------------------------------------------------
var controlKeyPressed = false; // Shared between processKeyDown() and processKeyUp().
var surface = new Surface(); // A set of points (in vector format) representing the surface.
// -----------------------------------------------------------------------------------------------------
function point(x, y, z)
/*
Given a (x, y, z) surface point, returns the 3 x 1 vector form of the point.
*/
{
return [x, y, z]; // Return a 3 x 1 vector representing a traditional (x, y, z) surface point. This vector form eases matrix multiplication.
}
// -----------------------------------------------------------------------------------------------------
function Surface()
/*
A surface is a list of (x, y, z) points, in 3 x 1 vector format. This is a constructor function.
*/
{
this.points = [];
// An array of surface points in vector format. That is, each element of this array is a 3 x 1 array, as in [ [x1, y1, z1], [x2, y2, z2], [x3, y3, z3], ... ]
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.equation = function(x, y)
/*
Given the point (x, y), returns the associated z-coordinate based on the provided surface equation, of the form z = f(x, y).
*/
{
var d = Math.sqrt(x*x + y*y); // The distance d of the xy-point from the z-axis.
return 4*(Math.sin(d) / d); // Return the z-coordinate for the point (x, y, z).
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.generate = function()
/*
Creates a list of (x, y, z) points (in 3 x 1 vector format) representing the surface.
*/
{
var i = 0;
for (var x = constants.xMin; x <= constants.xMax; x += constants.xDelta)
{
for (var y = constants.yMin; y <= constants.yMax; y += constants.yDelta)
{
this.points[i] = point(x, y, this.equation(x, y)); // Store a surface point (in vector format) into the list of surface points.
++i;
}
}
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.color = function()
/*
The color of a surface point is a function of its z-coordinate height.
*/
{
var z; // The z-coordinate for a given surface point (x, y, z).
this.zMin = this.zMax = this.points[0][Z]; // A starting value. Note that zMin and zMax are custom properties that could possibly be useful if this code is extended later.
for (var i = 0; i < this.points.length; i++)
{
z = this.points[i][Z];
if (z < this.zMin) { this.zMin = z; }
if (z > this.zMax) { this.zMax = z; }
}
var zDelta = Math.abs(this.zMax - this.zMin) / constants.colorMap.length;
for (var i = 0; i < this.points.length; i++)
{
this.points[i].color = constants.colorMap[ Math.floor( (this.points[i][Z]-this.zMin)/zDelta ) ];
}
/* Note that the prior FOR loop is functionally equivalent to the follow (much less elegant) loop:
for (var i = 0; i < this.points.length; i++)
{
if (this.points[i][Z] <= this.zMin + zDelta) {this.points[i].color = "#060";}
else if (this.points[i][Z] <= this.zMin + 2*zDelta) {this.points[i].color = "#090";}
else if (this.points[i][Z] <= this.zMin + 3*zDelta) {this.points[i].color = "#0C0";}
else if (this.points[i][Z] <= this.zMin + 4*zDelta) {this.points[i].color = "#0F0";}
else if (this.points[i][Z] <= this.zMin + 5*zDelta) {this.points[i].color = "#9F0";}
else if (this.points[i][Z] <= this.zMin + 6*zDelta) {this.points[i].color = "#9C0";}
else if (this.points[i][Z] <= this.zMin + 7*zDelta) {this.points[i].color = "#990";}
else if (this.points[i][Z] <= this.zMin + 8*zDelta) {this.points[i].color = "#960";}
else if (this.points[i][Z] <= this.zMin + 9*zDelta) {this.points[i].color = "#930";}
else if (this.points[i][Z] <= this.zMin + 10*zDelta) {this.points[i].color = "#900";}
else {this.points[i].color = "#C00";}
}
*/
}
// -----------------------------------------------------------------------------------------------------
function update(){
document.querySelector("#xa").innerHTML = xangle;
document.querySelector("#ya").innerHTML = yangle;
document.querySelector("#za").innerHTML = zangle;
}
function appendCanvasElement()
/*
Creates and then appends the "myCanvas" canvas element to the DOM.
*/
{
var canvasElement = document.createElement('canvas');
canvasElement.width = constants.canvasWidth;
canvasElement.height = constants.canvasHeight;
canvasElement.id = "myCanvas";
canvasElement.getContext('2d').translate(constants.canvasWidth/2, constants.canvasHeight/2); // Translate the surface's origin to the center of the canvas.
document.body.appendChild(canvasElement); // Make the canvas element a child of the body element.
}
//------------------------------------------------------------------------------------------------------
Surface.prototype.sortByZIndex = function(A, B)
{
return A[Z] - B[Z]; // Determines if point A is behind, in front of, or at the same level as point B (with respect to the z-axis).
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.draw = function()
{
var myCanvas = document.getElementById("myCanvas"); // Required for Firefox.
var ctx = myCanvas.getContext("2d");
var res;
var xm;
// this.points = surface.points.sort(surface.sortByZIndex); // Sort the set of points based on relative z-axis position. If the points are visibly small, you can sort of get away with removing this step.
for (var i = 0; i < this.points.length; i++)
{
ctx.fillStyle = this.points[i].color;
ctx.fillRect(this.points[i][X] * constants.surfaceScale, this.points[i][Y] * constants.surfaceScale, constants.pointWidth, constants.pointWidth);
}
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.font="12px Arial";
ctx.fillStyle = "#000000";
ctx.fillText("X",this.points[p1][X] * constants.surfaceScale, this.points[p1][Y] * constants.surfaceScale);
var c=document.getElementById("myCanvas");
var ctx1=c.getContext("2d");
ctx1.font="12px Arial";
ctx1.fillText("Y",this.points[p2][X] * constants.surfaceScale, this.points[p2][Y] * constants.surfaceScale);
var c=document.getElementById("myCanvas");
var ctx1=c.getContext("2d");
ctx1.font="12px Arial";
ctx1.fillText("Z",this.points[p3][X] * constants.surfaceScale, this.points[p3][Y] * constants.surfaceScale);
var c=document.getElementById("myCanvas");
var ctx1=c.getContext("2d");
ctx1.font="12px Arial";
ctx1.fillText("-Y",this.points[p4][X] * constants.surfaceScale, this.points[p4][Y] * constants.surfaceScale);
var c=document.getElementById("myCanvas");
var ctx1=c.getContext("2d");
ctx1.font="12px Arial";
ctx1.fillText("-Z",this.points[p5][X] * constants.surfaceScale, this.points[p5][Y] * constants.surfaceScale);
var c=document.getElementById("myCanvas");
var ctx1=c.getContext("2d");
ctx1.font="12px Arial";
ctx1.fillText("-X",this.points[p6][X] * constants.surfaceScale, this.points[p6][Y] * constants.surfaceScale);
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.multi = function(R)
/*
Assumes that R is a 3 x 3 matrix and that this.points (i.e., P) is a 3 x n matrix. This method performs P = R * P.
*/
{
var Px = 0, Py = 0, Pz = 0; // Variables to hold temporary results.
var P = this.points; // P is a pointer to the set of surface points (i.e., the set of 3 x 1 vectors).
var sum; // The sum for each row/column matrix product.
for (var V = 0; V < P.length; V++) // For all 3 x 1 vectors in the point list.
{
Px = P[V][X], Py = P[V][Y], Pz = P[V][Z];
for (var Rrow = 0; Rrow < 3; Rrow++) // For each row in the R matrix.
{
sum = (R[Rrow][X] * Px) + (R[Rrow][Y] * Py) + (R[Rrow][Z] * Pz);
P[V][Rrow] = sum;
}
}
}
Surface.prototype.multipt = function(R)
/*
Assumes that R is a 3 x 3 matrix and that this.points (i.e., P) is a 3 x n matrix. This method performs P = R * P.
*/
{
var Px = 0, Py = 0, Pz = 0; // Variables to hold temporary results.
var P = this.points; // P is a pointer to the set of surface points (i.e., the set of 3 x 1 vectors).
var sum; // The sum for each row/column matrix product.
for (var V = P.length-1; V < P.length; V++) // For all 3 x 1 vectors in the point list.
{
Px = P[V][X], Py = P[V][Y], Pz = P[V][Z];
for (var Rrow = 0; Rrow < 3; Rrow++) // For each row in the R matrix.
{
sum = (R[Rrow][X] * Px) + (R[Rrow][Y] * Py) + (R[Rrow][Z] * Pz);
P[V][Rrow] = sum;
}
}
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.erase = function()
{
var myCanvas = document.getElementById("myCanvas"); // Required for Firefox.
var ctx = myCanvas.getContext("2d");
ctx.clearRect(-constants.canvasWidth/2, -constants.canvasHeight/2, myCanvas.width, myCanvas.height);
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.xRotate = function(sign)
/*
Assumes "sign" is either 1 or -1, which is used to rotate the surface "clockwise" or "counterclockwise".
*/
{
var Rx = [ [0, 0, 0],
[0, 0, 0],
[0, 0, 0] ]; // Create an initialized 3 x 3 rotation matrix.
Rx[0][0] = 1;
Rx[0][1] = 0; // Redundant but helps with clarity.
Rx[0][2] = 0;
Rx[1][0] = 0;
Rx[1][1] = Math.cos( sign*constants.dTheta );
Rx[1][2] = -Math.sin( sign*constants.dTheta );
Rx[2][0] = 0;
Rx[2][1] = Math.sin( sign*constants.dTheta );
Rx[2][2] = Math.cos( sign*constants.dTheta );
this.multi(Rx); // If P is the set of surface points, then this method performs the matrix multiplcation: Rx * P
this.erase(); // Note that one could use two canvases to speed things up, which also eliminates the need to erase.
this.draw();
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.yRotate = function(sign)
/*
Assumes "sign" is either 1 or -1, which is used to rotate the surface "clockwise" or "counterclockwise".
*/
{
var Ry = [ [0, 0, 0],
[0, 0, 0],
[0, 0, 0] ]; // Create an initialized 3 x 3 rotation matrix.
Ry[0][0] = Math.cos( sign*constants.dTheta );
Ry[0][1] = 0; // Redundant but helps with clarity.
Ry[0][2] = Math.sin( sign*constants.dTheta );
Ry[1][0] = 0;
Ry[1][1] = 1;
Ry[1][2] = 0;
Ry[2][0] = -Math.sin( sign*constants.dTheta );
Ry[2][1] = 0;
Ry[2][2] = Math.cos( sign*constants.dTheta );
this.multi(Ry); // If P is the set of surface points, then this method performs the matrix multiplcation: Rx * P
this.erase(); // Note that one could use two canvases to speed things up, which also eliminates the need to erase.
this.draw();
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.zRotate = function(sign)
/*
Assumes "sign" is either 1 or -1, which is used to rotate the surface "clockwise" or "counterclockwise".
*/
{
var Rz = [ [0, 0, 0],
[0, 0, 0],
[0, 0, 0] ]; // Create an initialized 3 x 3 rotation matrix.
Rz[0][0] = Math.cos( sign*constants.dTheta );
Rz[0][1] = -Math.sin( sign*constants.dTheta );
Rz[0][2] = 0; // Redundant but helps with clarity.
Rz[1][0] = Math.sin( sign*constants.dTheta );
Rz[1][1] = Math.cos( sign*constants.dTheta );
Rz[1][2] = 0;
Rz[2][0] = 0
Rz[2][1] = 0;
Rz[2][2] = 1;
this.multi(Rz); // If P is the set of surface points, then this method performs the matrix multiplcation: Rx * P
this.erase(); // Note that one could use two canvases to speed things up, which also eliminates the need to erase.
this.draw();
}
Surface.prototype.xRotatept = function()
{
var Rx = [ [0, 0, 0],
[0, 0, 0],
[0, 0, 0] ];
Rx[0][0] = 1;
Rx[0][1] = 0;
Rx[0][2] = 0;
Rx[1][0] = 0;
Rx[1][1] = Math.cos(xangle);
Rx[1][2] = -Math.sin(xangle);
Rx[2][0] = 0;
Rx[2][1] = Math.sin(xangle);
Rx[2][2] = Math.cos(xangle);
this.multipt(Rx);
this.erase();
this.draw();
}
Surface.prototype.yRotatept = function()
{
var Ry = [ [0, 0, 0],
[0, 0, 0],
[0, 0, 0] ];
Ry[0][0] = Math.cos(yangle);
Ry[0][1] = 0;
Ry[0][2] = Math.sin(yangle);
Ry[1][0] = 0;
Ry[1][1] = 1;
Ry[1][2] = 0;
Ry[2][0] = -Math.sin(yangle);
Ry[2][1] = 0;
Ry[2][2] = Math.cos(yangle);
this.multipt(Ry);
this.erase();
this.draw();
}
Surface.prototype.zRotatept = function()
{
var Rz = [ [0, 0, 0],
[0, 0, 0],
[0, 0, 0] ];
Rz[0][0] = Math.cos(zangle);
Rz[0][1] = -Math.sin(zangle);
Rz[0][2] = 0;
Rz[1][0] = Math.sin(zangle);
Rz[1][1] = Math.cos(zangle);
Rz[1][2] = 0;
Rz[2][0] = 0
Rz[2][1] = 0;
Rz[2][2] = 1;
this.multipt(Rz);
this.erase();
this.draw();
}
// -----------------------------------------------------------------------------------------------------
function processKeyDown(evt)
{
if (evt.ctrlKey)
{
switch (evt.keyCode)
{
case constants.upArrow:
// No operation other than preventing the default behavior of the arrow key.
evt.preventDefault(); // This prevents the default behavior of the arrow keys, which is to scroll the browser window when scroll bars are present. The user can still scroll the window with the mouse.
break;
case constants.downArrow:
// No operation other than preventing the default behavior of the arrow key.
evt.preventDefault();
break;
case constants.leftArrow:
// console.log("ctrl+leftArrow");
zangle=zangle-0.05;
update();
if(zangle<=-2*Math.PI)
{
zangle=0;
}
surface.zRotate(-1); // The sign determines if the surface rotates "clockwise" or "counterclockwise".
evt.preventDefault();
break;
case constants.rightArrow:
// console.log("ctrl+rightArrow");
zangle=zangle+0.05;
update();
if(zangle>=2*Math.PI)
{
zangle=0;
}
surface.zRotate(1);
evt.preventDefault();
break;
}
return; // When the control key is pressed, only the left and right arrows have meaning, no need to process any other key strokes (i.e., bail now).
}
// Assert: The control key is not pressed.
switch (evt.keyCode)
{
case constants.upArrow:
// console.log("upArrow");
xangle=xangle+0.05;
update();
if(xangle>=2*Math.PI)
{
xangle=0;
}
surface.xRotate(1);
evt.preventDefault();
break;
case constants.downArrow:
// console.log("downArrow");
xangle=xangle-0.05;
update();
if(xangle<=-2*Math.PI)
{
xangle=0;
}
surface.xRotate(-1);
evt.preventDefault();
break;
case constants.leftArrow:
// console.log("leftArrow");
yangle=yangle-0.05;
update();
if(yangle<=-2*Math.PI)
{
yangle=0;
}
surface.yRotate(-1);
evt.preventDefault();
break;
case constants.rightArrow:
// console.log("rightArrow");
yangle=yangle+0.05;
update();
if(yangle>=2*Math.PI)
{
yangle=0;
}
surface.yRotate(1);
evt.preventDefault();
break;
}
}
// -----------------------------------------------------------------------------------------------------
Surface.prototype.plot = function(x, y, z)
/*
add the point (x, y, z) (in 3 x 1 vector format) to the surface.
*/
{
this.points.push(point(x, y, z)); // Store a surface point
var x=0;
for (var x = constants.xMin; x <= constants.xMax; x += constants.xDelta)
{
this.points.push(point(x, 0, 0));
}
p6=1;
p1=this.points.length-1;
p4=this.points.length;
/*var y=-0.2
for (var x = constants.xMax+1; x <= constants.xMax+2; x += constants.xDelta)
{
this.points.push(point(x, y, 0));
y=y+0.002
}*/
/*for (var x = constants.xMax+1; x <= constants.xMax+2; x += constants.xDelta)
{
this.points.push(point(11, 0, 0))
}*/
for (var x = constants.xMin; x <= constants.xMax; x += constants.yDelta)
{
this.points.push(point(0, x, 0));
}
p2=this.points.length-1;
p5=this.points.length;
for (var x = constants.xMin; x <= constants.xMax; x += constants.yDelta)
{
this.points.push(point(0,0,x));
}
p3=this.points.length-1;
}
Surface.prototype.plot1 = function(x, y, z)
/*
add the point (x, y, z) (in 3 x 1 vector format) to the surface.
*/
{
this.points.push(point(x, y, z)); // Store a surface point
surface.xRotatept();
surface.yRotatept();
surface.zRotatept();
this.draw();
}
function onloadInit()
{
appendCanvasElement(); // Create and append the canvas element to the DOM.
surface.draw(); // Draw the surface on the canvas.
document.addEventListener('keydown', processKeyDown, false); // Used to detect if the control key has been pressed.
}
// -----------------------------------------------------------------------------------------------------
//surface.generate(); // Creates the set of points reprsenting the surface. Must be called before color().
surface.plot(0,0,0);
surface.color(); // Based on the min and max z-coordinate values, chooses colors for each point based on the point's z-ccordinate value (i.e., height).
window.addEventListener('load', onloadInit, false); // Perform processing that must occur after the page has fully loaded.
</script>
</head>
<body>
<table align="center">
<tr><td>
<h5 style="color:#606">Enter the value of (X,Y,Z)</h5>
<input type="text" value="5" class="num-input" width="50" size="2" id="x-input">
<input type="text" value="5" class="num-input" width="50" size="2" id="y-input">
<input type="text" value="2" class="num-input" width="50" size="2" id="z-input">
<input type="button" value="Plot Point" onClick="surface.plot1(document.getElementById('x-input').value,document.getElementById('y-input').value,document.getElementById('z-input').value); ">
</td></tr></table>
<table align="center"> <tr><td>
<span id="xa">0</span>deg<br>
<span id="ya">0</span>deg<br>
<span id="za">0</span>deg</td></tr></table>
</body>
</html>

The final output of rotations along multiple axis can vary depending on the order that you rotate the axis'. What you need to do is keep track of the total rotation along each axis (as three numbers, not using matrices). And each time you update a rotation value, apply all three total rotations to an identity matrix in the correct order (try x,y,z). Always use the same order. Then use this to transform your coordinates.

here is my opinion:
JAVASCRIPT
var canvas = document.getElementById("myCanvas");
var ctx2 = canvas.getContext("2d");
ctx2.fillStyle='#333';
ctx2.fillRect(50,50,100,100);
var ctx = canvas.getContext("2d");
ctx.fillStyle='red';
var deg = Math.PI/180;
ctx.save();
ctx.translate(100, 100);
ctx.rotate(45 * deg);
ctx.fillRect(-50,-50,100,100);
ctx.restore();
ctx2 is the old position and ctx is the new position of the shape. You have to translate the shape with the same x,y coordinates according to where you want position your shape. Then you have to enter values to ctx.fillRect(x,y,w,h);keep x and y as the -ve values (half of height and width to keep it on the diagonal to the canvas otherwise change to manipulate it). and h, w as your desired values.
DEMO

Related

p5.js – Smoothly morphing random shape

first of all, i am a beginner on js and p5.js. My aim on this program is a smoothly morphing random shape. I was satisfied with the calculateShape()-function and the drawShape()-function, but when it comes to morphing (updateShape()) it gets really ugly. I thought it might be a good idea to save my current array into a temporary array, then loop over the array and add a random number to each x and y of each index and then replace the old x and y at this index. The main problem is, that it is always adding new shapes on the screen instead of changing the values of the vertices of the existing shape. Can anybody of you please give me a hint or point out my mistake(s)? THANK YOU IN ADVANCE!
var c1;
var c2;
var c3;
var centerX;
var centerY;
var fb;
var radius;
var angle;
var shape = [];
var temp;
/*function to calculate the inital shape*/
function calculateShape() {
//calculate coordinates and save into array
for (var i = 0; i < fb; i++) {
var x = cos(angle * i) * radius + random(-77,77);
var y = sin(angle * i) * radius + random(-77,77);
var v = createVector(x, y);
shape.push(v);
}
}
/*function for morphing the shape*/
function updateShape() {
var temp = shape;
for (var i = 0; i < shape.length - 1; i++) {
var x = temp[i].x + random(-1, 1);
var y = temp[i].y + random(-1, 1);
var p = temp[i];
var v = createVector(x, y);
shape.splice(p,1);
shape.push(v);
}
}
/*function for drawing the shape on the screen*/
function createShape(){
beginShape();
curveVertex(shape[shape.length-1].x, shape[shape.length-1].y);
for (var i = 0; i < shape.length; i++){
curveVertex(shape[i].x, shape[i].y);
}
curveVertex(shape[0].x, shape[0].y);
endShape(CLOSE);
}
function setup() {
createCanvas(windowWidth, windowHeight);
smooth();
background(250);
//frameRate(2);
// defining possible colors
c1 = color(0, 196, 181, 235);
c2 = color(50, 227, 232, 235);
c3 = color(248, 49, 62, 255);
var colors = [c1, c2, c3];
//center of the window
centerX = windowWidth/2;
centerY = windowHeight/2;
//defining all variables
fb = 8;
angle = radians(360 / fb);
radius = random(120, 140);
//calling thefunction that initalises the shape
calculateShape();
}
function draw() {
translate(centerX, centerY);
blendMode(BLEND);
fill(c3);
noStroke();
createShape();
updateShape();
}
The main problem is, that it is always adding new shapes on the screen instead of changing the values of the vertices of the existing shape.
Sure, you just need to clear the screen before drawing again. So, reset the background with the background(250) from setup, in draw.

Javascript 3d Terrain Without Three.js

I have searched around but I can't find anything like what I'm trying to do that doesn't use Three.js in some way (I can't use Three.js because my computer is too old to support Webgl). Here's what I've got so far:
HTML:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="terrain.js"></script>
<title>Terrain</title>
</head>
<body>
<canvas id="canvas" height="400" width="400"></canvas>
</body>
</html>
Javascript:
var canvas, ctx, row1 = [], row2 = [], intensity = 15, width = 20, height = 20, centery = 200, centerx = 200, minus, delta = 1.6, nu = .02;
window.onload = function() {
canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d');
ctx.lineStyle = '#000'
for (var i = 0; i < height; i++) {
row2 = [];
minus = 200
for (var j = 0; j < width; j++) {
row2[j] = {
x: centerx - (minus * (delta * (nu * i))),
y: Math.floor(Math.random() * intensity) + (height * i)
}
minus -= height;
}
ctx.beginPath();
ctx.moveTo(row2[0].x,row2[0].y)
for (var k = 1; k < row2.length; k++) {
ctx.lineTo(row2[k].x,row2[k].y)
if (k == row2.length) {ctx.clostPath()}
}
ctx.stroke();
if (row1[0] && row2[0]) {
for (var l = 0; l < row2.length; l++) {
ctx.beginPath();
ctx.moveTo(row2[l].x,row2[l].y)
ctx.lineTo(row1[l].x,row1[l].y)
ctx.closePath();
ctx.stroke();
}
}
row1 = row2;
}
}
Currently, the result looks like a Christmas tree but I want it to look more like actual 3d wireframe terrain.
3D wire frame basics
3D can be done on any systems that can move pixels. Thought not by dedicated hardware Javascript can do alright if you are after simple 3d.
This answers shows how to create a mesh, rotate and move it, create a camera and move it, and project the whole lot onto the 2D canvas using simple moveTo, and lineTo calls.
This answer is a real rush job so apologies for the typos (if any) and messy code. Will clean it up in the come few days (if time permits). Any questions please do ask in the comments.
Update
I have not done any basic 3D for some time so having a little fun I have added to the answer with more comments in the code and added some extra functionality.
vec3 now has normalise, dot, cross functions.
mat now has lookat function and is ready for much more if needed.
mesh now maintains its own world matrix
Added box, and line that create box and line meshs
Created a second vector type vec3S (S for simple) that is just coordinates no functionality
Demo now shows how to add more objects, position them in the scene, use a lookat transform
Details about the code.
The code below is the basics of 3D. It has a mesh object to create objects out of 3D points (vertices) connected via lines.
Simple transformation for rotating, moving and scaling a model so it can be placed in the scene.
A very very basic camera that can only look forward, move up,down, left,right, in and out. And the focal length can be changed.
Only for lines as there is no depth sorting.
The demo does not clip to the camera front plane, but rather just ignores lines that have any part behind the camera;
You will have to work out the rest from the comments, 3D is a big subject and any one of the features is worth a question / answer all its own.
Oh and coordinates in 3D are origin in center of canvas. Y positive down, x positive right, and z positive into the screen. projection is basic so when you have perspective set to 400 than a object at 400 units out from camera will have a one to one match with pixel size.
var ctx = canvas.getContext("2d");
// some usage of vecs does not need the added functionality
// and will use the basic version
const vec3Basic = { x : 0, y : 0, z: 0};
const vec3Def = {
// Sets the vector scalars
// Has two signatures
// setVal(x,y,z) sets vector to {x,y,z}
// setVal(vec) set this vector to vec
setVal(x,y = x.y,z = x.z + (x = x.x) * 0){
this.x = x;
this.y = y;
this.z = z;
},
// subtract v from this vector
// Has two signatures
// setVal(v) subtract v from this returning a new vec3
// setVal(v,vec) subtract v from this returning result in retVec
sub(v,retVec = vec3()){
retVec.x = this.x - v.x;
retVec.y = this.y - v.y;
retVec.z = this.z - v.z;
return retVec;
},
// Cross product of two vectors this and v.
// Cross product can be thought of as get the vector
// that is perpendicular to the plane described by the two vector we are crossing
// Has two signatures
// cross(vec); // returns a new vec3 as the cross product of this and vec
// cross(vec, retVec); // set retVec as the cross product
cross (v, retVec = vec3()){
retVec.x = this.y * v.z - this.z * v.y;
retVec.y = this.z * v.x - this.x * v.z;
retVec.z = this.x * v.y - this.y * v.x;
return retVec;
},
// Dot product
// Dot product of two vectors if both normalized can be thought of as finding the cos of the angle
// between two vectors. If not normalised the dot product will give you < 0 if v points away from
// the plane that this vector is perpendicular to, if > 0 the v points in the same direction as the
// plane perpendicular to this vector. if 0 then v is at 90 degs to the plane this is perpendicular to
// Using vector dot on its self is the same as getting the length squared
// dot(vec3); // returns a number as a float
dot (v){ return this.x * v.x + this.y * v.y + this.z * this.z },
// normalize normalizes a vector. A normalized vector has length equale to 1 unit
// Has two signitures
// normalise(); normalises this vector returning this
// normalize(retVec); normalises this vector but puts the normalised vector in retVec returning
// returning retVec. Thiis is unchanged.
normalize(retVec = this){
// could have used len = this.dot(this) but for speed all functions will do calcs internaly
const len = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
// it is assumed that all vector are valid (have length) so no test is made to avoid
// the divide by zero that will happen for invalid vectors.
retVec.x = this.x / len;
retVec.y = this.y / len;
retVec.z = this.z / len;
}
}
// Created as a singleton to close over working constants
const matDef = (()=>{
// to seed up vector math the following closed over vectors are used
// rather than create and dispose of vectors for every operation needing them
// Currently not used
const V1 = vec3();
return {
// The matrix is just 3 pointers one for each axis
// They represent the direction and scale in 3D of each axis
// when you transform a point x,y,z you move x along the x axis,
// then y along y and z along the z axis
xAxis : null,
yAxis : null,
zAxis : null,
// this is a position x,y,z and represents where in 3D space an objects
// center coordinate (0,0,0) will be. It is simply added to a point
// after it has been moved along the 3 axis.
pos : null,
// This function does most of the 3D work in most 3D environments.
// It rotates, scales, translates, and a whole lot more.
// It is a cut down of the full 4 by 4 3D matrix you will find in
// Libraries like three.js
transformVec3(vec,retVec = {}){
retVec.x = vec.x * this.xAxis.x + vec.y * this.yAxis.x + vec.z * this.zAxis.x + this.pos.x;
retVec.y = vec.x * this.xAxis.y + vec.y * this.yAxis.y + vec.z * this.zAxis.y + this.pos.y;
retVec.z = vec.x * this.xAxis.z + vec.y * this.yAxis.z + vec.z * this.zAxis.z + this.pos.z;
return retVec;
},
// resets the matrix
identity(){ // default matrix
this.xAxis.setVal(1,0,0); // x 1 unit long in the x direction
this.yAxis.setVal(0,1,0); // y 1 unit long in the y direction
this.zAxis.setVal(0,0,1); // z 1 unit long in the z direction
this.pos.setVal(0,0,0); // and position at the origin.
},
init(){ // need to call this before using due to the way I create these
// objects.
this.xAxis = vec3(1,0,0);
this.yAxis = vec3(0,1,0);
this.zAxis = vec3(0,0,1);
this.pos = vec3(0,0,0);
return this; // must have this line for the constructor function to return
},
setRotateY(amount){
var x = Math.cos(amount);
var y = Math.sin(amount);
this.xAxis.x = x;
this.xAxis.y = 0;
this.xAxis.z = y;
this.zAxis.x = -y;
this.zAxis.y = 0;
this.zAxis.z = x;
},
// creates a look at transform from the current position
// point is a vec3.
// No check is made to see if look at is at pos which will invalidate this matrix
// Note scale is lost in this operation.
lookAt(point){
// zAxis along vector from pos to point
this.pos.sub(point,this.zAxis).normalize();
// use y as vertical reference
this.yAxis.x = 0;
this.yAxis.y = 1;
this.yAxis.z = 0;
// get x axis perpendicular to the plane described by z and y axis
// need to normalise as z and y axis may not be at 90 deg
this.yAxis.cross(this.zAxis,this.xAxis).normalize();
// Get the y axis that is perpendicular to z and x axis
// Normalise is not really needed but rounding errors can be problematic
// so the normalise just fixes some of the rounding errors.
this.zAxis.cross(this.xAxis,this.yAxis).normalize();
},
}
})();
// Mesh object has buffers for the
// model as verts
// transformed mesh as tVerts
// projected 2D verts as dVerts (d for display)
// An a array of lines. Each line has two indexes that point to the
// vert that define their ends.
// Buffers are all preallocated to stop GC slowing everything down.
const meshDef = {
addVert(vec){
this.verts.push(vec);
// vec3(vec) in next line makes a copy of the vec. This is important
// as using the same vert in the two buffers will result in strange happenings.
this.tVerts.push(vec3S(vec)); // transformed verts pre allocated so GC does not bite
this.dVerts.push({x:0,y:0}); // preallocated memory for displaying 2d projection
// when x and y are zero this means that it is not visible
return this.verts.length - 1;
},
addLine(index1,index2){
this.lines.push(index1,index2);
},
transform(matrix = this.matrix){
for(var i = 0; i < this.verts.length; i++){
matrix.transformVec3(this.verts[i],this.tVerts[i]);
}
},
eachVert(callback){
for(var i = 0; i < this.verts.length; i++){
callback(this.tVerts[i],i);
}
},
eachLine(callback){
for(var i = 0; i < this.lines.length; i+= 2){
var ind1 = this.lines[i];
var v1 = this.dVerts[ind1]; // get the start
if(v1.x !== 0 && v1.y !== 0){ // is valid
var ind2 = this.lines[i+ 1]; // get end of line
var v2 = this.dVerts[ind2];
if(v2.x !== 0 && v2.y !== 0){ // is valid
callback(v1,v2);
}
}
}
},
init(){ // need to call this befor using
this.verts = [];
this.lines = [];
this.dVerts = [];
this.tVerts = [];
this.matrix = mat();
return this; // must have this line for the construtor function to return
}
}
const cameraDef = {
projectMesh(mesh){ // create a 2D mesh
mesh.eachVert((vert,i)=>{
var z = (vert.z + this.position.z);
if(z < 0){ // is behind the camera then ignor it
mesh.dVerts[i].x = mesh.dVerts[i].y = 0;
}else{
var s = this.perspective / z;
mesh.dVerts[i].x = (vert.x + this.position.x) * s;
mesh.dVerts[i].y = (vert.y + this.position.y) * s;
}
})
},
drawMesh(mesh){ // renders the 2D mesh
ctx.beginPath();
mesh.eachLine((v1,v2)=>{
ctx.moveTo(v1.x,v1.y);
ctx.lineTo(v2.x,v2.y);
})
ctx.stroke();
}
}
// vec3S creates a basic (simple) vector
// 3 signatures
//vec3S(); // return vec 1,0,0
//vec3S(vec); // returns copy of vec
//vec3S(x,y,z); // returns {x,y,z}
function vec3S(x = {x:1,y:0,z:0},y = x.y ,z = x.z + (x = x.x) * 0){ // a 3d point
return Object.assign({},vec3Basic,{x, y, z});
}
// vec3S creates a basic (simple) vector
// 3 signatures
//vec3S(); // return vec 1,0,0
//vec3S(vec); // returns copy of vec
//vec3S(x,y,z); // returns {x,y,z}
function vec3(x = {x:1,y:0,z:0},y = x.y ,z = x.z + (x = x.x) * 0){ // a 3d point
return Object.assign({},vec3Def,{x,y,z});
}
function mat(){ // matrix used to rotate scale and move a 3d point
return Object.assign({},matDef).init();
}
function mesh(){ // this is for storing objects as points in 3d and lines conecting points
return Object.assign({},meshDef).init();
}
function camera(perspective,position){ // this is for displaying 3D
return Object.assign({},cameraDef,{perspective,position});
}
// grid is the number of grids x,z and size is the overal size for x
function createLandMesh(gridx,gridz,size,maxHeight){
var m = mesh(); // create a mesh
var hs = size/2 ;
var step = size / gridx;
for(var z = 0; z < gridz; z ++){
for(var x = 0; x < gridx; x ++){
// create a vertex. Y is random
m.addVert(vec3S(x * step - hs, (Math.random() * maxHeight), z * step-hs)); // create a vert
}
}
for(var z = 0; z < gridz-1; z ++){
for(var x = 0; x < gridx-1; x ++){
if(x < gridx -1){ // dont go past end
m.addLine(x + z * gridx,x + 1 + z * gridx); // add line across
}
if(z < gridz - 1){ // dont go past end
m.addLine(x + z * (gridx-1),x + 1 + (z + 1) * (gridx-1));
}
}
}
return m;
}
function createBoxMesh(size){
var s = size / 2;
var m = mesh(); // create a mesh
// add bottom
m.addVert(vec3S(-s,-s,-s));
m.addVert(vec3S( s,-s,-s));
m.addVert(vec3S( s, s,-s));
m.addVert(vec3S(-s, s,-s));
// add top verts
m.addVert(vec3S(-s,-s, s));
m.addVert(vec3S( s,-s, s));
m.addVert(vec3S( s, s, s));
m.addVert(vec3S(-s, s, s));
// add lines
/// bottom lines
m.addLine(0,1);
m.addLine(1,2);
m.addLine(2,3);
m.addLine(3,0);
/// top lines
m.addLine(4,5);
m.addLine(5,6);
m.addLine(6,7);
m.addLine(7,4);
// side lines
m.addLine(0,4);
m.addLine(1,5);
m.addLine(2,6);
m.addLine(3,7);
return m;
}
function createLineMesh(v1 = vec3S(),v2 = vec3S()){
const m = mesh();
m.addVert(v1);
m.addVert(v2);
m.addLine(0,1);
return m;
}
//Create a land mesh grid 20 by 20 and 400 units by 400 units in size
var land = createLandMesh(20,20,400,20); // create a land mesh
var box = createBoxMesh(50);
var box1 = createBoxMesh(25);
var line = createLineMesh(); // line conecting boxes
line.tVerts[0] = box.matrix.pos; // set the line transformed tVect[0] to box matrix.pos
line.tVerts[1] = box1.matrix.pos; // set the line transformed tVect[0] to box1 matrix.pos
var cam = camera(200,vec3(0,0,0)); // create a projection with focal len 200 and at 0,0,0
box.matrix.pos.setVal(0,-100,400);
box1.matrix.pos.setVal(0,-100,400);
land.matrix.pos.setVal(0,100,300); // move down 100, move away 300
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center of canvas
var ch = h / 2;
function update(timer){
// next section just maintains canvas size and resets state and clears display
if (canvas.width !== innerWidth || canvas.height !== innerHeight) {
cw = (w = canvas.width = innerWidth) /2;
ch = (h = canvas.height = innerHeight) /2;
}
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.fillStyle = "black";
ctx.fillRect(0,0,canvas.width,canvas.height);
// end of standard canvas maintenance
// render from center of canvas by setting canvas origin to center
ctx.setTransform(1,0,0,1,canvas.width / 2,canvas.height / 2)
land.matrix.setRotateY(timer/1000); // set matrix to rotation position
land.transform();
// move the blue box
var t = timer/1000;
box1.matrix.pos.setVal(Math.sin(t / 2.1) * 100,Math.sin( t / 3.2) * 100, Math.sin(t /5.3) * 90+300);
// Make the cyan box look at the blue box
box.matrix.lookAt(box1.matrix.pos);
// Transform boxes from local to world space
box1.transform();
box.transform();
// set camera x,y pos to mouse pos;
cam.position.x = mouse.x - cw;
cam.position.y = mouse.y - ch;
// move in and out
if (mouse.buttonRaw === 1) { cam.position.z -= 1 }
if (mouse.buttonRaw === 4) {cam.position.z += 1 }
// Converts mesh transformed verts to 2D screen coordinates
cam.projectMesh(land);
cam.projectMesh(box);
cam.projectMesh(box1);
cam.projectMesh(line);
// Draw each mesh in turn
ctx.strokeStyle = "#0F0";
cam.drawMesh(land);
ctx.strokeStyle = "#0FF";
cam.drawMesh(box);
ctx.strokeStyle = "#00F";
cam.drawMesh(box1);
ctx.strokeStyle = "#F00";
cam.drawMesh(line);
ctx.setTransform(1,0,0,1,cw,ch / 4);
ctx.font = "20px arial";
ctx.textAlign = "center";
ctx.fillStyle = "yellow";
ctx.fillText("Move mouse to move camera. Left right mouse move in out",0,0)
requestAnimationFrame(update);
}
requestAnimationFrame(update);
// A mouse handler from old lib of mine just to give some interaction
// not needed for the 3d
var mouse = (function () {
var m; // alias for mouse
var mouse = {
x : 0, y : 0, // mouse position
buttonRaw : 0,
buttonOnMasks : [0b1, 0b10, 0b100], // mouse button on masks
buttonOffMasks : [0b110, 0b101, 0b011], // mouse button off masks
bounds : null,
event(e) {
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
if (e.type === "mousedown") { m.buttonRaw |= m.buttonOnMasks[e.which - 1] }
else if (e.type === "mouseup") { m.buttonRaw &= m.buttonOffMasks[e.which - 1] }
e.preventDefault();
},
start(element) {
m.element = element === undefined ? document : element;
"mousemove,mousedown,mouseup".split(",").forEach(name => document.addEventListener(name, mouse.event) );
document.addEventListener("contextmenu", (e) => { e.preventDefault() }, false);
return mouse;
},
}
m = mouse;
return mouse;
})().start(canvas);
canvas { position:absolute; top : 0px; left : 0px;}
<canvas id="canvas"></canvas>

Calculating angular velocity after a collision

I've got the linear component of collision resolution down relatively well, but I can't quite figure out how to do the same for the angular one. From what I've read, it's something like... torque = point of collision x linear velocity. (cross product) I tried to incorporate an example I found into my code but I actually don't see any rotation at all when objects collide. The other fiddle works perfectly with a rudimentary implementation of the seperating axis theorem and the angular velocity calculations. Here's what I've come up with...
Property definitions (orientation, angular velocity, and angular acceleration):
rotation: 0,
angularVelocity: 0,
angularAcceleration: 0
Calculating the angular velocity in the collision response:
var pivotA = this.vector(bodyA.x, bodyA.y);
bodyA.angularVelocity = 1 * 0.2 * (bodyA.angularVelocity / Math.abs(bodyA.angularVelocity)) * pivotA.subtract(isCircle ? pivotA.add(bodyA.radius) : {
x: pivotA.x + boundsA.width,
y: pivotA.y + boundsA.height
}).vCross(bodyA.velocity);
var pivotB = this.vector(bodyB.x, bodyB.y);
bodyB.angularVelocity = 1 * 0.2 * (bodyB.angularVelocity / Math.abs(bodyB.angularVelocity)) * pivotB.subtract(isCircle ? pivotB.add(bodyB.radius) : {
x: pivotB.x + boundsB.width,
y: pivotB.y + boundsB.height
}).vCross(bodyB.velocity);
Updating the orientation in the update loop:
var torque = 0;
torque += core.objects[o].angularVelocity * -1;
core.objects[o].angularAcceleration = torque / core.objects[o].momentOfInertia();
core.objects[o].angularVelocity += core.objects[o].angularAcceleration;
core.objects[o].rotation += core.objects[o].angularVelocity;
I would post the code that I have for calculating the moments of inertia but there's a seperate one for every object so that would be a bit... lengthy. Nonetheless, here's the one for a circle as an example:
return this.mass * this.radius * this.radius / 2;
Just to show the result, here's my fiddle. As shown, objects do not rotate on collision. (not exactly visible with the circles, but it should work for the zero and seven)
What am I doing wrong?
EDIT: Reason they weren't rotating at all was because of an error with groups in the response function -- it rotates now, just not correctly. However, I've commented that out for now as it messes things up.
Also, I've tried another method for rotation. Here's the code in the response:
_bodyA.angularVelocity = direction.vCross(_bodyA.velocity) / (isCircle ? _bodyA.radius : boundsA.width);
_bodyB.angularVelocity = direction.vCross(_bodyB.velocity) / (isCircle ? _bodyB.radius : boundsB.width);
Note that direction refers to the "collision normal".
Angular and linear acceleration due to force vector
Angular and directional accelerations due to an applied force are two components of the same thing and can not be separated. To get one you need to solve for both.
Define the calculations
From simple physics and standing on shoulders we know the following.
F is force (equivalent to inertia)
Fv is linear force
Fa is angular force
a is acceleration could be linear or rotational depending on where it is used
v is velocity. For angular situations it is the tangential component only
m is mass
r is radius
For linear forces
F = m * v
From which we derive
m = F / v
v = F / m
For rotational force (v is tangential velocity)
F = r * r * m * (v / r) and simplify F = r * m * v
From which we derive
m = F / ( r * v )
v = F / ( r * m )
r = F / ( v * m )
Because the forces we apply are instantaneous we can interchange a acceleration and v velocity to give all the following formulas
Linear
F = m * a
m = F / a
a = F / m
Rotational
F = r * m * a
m = F / ( r * a )
a = F / ( r * m )
r = F / ( a * m )
As we are only interested in the change in velocity for both linear and rotation solutions
a1 = F / m
a2 = F / ( r * m )
Where a1 is acceleration in pixels per frame2 and a2 is acceleration in radians per frame2 ( the frame squared just denotes it is acceleration)
From 1D to 2D
Because this is a 2D solution and all above are 1D we need to use vectors. I for this problem use two forms of the 2D vector. Polar that has a magnitude (length, distance, the like...) and direction. Cartesian which has x and y. What a vector represents depends on how it is used.
The following functions are used as helpers in the solution. They are written in ES6 so for non compliant browsers you will have to adapt them, though I would not ever suggest you use these as they are written for convenience, they are very inefficient and do a lot of redundant calculations.
Converts a vector from polar to cartesian returning a new one
function polarToCart(pVec, retV = {x : 0, y : 0}) {
retV.x = Math.cos(pVec.dir) * pVec.mag;
retV.y = Math.sin(pVec.dir) * pVec.mag;
return retV;
}
Converts a vector from cartesian to polar returning a new one
function cartToPolar(vec, retV = {dir : 0, mag : 0}) {
retV.dir = Math.atan2(vec.y, vec.x);
retV.mag = Math.hypot(vec.x, vec.y);
return retV;
}
Creates a polar vector
function polar(mag = 1, dir = 0) {
return validatePolar({dir : dir,mag : mag});
}
Create a vector as a cartesian
function vector(x = 1, y = 0) {
return {x : x, y : y};
}
True is the arg vec is a vector in polar form
function isPolar(vec) {
if (vec.mag !== undefined && vec.dir !== undefined) {return true;}
return false;
}
Returns true if arg vec is a vector in cartesian form
function isCart(vec) {
if (vec.x !== undefined && vec.y !== undefined) {return true;}
return false;
}
Returns a new vector in polar form also ensures that vec.mag is positive
function asPolar(vec){
if(isCart(vec)){ return cartToPolar(vec); }
if(vec.mag < 0){
vec.mag = - vec.mag;
vec.dir += PI;
}
return { dir : vec.dir, mag : vec.mag };
}
Copy and converts an unknown vec to cart if not already
function asCart(vec){
if(isPolar(vec)){ return polarToCart(vec); }
return { x : vec.x, y : vec.y};
}
Calculations can result in a negative magnitude though this is valid for some calculations this results in the incorrect vector (reversed) this simply validates that the polar vector has a positive magnitude it does not change the vector just the sign and direction
function validatePolar(vec) {
if (isPolar(vec)) {
if (vec.mag < 0) {
vec.mag = - vec.mag;
vec.dir += PI;
}
}
return vec;
}
The Box
Now we can define an object that we can use to play with. A simple box that has position, size, mass, orientation, velocity and rotation
function createBox(x,y,w,h){
var box = {
x : x, // pos
y : y,
r : 0.1, // its rotation AKA orientation or direction in radians
h : h, // its height
w : w, // its width
dx : 0, // delta x in pixels per frame 1/60th second
dy : 0, // delta y
dr : 0.0, // deltat rotation in radians per frame 1/60th second
mass : w * h, // mass in things
update :function(){
this.x += this.dx;
this.y += this.dy;
this.r += this.dr;
},
}
return box;
}
Applying a force to an object
So now we can redefine some terms
F (force) is a vector force the magnitude is the force and it has a direction
var force = polar(100,0); // create a force 100 units to the right (0 radians)
The force is meaningless without a position where it is applied.
Position is a vector that just holds and x and y location
var location = vector(canvas.width/2, canvas.height/2); // defines a point in the middle of the canvas
Directional vector holds the direction and distance between to positional vectors
var l1 = vector(canvas.width/2, canvas.height/2); // defines a point in the middle of the canvas
var l2 = vector(100,100);
var direction = asPolar(vector(l2.x - l1.x, l2.y - l1.y)); // get the direction as polar vector
direction now has the direction from canvas center to point (100,100) and the distance.
The last thing we need to do is extract the components from a force vector along a directional vector. When you apply a force to an object the force is split into two, one is the force along the line to the object center and adds to the object acceleration, the other force is at 90deg to the line to the object center (the tangent) and that is the force that changes rotation.
To get the two components you get the difference in direction between the force vector and the directional vector from where the force is applied to the object center.
var force = polar(100,0); // the force
var forceLoc = vector(50,50); // the location the force is applied
var direction2Center = asPolar(vector(box.x - forceLoc.x, box.y - forceLoc.y)); // get the direction as polar vector
var pheta = direction2Center - force.dir; // get the angle between the force and object center
Now that you have that angle pheta the force can be split into its rotational and linear components with trig.
var F = force.mag; // get the force magnitude
var Fv = Math.cos(pheta) * F; // get the linear force
var Fa = Math.sin(pheta) * F; // get the angular force
Now the forces can be converted back to accelerations for linear a = F/m and angular a = F/(m*r)
accelV = Fv / box.mass; // linear acceleration in pixels
accelA = Fa / (box.mass * direction2Center.mag); // angular acceleration in radians
You then convert the linear force back to a vector that has a direction to the center of the object
var forceV = polar(Fv, direction2Center);
Convert is back to the cartesian so we can add it to the object deltaX and deltaY
forceV = asCart(forceV);
And add the acceleration to the box
box.dx += forceV.x;
box.dy += forceV.y;
Rotational acceleration is just one dimensional so just add it to the delta rotation of the box
box.dr += accelA;
And that is it.
Function to apply force to Box
The function if attached to the box will apply a force vector at a location to the box.
Attach to the box like so
box.applyForce = applyForce; // bind function to the box;
You can then call the function via the box
box.applyForce(force, locationOfForce);
function applyForce(force, loc){ // force is a vector, loc is a coordinate
var toCenter = asPolar(vector(this.x - loc.x, this.y - loc.y)); // get the vector to the center
var pheta = toCenter.dir - force.dir; // get the angle between the force and the line to center
var Fv = Math.cos(pheta) * force.mag; // Split the force into the velocity force along the line to the center
var Fa = Math.sin(pheta) * force.mag; // and the angular force at the tangent to the line to the center
var accel = asPolar(toCenter); // copy the direction to center
accel.mag = Fv / this.mass; // now use F = m * a in the form a = F/m to get acceleration
var deltaV = asCart(accel); // convert acceleration to cartesian
this.dx += deltaV.x // update the box delta V
this.dy += deltaV.y //
var accelA = Fa / (toCenter.mag * this.mass); // for the angular component get the rotation
// acceleration from F=m*a*r in the
// form a = F/(m*r)
this.dr += accelA;// now add that to the box delta r
}
The Demo
The demo is only about the function applyForce the stuff to do with gravity and bouncing are only very bad approximations and should not be used for any physic type of stuff as they do not conserve energy.
Click and drag to apply a force to the object in the direction that the mouse is moved.
const PI90 = Math.PI / 2;
const PI = Math.PI;
const PI2 = Math.PI * 2;
const INSET = 10; // playfeild inset
const ARROW_SIZE = 6
const SCALE_VEC = 10;
const SCALE_FORCE = 0.15;
const LINE_W = 2;
const LIFE = 12;
const FONT_SIZE = 20;
const FONT = "Arial Black";
const WALL_NORMS = [PI90,PI,-PI90,0]; // dirction of the wall normals
var box = createBox(200, 200, 50, 100);
box.applyForce = applyForce; // Add this function to the box
// render / update function
var mouse = (function(){
function preventDefault(e) { e.preventDefault(); }
var i;
var mouse = {
x : 0, y : 0,buttonRaw : 0,
bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
mouseEvents : "mousemove,mousedown,mouseup".split(",")
};
function mouseMove(e) {
var t = e.type, m = mouse;
m.x = e.offsetX; m.y = e.offsetY;
if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
} else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];}
e.preventDefault();
}
mouse.start = function(element = document){
if(mouse.element !== undefined){ mouse.removeMouse();}
mouse.element = element;
mouse.mouseEvents.forEach(n => { element.addEventListener(n, mouseMove); } );
}
mouse.remove = function(){
if(mouse.element !== undefined){
mouse.mouseEvents.forEach(n => { mouse.element.removeEventListener(n, mouseMove); } );
mouse.element = undefined;
}
}
return mouse;
})();
var canvas,ctx;
function createCanvas(){
canvas = document.createElement("canvas");
canvas.style.position = "absolute";
canvas.style.left = "0px";
canvas.style.top = "0px";
canvas.style.zIndex = 1000;
document.body.appendChild(canvas);
}
function resizeCanvas(){
if(canvas === undefined){
createCanvas();
}
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext("2d");
if(box){
box.w = canvas.width * 0.10;
box.h = box.w * 2;
box.mass = box.w * box.h;
}
}
window.addEventListener("resize",resizeCanvas);
resizeCanvas();
mouse.start(canvas)
var tempVecs = [];
function addTempVec(v,vec,col,life = LIFE,scale = SCALE_VEC){tempVecs.push({v:v,vec:vec,col:col,scale:scale,life:life,sLife:life});}
function drawTempVecs(){
for(var i = 0; i < tempVecs.length; i ++ ){
var t = tempVecs[i]; t.life -= 1;
if(t.life <= 0){tempVecs.splice(i, 1); i--; continue}
ctx.globalAlpha = (t.life / t.sLife)*0.25;
drawVec(t.v, t.vec ,t.col, t.scale)
}
}
function drawVec(v,vec,col,scale = SCALE_VEC){
vec = asPolar(vec)
ctx.setTransform(1,0,0,1,v.x,v.y);
var d = vec.dir;
var m = vec.mag;
ctx.rotate(d);
ctx.beginPath();
ctx.lineWidth = LINE_W;
ctx.strokeStyle = col;
ctx.moveTo(0,0);
ctx.lineTo(m * scale,0);
ctx.moveTo(m * scale-ARROW_SIZE,-ARROW_SIZE);
ctx.lineTo(m * scale,0);
ctx.lineTo(m * scale-ARROW_SIZE,ARROW_SIZE);
ctx.stroke();
}
function drawText(text,x,y,font,size,col){
ctx.font = size + "px "+font;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.setTransform(1,0,0,1,x,y);
ctx.globalAlpha = 1;
ctx.fillStyle = col;
ctx.fillText(text,0,0);
}
function createBox(x,y,w,h){
var box = {
x : x, // pos
y : y,
r : 0.1, // its rotation AKA orientation or direction in radians
h : h, // its height, and I will assume that its depth is always equal to its height
w : w, // its width
dx : 0, // delta x in pixels per frame 1/60th second
dy : 0, // delta y
dr : 0.0, // deltat rotation in radians per frame 1/60th second
getDesc : function(){
var vel = Math.hypot(this.dx ,this.dy);
var radius = Math.hypot(this.w,this.h)/2
var rVel = Math.abs(this.dr * radius);
var str = "V " + (vel*60).toFixed(0) + "pps ";
str += Math.abs(this.dr * 60 * 60).toFixed(0) + "rpm ";
str += "Va " + (rVel*60).toFixed(0) + "pps ";
return str;
},
mass : function(){ return (this.w * this.h * this.h)/1000; }, // mass in K things
draw : function(){
ctx.globalAlpha = 1;
ctx.setTransform(1,0,0,1,this.x,this.y);
ctx.rotate(this.r);
ctx.fillStyle = "#444";
ctx.fillRect(-this.w/2, -this.h/2, this.w, this.h)
ctx.strokeRect(-this.w/2, -this.h/2, this.w, this.h)
},
update :function(){
this.x += this.dx;
this.y += this.dy;
this.dy += 0.061; // alittle gravity
this.r += this.dr;
},
getPoint : function(which){
var dx,dy,x,y,xx,yy,velocityA,velocityT,velocity;
dx = Math.cos(this.r);
dy = Math.sin(this.r);
switch(which){
case 0:
x = -this.w /2;
y = -this.h /2;
break;
case 1:
x = this.w /2;
y = -this.h /2;
break;
case 2:
x = this.w /2;
y = this.h /2;
break;
case 3:
x = -this.w /2;
y = this.h /2;
break;
case 4:
x = this.x;
y = this.y;
}
var xx,yy;
xx = x * dx + y * -dy;
yy = x * dy + y * dx;
var details = asPolar(vector(xx, yy))
xx += this.x;
yy += this.y;
velocityA = polar(details.mag * this.dr, details.dir + PI90);
velocityT = vectorAdd(velocity = vector(this.dx, this.dy), velocityA);
return {
velocity : velocity, // only directional
velocityT : velocityT, // total
velocityA : velocityA, // angular only
pos : vector(xx, yy),
radius : details.mag,
}
},
}
box.mass = box.mass(); // Mass remains the same so just set it with its function
return box;
}
// calculations can result in a negative magnitude though this is valide for some
// calculations this results in the incorrect vector (reversed)
// this simply validates that the polat vector has a positive magnitude
// it does not change the vector just the sign and direction
function validatePolar(vec){
if(isPolar(vec)){
if(vec.mag < 0){
vec.mag = - vec.mag;
vec.dir += PI;
}
}
return vec;
}
// converts a vector from polar to cartesian returning a new one
function polarToCart(pVec, retV = {x : 0, y : 0}){
retV.x = Math.cos(pVec.dir) * pVec.mag;
retV.y = Math.sin(pVec.dir) * pVec.mag;
return retV;
}
// converts a vector from cartesian to polar returning a new one
function cartToPolar(vec, retV = {dir : 0, mag : 0}){
retV.dir = Math.atan2(vec.y,vec.x);
retV.mag = Math.hypot(vec.x,vec.y);
return retV;
}
function polar (mag = 1, dir = 0) { return validatePolar({dir : dir, mag : mag}); } // create a polar vector
function vector (x= 1, y= 0) { return {x: x, y: y}; } // create a cartesian vector
function isPolar (vec) { if(vec.mag !== undefined && vec.dir !== undefined) { return true; } return false; }// returns true if polar
function isCart (vec) { if(vec.x !== undefined && vec.y !== undefined) { return true; } return false; }// returns true if cartesian
// copy and converts an unknown vec to polar if not already
function asPolar(vec){
if(isCart(vec)){ return cartToPolar(vec); }
if(vec.mag < 0){
vec.mag = - vec.mag;
vec.dir += PI;
}
return { dir : vec.dir, mag : vec.mag };
}
// copy and converts an unknown vec to cart if not already
function asCart(vec){
if(isPolar(vec)){ return polarToCart(vec); }
return { x : vec.x, y : vec.y};
}
// normalise makes a vector a unit length and returns it as a cartesian
function normalise(vec){
var vp = asPolar(vec);
vap.mag = 1;
return asCart(vp);
}
function vectorAdd(vec1, vec2){
var v1 = asCart(vec1);
var v2 = asCart(vec2);
return vector(v1.x + v2.x, v1.y + v2.y);
}
// This splits the vector (polar or cartesian) into the components along dir and the tangent to that dir
function vectorComponentsForDir(vec,dir){
var v = asPolar(vec); // as polar
var pheta = v.dir - dir;
var Fv = Math.cos(pheta) * v.mag;
var Fa = Math.sin(pheta) * v.mag;
var d1 = dir;
var d2 = dir + PI90;
if(Fv < 0){
d1 += PI;
Fv = -Fv;
}
if(Fa < 0){
d2 += PI;
Fa = -Fa;
}
return {
along : polar(Fv,d1),
tangent : polar(Fa,d2)
};
}
function doCollision(pointDetails, wallIndex){
var vv = asPolar(pointDetails.velocity); // Cartesian V make sure the velocity is in cartesian form
var va = asPolar(pointDetails.velocityA); // Angular V make sure the velocity is in cartesian form
var vvc = vectorComponentsForDir(vv, WALL_NORMS[wallIndex])
var vac = vectorComponentsForDir(va, WALL_NORMS[wallIndex])
vvc.along.mag *= 1.18; // Elastic collision requiers that the two equal forces from the wall
vac.along.mag *= 1.18; // against the box and the box against the wall be summed.
// As the wall can not move the result is that the force is twice
// the force the box applies to the wall (Yes and currently force is in
// velocity form untill the next line)
vvc.along.mag *= box.mass; // convert to force
//vac.along.mag/= pointDetails.radius
vac.along.mag *= box.mass
vvc.along.dir += PI; // force is in the oppisite direction so turn it 180
vac.along.dir += PI; // force is in the oppisite direction so turn it 180
// split the force into components based on the wall normal. One along the norm the
// other along the wall
vvc.tangent.mag *= 0.18; // add friction along the wall
vac.tangent.mag *= 0.18;
vvc.tangent.mag *= box.mass //
vac.tangent.mag *= box.mass
vvc.tangent.dir += PI; // force is in the oppisite direction so turn it 180
vac.tangent.dir += PI; // force is in the oppisite direction so turn it 180
// apply the force out from the wall
box.applyForce(vvc.along, pointDetails.pos)
// apply the force along the wall
box.applyForce(vvc.tangent, pointDetails.pos)
// apply the force out from the wall
box.applyForce(vac.along, pointDetails.pos)
// apply the force along the wall
box.applyForce(vac.tangent, pointDetails.pos)
//addTempVec(pointDetails.pos, vvc.tangent, "red", LIFE, 10)
//addTempVec(pointDetails.pos, vac.tangent, "red", LIFE, 10)
}
function applyForce(force, loc){ // force is a vector, loc is a coordinate
validatePolar(force); // make sure the force is a valid polar
// addTempVec(loc, force,"White", LIFE, SCALE_FORCE) // show the force
var l = asCart(loc); // make sure the location is in cartesian form
var toCenter = asPolar(vector(this.x - l.x, this.y - l.y));
var pheta = toCenter.dir - force.dir;
var Fv = Math.cos(pheta) * force.mag;
var Fa = Math.sin(pheta) * force.mag;
var accel = asPolar(toCenter); // copy the direction to center
accel.mag = Fv / this.mass; // now use F = m * a in the form a = F/m
var deltaV = asCart(accel); // convert it to cartesian
this.dx += deltaV.x // update the box delta V
this.dy += deltaV.y
var accelA = Fa / (toCenter.mag * this.mass); // for the angular component get the rotation
// acceleration
this.dr += accelA;// now add that to the box delta r
}
// make a box
ctx.globalAlpha = 1;
var lx,ly;
function update(){
// clearLog();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.lineWidth = 1;
ctx.strokeStyle = "black";
ctx.fillStyle = "#888";
ctx.fillRect(INSET, INSET, canvas.width - INSET * 2, canvas.height - INSET * 2);
ctx.strokeRect(INSET, INSET, canvas.width - INSET * 2, canvas.height - INSET * 2);
ctx.lineWidth = 2;
ctx.strokeStyle = "black";
box.update();
box.draw();
if(mouse.buttonRaw & 1){
var force = asPolar(vector(mouse.x - lx, mouse.y - ly));
force.mag *= box.mass * 0.1;
box.applyForce(force,vector(mouse.x, mouse.y))
addTempVec(vector(mouse.x, mouse.y), asPolar(vector(mouse.x - lx, mouse.y - ly)), "Cyan", LIFE, 5);
}
lx = mouse.x;
ly = mouse.y;
for(i = 0; i < 4; i++){
var p = box.getPoint(i);
// only do one collision per frame or we will end up adding energy
if(p.pos.x < INSET){
box.x += (INSET) - p.pos.x;
doCollision(p,3)
}else
if( p.pos.x > canvas.width-INSET){
box.x += (canvas.width - INSET) - p.pos.x;
doCollision(p,1)
}else
if(p.pos.y < INSET){
box.y += (INSET) -p.pos.y;
doCollision(p,0)
}else
if( p.pos.y > canvas.height-INSET){
box.y += (canvas.height - INSET) -p.pos.y;
doCollision(p,2)
}
drawVec(p.pos,p.velocity,"blue")
}
drawTempVecs();
ctx.globalAlpha = 1;
drawText(box.getDesc(),canvas.width/2,FONT_SIZE,FONT,FONT_SIZE,"black");
drawText("Click drag to apply force to box",canvas.width/2,FONT_SIZE +17,FONT,14,"black");
requestAnimationFrame(update)
}
update();

How to code an nth order Bezier curve

Trying to code an nth order bezier in javascript on canvas for a project. I want to be able to have the user press a button, in this case 'b', to select each end point and the control points. So far I am able to get the mouse coordinates on keypress and make quadratic and bezier curves using the built in functions. How would I go about making code for nth order?
Here's a Javascript implementation of nth order Bezier curves:
// setup canvas
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
ctx.fillText("INSTRUCTIONS: Press the 'b' key to add points to your curve. Press the 'c' key to clear all points and start over.", 20, 20);
// initialize points list
var plist = [];
// track mouse movements
var mouseX;
var mouseY;
document.addEventListener("mousemove", function(e) {
mouseX = e.clientX;
mouseY = e.clientY;
});
// from: http://rosettacode.org/wiki/Evaluate_binomial_coefficients#JavaScript
function binom(n, k) {
var coeff = 1;
for (var i = n - k + 1; i <= n; i++) coeff *= i;
for (var i = 1; i <= k; i++) coeff /= i;
return coeff;
}
// based on: https://stackoverflow.com/questions/16227300
function bezier(t, plist) {
var order = plist.length - 1;
var y = 0;
var x = 0;
for (i = 0; i <= order; i++) {
x = x + (binom(order, i) * Math.pow((1 - t), (order - i)) * Math.pow(t, i) * (plist[i].x));
y = y + (binom(order, i) * Math.pow((1 - t), (order - i)) * Math.pow(t, i) * (plist[i].y));
}
return {
x: x,
y: y
};
}
// draw the Bezier curve
function draw(plist) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
var accuracy = 0.01; //this'll give the 100 bezier segments
ctx.beginPath();
ctx.moveTo(plist[0].x, plist[0].y);
for (p in plist) {
ctx.fillText(p, plist[p].x + 5, plist[p].y - 5);
ctx.fillRect(plist[p].x - 5, plist[p].y - 5, 10, 10);
}
for (var i = 0; i < 1; i += accuracy) {
var p = bezier(i, plist);
ctx.lineTo(p.x, p.y);
}
ctx.stroke();
ctx.closePath();
}
// listen for keypress
document.addEventListener("keydown", function(e) {
switch (e.keyCode) {
case 66:
// b key
plist.push({
x: mouseX,
y: mouseY
});
break;
case 67:
// c key
plist = [];
break;
}
draw(plist);
});
html,
body {
height: 100%;
margin: 0 auto;
}
<canvas id="canvas"></canvas>
This is based on this implementation of cubic Bezier curves. In your application, it sounds like you'll want to populate the points array with user-defined points.
Here is a code example for any number of points you want to add to make a bezier curve. Here points you will pass is an array of objects containing x and y values of points. [ { x: 1,y: 2 } , { x: 3,y: 4} ... ]
function factorial(n) {
if(n<0)
return(-1); /*Wrong value*/
if(n==0)
return(1); /*Terminating condition*/
else
{
return(n*factorial(n-1));
}
}
function nCr(n,r) {
return( factorial(n) / ( factorial(r) * factorial(n-r) ) );
}
function BezierCurve(points) {
let n=points.length;
let curvepoints=[];
for(let u=0; u <= 1 ; u += 0.0001 ){
let p={x:0,y:0};
for(let i=0 ; i<n ; i++){
let B=nCr(n-1,i)*Math.pow((1-u),(n-1)-i)*Math.pow(u,i);
let px=points[i].x*B;
let py=points[i].y*B;
p.x+=px;
p.y+=py;
}
curvepoints.push(p);
}
return curvepoints;
}

Canvas: draw lots of elements with a changing gradient (emulate angular gradient)

for this project http://biduleohm.free.fr/ledohm/ (sorry, the user interface is in french but the code is in english) I need an angular gradient but it doesn't exists in native so I've implemented it using a linear gradient on a line and I draw the lines more and more longer to form a triangle. The result is graphically OK but the speed isn't really good (1850 ms for 125 triangles). It's in the tab [RĂ©partition], it redraws if there is a keyup event on one of the inputs, don't be afraid of the apparent slowness, I've limited to maximum one redraw every 2000 ms.
Before I used a simple linear gradient on the whole triangle (but this doesn't match the reality) and the speed was OK, it draws thousands of triangles in less than a second. This function was used :
drawFrontLightForColor : function(x, y, w, h, color) {
var x2 = x - w;
var x3 = x + w;
var gradient = Distri.frontCanvas.createLinearGradient(x2, y, x3, y);
gradient.addColorStop(0, 'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
gradient.addColorStop(0.5, 'rgba(' + color + ', ' + (color == Distri.lightColors.cw ? Distri.lightCenterAlphaCw : Distri.lightCenterAlphaOther) + ')');
gradient.addColorStop(1, 'rgba(' + color + ', ' + Distri.lightEdgeAlpha + ')');
Distri.frontCanvas.fillStyle = gradient;
Distri.frontCanvas.beginPath();
Distri.frontCanvas.moveTo(x, y);
Distri.frontCanvas.lineTo(x2, (y + h));
Distri.frontCanvas.lineTo(x3, (y + h));
Distri.frontCanvas.lineTo(x, y);
Distri.frontCanvas.fill();
Distri.frontCanvas.closePath();
},
Then I switched to this function :
drawFrontLightForColor : function(x, y, w, h, centerColor, edgeColor) {
var ratio = w / h;
var tmpY;
var tmpW;
var x2;
var x3;
var gradient;
Distri.frontCanvas.lineWidth = 1;
for (var tmpH = 0; tmpH < h; tmpH++) {
tmpY = y + tmpH;
tmpW = Math.round(tmpH * ratio);
x2 = x - tmpW;
x3 = x + tmpW;
gradient = Distri.frontCanvas.createLinearGradient(x2, tmpY, x3, tmpY);
gradient.addColorStop(0, edgeColor);
gradient.addColorStop(0.5, centerColor);
gradient.addColorStop(1, edgeColor);
Distri.frontCanvas.beginPath();
Distri.frontCanvas.moveTo(x2, tmpY);
Distri.frontCanvas.lineTo(x, tmpY);
Distri.frontCanvas.lineTo(x3, tmpY);
Distri.frontCanvas.strokeStyle = gradient;
Distri.frontCanvas.stroke();
Distri.frontCanvas.closePath();
}
},
You can find the whole source here
I can't put the beginPath, stroke, closePath out of the loop because of the gradient which is changing every iteration (I've tried but it used the last gradient for every line (which, ironically, is identical to the first function...) which is understandable but not what I want).
I accept any advice (including redo the whole function and modify his caller to outsource some code) to improve the speed let's say 5x (ideally more).
I think you took the wrong way from the start : when doing so much changes of color, you have better operate at the pixel level.
So yes that could be with a webgl pixel shader, but you'll have to fight just to get the boilerplate running ok on all platform (or get a lib to do that for you).
And anyway there's a solution perfect for your need, and fast enough (a few ms) : use raw pixel data, update them one by one with the relevant function, then draw the result.
The steps to do that are :
- create a buffer same size as the canvas.
- iterate through it's pixel, keeping track of the x,y of the point.
- normalize the coordinates so they match your 'space'.
- compute the value for the normalized (x,y) out of all the data that you have.
- write a color (in my example i choose greyscale) out of that value.
- draw the whole buffer to canvas.
I did a jsfiddle, and here's the result with 4 data points :
fiddle is here :
http://jsfiddle.net/gamealchemist/KsM9c/3/
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
var width = canvas.width,
height = canvas.height;
// builds an image for the target canvas
function buildImage(targetCanvas, valueForXY, someData) {
var width = targetCanvas.width;
var height = targetCanvas.height;
var tempImg = ctx.createImageData(width, height);
var buffer = tempImg.data;
var offset = 0;
var xy = [0, 0];
function normalizeXY(xy) {
xy[0] = xy[0] / width ;
xy[1] = xy[1] / height;
}
for (var y = 0; y < height; y++)
for (var x = 0; x < width; x++, offset += 4) {
xy[0] = x; xy[1]=y;
normalizeXY(xy);
var val = Math.floor(valueForXY(xy, someData) * 255);
buffer[offset] = val;
buffer[offset + 1] = val;
buffer[offset + 2] = val;
buffer[offset + 3] = 255;
}
ctx.putImageData(tempImg, 0, 0);
}
// return normalized (0->1) value for x,y and
// provided data.
// xy is a 2 elements array
function someValueForXY(xy, someData) {
var res = 0;
for (var i = 0; i < someData.length; i++) {
var thisData = someData[i];
var dist = Math.pow(sq(thisData[0] - xy[0]) + sq(thisData[1] - xy[1]), -0.55);
localRes = 0.04 * dist;
res += localRes;
}
if (res > 1) res = 1;
return res;
}
var someData = [
[0.6, 0.2],
[0.35, 0.8],
[0.2, 0.5],
[0.6, 0.75]
];
buildImage(canvas, someValueForXY, someData);
// ------------------------
function sq(x) {
return x * x
}
In fact the GameAlchemist's solution isn't fast or I do something really wrong. I've implemented this algo only for the top view because the front view is much more complex.
For 120 lights the top view take 100-105 ms with the old code and it take 1650-1700 ms with this code (and moreover it still lacks a few things in the new code like the color for example):
drawTopLightForColor_ : function(canvasW, canvasD, rampX, rampY, rampZ, ledsArrays, color) {
function sq(x) {
return x * x;
}
var tmpImg = Distri.topCanvasCtx.createImageData(canvasW, canvasD);
var rawData = tmpImg.data;
var ledsArray = ledsArrays[color];
var len = ledsArray.length;
var i = 0;
for (var y = 0; y < canvasD; y++) {
for (var x = 0; x < canvasW; x++, i += 4) {
var intensity = 0;
for (var j = 0; j < len; j++) {
intensity += 2 * Math.pow(
sq((rampX + ledsArray[j].x) - x) +
sq((rampZ + ledsArray[j].y) - y),
-0.5
);
}
if (intensity > 1) {
intensity = 1;
}
intensity = Math.round(intensity * 255);
rawData[i] = intensity;
rawData[i + 1] = intensity;
rawData[i + 2] = intensity;
rawData[i + 3] = 255;
}
}
Distri.topCanvasCtx.putImageData(tmpImg, 0, 0);
},
Am I doing something wrong?

Categories