Animation while loop in HTML5 Canvas / Fabric js - javascript

EDIT: For clarity, the question is why doesn't the code below work as expected (why does it not animate for the duration of the while loop), how can I improve it and how can I red in the unit that it should travel via a user input slider.
My aim is to have shape animates it's way down the screen.
A button will start and stop the animation. There will also be an input for the rate of change or speed at which it travels.
I can make it continuously travel down the screen but the following code doesn't work - I've used ble as a test variable, in the final scenario I'd hope that this would be replaced with something similar to while(stop != true) or something similar.
startDescent.onclick = function(){
startDescent.disabled = true; //prevent it being clicked again
//animation process
var ble = 1;
while(ble < 10){
console.log(ble);
testDrillbit.animate('top', '+=1',{
duration: 1000,
onChange: canvas.renderAll.bind(canvas),
onComplete: function(){
startDescent.disabled = false;
}
});
ble++;
}
};
the +=1 increment should also read in from a user input box, any suggestions on how to achieve this would also be very welcome. Thanks for all and any help.

I believe you are making use of Fabric JS to provide the animation logic. My answer is based on that assumption.
The issue has to do with your intepretation of how the animate function works. It is not a synchronous call. So your loop will essentially initialize the animate action 10 times, not execute 10 animations. Given that the action you defined was to move object "testDrillBit" down 1 pixel over a period of 1 seconds, it would probably appear like nothing happened.
To approximate the operation you are trying to perform, you would need to employ a callback that basically indicates when the animation is complete, do it again, until the user hits their "stop" button. This would probably cause a jerky animation though. Alternatively you can set an arbitrarly large endpoint for the animation and add an abort handler, but you would then need to determine your rate of change (pixels/time) to come up with the right duration.
It's not clear that this library is appropriate for your implementation, but I cannot offer an alternative at this time. The code example below illustrates the second option while illustrating the points you had asked for, a stop mechanism, arbitray rate of change etc. The significant change is rather than specifying +=1 for the rate of change, we alter the duration it takes for the animation to complete and animate over a larger distance (in this case the canvas height).
First, we add a stop button and an input for our speed:
<button id="stop" disabled="true" onclick="stop=true;">Stop</button>
<form>
<input type="text" id="speed" value="10" />
</form>
Then, in our script box we make sure we can use these values and then employ them in the onclick handler.
var stopBtn = document.getElementById('stop');
var speedBox = document.getElementById('speed');
var stop = false;
startDescent.onclick = function() {
// Get our speed, in case the user changes it. Speed here is actually the duration
// of the animation, not a true velocity. But, we can do something like entering 0.5
// and "slow down" the animation
var speed = 10000 / (new Number(speedBox.value));
stop = false; // ensure that we won't abort immediately
stopBtn.disabled = false; // enable the stop button
startDescent.disabled = true;
// I chose canvas.height as an arbitrary fixed distance. Not this won't stop the
// the element from rolling out of the canvas, its just a fixed value.
// The significant change is the addition of the "abort" function which basically
// polls our stop variable to determine whether the animation should be aborted.
testDrillbit.animate('top', "+="+canvas.height, {
duration: speed,
abort: function () {
// If the user has clicked the stop button, flip our buttons
if (stop) {
startDescent.disabled = false;
stopBtn.disabled = true;
}
return stop;
},
onChange: canvas.renderAll.bind(canvas),
onComplete: function() {
startDescent.disabled = false;
stopBtn.disabled = true;
}
});
};
The above code should allow the user to alter the "speed" by stretching or shortening the amount of time to perform the animation. In addition you have your mechanism to stop the animation partway through the execution.

Related

How to ensure sequence of commands run sequentially (Timeout issue in jQuery - Qualtrics)

I am designing an experiment in qualtrics using java script. For each question in my study, a participant needs to guess the correct answer, type it in, and then hit the space button. Once they hit the space button, a logic checks whether their answer is correct or not. If the answer was correct, a sound should be played for 1.2 seconds, the background color should change into red for a second and then back to white, and lastly the experiment should automatically moves on to the next question.
So far, I figured out the correct sequence of commands. However, it seems like i am not using the setTimeout logic correctly. no matter what combination of timeout values I used, I couldn't get it play all the middles steps, including the audio play and red background flash, before the screen moves to the next question.
Here, I 've shared my code. I would very much appreciate any help.
Qualtrics.SurveyEngine.addOnload(function()
{
/*Place your JavaScript here to run when the page loads*/
});
Qualtrics.SurveyEngine.addOnReady(function()
{
/*Place your JavaScript here to run when the page is fully displayed*/
var qid = this.questionId;
var changepage = null;
/*focus on the box*/
jQuery("#"+qid+" .InputText").select().focus();
document.onkeydown = function(event) {
console.log('keydown',event);
if (event.which == 32) { //hit the space button
event.preventDefault();
//read the input
input = jQuery("#"+qid+" .InputText").val();
console.log(input);
if (input.trim().toLowerCase() == "cat") {
//alert(input.trim().toLowerCase());
document.getElementById("correct").play(); //play the correct sound which is 1.2seconds long
setTimeout(jQuery("body").css("background-color","red"), 3000); // change the background color
setTimeout( jQuery('#NextButton').click(), 2000) //move on to the next question
} //end of if
} // end of if 32
} //end of down button
});
Qualtrics.SurveyEngine.addOnUnload(function()
{
jQuery("body").css("background-color","white");
});
setTimeout is asynchronous. If you want multiple setTimeouts to execute in a fixed order then the times have to be additive. So your NextButton timing should be 5000ms (3000ms + 2000ms).
Alternatively, you could put the NextButton click inside the background color change setTimeout function. To add to that, could you put both inside an 'ended' event on your audio.
if (input.trim().toLowerCase() == "cat") {
//alert(input.trim().toLowerCase());
//play the correct sound which is 1.2seconds long
vid.play();
vid.onended = function()
{
jQuery(".skirt").css("background-color","red");
jQuery('#NextButton').click();
};
} //end of if

getBoundingClientRect during throttled scroll event handling

I faced an issue during my project developing, it's related to a difference between getBoundingClientRect values with and without preventive break points during debugging. Trying to minimize repro, I got following.
const scrollHandler = () => {
setTimeout(() => {
const top = document.getElementById('test').getBoundingClientRect().top;
console.log(top);
});
};
document.getElementById('viewport').addEventListener('scroll', scrollHandler);
where the viewport is just scrollable div with fixed height. The content of the viewport is big enough to be able to fire at least one scroll event. I also created Plunker demo. All magic happens inside of setTimeout callback.
Now the steps. Success scenario.
Set break point at the beginning of the setTimeout callback.
Fire scroll event.
Make "step over" to obtain top value.
Execute document.getElementById('test').getBoundingClientRect().top in the console.
The results of 3 and 4 are the same.
Failed scenario.
Set break point at the end of the setTimeout callback.
Fire scroll event.
Get the value of top variable (no action is needed).
Execute document.getElementById('test').getBoundingClientRect().top in the console.
The results of 3 and 4 are not the same.
To avoid any misunderstanding with this repro, I recorded a short demo movie with the above steps.
In my project I'm doing calculations in a similar environment (throttled scroll events) and getting wrong results that don't match expectations. But when I debug it, I'm getting different picture; a preventive break point fixes calculations.
Is it a bug or known issue? Or did I miss something? should I refuse to use getBoundingClientRect in such a situation?
I'm not sure what it is that you are looking for but assume your animation on scroll isn't working as expected.
There is a tip on throttling scroll here that is incorrect. Let's say you throttle to every 30 milliseconds. If page stops scrolling 25 milliseconds after last animation then you're stuck with a scroll position 25 milliseconds before it stopped scrolling.
Maybe a better way to do it is this (can test in the console on this page):
var ol = document.querySelectorAll("ol")[2];
window.onscroll = ((lastExecuted)=>{
const expensiveHandler = e => {
lastExecuted=Date.now();
//do some animation maybe with requestAnimationFrame
// it may prevent overloading but could make animation
// glitchy if system is busy
console.log("1:",ol.getBoundingClientRect().top);
};
var timerID
return e=>{
if(Date.now()-lastExecuted>19){
//only animate once every 20 milliseconds
clearTimeout(timerID);
expensiveHandler(e);
}else{
//make sure the last scroll event does the right animation
// delay of 20 milliseconds
console.log("nope")
clearTimeout(timerID);//only the last is needed
timerID=setTimeout(e=>expensiveHandler(e),20);
}
}
})(Date.now())

SetInterval loop relatively confusing to me

HTML
<div id="backspace" ng-click="deleteString(''); decrementCursor();">
JS
<script>
$scope.deleteString = function() {
if($scope.cursorPosVal > 0){
//$scope.name = $scope.name - letter;
$scope.name = [$scope.name.slice(0, $scope.cursorPosVal - 1) + $scope.name.slice($scope.cursorPosVal)].join('');
console.log($scope.name);
setTimeout(function(){ setCaretPosition("inputBox", $scope.cursorPosVal); }, 30);
} else {
$scope.cursorPosVal = 1;
}
};
</script>
I am designing an on screen touchscreen keyboard. This is my backspace button. I am going to make it so that when you click and hold the backspace button, it starts removing characters automatically. I don't know where to begin with creating a setInterval, and I know a setInterval is exactly what I need to use here.
If I'm not wrong, you want that while you're keeping your button pressed, a function repeats itself.
You're right with setInterval(). However, the way you manage the event is wrong.
Take a look at this fiddle (It's not your code, but a simple example is the best way to understand):
http://jsfiddle.net/daq9atdd/1/
$(function(){
var interval = null;
$('#myButton').mousedown(function(){
interval = setInterval(function(){
console.log('Hello !');
}, 250);
});
$('#myButton').mouseup(function(){
clearInterval(interval);
});
});
I start the interval when the button is pressed, store it, and clear it when the button is released.
You’re so sure about setInterval.
If browser briefly hangs for whatever reason (say some background task), setInterval would go on queueing your backspace calls until it has some CPU time. This means user may see no change and hold backspace longer than needed, and then see a whole bunch of characters suddenly vanish when browser is back to normal.
Thus by setting a timeout after every call you’re making sure user won’t remove more characters than needed. Might be important if the goal is to improve UX.
Example implementation with AngularJS directives and setTimeout
See also:
setTimeout or setInterval?
noKid’s fiddle updated with setTimeout in mind

html5 canvas javascript wait for user input

I am very new to javascript.
I am working a webgame. I am trying to use for loop to let players enter their names and select gamepiece by clicking. I'm struggling to make code to wait for user input (click on a game piece image) before moving on to the next player.
function GetPlayerNames(){
for (var i=1; i<=NumberPlayers; i++) {
ctxPlayers.fillStyle = "blue";
ctxPlayers.textAlign = "center";
var PlayerName = prompt("Name");
ctxPlayers.fillText (PlayerName + " Select Game Piece", cnvPlayers.width/2,cnvPlayers.height* (1/6));
// here i want to wait for a player to click on a game piece image
};
};
in vb.net version i used do while loop with application.doevents. It's my understanidng javascript doesn't have an equivalent, but i hope for a rather simple solution that will allow my to accomplish the same.
Thanks in advance.
The problem is that you're using prompt, which stops the execution of the script, and then you need to wait for a click, but there's no way to stop the execution for that like you can with prompt.
A better approach would be to check the values of all player names and images each time there is a change in a name or an image is clicked.
Will not give you any code for that, learn how you'd do it, it should be enough to get you started. So please don't use prompt combined with click listeners, but create an input field.
Here's a super simple example:
// keeping our input elements in a variable
var inputs = document.getElementsByTagName('input');
// loop over them to attach them a keyup listener
for(var i = 0; i < inputs.length; i++) {
inputs[i].addEventListener('keyup', checkNames);
}
// this function is called on each keyup event (and once in the beginning)
function checkNames() {
var playersReady = 0;
for(var i = 0; i < inputs.length; i++) {
if (inputs[i].value != "") {
playersReady++; // increment the value if the input is not empty
}
}
// output the message
document.getElementById('status').textContent = "Waiting for " +
(inputs.length - playersReady) + " more player" +
(inputs.length - playersReady == 1 ? "" : "s") + // this line is just for pluralizing
"..." +
(inputs.length - playersReady == 0 ? "The game can start now!" : "");
}
// initial call, just so we get the first status message
// without needing a keyup event first
checkNames();
<input type="text" placeholder="Player1 name" />
<input type="text" placeholder="Player2 name" />
<input type="text" placeholder="Player3 name" />
<p id="status"></p>
You can expand this example by adding images, and keeping track of how many of them are selected, similarly to how I keep track of non-empty player names.
Once you get comfortable with that, a bit more advanced task would be to try creating boxes for player names directly on canvas and listening for focus and keyboard inputs on them, but that's a whole another question...
Not required. When input occurs, the browser will call the JavaScript functions that you have attached to those events. Simply put, the browser has its own main loop that calls your code on a standardized set of conditions.
Let's say someone presses something on the keyboard. The browser will fire keydown and keyup events as the button is pressed and released. Also, if you use the statement window.requestAnimationFrame(yourFunction);, then yourFunction() will be called as early as possible in the next frame. If yourFunction() also calls window.requestAnimationFrame(yourFunction);, then yourFunction() will be your main loop.
In your case, most of your heavy code will be attached to the mousedown, mouseup, or click events. You should not need a main loop.
function yourClickHandler() {
//Whatever happens when your gamepiece is clicked.
}
/*
* This attaches the click event to some element that you use as your gamepiece.
* If you're using Canvas, you will attach it to the canvas (instead of #GamePiece
* and then need to figure out what is in the pixel that you clicked on in it.
*/
document.getElementById("#GamePiece").addEventListener("click", yourClickHandler, false);
Your game can just sleep between user clicks, unless you need complex animations and stuff that cannot be done with CSS transitions and so forth. If it's, say, a turn-based strategy, then you can just make the piece look clicked, then sleep, then give it a destination, then sleep, then select something else, then sleep. Etc.
If you do need complex animations and stuff...
Then it is best to have yourClickHandler() do as little as possible. Try to do nothing more than set a variable and return. Your requestAnimationFrame()-based draw/update functions should use those variables to perform the intense calculations then.
For instance, if you are making a character walk, let all the walking / falling / etc. happen once per frame. Just keep track of whether the button is pressed or not (or how far a joystick is tilted, etc.)
The reason for this is simple: Drawing happens once per frame, but input events might happen several times. You want to draw to the screen only once per draw to the monitor. Also, input events can happen at any time. You don't want a big calculation to happen a tenth of a millisecond before the frame is needed by the monitor. You want that to happen as early in the frame as possible.

javascript - bad algorithm firing concurrent loops

I created a relatively small dynamic banner rotation script with icons at the bottom for bringing a particular banner into focus. Firing a mouseenter over a banner pauses the show, but sometimes when I mouseout from my banner, the delay for certain banners gets shortened. I'd even understand if it just happened once, but the delay is then set for that shorter amount of time every time the banner comes back around in the rotation, and often the shortening happens in one other place in the list of banners, as well. Sometimes this can be corrected by an as yet undetermined set of actions. I'm starting to suspect that my logic is catching the loop in the middle somewhere and so the process branches out, runs two loops, which appear to speed up the calling of the showNextBanner function. Not sure how to solve this. I've put in tests to see if it's currently in play mode, to no avail.
I include what I think are the relevant parts of the code below.
var firstRun = true;
var play = true;
var current = 0;
var banners = $$( '.banner' );
banners.invoke( 'hide' );
var images = $$( '.image' );
var icons = $$( '.icon' );
//dynamically clones an initial icon to match the number of banners
initIcons();
banners.invoke( 'observe', 'mouseenter', function( field ) {
play = false;
});
banners.invoke( 'observe', 'mouseleave', function( field ) {
if( !play ) {
play = true;
showNextBanner().delay(3);
}
});
icons.invoke( 'observe', 'click', function( field ) {
play = false;
hideBanner( current );
showBanner( findObj( icons, field.findElement()));
});
showNextBanner().delay(3);
function hideBanner( which ) {
icons[ which ].src = blankIconSRC;
banners[ which ].hide();
}
function showBanner( which ) {
icons[ which ].src = selectedIconSRC;
banners[ which ].show();
current = which;
}
// loops the hiding and showing of icons
// (mouseenter sets play to false)
function showNextBanner() {
if( play ) {
if( !firstRun ) {
if( ++current == banners.length ) current = 0;
var previous = 0;
( current == 0 )? previous = banners.length - 1: previous = current - 1;
hideBanner( previous );
} else {
icons[0].src = selectedIconSRC;
firstRun = false;
}
showBanner( current );
showNextBanner.delay(3);
}
}
}());
After all that, the client wants a jQuery solution so he can have a slide-in effect not available via scriptaculous. So all that work is down the drain. The good news is that I can just use jCarousel, probably, and tweak the stylesheet. Thanks for the help!
I suspect what is happening is that you've got multiple .delay calls stacking up. So you've got one with less than 3 seconds remaining and showNextBanner is called again, setting another delay timer.
As I read the docs, it appears .delay is intended to put gaps in the jquery event pipeline, rather than actually delay function calls. You may benefit from switching to calling setTimeout instead of delay, so that you get a handle to the timeout, which you can then cancel before setting a new timeout (or cancel if play is set to false, then reset when play is true again) This is mentioned in the JQuery docs for .delay
My guess is that since you don't "cancel" the delay()'ed function, they hang around for too long, but they don't do anything when they fire, because play is false. But once play is true again, the all start having an effect again.
You can save the returned value for delay() and cancel the timer by using clearTimeout() with the value.
However, I'd also suggest that you use a single container for all the banners (and maybe put the the icons in there too), and set the mouseenter/mouseleave events on that, rather than on individual banners. Then there's just a single element that'll start/stop the banner rotation. If you also split everything up in specific functions that play and stop the rotation, and one to show a specific banner, you can possibly get a cleaner code structure.
Here's an example (it's just something I put together for fun rather than an edit of your code, so it's quite different. Sorry. But hopefully you can still use for something)

Categories