I thought I fixed it but looks like not. Heres whats happening:
The canvas.mousemove event is handled by viewport.onMouseMove.bind(viewport) function (viewport is an instance of a class).
At the end of the onMouseMove function it calls this.Draw() (referring to viewport.Draw() function).
viewport.Draw() loops through all the items and calls Items[i].Draw(ctx) on each of them where ctx is a back buffer canvas context.
Now if If the item that is being drawn goes ahead and uses the ctx to draw something right there and then (in its Draw function), using this to refer to itself, everything works fine. For example
this.Draw = function(ctx) {
ctx.beginPath();
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
ctx.lineWidth = 1;
ctx.strokeStyle = "#000000";
ctx.stroke();
};
However, if the object is a container that has items in itself and tries to loop and draw them like this
this.Draw = function(ctx) {
for (j = 0; j < this.Items.length; j++) {
this.Items[j].Draw(ctx);
}
};
When it gets into the Items[j].Draw, "this" loses all meaning. alert(this) produces "object object" and I cant figure out what its referring to (it's not the viewport nor the container nor the item it needs to be). Also another weird thing - I had to change the container object loop to use j instead of i because otherwise it would create a perpetual loop (like the i's of the viewport.draw and item[i].draw were the same).
Your question is somewhat unclear. Is this.Items an array of objects with the same prototype as this? ie. nested? Also, is the j counter intended to be shared?
Regardless, function contexts' this values can be changed rather easily to whatever you need them to be with the .apply and .call functions:
this.Draw = function(ctx) {
for (var j = 0; j < this.Items.length; j++) {
// These two are the same as what you have in the question
this.Draw.call(this.Items[j], ctx);
this.Draw.apply(this.Items[j], [ctx]);
// This is what you had in the question if Draw is different for Items:
this.Items[j].Draw(ctx);
this.Items[j].Draw.call(this.Items[j], ctx);
// Will preserve the this reference within the nested call
this.Items[j].Draw.call(this, ctx);
}
};
Not sure what the problem is but as my comment suggest this is the invoking object:
//this in someFunction is window
setTimeout(myObject.someFunction, 200);
//this in someFunction is button
button.onClick=myObject.someFunction;
Not sure what you would like this to be when it's called but if it has to be Items[j] then your code is fine and maybe something else is causing you problems. I suggest console.log objects in Chrome or Firefox with firebug, use F12 to open the console and inspect the logged objects.
Here is sample code of items that can be Square or Circle;
var Shape = function Shape(args){
//args.x1 or y1 can be 0, defaults to 2
this.x1 = (args.x1 === undefined)? 2:args.x1;
this.y1 = (args.y1 === undefined)? 2:args.y1;
this.name = args.name||"unnamed";
}
//in this example Square and Cirle draw does the same
// so they can inherit it from Shape
Shape.prototype.draw=function(){
console.log("this x1:",this.x1,"this y1:",this.y1,"name",this.name);
//you can log complex values as well and click on them in the console
// to inspect the details of the complex values (objects)
// the above can be done in the following log
console.log("in draw, this is:",this);
}
var Square = function Square(args){
//re use parent constructor (parent is Shape)
Shape.call(this,args);
}
//set prototype part of inheritance and repair constructor
Square.prototype=Object.create(Shape.prototype);
Square.prototype.constructor=Square;
var Circle = function Circle(args){
//re use parent constructor (parent is Shape)
Shape.call(this,args);
}
//set prototype part of inheritance
Circle.prototype=Object.create(Shape.prototype);
Circle.prototype.constructor=Circle;
//there is only one app so will define it as object literal
var app = {
items:[],
init:function(){
var i = -1;
while(++i<10){
this.items.push(new Circle({x1:i,y1:i,name:"circle"+i}));
}
while(++i<20){
this.items.push(new Square({x1:i,y1:i,name:"square"+i}));
}
},
draw:function(){
var i = -1;len=this.items.length;
while(++i<len){
this.items[i].draw();
}
}
}
app.init();
app.draw();//causes console.logs
Related
For context, I am trying to code a memory game where you have to pair two of the same colored circles until the whole board is complete. I've called it Match-Two. Here is the code that I'll reference from:
class Circle {
constructor(element, circleColor){
this.elem = element;
this.color = circleColor;
}
}
var frequency = [0, 0, 0, 0, 0, 0, 0, 0];
var num;
var hue = new Array(8);
var circle = new Array(16);
hue[0] = "#0039ff";
hue[1] = "#ff0000";
hue[2] = "#43ff00";
hue[3] = "#fffa00";
hue[4] = "#7405b5";
hue[5] = "#ff9d00";
hue[6] = "#ff00c3";
hue[7] = "#00fff6";
onload = function() {
for(var i = 0; i < 16; i++){
circle[i] = new Circle(document.getElementById("circle" + i));
while(circle[i].color === undefined){
num = Math.floor(Math.random() * 8);
if(frequency[num] != 2){
frequency[num]++;
circle[i].color = hue[num];
circle[i].elem.addEventListener('click', function(){
main(circle[i])
});
}
}
}
}
function main(circle){
circle.elem.style.backgroundColor = circle.color;
}
So in this code I create a class of Circle and I create an array of Circle objects which is identified as 'circle'. When the page is loaded, I give each circle object an element reference from my html document (There are 16 circles and they each have an id of circle0, circle1, circle2.. etc. Then there's a small algorithm to ensure there are only two of each color in the matrix so they all have a matching pair. In each iteration of the for loop, I add an event listener to each circle. If the circle is clicked, I want it to change to its color which is stored in color[i].color. However, when I click the circles all it returns is:
Uncaught TypeError: Cannot read property 'elem' of undefined
at main (script.js:39)
at HTMLDivElement.<anonymous> (script.js:31)
Which is referencing:
circle.elem.style.backgroundColor = circle.color;
So I put some console.log() functions in to see what was going on:
if(frequency[num] != 2){
frequency[num]++;
circle[i].color = hue[num];
console.log(circle[i].elem);
console.log(circle[i].color);
circle[i].elem.addEventListener('click', function(){
main(circle[i])
});
}
And this spits out exactly what I expect:
script.js:31 #ff9d00
script.js:30 div data-brackets-id="11" class="circle" id="circle1" /div
script.js:31 #ff9d00
script.js:30 div data-brackets-id="12" class="circle" id="circle2" /div
script.js:31 #0039ff
script.js:30 div data-brackets-id="13" class="circle" id="circle3" /div
script.js:31 #0039ff
So it returns the element reference and the color of the circle. So then I try putting the "circle[i].elem.style.backgroundColor = circle[i].color" into the event listener and I get the same issue as before...
if(frequency[num] != 2){
frequency[num]++;
circle[i].color = hue[num];
console.log(circle[i].elem);
console.log(circle[i].color);
circle[i].elem.addEventListener('click', function(){
circle[i].elem.style.backgroundColor = circle[i].color
});
}
Circles without their colors. The console log statements are on the right-hand side with their specific colors as well...
So I gave up and decided to write that exact line of code outside the event listener to see if that works, and it changed all the circle's colors to their specific color...
if(frequency[num] != 2){
frequency[num]++;
circle[i].color = hue[num];
console.log(circle[i].elem);
console.log(circle[i].color);
circle[i].elem.style.backgroundColor = circle[i].color;
circle[i].elem.addEventListener('click', function(){
circle[i].elem.style.backgroundColor = circle[i].color
});
}
The circles with their specific colors...
There is some problem the event listener not being able to pass the object of a Circle or something... I don't know please help :(
Your problem boils down to the way JS treats var variables - they sort of "leak" into the global scope.
Consider the event listener that you've attached:
circle[i].elem.addEventListener('click', function(){
main(circle[i])
});
So, whenever the listener gets triggered, it calls main() function and passes circle[i] into it. But since i is the variable that's leaked outside of the supposed scope, it always has the value of 16 - the value assigned to it during the last iteration of the for loop. That's why the main() function tries to access a style property of undefined - it's the value of circle[16] that was passed into it.
Here's a couple of ways to fix it:
If you can use ES6 let variables:
Use let i instead of var i in your for loop:
for (let i = 0; i < 16; i++) {
//...
}
If not, a classic way with function closure:
function createListener(j) {
return function () {
main(circle[j])
}
}
// and use it in your 'for' loop later:
circle[i].elem.addEventListener('click', createListener(i));
Here's a useful topic that provides more techniques to avoid this: JavaScript closure inside loops – simple practical example
I have written code that takes two arrays, both of which contain co-ordinates for a four-cornered shape (effectively a start frame and an end frame), a canvas ID and a time value. The function then calculates dX and dY of each corner and uses window.performance.now() to create a timestamp. Then, on every requestAnimationFrame(), it calculates what the co-ordinates should be by using dX, dY, the old timestamp, a new timestamp and the time value from the function call. It looks like this:
function doAnim(cv, startFrame, endFrame, animTime)
{
this.canvas = document.getElementById(cv);
this.ctx = this.canvas.getContext('2d');
if(startFrame.length != endFrame.length)
{
return('Error: Keyframe arrays do not match in length');
};
this.animChange = new Array();
for(i=1;i<=startFrame.length;i++)
{
var a = startFrame[i];
var b = endFrame[i]
var c = b - a;
this.animChange[i] = c;
}
this.timerStart = window.performance.now();
function draw()
{
this.requestAnimationFrame(draw, cv);
this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);
this.currentFrame = new Array();
for(i=1;i<=startFrame.length;i++)
{
this.currentFrame[i] = startFrame[i]+(this.animChange[i]*((window.performance.now()-this.timerStart)/animTime));
}
if((window.performance.now()-this.timerStart)>=animTime)
{
this.ctx.beginPath()
this.ctx.moveTo(endFrame[1], endFrame[2]);
this.ctx.lineTo(endFrame[3], endFrame[4]);
this.ctx.lineTo(endFrame[5], endFrame[6]);
this.ctx.lineTo(endFrame[7], endFrame[8]);
this.ctx.fill();
return;
}
else
{
this.ctx.beginPath()
this.ctx.moveTo(this.currentFrame[1], this.currentFrame[2]);
this.ctx.lineTo(this.currentFrame[3], this.currentFrame[4]);
this.ctx.lineTo(this.currentFrame[5], this.currentFrame[6]);
this.ctx.lineTo(this.currentFrame[7], this.currentFrame[8]);
this.ctx.fill();
}
}
draw();
}
The goal is to have multiple animations of objects happening at once. I took the whole co-ordinate approach because I want the objects to appear as if they are coming from the horizon, creating a fake 3D perspective effect (all objects' starting frames would be a single point at the center of the canvas), and I do not want to warp the objects' textures.
Well, it works great for a single animation, but if I try to start a new animation on a completely different canvas while the first one is running, then the first animation stops dead in its tracks.
As you can see from my JS, I've tried getting around this with gratuitous use of this (I do not fully understand how this works yet, and every explanation I've read has left me even more confused), but it has not worked. I also tried a horrific approach which stored all the functions' own variables in one global array (the first time the function runs, all the variables are put in entries 1-30, the second time they're put in 31-60, etc). Unsurprisingly, that did not work either.
Here is a JSFiddle so you can see this scenario for yourself and play with my code. I am officially out of ideas. Any help will be greatly appreciated.
Like markE linked too, trying to call requestAnimationFrame multiple times won't work.
Instead you make multiple objects and then call some sort of function on them each frame.
I have created an example using your code:
https://jsfiddle.net/samcarlin/2bxn1r79/7/
var anim0frame1 = new Array();
anim0frame1[1] = 0;
anim0frame1[2] = 0;
anim0frame1[3] = 50;
anim0frame1[4] = 0;
anim0frame1[5] = 50;
anim0frame1[6] = 150;
anim0frame1[7] = 0;
anim0frame1[8] = 150;
var anim0frame2 = new Array();
anim0frame2[1] = 200;
anim0frame2[2] = 200;
anim0frame2[3] = 300;
anim0frame2[4] = 250;
anim0frame2[5] = 300;
anim0frame2[6] = 300;
anim0frame2[7] = 200;
anim0frame2[8] = 250;
//Call global
animations = [];
requestAnimationFrame( GlobalStep );
function GlobalStep(delta){
//Functions called by request animation frame have the new time as an argument
//so delta should be approximately the same as window.performance.now()
//especially in realtime applications, which this is
//Check if we have any animation objects
if(animations.length > 0){
//Iterate through and call draw on all animations
for(var i=0; i<animations.length; i++){
if(animations[i].draw(delta)){
//Basically we have it so if the draw function returns true we stop animating the object
//And remove it from the array, so have the draw function return true when animation is complete
animations[i].splice(i, 0);
//We removed an object from the array, so we decrement i
i--;
}
}
}
//And of course call requestAnimationFrame
requestAnimationFrame( GlobalStep );
}
function AnimationObject(cv, startFrame, endFrame, animTime){
//Add this object to the objects arrays
animations.push(this);
//We need to store start and end frame
this.startFrame = startFrame;
this.endFrame = endFrame;
this.animTime = animTime;
//Your code
this.canvas = document.getElementById(cv);
this.ctx = this.canvas.getContext('2d');
if (startFrame.length != endFrame.length) {
return ('Error: Keyframe arrays do not match in length');
};
this.animChange = new Array();
for (i = 1; i <= startFrame.length; i++) {
var a = startFrame[i];
var b = endFrame[i]
var c = b - a;
this.animChange[i] = c;
}
this.timerStart = window.performance.now();
}
//This adds a function to an object, but in such a way that every object shares the same function
//Imagine a kitchen, each object is a person, and this function is a spoon
//by defining this function in this manner Object.prototype.function_name = function(arguments){}
//We make it so one function definition is needed, essentially allowing all the people to share one spoon,
//the 'this' variable still refers to whichever object we call this method, and we save memory etc.
AnimationObject.prototype.draw = function(newTime){
//I added this to start frame so we get what we stored earlier
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.currentFrame = new Array();
for (i = 1; i <= this.startFrame.length; i++) {
this.currentFrame[i] = this.startFrame[i] + (this.animChange[i] * ((newTime - this.timerStart) / this.animTime));
}
if ((newTime - this.timerStart) >= this.animTime) {
this.ctx.beginPath()
this.ctx.moveTo(this.endFrame[1], this.endFrame[2]);
this.ctx.lineTo(this.endFrame[3], this.endFrame[4]);
this.ctx.lineTo(this.endFrame[5], this.endFrame[6]);
this.ctx.lineTo(this.endFrame[7], this.endFrame[8]);
this.ctx.fill();
return;
} else {
this.ctx.beginPath()
this.ctx.moveTo(this.currentFrame[1], this.currentFrame[2]);
this.ctx.lineTo(this.currentFrame[3], this.currentFrame[4]);
this.ctx.lineTo(this.currentFrame[5], this.currentFrame[6]);
this.ctx.lineTo(this.currentFrame[7], this.currentFrame[8]);
this.ctx.fill();
}
}
Notes:
Everytime you press the button a new object is added and simply overwrites previous ones for each frame, you should implement your program so that it checks if a specific animation has already started, you could also use the builtin mechanism to stop animation when complete (read the comments in the code)
You also need to change the on button click code
<button onclick="new AnimationObject('canvas1', anim0frame1, anim0frame2, 3000);">
Lastly if you have further questions feel free to contact me
i'm trying to call an objects methods at the same time with a loop.
I would create a game with a group of enemies that are move at the same time.
I create an object "Warrior"
function Warrior(x, y) {
// Coordinates x, y
this.x = x;
this.y = y;
this.character = $('<div class="enemy"></div>');
// Inserting the div inside #map
this.character.appendTo('#map');
// assigning x and y parameters
this.character.css({
marginLeft: x + 'px',
marginTop: y + 'px'
});
// walk method that move the div
this.walk = function() {
// Moving div 10px to the right
this.character.css('left', "+=10");
}
}
var enemies = new Array();
enemies[0] = new Warrior(0, 0);
enemies[1] = new Warrior(10, 20);
enemies[2] = new Warrior(60, 80);
for(var i=0;i<enemies.length;i++) {
setInterval(function(){enemies[i].walk()}, 1000);
}
I've created an array "enemies"
and I tried to call walk() methods at the same time
But nothing it happens. I would like that the divs are moving at the same time!
Thanks!
Based on this question
You might want to pass a parameter by value to setInterval:
for(var i=0;i<enemies.length;i++) {
setInterval(
function(index){ //<-- accept parameter here
enemies[index].walk();
}, 1000, i); //<-- pass i here
}
If you do not pass the parameter, x variable changes while loop is running, thus all the functions will be called having x = 2 (because you have 3 elements in your array)
You have two major problems where:
CSS does not allow programmatic modification in the way that javascript does. You modify values in javascript, and then set them in CSS:
this.walk = function() {
var current = parseInt(this.character.css('left')),
stepped = current + 10;
this.character.css('left', stepped);
}
Your loop creates a closure on var i which is only declared once, so the same value will be shared across the functions. You can either have the function take a parameter, and provide it in the setInterval call:
setInterval(function(index) { /* ... */}, 1000, i);
or you can put the loop within the interval function:
setInterval(function() {
for (var i = 0; i < enemies.length; i++) {
enemies[i].walk();
}
});
You might also want to reconsider whether your walk function should modify the CSS directly. You've already forgotten about your x and y coordinate attributes in this code. Perhaps you should have a walk method that modifies these coordinates, and a draw function that updates the sprites' positions based on their current coordinates.
I've recently started working on a side project, and experimenting with the canvas as I go. So let me explain what I want to happen:
In my JS, the main() function is set to an interval. During main() the drawStep() function is called. This does several things, or it should.
Sorry for the long explanation.
Clear the canvas, so that what is drawn doesn't stick.
Using a for loop, iterate through the array containing things that need to be drawn. currently only menu objects.
checking if the variable debug is true, if so, draw the mouse coordinates as two red lines.
However, when step 1 is done, drawing the menu objects fails, it flickers and the canvas clears to whatever color step 1 set the canvas to. When the debug var is set to true (through the console) Drawing the debug lines functions fine.
Here is the code blocks that matter.
Variables defined in an init() function, including the cookie cutter function for defining menu objects:
canvas = document.getElemntByID("canvas");
room = canvas.getContext("2d");
gameMode = 1; // telling the code that it is in the main menu
debug = false; //not drawing the cursor coordinates
menObj = new Array(); //an array of menu objects
mouse_x = 0; //variable set through onMouseMove event in the canvas
mouse_y = 0; //variable set through onMouseMove event in the canvas
drawList = {}; //array of menu object draw() functions, and any other item that
needs to be drawn during the main loop
function menu(mxpos,mypos,mwid,mhid,name, funct,drawing){
this.x = mxpos;
this.y = mypos;
this.width = mwid;
this.height = mhid;
this.value = name;
this.doing = funct;
this.canDraw = drawing; //the code relies on a gameMode variable, only drawing what is allowed to when the game mode is set correctly.
this.expand = 0; //not important, but was going to make buttons expand on mouse over
this.maxExpand = 10; // same as above
//The draw function passed on to the drawList array:
this.draw = function(){
if (this.canDraw == gameMode){
room.fillStyle = "rgba(150,150,150,1)";
room.strokeStyle = "rgba(200,200,200,1)"
room.fillRect(this.x-this.width/2,this.y-this.height/2,this.width,this.height);
room.strokeRect(this.x-this.width/2,this.y-this.height/2,this.width,this.height);
room.strokeStyle = "rgb(30,150,90)";
var xoff = room.measureText(this.value).width;
var yoff = room.measureText(this.value).height;
room.strokeText(this.value,this.x-xoff/2,this.y-yoff/2);
}
}
}
Sample menu object creation and the for loop that adds that objects draw event to the drawList array:
var temMenVal = new menu(width/2,height/5,96,32,"Start",function(){gamemode = 1},0)
menObj.push(temMenVal);
for(var mobj in menObj){
if (!menObj.hasOwnProperty(mobj)) continue;
drawList[mobj]=menObj[mobj].draw(); //push wasn't working, so I improvised.
}
Main function, called from an interval timer.
function main(){
drawStep();
}
This is the draw function, where my issue is:
function drawStep(){
//the latest attempt at a fix, instead of using a clearRect(), which failed.
//I tried this
room.save()
room.fillStyle="black";
room.fillRect(0,0,width,height);
room.restore();
for (var n in drawList){
room.save();
if (!drawList.hasOwnProperty(n)) continue;
if (n<drawList.length){
drawList[n](); //calling the draw() from the nested menu object, it DOES work, when the above clear to black is NOT called
}
room.restore();
}
if (debug == true){
room.beginPath();
room.strokeStyle="rgb(255,0,0)";
room.moveTo(mouse_x,0); //variable set through onmousemove event in the canvas
room.lineTo(mouse_x,height);
room.moveTo(0,mouse_y); //variable set through onmousemove event in the canvas
room.lineTo(width,mouse_y);
room.stroke();
room.closePath();
}
}
I can't figure out why it keeps clearing to black, when the menu objects SHOULD be drawn after the clear. Like I said way up there, setting debug to true DOES draw the cursor coordinates correctly.
When you set up your draw list, try removing the two parens in menObj[mobj].draw()
What it seems like is that you are actually calling the method instead of passing it as a variable.
Try init menu with drawing = 1 and edit code as Hylianpuffball point out. I think this.canDraw == gameMode is always false.
I have found an API that'll make working with CANVAS a lot easier. It allows selection and modification of individual elements on the canvas very easily. It's EaselJS. The API doc is here. http://easeljs.com/docs/
I am Ok with the API so far. What confuses me is actually some javascript in there. The part that's in bold or within * *(couldn't get the formatting to work)
What kind of javascript structure is this?
(function(target){...content...})(bitmap)
and in the content, it references bitmap, which is something outside.
HERE IS THE CODE
for(var i = 0; i < 100; i++){
bitmap = new Bitmap(image);
container.addChild(bitmap);
bitmap.x = canvas.width * Math.random()|0;
bitmap.y = canvas.height * Math.random()|0;
bitmap.rotation = 360 * Math.random()|0;
bitmap.regX = bitmap.image.width/2|0;
bitmap.regY = bitmap.image.height/2|0;
bitmap.scaleX = bitmap.scaleY = bitmap.scale = Math.random()*0.4+0.6;
bitmap.mouseEnabled = true;
bitmap.name = "bmp_"+i;
(function(target) {
*bitmap.onPress = function(evt) *
{if (window.console && console.log) { console.log("press!"); }
target.scaleX = target.scaleY = target.scale*1.2;
container.addChild(target);
// update the stage while we drag this target
//Ticker provides a central heartbeat for stage to listen to
//At each beat, stage.tick is called and the display list re-rendered
Ticker.addListener(stage);
var offset = {x:target.x-evt.stageX, y:target.y-evt.stageY};
evt.onMouseMove = function(ev) {
target.x = ev.stageX+offset.x;
target.y = ev.stageY+offset.y;
if (window.console && console.log) { console.log("move!"); }
}
evt.onMouseUp = function() {
target.scaleX = target.scaleY = target.scale;
// update the stage one last time to render the scale change, then stop updating:
stage.update();
Ticker.removeListener(stage);
if (window.console && console.log) { console.log("up!"); }
}
** }})(bitmap); **
bitmap.onClick = function() { if (window.console && console.log) { console.log("click!"); } }
}
(function(target){...content...})(bitmap)
is creating a lexical scope for content so that any var or function declarations in content do not leak into the global scope. Inside content, target is just another name for
bitmap.
You can think of this as similar to
function init(target) { ...content... }
and then an immediate call to it passing bitmap as the actual value of the target parameter
but the first version interferes with the global scope even less -- it doesn't define init as a name in the global scope.
EDIT:
I think the purpose is not lexical scoping, but to make sure that the event handlers point to the right bitmap, instead of the last bitmap the for loop deals with.
init(bitmap);
See Event handlers inside a Javascript loop - need a closure? for more detail.