JavaScript iteration - javascript

I want to use JavaScript to draw a series of images onto an HTML5 canvas. I have the following while loop, which I had hoped would draw all of the images to the canvas, however, it is currently only drawing the first one:
function drawLevelOneElements(){
/*First, clear the canvas */
context.clearRect(0, 0, myGameCanvas.width, myGameCanvas.height);
/*This line clears all of the elements that were previously drawn on the canvas. */
/*Then redraw the game elements */
drawGameElements();
/*Draw the elements needed for level 1 (26/04/2012) */
var fileName = 1;
var imagePositionX = 20;
var imagePositionY = 30;
while(fileName < 11){
/*Create an array of images here, move to next element of array on each iteration */
var numbers = new Array();
numbers[0] = "1.png"
numbers[1] = "2.png"
numbers[3] = "3.png"
numbers[4] = "4.png"
numbers[5] = "5.png"
image.src = fileName+".png";
image.src = numbers[0];
image.onload = function(){
context.drawImage(image, imagePositionX, imagePositionY, 50, 50);
}
fileName = fileName+1;
imageY = imageY+20;
console.dir(fileName); /* displays in the console- helpful for debugging */
}
To talk through what I had hoped this function would do:
Load each of the images into a different element of the array (so 1.png would be in numbers[0], 2.png in numbers[1], etc. )
It would then take the global variable 'image', and assign its source to the contents of numbers[0]
Then draw that image at the specified position on the canvas.
Then increment the value of the variable fileName by 1, giving it a value of '2'
Next it would increment the value of the Y co-ordinate where it will draw the image on the canvas by 20- moving the position of the image to be drawn down by 20 pixels
After that it would go back to the start of the loop and draw the next image (2.png) on the canvas in a position that is 20 pixels below the position of the first image that was drawn.
It should continue doing this while the value of the variable 'fileName' is less than 11, i.e. it should draw 10 images each new one below the last one that was drawn.
However, for some reason, my function only draws the first image. Could someone point out what I'm doing wrong, and how I could correct this?
Thanks very much.

Edited and commented some points of your code.
The most effective change was at imageY = imageY+20; that was edited to use imagePositionY variable.
function drawLevelOneElements() {
/*First, clear the canvas */
context.clearRect(0, 0, myGameCanvas.width, myGameCanvas.height);
/*This line clears all of the elements that were previously drawn on the canvas. */
/*Then redraw the game elements */
drawGameElements();
/*Draw the elements needed for level 1 (26/04/2012) */
var fileName = 1;
var imagePositionX = 20;
var imagePositionY = 30;
while(fileName < 11){
/*Create an array of images here, move to next element of array on each iteration */
var numbers = new Array();
/* what is not used is not necessary :)
numbers[0] = "1.png"
numbers[1] = "2.png"
numbers[3] = "3.png"
numbers[4] = "4.png"
numbers[5] = "5.png"*/
image.src = fileName+".png";
// image.src = numbers[0];
image.onload = function(){
context.drawImage(image, imagePositionX, imagePositionY, 50, 50);
}
fileName = fileName+1;
imagePositionY = imagePositionY+20; //before: imageY = imageY+20;
console.dir(fileName); /* displays in the console- helpful for debugging */
}

If you take the drawImg stuff and shove it in its own function you can clean this up a bit :) Now we've yanked the async stuff out of the loop, so the image variable doesn't get over-written each time you loop. You're also using a for loop now, which to me is clearer to understand.
function drawLevelOneElements() {
// your stuff
for (var i = 0; i > 5; i++) {
drawImg(ctx, i, x, y);
// update x or y and whatever else
}
}
// put all your image drawing stuff here
function drawImg(ctx, i, x, y) {
var img = new Image();
img.src = i + ".png";
img.onload = function(){
ctx.drawImage(img, x, y, 50, 50);
}
}

Related

TypeError: Cannot Read Property of Undefined Even Though I Defined It

I'm a beginner to JavaScript. I've looked through a few questions on here regarding "TypeError: ... undefined even though the property is defined" but their examples are either too dense for me to grasp or they realize they've incorrectly assigned something downstream. Here I think my example is quite simple, and I don't think I've incorrectly assigned anything.
I've defined position in my class MoveableObjects and when I call player = new MoveableObject(position, other params ...) I don't get any errors.
When I change this to player = new Sprite(position, other params ...) where Sprite extends MoveableObjects and then try to call a function inside Sprite it tells me position.x is undefined. I put console.log(player) immediately after the declaration and it does in fact show position as undefined, but I don't understand why it does since I've clearly defined it.
Here is the constructor of MoveableObjects
class MoveableObject {
constructor({ colorRGB, width, height, position, velocity}) {
this.width = width;
this.height = height;
this.position = position;
this.velocity = velocity;
this.colorRGB = colorRGB;
// If the side of the object is touching or beyond the side of the canvas it is flagged as bounded.
this.inUpLimit = false;
this.inDownLimit = false;
this.inLeftLimit = false;
this.inRightLimit = false;
// Translate the velocity into directional words
this.movingLeft = false;
this.movingRight = false;
this.movingUp = false;
this.movingDown = false;
}
Here is the constructor of Sprite and the draw() function it uses.
// Sprite represents moveable objects with an animated image.
class Sprite extends MoveableObject {
constructor({ imageSrc, numRows = 1, numCols = 1, spacing = 0, margin = 0, animationSpeed = 10, position, velocity}) {
super(position, velocity)
this.image = new Image() // Image can be a single image or a spritesheet.
this.image.src = imageSrc
this.numRows = numRows // # animation sequences in the spritesheet
this.numCols = numCols // # frames in an animation sequence
this.spacing = spacing // # pixels between each frame
this.margin = margin // # pixels between image border and sprite border. Vertical and horizontal margins should be equal.
this.animation = new Object()
this.animation.speed = animationSpeed // # times draw function is called before the next frame in the animation sequence is called.
this.animation.counter = 0 // # times the draw function has been called
this.animation.enabled = false // Whether or not the animation sequence is currently running
this.animation.row = 1; // The row of the current frame being drawn
this.animation.col = 1; // The column of the current frame being drawn
this.animation.firstRow = 1; // The row of the frame to initialize the animation loop on
this.animation.firstCol = 1; // The column of the frame to initialize the animation loop on
this.animation.lastRow = 1; // The row of the frame to restart the animation loop on
this.animation.lastCol = 1; // The column of the frame to restart the animation loop on
this.frame = new Object();
this.frame.width = 0; // Init image.onload
this.frame.height = 0; // Init image.onload
this.image.onload = () => { // Calculates width and height of animation frame based on numRows, numColumns, spacing, and margin
let imageWidth = this.image.width;
let spriteSheetWidth = imageWidth - (2 * this.margin);
let widthNoSpacing = spriteSheetWidth - this.spacing * (this.numCols - 1);
let frameWidth = widthNoSpacing / this.numCols;
let imageHeight = this.image.height;
let spriteSheetHeight = imageHeight - (2 * this.margin);
let heightNoSpacing = spriteSheetHeight - this.spacing * (this.numRows - 1);
let frameHeight = heightNoSpacing / numRows;
this.frame.width = frameWidth;
this.frame.height = frameHeight;
}
}
draw() {
// Draw the frame at the current row and column.
context.drawImage(
this.image, // the entire image being loaded
this.animation.col * this.frame.width, // sx, x coordinate to begin crop
this.animation.row * this.frame.height, // sy, y coordinate to begin crop
this.frame.width, // swidth, width of cropped image
this.frame.height, // sheight, height of cropped image
this.position.x, // x coordinate where to place image on canvas
this.position.y, // y coordinate where to place image on canvas
this.frame.width, // the width to stretch/shrink it to
this.frame.height // the height to stretch/shrink it to
)
Here I create a new MoveableObject with position defined and then console.log() it. It shows position to be what I set it to. I copy/paste that declaration and change "new MoveableObject" to "new Sprite" (with a new variable name of course) and print that, and it shows position is undefined:
// Initializers for main()
const gravity = 0.8;
const player = new Sprite({
imageSrc: './lib/img/template.png',
numRows: 21,
numCols: 4,
spacing: 0,
margin: 0,
position: {
x: 0,
y: 0
},
velocity: {
x: 0,
y: 0
}
})
console.log(player)
const enemy = new MoveableObject({
colorRGB: 'blue',
width: 50,
height: 150,
position: {
x: 150,
y: 50
},
velocity: {
x: 0,
y: 0
}
})
console.log(enemy)
const moveableObjects = [player, enemy];
// The main function of the script updates the game state every frame.
function main() {
// When the game starts the canvas, then player, then enemy are rendered.
context.fillStyle = 'black';
context.fillRect(0, 0, canvas.width, canvas.height);
player.draw();
enemy.draw();
The error is raised on player.draw() and reads "Uncaught TypeError: Cannot read properties of undefined (reading 'x')"
Does anyone understand why this is the case? It seems to me like the error is maybe produced by not properly extending my class or using super() incorrectly to pass attributes, but I haven't found an example that proves that.

Images not displaying the first time in this object program in JS

I am making a battleship game with polar coordinates. After the user chooses two points, a battleship should be drawn in the middle. My Battleship constructor looks like this:
function Battleship(size, location, source){
this.size = size;
//initializing the image
this.image = new Image();
this.image.src = source;
this.getMiddlePoint = function(){
//get midpoint of ship
...
}
this.distanceBetween = function(t1, t2){
//dist between two points
}
this.display = function(){
var point = [this.radius];
point.push(this.getMiddlePoint());
point = polarToReal(point[0], point[1] * Math.PI / 12);
//now point has canvas coordinates of midpoint
var width = this.distanceBetween(this.info[0][0], this.info[this.info.length-1][0]);
var ratio = this.image.width / width;
ctx.drawImage(this.image, point[0] - width/2, point[1] - this.image.height / ratio / 2, width, this.image.height / ratio);
//draws the image
}
}
The display method of each ship gets called at a certain point (after the user has chosen the location). For some reason, the images do not show the first time I do this, but when I run this code at the very end:
for(var i = 0; i<playerMap.ships.length; i++){
playerMap.ships[i].display();
}
All ships are displayed correctly (not aligned well, but they are displayed). I think there is a problem with loading the images. I am not sure how to fix this. I tried using image.onload but I never got that to work. I also tried something like this:
var loadImage = function (url, ctx) {
var img = new Image();
img.src = url
img.onload = function () {
ctx.drawImage(img, 0, 0);
}
}
but the same problem kept happening. Please help me fix this problem. Here is the game in its current condition. If you place ships, nothing happens, but after you place 5 (or 10) ships, they suddenly all load.
EDIT:
I solved the problem by globally defining the images. This is still very bad practice, since I wanted this to be in the battleship object. This is my (temporary) solution:
var sub = [];
for(var i = 1; i<5; i++){
sub[i] = new Image();
sub[i].src = "/img/ships/battleship_"+i+".png";
}

How to animate sprites on keypress on canvas?

How do I animate the character on keypress? I changed the sprite position which shows the next image, but how do I loop through the two images so that I can show the player running while the key is pressed.
I need the first and the second frame.
Keyevents:
if(keys[39]){
//right arrow
if (mario.velX < mario.speed){
mario.velX++;
if(!mario.jumping){
//mario sprite position
mario.frame = 0;
}
}
}
And the draw function
this.frame = 0;
var marioImg; //mario image
var that = this;
this.init = function() {
marioSprite = new Image();
marioSprite.src = 'images/mario-sprites.png';
}
this.draw = function(){
that.sX = that.width * that.frame;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(marioSprite, that.sX, that.sY, that.width, that.height, that.x, that.y, that.width, that.height);
}
Load the two images into and array
var imageArray = []; // array to hold images
var img = new Image(); // create and load first image
img.src = "imageOne.png";
imageArray.push(img); // put it in the array
img = new Image(); // same for image two
img.src = "imageTwo.png";
imageArray.push(img);
You will need some variables. One to control how long each image is displayed and another to hold which image is displayed. You can use the current time to keep it nice and even.
var millsecondsPerImage = 100; // each frame is 100 ms 1/10th of a second
var currentTime = new Date().valueOf(); // get the time in milliseconds
// Divide current time by how long to display for. Round down with floor
// then modulo the length of the image array
var imageToDraw = imageArray[Math.floor(currentTime / millsecondsPerImage) % imageArraylength];
// draw the current image image
ctx.drawImage(imageToDraw, posx, posy);
That will cycle any number of images, how ever many you put in the array.

combine array of images with javascript (with or without canvas)

I would like to create a strip of images and compose a new image, like image = [image0-image1-image2].
We'll use:
images = ['https://upload.wikimedia.org/wikipedia/commons/5/55/Al-Farabi.jpg',
'https://upload.wikimedia.org/wikipedia/commons/e/e1/FullMoon2010.jpg',
'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2c/3D_coordinate_system.svg/10000px-3D_coordinate_system.svg.png']
I would like to take external above, and make a collage.
I would like to do it in background.
I learnt that is possible to use a canvas element off the dom; for the sake of watching what I am doing, I will use a canvas element here.
// create an off-screen canvas using document.createElement('canvas')
// here I use a canvas in DOM cause I cannot find a way to displayed the final collage
var canvas = document.getElementById('canvas'),
context = canvas.getContext('2d');
// set its dimension to target size
canvas.width = 1200;
canvas.height = 630;
and found three different behaviors for what I think should give same result. Could you explain me why?
If I manually copy and paste in console code for each image, one at a timeenter code here`
var image = new Image();
// i is the i-th element in images
image.src = images[i];
image.onload = function() {
context.save();
context.drawImage(image, canvas.width * 0.3 * i, 0, canvas.width*0.3, canvas.height);
}
I can see the elements are positioned one aside of the other, like I would like to have.
But If I copy all of three pieces of code at once, either in a loop, I can see only the last image placed in all of the three different positions:
for (var i = images.length; i <= 0; i++) {
var image = new Image();
image.src = images[i];
image.onload = function(){
context.save();
context.drawImage(image, canvas.width*0.3 * i, 0, canvas.width*0.3, canvas.height);
}
}
So I thought, maybe it's a matter of using a callback after image is loaded - I tried the following but nothing happens: canvas stays empty.
// my callback
function addImage(image, position){
image.onload = function(){
context.save();
context.drawImage(image, canvas.width*0.3 * position, 0, canvas.width*0.3, canvas.height);
}
}
function loadImages (images, callback) {
for (var i = images.length-1; i >= 0; i--) {
var image = new Image();
image.src = images[i];
callback(image, i);
}
}
// canvas will stay empty:
loadImages(images, addImage);
Can you help in clarifying the differences in the three parts, and figure out how to combine an array of images in a single one?
Possibly in background, I want to then save the image and post it via ajax.
In your loop example, all the onload functions are sharing the same i and image variables from the loop. But the onload functions are callback functions that get called after the loop completes. Thus, all the onload functions are using the same i and image values from after the loop completed. You need to create a local scope such that each onload function has its own i and image values. For example...
for (var i = 0; i < images.length; i++) {
var image = new Image();
image.src = images[i];
image.onload = function(image, i) {
return function(){
context.drawImage(image, canvas.width*0.3 * i, 0, canvas.width*0.3, canvas.height);
}
}(image, i);
}

HTML5 canvas continuously stroking lines

I want to draw some continuously growing lines in HTML5 and Javascript. Here is what I want to do:
A point located at the center of my screen will have 3 lines growing (120 degree to each other) to a certain length, say 50 pix, then each of this 3 vertex will become a new center and have another 3 lines.
(I couldnt post images due to low reputation I have, hopefully you know what I mean abt the image here...)
I already written the function to have a array of all the points I need as the centers, starting from the center of my screen. I am thinking to write a loop over this array to draw the lines. I DO NOT want to directly use the stroke so that the line just appears on the screen. I want to have something like the the lines are drawn bit by bit (bad english here, please excuse my english) until it reaches the pre-defined length. However my code dont work quite well here, it only displays all the center points and only the last center point has the movement to have the 3 lines to grow...
I need to know the correct way to do this... many thanks in advance!
(please ignore the variable time or startTime in my code... )
<script>
window.requestAnimFrame = (function(callback) {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
var canvas = document.getElementById('myCanvas');
canvas.width= window.innerWidth;
canvas.height= window.innerHeight;
var context = canvas.getContext('2d');
var totalLength = 50;
var centreSet = new Array();
var counter = 0;
var centre = {
x: canvas.width / 2,
y: canvas.height / 2,
};
var myLine = {
length : 0,
color : 'grey',
lineWidth : 0.5,
};
function drawLine(centre, context, mylength) {
context.beginPath();
context.moveTo(centre.x, centre.y);
context.lineTo(centre.x, centre.y - mylength);
context.moveTo(centre.x, centre.y);
context.lineTo(centre.x - 0.866 * mylength, centre.y + mylength/2);
context.moveTo(centre.x, centre.y);
context.lineTo(centre.x + 0.866 * mylength, centre.y + mylength/2);
context.lineWidth = myLine.lineWidth;
context.strokeStyle = myLine.color;
context.closePath();
context.stroke();
}
function startAnimate(centre, canvas, context, startTime, mylength) {
// update
var time = (new Date()).getTime() - startTime;
var linearSpeed = 5;
// pixels / second
var newX = linearSpeed / 10;
if(mylength < totalLength) {
mylength = mylength + newX;
// clear
//context.clearRect(0, 0, canvas.width, canvas.height);
drawLine(centre, context, mylength);
// request new frame
requestAnimFrame(function() {
startAnimate(centre, canvas, context, startTime, mylength);
});
}
}
function animate(centre, canvas, context, startTime){
//create array to have all the center points
centreSet = getCentres();
for (var i = 0; i < centreSet.length; i++){
//pass the x and y values in a object for each center we have in the array
centre.x = str2x(centreSet[i]);
centre.y = str2y(centreSet[i]);
startAnimate(centre, canvas, context, startTime, 0);
}
}
setTimeout(function() {
var startTime = (new Date()).getTime();
animate(centre, canvas, context, startTime);
}, 1000);
I just edited your code, I added the following part:
var length = 0;
for(var i = 0; i < 380; i++){
window.setTimeout(function() {drawFrame(length);},16.67);
length = length + 0.25;
}
I expect the screen appears to draw the incremental lines bit by bit until it reaches the length I want. However, it seems like the whole incremental process is not shown and it only shows the finished drawing.
Can anyone tell me why?
Regarding your followup question about why your animation loop fails
By putting your setTimeout in a for-loop, each new setTimeout is cancelling the previous setTimeout.
So you’re just left with the very last setTimeout running to completion.
In an animation loop, you typically do 3 things during each "frame":
Change some data to reflect how the new frame is different from the previous frame.
Draw the frame.
Test if the animation is complete. If not, do another frame (go to #1).
The setTimeout function is used to do the last part of #3 (do another frame)
So setTimeout is really acting as your animation loop. --- Your for-loop is not needed.
This is how you would restructure your code to follow this pattern:
var length=0;
var maxLength=50;
function draw(){
// make the line .25 longer
length=length+.25;
// draw
drawFrame(length);
// test if the line is fully extended
// if not, call setTimeout again
// setTimeout(draw,100) will call this same draw() function in 100ms
if(length<maxLength){
setTimeout(draw,100);
}
}
[Edited: to include spawning of child objects after lines reach terminal distance]
In your code you were not spawning new center points when the lines reached their maximum extension.
I would suggest that each of your centre objects have at least this much information in order to spawn a new set of centre objects when their lines reach terminal length:
var newCentrePoint={
x:x,
y:y,
maxLength:newMaxLength,
growLength:growLength,
currentLength:0,
isActive:true
}
The x,y are the centerpoint’s coordinates.
maxLength is the maximum extension of the 3 lines before they are terminated.
growLength is the amount by which each line will grow in each new frame.
currentLength is the current length of the line.
isActive is a flag indicating if this point is growing lines (true) or if it’s terminated (false)
Then when each line reaches terminal length you can spawn a new set of lines like this:
// spawns 3 new centre points – default values are for testing
function spawn(point,newMaxLength,newColor,growLength,newLineWidth){
var max=newMaxLength||point.maxLength/2;
var color=newColor|| (colors[++colorIndex%(colors.length)]);
var grow=growLength||point.growLength/2;
var lw=newLineWidth||point.lineWidth-1;
// new center points are spawned at the termination points of the 3 current lines
newPoint((point.x),(point.y-point.maxLength),max,color,grow,lw);
newPoint((point.x-0.866*point.maxLength),(point.y+point.maxLength/2),max,color,grow,lw);
newPoint((point.x+0.866*point.maxLength),(point.y+point.maxLength/2),max,color,grow,lw);
}
// creates a new point object and puts in the centreSet array for processing
function newPoint(x,y,newMaxLength,newColor,growLength,newLineWidth){
var newPt={
x:x,
y:y,
maxLength:newMaxLength,
color:newColor,
lineWidth:newLineWidth,
growLength:growLength,
currentLength:0,
isActive:true
}
centreSet.push(newPt);
}
Here is code and a Fiddle: http://jsfiddle.net/m1erickson/Vc8Gf/
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; }
canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var context = canvas.getContext('2d');
// colors
var colors=["red","blue","gold","purple","green"];
var colorIndex=0;
//
var centreSet=[]
var generations=1;
// seed point
newPoint(canvas.width/2,canvas.height/2,100,"red",15);
// start
draw();
//
function draw(){
//
context.clearRect(0,0,canvas.width,canvas.height);
//
for(var i=0;i<centreSet.length;i++){
//
var centre=centreSet[i];
//
if(centre.isActive){
//
centre.currentLength+=centre.growLength;
//
if(centre.currentLength>=centre.maxLength){
centre.isActive=false;
centre.currentLength=centre.maxLength;
spawn(centre);
}
}
//
drawLines(centre);
}
//
if(generations<120){
setTimeout(draw,500);
}else{
context.font="18pt Verdana";
context.fillText("Finished 120 generations",40,350);
}
}
function spawn(point,newMaxLength,newColor,growLength,newLineWidth){
var max=newMaxLength||point.maxLength/2;
var color=newColor|| (colors[++colorIndex%(colors.length)]);
var grow=growLength||point.growLength/2;
var lw=newLineWidth||point.lineWidth-1;
newPoint((point.x),(point.y-point.maxLength),max,color,grow,lw);
newPoint((point.x-0.866*point.maxLength),(point.y+point.maxLength/2),max,color,grow,lw);
newPoint((point.x+0.866*point.maxLength),(point.y+point.maxLength/2),max,color,grow,lw);
generations++;
}
function newPoint(x,y,newMaxLength,newColor,growLength,newLineWidth){
var newPt={
x:x,
y:y,
maxLength:newMaxLength,
color:newColor,
lineWidth:newLineWidth,
growLength:growLength,
currentLength:0,
isActive:true
}
centreSet.push(newPt);
}
function drawLines(centre) {
var length=centre.currentLength;
//
context.beginPath();
context.moveTo(centre.x, centre.y);
context.lineTo(centre.x, centre.y - length);
//
context.moveTo(centre.x, centre.y);
context.lineTo(centre.x - 0.866 * length, centre.y + length/2);
//
context.moveTo(centre.x, centre.y);
context.lineTo(centre.x + 0.866 * length, centre.y + length/2);
//
context.strokeStyle=centre.color;
context.lineWidth = centre.lineWidth;
context.stroke();
}
}); // end $(function(){});
</script>
</head>
<body>
<canvas id="canvas" width=400 height=400></canvas>
</body>
</html>

Categories