-1

Here are the things I know:

(1) The ship object is getting rendered to some degree, because I put an extra circle in its render code and that's being drawn.

(2) There's nothing wrong (at least, nothing obvious to me) with my rotation methods, as the numbers show up correctly in the console. And the points top.x, top.y, bottomLeft.x, bottomLeft.y, bottomRight.x, and bottomRight.yare all the numbers they should be.

(3) The actual code using the variables mentioned in (2) to draw the ship is working OK. When I replace the variables by hard-coded numbers, the ship is drawn. So what gives?

The faulty code is in the Ship object at line 140 and you can test it yourself here: http://noetherherenorthere.com/practice/landscape.html.

  1 /* landscape.js */
  2 
  3 var canvas;
  4 var context;
  5 var landscape;
  6 var ship;
  7 
  8 function init(){
  9 
 10     canvas = document.getElementById('canvas');
 11     context = canvas.getContext('2d');
 12     landscape = new Landscape();
 13     ship = new Ship(375, 400);
 14     animate();
 15     
 16     window.addEventListener('keydown', function(e){
 17         switch(e.keyCode){
 18             case 37: // left
 19                 ship.theta -= 0.1;
 20                 ship.theta %= 2*Math.PI;
 21                 break;
 22             case 38: // up
 23                 ship.v_x += 1*Math.cos(ship.theta);
 24                 ship.v_y -= 1*Math.sin(ship.theta);
 25                 break;
 26             case 39: // right
 27                 ship.theta += 0.1;
 28                 ship.theta %= 2*Math.PI;
 29                 break;
 30             case 40: // down
 31                 // do nothing
 32                 break;
 33             default:
 34         }
 35     });
 36     
 37 }
 38 
 39 function animate(){
 40     
 41     if(this.i == null){
 42         this.i = 0;
 43     } else{
 44         this.i = (this.i + 0.1)%628; // switch it out after 100*2*PI cycles so i doesn't get too big.
 45     }
 46     
 47     landscape.sun.y += 10*Math.sin(this.i);
 48     landscape.moon.y -= 10*Math.cos(this.i);
 49     ship.v = Math.sqrt(Math.pow(ship.v_x, 2) + Math.pow(ship.v_y, 2)); 
 50     ship.x += (0.1 * ship.v_x);
 51     ship.y += (0.1 * ship.v_y);
 52     if(ship.x < 0){
 53         ship.x = canvas.width;
 54     }
 55     if(ship.x > canvas.width){
 56         ship.x = 0;
 57     }
 58     if(ship.y < 0){
 59         ship.y = canvas.height;
 60     }
 61     if(ship.y > canvas.height){
 62         ship.y = 0;
 63     }
 64     if(ship.v > ship.vMax){
 65         ship.v = ship.vMax;
 66     }
 67     if(ship.v < -ship.vMax){
 68         ship.v = -ship.vMax;
 69     }
 70     
 71     context.clearRect(0, 0, canvas.width, canvas.height);
 72     
 73     landscape.render();
 74     
 75     ship.render();
 76     
 77     // draw the dialog box  
 78     context.font = "14px Verdana";
 79     context.fillStyle = 'DodgerBlue';
 80     context.fillText("Velocity: (x: " + this.ship.v_x.toFixed(2) + ", y: " + this.ship.v_y.toFixed(2) + 
 81         ", angle: " + radToDeg(this.ship.theta).toFixed(2) + ")", 420, 465);
 82     context.fillText("Position: (x: " + this.ship.x.toFixed(2) + ", y: " + this.ship.y.toFixed(2) + ")", 420, 485);
 83     
 84     window.setTimeout(animate, 40);
 85     
 86 }
 87 
 88 function radToDeg(radians){
 89 
 90     return radians*(180/Math.PI);
 91     
 92 }
 93 
 94 function degToRad(degrees){
 95     
 96     return degrees*(Math.PI/180);
 97     
 98 }
 99 
100 function Landscape(){
101 
102     // order of the elements matters here
103 
104     this.sky = new Sky();
105     this.sun = new Sun(600, 150, 50);
106     this.moon = new Moon(100, 100, 50);
107     
108     this.render = function(){
109     
110         for(var element in this){
111             if(this[element].hasOwnProperty("render")){
112                 this[element].render();
113             }
114         }
115     
116     }
117 }
118 
119 function Ship(x, y){
120     
121     this.x = x;
122     this.y = y;
123     this.r = 10;
124     this.v = 0; // initial velocity of zero
125     this.v_x = 0;
126     this.v_y = 0;
127     this.theta = Math.PI/2; // starts out pointing upwards
128     this.vMax = 200;
129     this.render = function(){
130         var top = new Vector(this.x, this.y);
131         var bottomLeft = new Vector(this.x - 10, this.y + 30);
132         var bottomRight = new Vector(this.x + 10, this.y + 30);
133         
134         top.rotate(this.theta, true);
135         bottomLeft.rotate(this.theta, true);
136         bottomRight.rotate(this.theta, true);
137         
138         // console.log("top: " + top + ", bottomLeft: " + bottomLeft + ", bottomRight: " + bottomRight);
139         
140         context.fillStyle = 'SlateGray';
141         context.beginPath();
142         context.moveTo(top.x, top.y);
143         context.lineTo(bottomLeft.x, bottomLeft.y);
144         context.lineTo(bottomRight.x, bottomRight.y);
145         context.closePath();
146         context.fill();
147         
148         // this code works even though the code above doesn't.
149         // context.fillStyle = 'SlateGray';
150 //          context.beginPath();
151 //          context.moveTo(this.x, this.y);
152 //          context.lineTo(this.x - 10, this.y + 30);
153 //          context.lineTo(this.x + 10, this.y + 30);
154 //          context.closePath();
155 //          context.fill();
156         
157         context.fillStyle = 'LightGreen';
158         context.beginPath();
159         context.arc(200, 200, 30, 0, 2*Math.PI);
160         context.closePath();
161         context.fill();
162     }
163     
164     function Vector(x, y){
165         
166         this.x = x;
167         this.y = y;
168         this.rotate = function(theta, round){
169             
170             var rotationMatrix = new Matrix(2, 2, Math.cos(theta), -Math.sin(theta), Math.sin(theta), Math.cos(theta));
171             var vector = new Matrix(2, 1, this.x, this.y);
172             var resultVector = Matrix.multiply(rotationMatrix, vector);
173             this.x = resultVector[0][0];
174             this.y = resultVector[1][0];
175             if(round){
176                 this.x = Math.floor(this.x);
177                 this.y = Math.floor(this.y);
178             }
179             
180             return this;
181             
182         }
183         
184         this.toString = function(){
185             
186             return "x: " + x + ", y: " + y;
187             
188         }
189         
190     }
191     
192     function Matrix(rows, cols /*, var args */){
193     
194         // constructor
195         
196         if(rows == null || cols == null){
197             throw new Error("null rows or cols argument");
198         } else if(!isPositiveInteger(rows) || !isPositiveInteger(cols)){
199             throw new Error("rows and cols must be whole numbers");
200         } else if(rows > 1000 || cols > 1000){
201             throw new Error("rows and cols must be < 1000 in size");
202         }
203         
204         this.numRows = rows;
205         this.numCols = cols;
206         if(arguments.length - 2 > rows*cols){
207             throw new Error("too many arguments to Matrix constructor");
208         } else if(arguments.length > 2 && arguments.length - 2 < rows*cols){
209             throw new Error("too few arguments to Matrix constructor for initializing Matrix." + 
210                 " Usage: rows, cols [, row-major list of row and col entries]");
211         }
212         
213         if(rows === undefined){
214             console.log(arguments);
215         }
216         
217         for(var a = 0; a < rows; a++){
218             this[a] = new Array();
219         }
220         
221         for(var a = 2; a < arguments.length; a++){
222             try{
223                 var row = Math.floor((a - 2)/rows);
224                 var col = Math.floor((a - 2)%rows);
225                 this[row][col] = arguments[a];
226             } catch(e){
227                 console.log(row + ", " + col);
228                 throw e;
229             }
230         }   
231         
232     }
233     
234     Matrix.multiply = function(matrixA, matrixB){
235         
236         if(matrixA.numCols != matrixB.numRows){
237             throw new Error("# of cols in first matrix must equal # of rows in second matrix");
238         }
239     
240         var resultMatrix = new Matrix(matrixA.numRows, matrixB.numCols);
241         var sum, i, j, k;
242         
243         for(i = 0; i < matrixA.numRows; i++){
244             for(j = 0; j < matrixB.numCols; j++){
245                 sum = 0;
246                 for(k = 0; k < matrixA.numCols; k++){
247                     sum += matrixA[i][k] * matrixB[k][j];
248                 }
249                 resultMatrix[i][j] = sum;
250             }
251         }
252         
253         return resultMatrix;
254     }
255     
256     function isPositiveInteger(n){
257     
258         if(n == null){ return false; }
259         if(typeof n != "number"){ return false; }
260         if(!isFinite(n)){ return false; }
261         if(n <= 0){ return false; }
262         if(n%1 !== 0){ return false; }
263         return true;
264     
265     }
266                         
267 }
268 
269 function Sky(){
270 
271     this.x = 0;
272     this.y = 0;
273     this.width = canvas.width;
274     this.height = canvas.height;
275     this.render = function(){
276         context.fillStyle = 'Black'; // previously Indigo
277         context.fillRect(this.x, this.y, this.width, this.height);
278     }
279     
280 }
281 
282 function Sun(x, y, radius){
283 
284     this.x = x;
285     this.y = y;
286     this.r = radius;
287     this.render = function(){
288         context.fillStyle = 'Gold';
289         context.beginPath();
290         context.arc(this.x, this.y, this.r, 0, 2*Math.PI);
291         context.closePath();
292         context.fill();
293     }
294     
295 }
296 
297 function Moon(x, y, radius){
298 
299     this.x = x;
300     this.y = y;
301     this.r = radius;
302     this.render = function(){
303         context.fillStyle = 'LightYellow';
304         context.beginPath();
305         context.arc(this.x, this.y, this.r, 0, 2*Math.PI);
306         context.closePath();
307         context.fill();
308     }
309     
310 }
brasofilo
  • 25,496
  • 15
  • 91
  • 179
Chris Middleton
  • 5,654
  • 5
  • 31
  • 68
  • Your `Matrix.multiply` returns `(NaN, NaN)` as the resulting vector. – Nikola Dimitroff Dec 18 '13 at 13:09
  • Hmm... when I uncomment the console.log print on line 138, I see the correct values for all the `top`, `bottomLeft`, and `bottomRight` vectors. So that would seem to imply that the `rotate` function is working properly, no? Why do you think it's returning `(NaN, NaN)`? – Chris Middleton Dec 18 '13 at 13:22
  • You should use firebug or comment out pieces of the function to see what it's doing piece by piece – Zach Saucier Dec 18 '13 at 14:17
  • The crazy thing is that I've been doing that and it makes no sense to me why it isn't working properly. As I noted in the question, the numbers for the boundaries of the ship are what they should be, other code in the render function gets rendered properly, and if I replace the rotated-points code with the original non-rotated points code, it works fine. No errors either. It seems logically impossible that it isn't working, so I don't know what I'm missing. – Chris Middleton Dec 18 '13 at 14:42
  • @NikolaDimitroff Thank you! I finally got the result you were talking about - the matrix is indeed just returning NaN. I tricked myself into thinking it was working because I had `return "x: " + x + ", y: " + y;` as my toString method for the Point class, where it should have been `this.x` and `this.y`. (See lines 184-186.) Now I have something to go on. – Chris Middleton Dec 18 '13 at 15:00

1 Answers1

0

EDIT 12/19/13 5:02 PM: Updated with proper wraparound in case ship goes off screen and reference list.

The main issue turns out to be that I was rotating around the origin, when I should have been rotating the ship around its center. So I was essentially throwing it off screen, which is why it wasn't showing up. I found this out after scrapping my Matrix class and opting for the much simpler canvas.rotate and canvas.translate methods to do the job:

var top = new Point(this.x, this.y);
var bottomLeft = new Point(this.x - 10, this.y + 30);
var bottomRight = new Point(this.x + 10, this.y + 30);
var center = new Point((top.x + bottomLeft.x + bottomRight.x)/3, (top.y + bottomLeft.y + bottomRight.y)/3);

context.save();
context.translate(center.x, center.y);
context.rotate(-this.theta);
context.translate(-center.x, -center.y);

context.fillStyle = 'SlateGray';
context.beginPath();
context.moveTo(top.x, top.y);
context.lineTo(bottomLeft.x, bottomLeft.y);
context.lineTo(bottomRight.x, bottomRight.y);
context.closePath();
context.fill();

context.restore();

Also, rather than rotating inside the keydown event, I made booleans for ship.rotatingLeft and ship.rotatingRight. On keydown, ship.rotatingLeft becomes true, and on keyup it becomes false. The animate() method does the rotating if and only if the variable is true. Because the animation seems to happen more frequently (every 40 ms) than the keydown event firing, the ship is able to turn quickly, but also smoothly. (Before, when it turned, it either turned like a tortoise or would jump from one position to the next.)

Here's the complete code. It's working at the same link mentioned in the question. (I tried to make a JSFiddle, but couldn't get it to work properly.)

  1 /* landscape.js */
  2 
  3 var canvas;
  4 var context;
  5 var landscape;
  6 var ship;
  7 
  8 function init(){
  9 
 10     canvas = document.getElementById('canvas');
 11     context = canvas.getContext('2d');
 12     landscape = new Landscape();
 13     ship = new Ship(375, 400);
 14     animate();
 15     
 16     window.addEventListener('keydown', function(e){
 17         switch(e.keyCode){
 18             case 37: // left
 19                 ship.rotatingLeft = true;
 20                 break;
 21             case 38: // up
 22                 ship.thrusting = true;
 23                 break;
 24             case 39: // right
 25                 ship.rotatingRight = true;
 26                 break;
 27             case 40: // down
 28                 // do nothing
 29                 break;
 30             default:
 31         }
 32     });
 33     
 34     window.addEventListener('keyup', function(e){
 35         switch(e.keyCode){
 36             case 37: // left
 37                 ship.rotatingLeft = false;
 38                 break;
 39             case 38: // up
 40                 ship.thrusting = false;
 41             case 39: // right
 42                 ship.rotatingRight = false;
 43                 break;
 44             default:
 45         }
 46     });
 47     
 48 }
 49 
 50 function animate(){
 51     
 52     if(this.i == null){
 53         this.i = 0;
 54     } else{
 55         this.i = (this.i + 0.1)%628; // switch it out after 100*2*PI cycles so i doesn't get too big.
 56     }
 57     
 58     // move the sun and the moon in a periodic fashion
 59     
 60     landscape.sun.y += 10*Math.sin(this.i);
 61     landscape.moon.y -= 10*Math.cos(this.i);
 62     
 63     
 64     // if ship gets off screen, put it back at the same spot on the other side
 65     // not a proper wraparound
 66     
 67     if(ship.x < (0 - ship.maxBreadth) || ship.x > (canvas.width + ship.maxBreadth)){
 68         ship.x = mod(ship.x, canvas.width);
 69     }
 70     
 71     if(ship.y < (0 - ship.maxBreadth) || ship.y > (canvas.height + ship.maxBreadth)){
 72         ship.y = mod(ship.y, canvas.height);
 73     }
 74     
 75     // thrusting
 76     
 77     ship.v = Math.sqrt(Math.pow(ship.v_x, 2) + Math.pow(ship.v_y, 2)); 
 78     
 79     if(ship.thrusting && ship.v < ship.vMax){
 80         ship.v_x += 1*Math.cos(ship.theta + Math.PI/2);
 81         ship.v_y += 1*Math.sin(ship.theta + Math.PI/2);
 82     }
 83     
 84     ship.x += (0.1 * ship.v_x);
 85     ship.y -= (0.1 * ship.v_y);
 86     
 87     // rotation
 88     
 89     if(ship.rotatingLeft){
 90         ship.theta += 0.04;
 91         ship.theta %= 2*Math.PI;
 92     }
 93     
 94     if(ship.rotatingRight){
 95         ship.theta -= 0.04;
 96         ship.theta %= 2*Math.PI;
 97     }
 98     
 99     context.clearRect(0, 0, canvas.width, canvas.height);
100     
101     landscape.render();
102     
103     ship.render();
104     
105     // draw the velocity and position indicators
106         
107     context.font = "14px Verdana";
108     context.fillStyle = 'DodgerBlue';
109     context.fillText("Velocity: (x: " + this.ship.v_x.toFixed(2) + ", y: " + this.ship.v_y.toFixed(2) + 
110         ", angle: " + radToDeg(this.ship.theta).toFixed(2) + ")", 420, 465);
111     context.fillText("Position: (x: " + this.ship.x.toFixed(2) + ", y: " + this.ship.y.toFixed(2) + ")", 420, 485);
112     
113     window.setTimeout(animate, 40);
114     
115 }
116 
117 function radToDeg(radians){
118 
119     return radians*(180/Math.PI);
120     
121 }
122 
123 function degToRad(degrees){
124     
125     return degrees*(Math.PI/180);
126     
127 }
128 
129 function Landscape(){
130 
131     // order of the elements matters here
132 
133     this.sky = new Sky();
134     this.sun = new Sun(600, 150, 50);
135     this.moon = new Moon(100, 100, 50);
136     
137     this.render = function(){
138     
139         for(var element in this){
140             if(this[element].hasOwnProperty("render")){
141                 this[element].render();
142             }
143         }
144     
145     };
146 }
147 
148 function Ship(x, y){
149     
150     this.x = x;
151     this.y = y;
152     this.r = 10;
153     this.v = 0; // initial velocity of zero
154     this.v_x = 0;
155     this.v_y = 0;
156     this.theta = 0; // starts out pointing upwards
157     this.vMax = 200;
158     this.maxBreadth = 32; // approximate
159     this.rotatingLeft = false;
160     this.rotatingRight = false;
161     this.thrusting = false;
162     
163     this.render = function(){
164         this._render(this.x, this.y);
165         
166         // drawing a second time for wraparound
167         
168         if((ship.x < 0) || (ship.x > canvas.width) || (ship.y < 0) || (ship.y > canvas.height)){
169             this._render(mod(this.x, canvas.width), mod(this.y, canvas.height));
170         }
171     };
172     
173     this._render = function(x, y){ // internal render function
174         
175         var top = new Point(x, y);
176         var bottomLeft = new Point(x - 10, y + 30);
177         var bottomRight = new Point(x + 10, y + 30);
178         var center = new Point((top.x + bottomLeft.x + bottomRight.x)/3, (top.y + bottomLeft.y + bottomRight.y)/3);
179         
180         context.save();
181         context.translate(center.x, center.y);
182         context.rotate(-this.theta);
183         context.translate(-center.x, -center.y);
184         
185         context.fillStyle = 'SlateGray';
186         context.beginPath();
187         context.moveTo(top.x, top.y);
188         context.lineTo(bottomLeft.x, bottomLeft.y);
189         context.lineTo(bottomRight.x, bottomRight.y);
190         context.closePath();
191         context.fill();
192         
193         context.restore();
194         
195     };  
196                         
197 }
198 
199 function Point(x, y){
200     
201     this.x = x;
202     this.y = y;
203     this.rotate = function(theta, round){
204     
205         this.x = Math.cos(theta)*this.x - Math.sin(theta)*this.y;
206         this.y = Math.sin(theta)*this.x + Math.cos(theta)*this.y;
207         
208         if(round){
209             
210             this.x = Math.round(this.x);
211             this.y = Math.round(this.y);
212             
213         }
214         
215         return this; // for method chaining
216         
217     };
218     
219     this.toString = function(){
220         
221         return "x: " + this.x + ", y: " + this.y;
222         
223     };
224     
225 }
226 
227 function isPositiveInteger(n){
228 
229     if(n == null){ return false; }
230     if(typeof n != "number"){ return false; }
231     if(!isFinite(n)){ return false; }
232     if(n <= 0){ return false; }
233     if(n%1 !== 0){ return false; }
234     return true;
235 
236 }
237 
238 function Sky(){
239 
240     this.x = 0;
241     this.y = 0;
242     this.width = canvas.width;
243     this.height = canvas.height;
244     this.render = function(){
245         context.fillStyle = 'Black'; // previously Indigo
246         context.fillRect(this.x, this.y, this.width, this.height);
247     };
248     
249 }
250 
251 function Sun(x, y, radius){
252 
253     this.x = x;
254     this.y = y;
255     this.r = radius;
256     this.render = function(){
257         context.fillStyle = 'Gold';
258         context.beginPath();
259         context.arc(this.x, this.y, this.r, 0, 2*Math.PI);
260         context.closePath();
261         context.fill();
262     };
263     
264 }
265 
266 function Moon(x, y, radius){
267 
268     this.x = x;
269     this.y = y;
270     this.r = radius;
271     this.render = function(){
272         context.fillStyle = 'LightYellow';
273         context.beginPath();
274         context.arc(this.x, this.y, this.r, 0, 2*Math.PI);
275         context.closePath();
276         context.fill();
277     };
278     
279 }
280 
281 function mod(x, y) {
282         return ((x % y) + y) % y;
283 }

References:

  1. rotation around a fixed point: using HTML5 Canvas - rotate image about arbitrary point
  2. modulo operator: Javascript modulo not behaving
  3. wraparound: Wraparound for HTML5 Canvas: How to Get (Static) Shapes to Continue around Edges of Canvas
  4. formula for center of the ship: http://en.wikipedia.org/wiki/Centroid
Community
  • 1
  • 1
Chris Middleton
  • 5,654
  • 5
  • 31
  • 68