JavaScript Lesson 2: Moving in circles around a moving point.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
 
//***************
//Initialize any array holding the
//string hex representation of 0-255
//=>borrowed from the JavaScript Wolfenstein
//***************
h="0123456789ABCDEF"
N=[];
f=Math.floor;
for(i=0;i<256;i++)
 N[i]="0x"+h.substr(f(i/16),1)+h.substr(i%16,1)
//***************
 
//what size we want out screen to be
screenW = 160;
screenH = 120;
 
//we need to change the frame number
//every frame or the browser will not
//update the image
frame=0;
 
//********************
//code to initialize the
//screen memory
//********************
var screen = [];
for(y=0;y<screenH;y++)
 for(x=0;x<screenW/8;x++)
  screen[x+y*screenW/8]=0;
 
 
//********************
//function to take the screen
//and display it to the user
//********************
function render()
{
 //we have to convert the numerical values of the screen into
 //the hex string representation (example: 255 => 0xff)
 conv = [];
 for(y=0;y<240;y++)
  for(x=0;x<screenW/8;x++)
   conv[x+y*screenW/8] = N[255-screen[x+y*screenW/8]];
 //send the result into the document image
 z="#define pic_";
 im=z+"width "+screenW+"\n"+z+"height "+screenH+"\nstatic char pic_bits[] = {"+conv.join(",")+"}";
 document.screen.src="JavaScript:"+frame+";im;";
}
 
//********************
//our main loop
//we handle updating
//and rendering everything
//from here
//********************
function main()
{
 frame++;
 //document.d.debug.value=frame + "\n" + pointX + ", " + pointY;
 draw();
 render();
}
 
//********************
//The ClearScreen function
//used to zero out the screen
//array
//********************
function ClearScreen()
{
 max=screenW/8 * screenH;
 for(x=0;x<max;x++)
  screen[x]=0;
}
 
//********************
//The PutPixel function
//is used to place a pixel
//on the screen.
//x is the horizontal position
//y is the vertical position
//c is the color (0 or 1 in our case)
//********************
function PutPixel(x,y,c)
{
 //first thing is to make sure the
 //pixel is within range of the screen
 if(x<0 || x>screenW)
  return;
 if(y<0 || y>screenH)
  return;
 
 //make sure x and y are integers
 x=Math.floor(x);
 y=Math.floor(y);
 
 if(c>0) // set the pixel bit to 1
  screen[Math.floor(x/8) + y*screenW/8] |= Math.pow(2,x%8);
 else // set the pixel bit to 0
  screen[Math.floor(x/8) + y*screenW/8] &= 255-Math.pow(2,x%8);
}
 
//********************
//The variables we will be using
//to keep track of our points
//********************
 
var pointX = 0;
var pointY = 0;
 
var pointX2 = 0;
var pointY2 = 0;
 
var angle = 0;
var angle2 = 0;
 
var rad = Math.PI / 180.0;
 
//********************
//our function to assemble
//what will be shown to the user
//********************
function draw()
{
 //set entire screen to black
 ClearScreen();
 
 //put the first pixel on the screen
 PutPixel(pointX,pointY,1);
 
 //update the point
 pointX+=Math.cos(angle*rad);
 pointY+=Math.sin(angle*rad);
 
 angle++;
 
 //check the point to make
 //sure it's not out of range
 angle %=360;
 if(pointX<0)
  pointX=screenW-pointX;
 if(pointY<0)
  pointY=screenH-pointY;
 if(pointX>screenW)
  pointX=pointX-screenW;
 if(pointY>screenH)
  pointY=pointY-screenH;
 
 //put the second pixel on the screen
 PutPixel(pointX2,pointY2,1);
 angle2+=10;
 pointX2=Math.cos(angle2*rad)*50+pointX;
 pointY2=Math.sin(angle2*rad)*50+pointY;
 angle2 %=360;
 
 
}

In the prior lesson we just filled the entire screen with random information. In this lesson we’re going to be moving two pixels around using some basic trig. As a result we need to implement a function that will clear the screen. We could simply set the old pixel positions to black and then draw the new pixel positions. This would seem to be faster. But, due to the math required to plot a pixel there’s not a signifant gain and it won’t be long before there are too many pixels being drawn to utilize this method. So we’re not going to stick with clearing the whole screen right from the beginning.

1
2
3
4
5
6
 function ClearScreen()
 {
  max=screenW/8 * screenH;
  for(x=0;x<max;x++)
   screen[x]=0;
 }

In for loops, the conditional part of the function is evaluated every loop. This is why we precalculate the number of elements in the array. Divides are computationally expensive and if we didn’t precalculate, we’d be dividing and getting the same result 2400 times. Setting an array position to 0 is significantly faster than plotting a pixel. And since we’re working with monocrome, setting one element to 0 actually sets eight pixels to black.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 function PutPixel(x,y,c)
 {
  //first thing is to make sure the
  //pixel is within range of the screen
  if(x<0 || x>screenW)
   return;
  if(y<0 || y>screenH)
   return;
 
  //make sure x and y are integers
  x=Math.floor(x);
  y=Math.floor(y);
 
  if(c>0) // set the pixel bit to 1
   screen[Math.floor(x/8) + y*screenW/8] |= Math.pow(2,x%8);
  else // set the pixel bit to 0
   screen[Math.floor(x/8) + y*screenW/8] &= 255-Math.pow(2,x%8);
 }

The function PutPixel takes three values. X is the horizontal position of the pixel, Y is the veritcal position of the pixel and C is the color of the pixel. There are then three sections to this function. The first section verifies that the X and Y values fall in the visible range of the screen. If they don’t then the function simply returns and the screen buffer isn’t changed.

The next section of code removes the decimal portion of the X and Y values. Since a screen cannot actually display fractional pixels we just ignore the fractional part. Math.floor is the JavaScript function to handle this operation. For example, if X is 5.9 then Math.floor(X) is equal to 5. If we were dealing with more colors we could use the fractional value to blend neighboring pixels. This is called “anti-aliasing.” It will be several lessons before we come back to that concept.

The final section of code for the PutPixel routine actually stores the pixel in the screen buffer. The first step is to find the section of 8 pixels that this pixel falls into.

1
 Math.floor(x/8) + y*screenW/8

We’re again using the floor function so that if for example we’re placing a pixel Y is 0 and X is 63 then 63/8 = 7.875. We can’t have a fractional position in an array so the floor value is 7. The second part of the equation finds the exact pixel we’re setting.

1
 Math.pow(2,x%8)

63 mod 8 is equal to 7. This means we’re setting the 7th pixel in the 7th block of 8 pixels on the 0th row. The value of C determines how the second part is applied to the first part. We’re using binary operations since we’re working with bits. | is the binary OR and & is the binary AND. Rather than forcing the values 0 and 1 and doing an extra check we assume if C is 0 or less then the user is setting the pixel to black and if it’s greater than 0 they want to set it to white.

1
2
3
4
5
6
7
8
9
10
11
12
13
 Truth Table for AND
 
 1 &  0 = 0
 1 & 1 = 1
 0 & 0 = 0
 0 & 1 = 0
 
 Truth Table for OR
 
 1 |  0 = 1
 1 | 1 = 1
 0 | 0 = 0
 0 | 1 = 1

Let’s assume the current block of 8 contains the values

1
2
 0 1 2 3 4 5 6 7
 1 0 1 0 1 0 1 0

and we want to set the 7th position to 1. We check the truth table and see that the only way to ensure that the final value is 1 is to OR the last position with the value 1.

Math.pow(2,x%8) where x%8 is 7 equals 128. 128 in binary is

1
2
 0 1 2 3 4 5 6 7
 0 0 0 0 0 0 0 1

Zeros don’t affect the original values when ORing. So if we OR 10101010 with 0000001 the result is 10101011 which is what we wanted.

If the 7th position were already 1 then it would simply remain 1 as seen by the truth table.

Now, let’s set that same pixel back to 0. We can’t OR a non zero value to zero. So what we need to do is AND our way back to a zero. If we were to AND 10101011 with 00000001 the result would be 00000001. When ANDing, 1s have no effect on the original values. This is the exact inverse of OR. So what we do is invert the binary values so we get 11111110. This is accomplished easily.

255-Math.pow(2,x%8)

255 – 128 = 127 = 11111110 in binary. We then perform the operation 10101011 & 11111110 and get 10101010 which is what we wanted.

Understanding how binary numbers work is important when programming. If we couldn’t perform binary operations it would either be necessary to enlarge the screen buffer array by a multiple of 8 and waste a lot of memory or do a lot of expensive math to decode the starting value into it’s component values and then reassemble it back to a single value. It’s best to learn the concepts of binary numbers doing something relativly simple than later for a more complicated algorithm. It’s just like phonics. You need to learn how the individual parts of the word work before you can understand the word. You need to learn how the basic components of the algorithm work before you can understand the algorithm as a whole.

Now that we can put pixels on the screen it’s time to do something a little more interesting with them. In this lesson we have 2 pixels moving around. The first uses the following math:

1
2
3
 pointX+=Math.cos(angle*rad);
 pointY+=Math.sin(angle*rad);
 angle++;

rad is predefined as PI/180.0. Any time you have a value that requires calculation but doesn’t change it’s best to precalculate it and store it somewhere that the program has access to. With JavaScript the option is to make it a global. That entails defining the value outside of a function and then simply not changing it after it’s been calculated.

The net result of this simple math is that we move in the direction of angle where 0 is directly right, 90 is directly down(remember that SIN(90) is positive 1 and Y increases down the screen), 180 is directly left and 270 is directly up(remember that SIN(270) is -1 and Y decreases going up the screen). This forumla is used to move characters around the playing field.

1
2
3
4
 angle2+=10;
 pointX2=Math.cos(angle2*rad)*50+pointX;
 pointY2=Math.sin(angle2*rad)*50+pointY;
 angle2 %=360;

Using these basic functions we now have a second point rotating around the first point with a radius of 50 pixels. The angle for this point changes 10 degrees every frame. With angles, COS relates to the horizontal position and SIN relates to the vertical position. If you’re getting some wierd results with rotations check your SIN and COS. It’s easy to get them mixed up if you’re not familiar with their function.

You’ll also notice that the pixel rotating around the other disappears when it goes outside the visible area rather than wrapping around like the first pixel. Because pixels will inevitably get out of range it’s not a good idea to build wrapping into the core pixel plotting function. It’s most often not the desired result of going off the screen.

  1. No comments yet.
(will not be published)


eight − 4 =

  1. No trackbacks yet.