As promised here is the blog post to describe most of the details involved in my reverse engineering process of the code from dionyziz’ video to create my own 3D vector graphing tool.
The main aspect to keep in mind is a 3D point is converted into a 2D point. This 2D point is then graphed on a HTML canvas in 2D context. The code I used to produce the 2D context:
var ctx2 = cnv2.getContext('2d');
To read more about canvas and context please check out this link.
These are the main steps involved in my reverse engineering process:
I. Establishing a Points Array
A major part of the program is establishing a points array. In dionyziz's program a 'for' loop produced points for the array. However, in my program I coded a points array at the start to hold the main 3D graphing points to create the x, y, and z axes. This array also contains names to distinguish drawn lines for the graph and number labels for the axes.
var points = [
{ //0
name: "origin",
v3d: [0,0,0],
text3d: [0,0,0],
},
{ //1
name: "neg_x",
v3d:[-1*graph_size, 0, 0],
text3d: [0,0,0],
},
{ //2
name: "pos_x",
v3d: [graph_size, 0, 0],
text3d: [0,0,0],
},
{ //3
name: "neg_y",
v3d: [0, -1*graph_size, 0],
text3d: [0,0,0],
},
{ //4
name: "pos_y",
v3d: [0, graph_size, 0],
text3d: [0,0,0],
},
{ //5
name: "neg_z",
v3d: [0, 0, -1*graph_size],
text3d: [0,0,0],
},
{ //6
name: "pos_z",
v3d: [0, 0, graph_size],
text3d: [0,0,0],
},
{ //7
name: "c_nxnz",
v3d: [-1*graph_size, 0, -1*graph_size],
text3d: [0,0,0],
},
{ //8
name: "c_nxpz",
v3d: [-1*graph_size, 0, graph_size],
text3d: [0,0,0],
},
{ //9
name: "c_pxpz",
v3d: [graph_size, 0, graph_size],
text3d: [0,0,0],
},
{ //10
name: "c_pxnz",
v3d: [graph_size, 0, -1*graph_size],
text3d: [0,0,0],
}
];
II. The Function render() is the Primary Function.
The function render() is called to start the entire process. Within this function each 3D point goes through rotateY, rotateX and rotateZ functions (in that specific order) to place each 3D point in its proper position. If any changes are made to the angles ytheta, xtheta, and ztheta (by changes from any of the rotate buttons - scroll down below for details on the rotate button functions) then render() will update the points when the entire graph is drawn again. The rotate functions have as parameter inputs:
- point - array of three numbers, for each x, y, and z value.
- theta - an angle value corresponding to rotation amount around the x, y, or z axes.
//note - this is only a portion of the for loop code
for(var i = 0; i < points.length; i++){
point = points[i].v3d;
txt3d = points[i].text3d;
name = points[i].name;
point = rotateY(point, ytheta);
point = rotateX(point, xtheta);
point = rotateZ(point, ztheta);
axis = renderPoint(point);
if(i == 0){
o_x = axis[0];
o_y = axis[1];
}else if(i == 1){
x_n = axis[0];
y_n = axis[1];
}else if(i == 2){
x_p = axis[0];
y_p = axis[1];
}
/*ROTATION FUNCTIONS*/
function rotateY(point, theta){
var x = point[0],
y = point[1],
z = point[2]*-1;//*-1.0 so z+ points out of page
return [
Math.cos(theta)*x - Math.sin(theta)*z,//this is new x value
y,//y value
Math.sin(theta)*x + Math.cos(theta)*z//this is the new z value
];
}
function rotateX(point, theta){
var x = point[0],
y = point[1],
z = point[2];
return [
x,//x value
Math.cos(theta)*y + Math.sin(theta)*z,//this is the new y value
-Math.sin(theta)*y + Math.cos(theta)*z//this is the new z value
];
}
function rotateZ(point, theta){
var x = point[0],
y = point[1],
z = point[2];
return [
Math.cos(theta)*x + Math.sin(theta)*y,
-Math.sin(theta)*x + Math.cos(theta)*y,
z
];
}
III. A Succession of Other Functions
Each 3D point goes through a succession of functions after returning from the rotate functions.
1. Function renderPoint(point) is first. And the first thing that happens in this function is point becomes a parameter in the function project(point).
/*DISPLAY 3D POINTS FUNCTIONS*/
function renderPoint(point){//this function takes a point and draws it
var projectedPoint = project(point);
var x = projectedPoint[0],
y = projectedPoint[1];
ctx2.beginPath();
ctx2.moveTo(x,y);
ctx2.lineTo(x + 1, y + 1);
ctx2.lineWidth = 1;
ctx2.strokeStyle = color.colordots;
ctx2.stroke();
ctx2.closePath();
return [x,y];
}
2. In function project(point) the point is sent immediately to function perspectiveProjection(point).
function project(point){//this gives the actual pixel number of x, y, so can be graphed
var perspectivePoint = perspectiveProjection(point);//returns an array of 2,
//[0] and [1], which is x and y below
var x = perspectivePoint[0],
y = perspectivePoint[1];
var new_x = (canvas_w/2) + ((x*grid_sz)/(grid_multipl));
var new_y = (canvas_h/2) - ((y*grid_sz)/(grid_multipl));
var cw_plus = canvas_w + 5.0;
var ch_plus = canvas_h + 5.0;
return [new_x, new_y];
}
3. And in function perspectiveProjection(point) this is where a 3D point consisting of x, y, and z values is transformed into a 2D point, so it can be drawn on a flat 2D canvas surface.
Note that I determined through trial and error to find perspective_distance.amnt of 50 to work best:
var perspective_distance = {amnt: 50};
More about the mathematics behind how this works can be read here.
function perspectiveProjection(point){//this takes an array of 3 elements
//and returns an array of 2 elements
var x = point[0],
y = point[1],
z = point[2];
var xvalue = 0;
var yvalue = 0;
xvalue = x/(z + perspective_distance.amnt);
yvalue = y/(z + perspective_distance.amnt);
return [xvalue, yvalue];
}
/*END DISPLAY 3D POINTS FUNCTIONS*/
4. Once returning from projectedPoint() in renderPoint(), the point and other subsequent points are then used to draw graph lines via certain canvas functions, such as beginPath(), moveTo(), and lineTo(), along with other functions until closePath() is invoked. Learn more about these functions at W3Schools.
IV. How the Vectors are Drawn
For example, shown below is the code to produce the addition of two vectors.
/*3D VECTOR FUNCTIONS*/
function vec3dAdd(){
add3d = true;
subtract = false;
norm3d = false;
scalar = false;
crossproduct = false;
mag3d = false;
dot3d = false;
pts = false;
dist = false;
projba = false;
var obj1 = {};
var obj2 = {};
var obj3 = {};
var obj4 = {};
var fix = [];
var index = 0;
var x1, y1, z1, x2, y2, z2, sum, vx, vy, vz = 0.0;
var x1fix, y1fix, z1fix, x2fix, y2fix, z2fix, vxfix, vyfix, vzfix = 0.0;
var fix = [];
x1 = parseFloat(document.getElementById("vx3d1").value);
y1 = parseFloat(document.getElementById("vy3d1").value);
z1 = parseFloat(document.getElementById("vz3d1").value);
x2 = parseFloat(document.getElementById("vx3d2").value);
y2 = parseFloat(document.getElementById("vy3d2").value);
z2 = parseFloat(document.getElementById("vz3d2").value);
vx = x1 + x2;
vy = y1 + y2;
vz = z1 + z2;
fix = determineIfCutoffNeeded(x1,y1,z1,x2,y2,z2,vx,vy,vz);
x1fix = fix[0];
y1fix = fix[1];
z1fix = fix[2];
x2fix = fix[3];
y2fix = fix[4];
z2fix = fix[5];
vxfix = fix[6];
vyfix = fix[7];
vzfix = fix[8];
x1 = cutDecimal(x1);
//alert("xl = " + xl);
y1 = cutDecimal(y1);
z1 = cutDecimal(z1);
x2 = cutDecimal(x2);
y2 = cutDecimal(y2);
z2 = cutDecimal(z2);
vx = cutDecimal(vx);
vy = cutDecimal(vy);
vz = cutDecimal(vz);
document.getElementById("answer3").innerHTML = "Addition of vectors (" + x1 + ", " + y1 + ", "+z1+") + ("+x2+", "+y2+", "+z2+") = ("+vx+", "+vy+", "+vz+")";
document.getElementById('answer4').innerHTML = "Note: vectors may be ratio-altered to fit within graph parameters. Actual coordinates on graph: ("+ cutDecimal(x1fix) + ", " + cutDecimal(y1fix) + ", "+ cutDecimal(z1fix)+") + ("+cutDecimal(x2fix)+", "+cutDecimal(y2fix)+", "+cutDecimal(z2fix)+") = ("+cutDecimal(vxfix)+", "+cutDecimal(vyfix)+", "+cutDecimal(vzfix)+")";
if(points.find(p => p.name == "addv1") == undefined){
obj1.name = "addv1";
obj1.v3d = [x1fix, y1fix, z1fix];
obj1.text3d = [x1, y1, z1];
points.push(obj1);
}else{
index = points.findIndex(p => p.name == "addv1");
obj1.name = "addv1";
obj1.v3d = [x1fix, y1fix, z1fix];
obj1.text3d = [x1, y1, z1];
points[index] = obj1;
}
if(points.find(p => p.name == "addv2") == undefined){
obj2.name = "addv2";
obj2.v3d = [x2fix, y2fix, z2fix];
obj2.text3d = [x2, y2, z2];
points.push(obj2);
}else{
index = points.findIndex(p => p.name == "addv2");
obj2.name = "addv2";
obj2.v3d = [x2fix, y2fix, z2fix];
obj2.text3d = [x2, y2, z2];
points[index] = obj2;
}
if(points.find(p => p.name == "sumv") == undefined){
obj3.name = "sumv";
obj3.v3d = [vxfix, vyfix, vzfix];
obj3.text3d = [vx, vy, vz];
points.push(obj3);
}else{
index = points.findIndex(p => p.name == "sumv");
obj3.name = "sumv";
obj3.v3d = [vxfix, vyfix, vzfix];
obj3.text3d = [vx, vy, vz];
points[index] = obj3;
}
if(points.find(p => p.name == "sumv2") == undefined){
obj4.name = "sumv2";
obj4.v3d = [0, 0, 0];
obj4.text3d = [0, 0, 0];
points.push(obj4);
}else{
index = points.findIndex(p => p.name == "sumv2");
obj4.name = "sumv2";
obj4.v3d = [0, 0, 0];
obj4.text3d = [0, 0, 0];
points[index] = obj4;
}
render();
}
As can be seen from the function vec3dAdd(), render() is called after the previous lines of code are completed. Within render() certain 'if' statement code will be run if the add3d bool parameter is 'true'.
if(add3d || subtract){
if(name == "addv1"){
addx1 = axis[0];
addy1 = axis[1];
vector1[0] = txt3d[0];
vector1[1] = txt3d[1];
vector1[2] = txt3d[2];
}else if(name == "addv2"){
addx2 = axis[0];
addy2 = axis[1];
vector2[0] = txt3d[0];
vector2[1] = txt3d[1];
vector2[2] = txt3d[2];
}else if(name == "sumv"){
sumx = axis[0];
sumy = axis[1];
vector3[0] = txt3d[0];
vector3[1] = txt3d[1];
vector3[2] = txt3d[2];
}
//draw 1st vector
drawVector(o_x, o_y, addx1, addy1, "green", vector1);
//draw 2nd vector
drawVectorOpposite(addx1, addy1, sumx, sumy, "blue", vector2);
//draw sum vector
drawVector(o_x, o_y, sumx, sumy, "magenta", vector3);
//draw 2nd vector from 0,0
drawVector(o_x, o_y, addx2, addy2, "blue", vector2);
}
Here is an example of one of the draw functions (and the other draw functions are similar but vary in direction and distance):
function drawVector(a, b, c, d, color, point){
var txt = " ";
ctx2.beginPath();
ctx2.moveTo(a,b);
ctx2.lineTo(c,d);
ctx2.lineWidth = 1;
ctx2.strokeStyle = color;
ctx2.stroke();
ctx2.closePath();
ctx2.font = "10px Comic Sans MS";
ctx2.fillStyle = "red";
ctx2.textAlign = "center";
txt = "("+point[0]+", "+point[1]+", "+point[2]+")";
ctx2.fillText(txt,c,d);
}
Whenever any of the rotate or zoom functions are called the angle value (ytheta, xtheta, or ztheta) is altered or the amount of the variable grid_multipl is changed, respectively.
/*FUNCTIONS FOR ROTATING AROUND X, Y OR Z AXES*/
function rotate_nx(){
xtheta -= dtheta;
render();
}
function rotate_px(){
xtheta += dtheta;
render();
}
function rotate_ny(){
ytheta -= dtheta;
render();
}
function rotate_ny_mousedown(){
ytheta -= dtheta;
render();
}
function rotate_py(){
ytheta += dtheta;
render();
}
function rotate_nz(){
ztheta -= dtheta;
render();
}
function rotate_pz(){
ztheta += dtheta;
render();
}
/*FUNCTIONS TO CONTROL ZOOM IN AND ZOOM OUT*/
function decreaseGridM(){
var length = grid_mult_arr.length;
for(var i = 0; i < length; i++){
if(grid_mult_arr[i] == grid_multipl){
if(grid_mult_arr[i] != grid_mult_arr[0]){
grid_multipl = grid_mult_arr[i - 1];
break;
}
}
}
render();
}
function increaseGridM(){
//var current_gridmult = grid_multipl;
var length = grid_mult_arr.length;
for(var i = 0; i < length; i++){
if(grid_mult_arr[i] == grid_multipl){
if(grid_mult_arr[i] != grid_mult_arr[length - 1]){
grid_multipl = grid_mult_arr[i + 1];
break;
}
}
}
render();
}
So these are the main functions and code I used after figuring out how the original program operates, in order to achieve a workable 3D graphing tool for 3D vectors.
V. Some Details to Investigate or Upgrade on the Next 3D Graphing Program
Some details I still want to investigate and/or upgrade for my next 3D graphing program.
- I want to run a mathematical analysis on why perspective_distance.amnt, grid_sz, and grid_multipl variables work as they do within this program. Currently perspective_distance.amnt is set to 50 at onset and is used in function perspectiveProjection() to render the 3 (x, y, z) points to a 2 (x, y) point. And grid_sz and grid_multipl have their values set at 200 and 0.3 at onset, respectively. These particular numbers all work well together. I would like to write another blog when I complete my analysis.
- The use of the cross product to determine the item in the graph closest to the viewing window. For instance, currently, when a vector is displayed behind the y axis, the vector will instead appear in front of it. I plan to reverse engineer the solid cube by dionyziz since he used the cross product to make items in the foreground or the background appear as they should.
- As the graph works now, vector rotations around the x or z axes appear to turn around the x or z axes of the canvas itself. I will investigate how to make vector rotations appear to rotate around the actual, drawn x or z axes.
That concludes this blog. If you are interested in knowing more about my graphing program, simply take a look at the source code...where you can investigate the code...and comments (sorry if any of the comments are vague...or silly...I'll try to do better next time, I promise :-)).
If you have any questions or concerns about this blog post, or would like more information about the 3D vector graphing program discussed in this blog, please contact me at dcwendeavors@gmail.com. Thank you :-)