I am doing some simple animation in Javascript. In light of the recent earthquake on the East Coast, I have implemented an earthquake effect whereby a table of information jostles around for a while when you click a button. I want the jostling to start out strong, and then peter out.
I have a utility function that repeatedly calls another function at a set interval. Then it calls a second function when it is all done calling the first function a bunch of times. This is so that you can schedule something to occur when the animation is over. Here is the code for it:
function countIterate(timeout, count, func1, func2)
{
if (count > 0) {
func1();
setTimeout(function() { countIterate(timeout, --count, func1, func2); }, timeout);
}
else
func2();
}
Here is the earthquake routine:
function earthQuake()
{
console.log("earthQuake()");
$("table").css("position", "relative");
var quake = function(magnitude)
{
var top = Math.floor(Math.random() * (2 * magnitude + 1)) - magnitude;
var left = Math.floor(Math.random() * (2 * magnitude + 1)) - magnitude;
$("table").css("top", top).css("left", left);
}
var func = new Array();
func[0] = function() {};
for (var i = 1; i <= 4; i++) {
func[i] = function() { countIterate(35, 40, function() { quake(i); }, func[i-1]); };
console.log(func[i]);
}
func[4]();
}
Unfortunately, I am getting an infinite earthquake loop.
If I hard-code things instead of the for loop:
var func0 = function() {};
var func1 = function() { countIterate(35, 40, function() { quake(1); }, func0); };
var func2 = function() { countIterate(35, 40, function() { quake(2); }, func1); };
var func3 = function() { countIterate(35, 40, function() { quake(3); }, func2); };
var func4 = function() { countIterate(35, 40, function() { quake(4); }, func3); };
func4();
it works fine. But this is an ugly solution.
By the way, here is the console.log() output from the first (more elegant, but broken) solution:
function () { countIterate(35, 40, function() { quake(i); }, func[i-1]); }
function () { countIterate(35, 40, function() { quake(i); }, func[i-1]); }
function () { countIterate(35, 40, function() { quake(i); }, func[i-1]); }
function () { countIterate(35, 40, function() { quake(i); }, func[i-1]); }
If there is some library that will take care of this sort of thing, please let me know, but I want to get this version working anyway as a learning experience.
The function:
function() { countIterate(35, 40, function() { quake(i); }, func[i-1]); }
is always executed with i = 5 because by the time func[4](); is reached the for loop has already completed. This can be easily shown to be the problem by binding the value of i to the function by returning the function from another:
(function(i) {
return function() {
countIterate(35, 40, function() { quake(i); }, func[i-1]);
};
}(i))
For further explanation, you can refer to answers to the many other "for loop problem" questions that have been asked here, including the top answer for Javascript infamous Loop issue?.
Related
Wow. I finally figured about what is causing the bug, but I can't figure out why. I have an object with a property (excuse the massive code dump)
// relatives second indices in the video to events
// that are called when the video reaches that second
this.PausePoints = [
{
sec: 10,
name: "Point number 1",
passed: false,
func: (function(that) {
this.$layer = that.GetLayerElement(10);
this.$layer.hide();
this.to = function () {
that.videlem.pause(); // pause video
$(window).resize(); // re-proportion stuff
// point the 3 mouse pointers
var $mptrs = this.$layer.find('.filmstrip-pointer');
for (var i = 0; i < $mptrs.length; ++i) {
(function (j) {
setTimeout(function () {
Point($mptrs.eq(j));
}, j * 1000);
})(i);
}
};
// attach click event to 3 sections
$clickRegions = $layer.find('div.click-region');
$clickRegions.click(function(){
$clickRegions.removeClass('clicked');
$(this).addClass('clicked');
});
this.away = function () {
this.$layer.hide();
}
// attach event to next button
$layer.find('.next-btn').click(function(){
this.away();
that.videlem.play();
}.bind(this));
return this;
})(this)
},
{
sec: 26,
name: "Point number 2",
passed: false,
func: (function(that) {
this.$layer = that.GetLayerElement(26);
this.$layer.hide();
this.to = function () {
// loop video between 0:26-0:31
this.loop = setInterval(function () {
that.videlem.currentTime = 26;
that.videlem.play();
}, 5000);
// point the 3 mouse pointers
var $mptrs = this.$layer.find('.filmstrip-pointer');
for (var i = 0; i < $mptrs.length; ++i) {
(function (j) {
setTimeout(function () {
Point($mptrs.eq(j));
}, j * 1000);
})(i);
}
this.$layer.show();
}
// separate pargraph words by spans
this.$layer.find('p').each(function () {
var spanned = $(this).text().split(" ").map(function (w) { return '<span class="word">' + w + '</span>'; }).join(" ");
$(this).html(spanned);
});
// add event click event on headlines
var timeouts = [];
this.$layer.find('h3').click(function () {
// clear any current 'showing' animations
timeouts.forEach(function(t){ clearTimeout(t); });
timeouts = [];
// unshow all words on the slide
this.$layer.find('span.word').removeClass('shown');
// show all words associated with the headline that was clicked
var $wspans = $(this).closest('.tower-layer').find('span.word');
for ( var i = 0; i < $wspans.length; ++i )
{
(function(j){
timeouts.push(setTimeout(function(){
$wspans.eq(j).addClass('shown');
},j*100));
})(i);
}
}.bind(this));
this.away = function () {
clearInterval(this.loop);
this.$layer.find('span.word').removeClass('shown');
$layer.hide();
that.videlem.currentTime = 31;//go to end of loop
};
// set action of "Next" button
this.$layer.find('.next-btn').click(function () {
this.away();
that.videlem.play();
}.bind(this));
return this;
})(this)
},
{
sec: 38,
name: "Point number 3",
passed: false,
func: (function(that) {
this.$layer = that.GetLayerElement(38);
this.$layer.hide();
this.to = function ( ) {
// loop video between 0:38-0:43
this.loop = setInterval(function () {
that.videlem.currentTime = 38;
that.videlem.play();
}, 5000);
this.$layer.show();
}
this.away = function(){
clearInterval(this.loop);
this.$layer.hide();
};
this.$layer.find('.next-btn').click(function(){
that.videlem.currentTime = 43;
this.away();
that.videlem.play();
}.bind(this));
return this;
})(this)
},
{
sec: 47,
name: "Point number 4",
passed: false,
func: (function(that){
this.$layer = that.GetLayerElement(47);
this.$layer.hide();
this.to = function ()
{
// loop video between 0:47-0:52
this.loop = setInterval(function() {
that.videlem.currentTime = 47;
that.videlem.play();
}, 5000);
// show layer
this.$layer.show();
}
this.away = function () {
clearInterval(this.loop);
this.$layer.hide();
};
this.$layer.find('.next-btn').click(function () {
that.videlem.currentTime = 52;
this.away();
that.videlem.play();
}.bind(this));
return this;
})(this)
},
{
sec: 57,
name: "Point number 5",
passed: false,
func: (function(that){
this.$layer = that.GetLayerElement(57);
// hide initially
this.$layer.hide();
this.to = function ()
{
// loop video between 0:57-1:02
this.loop = setInterval(function () {
that.videlem.currentTime = 57;
that.videlem.play();
}, 5000);
this.$layer.show();
}
this.away = function(){
clearInterval(this.loop);
$layer.hide();
};
this.$layer.find('.next-btn').click(function () {
that.videlem.currentTime = 62;
this.away();
that.videlem.play();
}.bind(this));
return this;
})(this)
}
];
and what I'm noticing is that when I try to call any of the to functions it always calls the one in the last element of the array.
For example,
VidHandler.PausePoints[0].func.to()
calls
this.to = function ()
{
// loop video between 0:57-1:02
this.loop = setInterval(function () {
that.videlem.currentTime = 57;
that.videlem.play();
}, 5000);
this.$layer.show();
}
instead of the expected
this.to = function () {
that.videlem.pause(); // pause video
$(window).resize(); // re-proportion stuff
// point the 3 mouse pointers
var $mptrs = this.$layer.find('.filmstrip-pointer');
for (var i = 0; i < $mptrs.length; ++i) {
(function (j) {
setTimeout(function () {
Point($mptrs.eq(j));
}, j * 1000);
})(i);
}
};
Why is this happening and how can I fix it?
The problem is you're trying to assign something to func using an immediately invoked function expression (IIFE). Those IIFEs are executed before the object is constructed, meaning this refers to something else. Your code can basically be broken down like this:
this.to = function() {
// version for "Point number 1"
};
this.to = function() {
// version for "Point number 2"
// notice that you're overwriting the previous one
};
// repeat for all points
var self = this;
this.PausePoints = [
{
name: "Point number 1",
func: self
},
// repeat for all points
];
So what you're actually doing is assigning a to value to the same object that has the PausePoints property.
I am a beginner level javascript programmer. I can't find a reason why there is undefined values for the winX and WinY variables in the following function:
var game = new Phaser.Game(
800,
600,
Phaser.AUTO,
'game',
{ init: init,
preload: preload,
create: create,
update: update,
randomizeWin: randomizeWin,
myMethod: myMethod
}
);
function init() {
this.sss=56;
this.marginX=150;
this.marinY=100;
}
function preload() {
var wood = game.load.image('wood', 'assets/wood.jpg');
randomizeWin();
console.log(this.winY);
}
function create() {
this.p=[];
for(var i=0;i<3;i++) {
this.p[i]=[];
for(var j=0;j<3;j++){
this.p[i][j]= game.add.sprite(this.marginX+i*170, this.marinY+j*170, 'wood');
this.p[i][j].scale.x=0.2;
this.p[i][j].scale.y=0.2;
this.p[i][j].anchor.setTo(.5,.5);
this.p[i][j].inputEnabled = true;
this.p[i][j].input.useHandCursor = true;
this.p[i][j].events.onInputDown.add(myMethod, this);
}
}
}
function update() {
}
function myMethod(sprite) {
console.log(this.p[this.winX][this.winY]==sprite);// winX is undefined here why??
if(this.p[this.winX][this.winY]==sprite){
game.add.tween(sprite.scale).to ({
x: sprite.scale.x * -1,
// y: sprite.scale.y * -1
}, 1000, Phaser.Easing.Bounce.Out, true);
}
}
function randomizeWin() {
console.log("rand");
this.winX = Math.floor(Math.random() * 3);
this.winY = Math.floor(Math.random() * 3);
}
What is happening here and how to fix it?
The value of this depends on the calling context.
You'd either need to bind the object in question, roughly:
this.p[i][j].events.onInputDown.add(myMethod.bind(this), this);
(But as your code currently stands this would have the same issue) or rely on whatever the framework in question provides in the way of binding.
Welcome to JS.
Using this on JS opens up a whole world of pain. You should avoid it until you are really sure how it works or rename it to something else on your scope:
var self = this;
function create() {
self.p = [];
//etc
}
function randomizeWin(){
self.winX = Math.floor(Math.random() * 3);
self.winY = Math.floor(Math.random() * 3);
}
function myMethod(sprite){
console.log(self.p[self.winX][self.winY]==sprite);
}
My console message:
Uncaught TypeError: Cannot read property 'x' of undefined.
However, when I log to the console the snake.head object, it gives me a valid "response"! How could it be that an object is defined but when trying to store a property of that object in a variable it doesn't work? I also tried just continuing the game without storing the x and y position in a variable and instead accessing it within the head object and guess what? Doesn't work. I even tried just accessing the game.SNAKE[game.SNAKE.length-1].(property) but it also doesn't work! However, when I try it in my snake.init function, it works fine, but I can't do this, as my init only runs once!
var game = {};
game.contextBackground = document.getElementById("bgCanvas").getContext("2d");
game.contextSnake = document.getElementById("snakeCanvas").getContext("2d");
game.contextFruit = document.getElementById("fruitCanvas").getContext("2d");
game.keys = [];
game.SNAKE = [];
game.width = document.getElementById("snakeCanvas").width;
game.height = document.getElementById("snakeCanvas").height;
game.COLUMNS = 35;
game.ROWS = 35;
game.boxSide = game.width/game.COLUMNS;
game.snake = {
snakeLength: 3,
tailx: null,
taily: null,
head: null,
headx: null,
heady: null,
direction: "right",
createSnake: function (snakeLength) {
game.SNAKE.length = this.snakeLength;
for (i=0;i<snakeLength;i++) {
game.SNAKE[i] = {
x: i ,
y: 0
};
}
},
updateSnake: function (length) {
this.head = game.SNAKE[length-1];
this.headx = this.head.x;
for (i in game.SNAKE) {
}
},
init: function() {
this.createSnake(this.snakeLength);
},
update: function() {
this.updateSnake(game.SNAKE.length);
console.log(this.head);
for (i in game.SNAKE) {
game.SNAKE[i].x ++;
}
},
render: function() {
game.contextSnake.clearRect(0,0,game.width,game.height);
for (i in game.SNAKE) {
drawBox(game.SNAKE[i].x,game.SNAKE[i].y,game.contextSnake,"green");
}
}
};
game.fruit = {
x: null,
y: null,
newFruitNecessary: true,
init: function() {
},
update: function() {
},
render: function() {
}
};
function init() {
loop();
game.snake.init();
game.fruit.init();
}
function update() {
game.snake.update();
game.fruit.update();
}
function render() {
game.fruit.render();
game.snake.render();
}
function loop() {
setTimeout(function(){
loop();
},1000/10);
update();
render();
}
function drawBox (gridx,gridy,context,color) {
if (color == null) {
color = "black";
}
context.fillStyle = color;
context.fillRect((gridx * game.boxSide), (gridy * game.boxSide), game.boxSide,game.boxSide);
}
init();
You have to init your snake (and later fruit) before you can loop; otherwise, game.SNAKE will point at an empty array, so the lines
this.head = game.SNAKE[length-1];
this.headx = this.head.x;
will fail to work properly when the first update() calls inside your loop(). game.SNAKE is empty, so you store undefined to this.head; thus, this.head.x resolves to undefined.x, which fails.
Try shifting around the lines in init:
function init() {
game.snake.init();
game.fruit.init();
loop();
}
I'm trying to define a Javascript class with a repeating function but I can't get it to work:
var Repeater = function() {
this.init.apply(this, arguments);
};
Repeater.prototype = {
run: 0, // how many runs
interval: 5, // seconds
init: function() {
this.repeat();
},
repeat: function() {
console.log(++this.run);
setTimeout(this.repeat, this.interval * 1000);
}
};
var repeater = new Repeater();
How should this be done?
Try this code:
var Repeater = function() {
this.run = 0; // how many runs
this.interval = 5; // seconds
this.init.apply(this, arguments);
};
Repeater.prototype.init = function() {
this.repeat();
}
Repeater.prototype.repeat = function() {
var _this = this;
console.log(++this.run);
setTimeout(function () { _this.repeat() }, this.interval * 1000);
};
var repeater = new Repeater();
I've moved run and interval into constructor, because if you add this to prototype then this will be spread over all instances.
Your problem lies into seTimeout - in your code this timer set new scope for repeater and this was no longer pointing to Repeater instance but for Timeout instance. You need to cache this (I've called this cache _this) and call it into new function passed to setTimeout.
Try like that:
var Repeater = function() {
this.init.apply(this, arguments);
};
Repeater.prototype = {
run: 0, // how many runs
interval: 5, // seconds
init: function() {
this.repeat();
},
repeat: function() {
console.log(++this.run);
var that = this;
setTimeout(function() {that.repeat()}, this.interval * 1000);
}
};
var repeater = new Repeater();
You can read more on how this behaves in this question : How does the "this" keyword work?
Change your repeat function to use a closure in the setTimeout call like so:
repeat: function() {
var ctx = this;
console.log(++this.run);
setTimeout(function(){ctx.repeat()}, this.interval * 1000);
}
You need to set the context explicitly in these kinds of scenarios- that's what the ctx variable is for
I want to have two functions (an animation downwards and animation upwards) executing one after the other in a loop having a timeout of a few seconds between both animations. But I don't know how to say it in JS …
Here what I have so far:
Function 1
// Play the Peek animation - downwards
function peekTile() {
var peekAnimation = WinJS.UI.Animation.createPeekAnimation([tile1, tile2]);
// Reposition tiles to their desired post-animation position
tile1.style.top = "-150px";
tile2.style.top = "-150px";
peekAnimation.execute();
}
Function 2
// Play the Peek animation - upwards
function unpeekTile() {
var peekAnimation = WinJS.UI.Animation.createPeekAnimation([tile1, tile2]);
// Reposition tiles to their desired post-animation position
tile1.style.top = "0px";
tile2.style.top = "0px";
peekAnimation.execute();
}
And here's a sketch how both functions should be executed:
var page = WinJS.UI.Pages.define("/html/updateTile.html", {
ready: function (element, options) {
peekTile();
[timeOut]
unpeekTile();
[timeOut]
peekTile();
[timeOut]
unpeekTile();
[timeOut]
and so on …
}
});
You can do this using setTimeout or setInterval, so a simple function to do what you want is:
function cycleWithDelay() {
var delay = arguments[arguments.length - 1],
functions = Array.prototype.slice.call(arguments, 0, arguments.length - 1),
pos = 0;
return setInterval(function () {
functions[pos++]();
pos = pos % functions.length;
}, delay);
}
Usage would be like this for you:
var si = cycleWithDelay(peekTile, unpeekTile, 300);
and to stop it:
clearInterval(si);
This will just cycle through the functions calling the next one in the list every delay msec, repeating back at the beginning when the last one is called. This will result in your peekTile, wait, unpeekTile, wait, peekTile, etc.
If you prefer to start/stop at will, perhaps a more generic solution would suit you:
function Cycler(f) {
if (!(this instanceof Cycler)) {
// Force new
return new Cycler(arguments);
}
// Unbox args
if (f instanceof Function) {
this.fns = Array.prototype.slice.call(arguments);
} else if (f && f.length) {
this.fns = Array.prototype.slice.call(f);
} else {
throw new Error('Invalid arguments supplied to Cycler constructor.');
}
this.pos = 0;
}
Cycler.prototype.start = function (interval) {
var that = this;
interval = interval || 1000;
this.intervalId = setInterval(function () {
that.fns[that.pos++]();
that.pos %= that.fns.length;
}, interval);
}
Cycler.prototype.stop = function () {
if (null !== this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
Example usage:
var c = Cycler(peekTile, unpeekTile);
c.start();
// Future
c.stop();
You use setInterval() to call unpeekTile() every 1000 milliseconds and then you call setTimeOut() to run peekTile() after 1000 milliseconds at the end of the unpeekTile() function:
function peekTile() {
var peekAnimation = WinJS.UI.Animation.createPeekAnimation([tile1, tile2]);
// Reposition tiles to their desired post-animation position
tile1.style.top = "-150px";
tile2.style.top = "-150px";
peekAnimation.execute();
}
function unpeekTile() {
/* your code here */
setTimeout(peekTile, 1000);
}
setInterval(unpeekTile, 1000);
Check out the fiddle
var animation = (function () {
var peekInterval, unpeekInterval, delay;
return {
start: function (ip) {
delay = ip;
peekInterval = setTimeout(animation.peekTile, delay);
},
peekTile: function () {
//Your Code goes here
console.log('peek');
unpeekInterval = setTimeout(animation.unpeekTile, delay);
},
unpeekTile: function () {
//Your Code goes here
console.log('unpeek');
peekInterval = setTimeout(animation.peekTile, delay);
},
stop: function () {
clearTimeout(peekInterval);
clearTimeout(unpeekInterval);
}
}
})();
animation.start(1000);
// To stop
setTimeout(animation.stop, 3000);
I can't use this instead of animation.peekTile as setTimeout executes in global scope