Javascript easing not animating - javascript

Trying to implement simple easing in javascript (without jQuery) but unfortunately there doesn't seem to be any animation when the button is clicked (see below).
My goal is to get the hidden list item (the last item) visible by tweening the first item's margin left property. I know it isn't a CSS issue because manually modifying the style moves the list, but I'm not sure what the issue is. My guess is with how I'm calling the ease function but changing the params still wasn't working for me.
The easing part is below, entire code is here: Fiddle
JS:
var start = document.getElementById('start'),
list = document.getElementById('my-list'),
imgs = list.getElementsByTagName('img'),
last_img = imgs[imgs.length -1 ];
ease = function(t, b, c, d) {
if ((t/=d/2) < 1) return c/2*t*t + b;
return -c/2 * ((--t)*(t-2) - 1) + b;
},
shift_imgs = function(el) {
var orig_value = parseFloat( el.style.marginLeft ),
end_value = -37,
change = Math.abs( end_value - orig_value ),
duration = 1, // 1 second
time = 0;
for ( var i = 0; i < change; i++ ) {
setTimeout(function() {
el.style.marginLeft = ( parseFloat( el.style.marginLeft ) + 1 ) + 'px';
}, time);
time = ease(time, orig_value, change, duration);
}
};
start.onclick = function() {
shift_imgs(last_img);
}

Your orig_value is NaN as parseFloat(el.style.marginLeft) returns nothing, even if you set an initial value in the css. i.e: margin-left: 15px; still will return nothing.
You can use window.getComputedStyle(...).getPropertyValue, similar to this:
window.getComputedStyle(el, null).getPropertyValue("margin-left");
This will give you the actual current value along with the px, i.e: 0px.
(It always return the value in px even if set in CSS as em or pt)
So you need to remove the px and the get the float value.
You can wrap this into a little helper similar to this:
getElementMarginLeftAsFloat = function (el) {
var pxValue = window.getComputedStyle(el, null).getPropertyValue("margin-left");
var valueOnly = pxValue.substring(0, pxValue.length - 2);
return parseFloat(valueOnly);
}
Another issue is that the moving of the actual element occurs within setTimeout executed inside a loop. The loop which calls setTimeout, causes each setTimeout to be queued nearly simultaneously, hence they all execute close to the same time, causing the element to just jump.
You can use a recursive sub-method inside your method which uses setTimeout to call itself until it is done. That way each setTimeout is triggered only after the specified interval, causing them to be executed close enough apart to the specified interval, similar to this:
shift_imgs = function (el) {
var orig_value = getElementMarginLeftAsFloat(el),
end_value = -37,
change = Math.abs(end_value - orig_value),
duration = 1, // 1 second
time = 0;
function doShift() {
currentValue = getElementMarginLeftAsFloat(el);
if(currentValue+1 > change){
return;
};
el.style.marginLeft = (currentValue + 1) + 'px';
time = ease(time, orig_value, change, duration);
setTimeout(doShift, time);
}
doShift();
};
By having the setTimeout function call itself, it releases the resources, ensuring the drawing of the element can occur between each "iteration".
I updated your code to use this approach and it seems to work now.
DEMO - animating movement using computed style
You can most likely do this many other ways and also prettify this code for sure but but this should get you started either way.

Related

Javascript using a For Loop with setTimeout (or another delay)

I apparently don't fully understand how the setTimeout function works in Javascript:
function move() {
var a;
for(a = 0; a < 101; a++){
setTimeout(function(){
block.style.marginTop = (750 - a) + 'px');
}, 1000);
}
for(a = 0; a < 101; a++){
setTimeout(function(){
block.style.marginTop = (650 + a) + 'px');
}, 1000);
}
}
I have tried writing this out in many different ways, but the For Loops always execute instantly every single time. How can I make a For Loop wait for 1 second between each value of 'a'? So, when a = 0, the code executes and then waits for 1 second before running when a = 1, etc. until the first For Loop is finished, then the second For Loop executes in the same way.
Also, is there a more efficient way of doing this than using setTimeout? Like a way of just writing
sleep(1000);
or something like that. This whole setTimeout feature seems very overly complicated if it is the only way of producing delays in javascript. I tried this once but it didn't work at all in any way
await sleep(1000);
Any help with Timeouts and delays in Javascript, especially within a loop, would be greatly appreciated!
You may find value in the answer I posted here. That will explain setTimeout in loops a little more.
Separately, you may want to explain what you are trying to accomplish. It looks like you are either
trying to move an element one pixel per second
trying to move an element ~100 pixels after one second
For the first option I would use CSS Transitions instead. You'll have a lot more flexibility over how the element moves and you only need to dictate the direction and distance.
For the second option, you could toss the loop and keep the stuff inside, setting the new marginTop to the full value after some timeout.
setTimeout(function(){
block.style.marginTop = (750 - a) + 'px');
}, 1000);
this part of your code use 'a' variable after 1000 miliseconds. in this time 'a' is 100, because your loop not stoped for run setTimeout function, and it happened because javascript is asyncronous.
one solution for solving this problem in js is using recursive functions. if is not necessary to use for loop, you can use this code:
var a = 0;
function my_loop(a) {
if (a < 101) {
setTimeout(function() {
block.style.marginTop = (750 - a) + 'px');
my_loop(a);
}, 100);
a++;
}
}
my_loop(a);
but if you want do your question's job, i seriously recommened you to use CSS.
As #squint mentioned, you can use setInterval for your task.
Here's an example:
// create an element
const width = 10;
const el = document.createElement('div');
el.setAttribute('id', 'main');
document.body.appendChild(el);
el.style.width = width + 'px';
// question-relevant code starts here
const a = [...Array(101).keys()]; // fancy way to create [0, 1, 2, 3, ...]
const it = a[Symbol.iterator](); // for convenience
const int = setInterval(() => {
const { value, done } = it.next(); // next iteration
if (done) { clearInterval(int); return }; // finished?
el.style.width = width + value + 'px'; // adjust width
}, 10);
#main {
height: 100px;
width: 10px;
background: green;
}

While loop and setInterval()

I am trying to mix the initial string and randomly compare the string's elements with the right elements on the right indexes, and if true push them into a set, to reconstruct the initial string. Doing this I met the problem that while loop does nothing just crushng the browser. Help me out with this.
function checker() {
var text = document.getElementById("inp").value;
var a = [];
var i = 0;
while (a.length < text.length) {
var int = setInterval((function() {
var rnd = Math.floor(Math.random() * text.length);
if (text[rnd] === text[i]) {
a.push(text[rnd]);
clearInterval(int);
i++;
}
}), 100)
}
}
P.S. I need the setInterval() function because I need the process to happen in exactly the same periods of time.
So, you stumbled into the pitfall most people hit at some point when they get in touch with asynchronous programming.
You cannot "wait" for an timeout/interval to finish - trying to do so would not work or block the whole page/browser. Any code that should run after the delay needs to be called from the callback you passed to setInterval when it's "done".
In my answer its doing exactly what you want - creating exactly the same string by randomly mixing the initial, and also using setInterval. You didn't write where you want the result, so you have it written in the console and also in another input field with id output_string.
HTML:
<input id="input_string" value="some_text" />
<input id="output_string" value="" readonly="readonly" />
JavaScript:
function checker() {
var text = document.getElementById("input_string").value;
var result = '';
// split your input string to array
text = text.split('');
var int = setInterval((function() {
var rnd = Math.floor(Math.random() * text.length);
// add random character from input string (array) to the result
result += text[rnd];
// remove used element from the input array
text.splice(rnd, 1);
// if all characters were used
if (text.length === 0) {
clearInterval(int);
console.log(result);
document.getElementById("output_string").value = result;
}
}), 100);
}
checker();
DEMO
Honestly, I have no idea what you are trying to do here, but you seem to have lost track of how your code is operating exactly.
All your while loop does, is creating the interval, which is ran asynchronous from the loop itself.
In other words, the only way your while condition equates to false, is after multiple 100ms intervals have elapsed. 100 miliseconds is an eternity when comparing it to the speed of 1 loop iteration. We're looking at 1000s of iterations before your first setInterval even triggers, not something a browser can keep up with, let alone wait several of these intervals before you change a.length.
Try more like this:
function checker() {
var text = document.getElementById("inp").value;
var a = [];
var i = 0;
// start to do a check every 100ms.
var interv = setInterval(function() {
var rnd = Math.floor(Math.random() * text.length);
if (text[rnd] === text[i]) {
a.push(text[rnd]);
i++;
}
// at the end of each call, see if a is long enough yet
if(a.length > text.length){
clearInterval(interv); // if so, stop this interval from running
alert(a); // and do whatever you need to in the UI.
}
}, 100);
}
}

jQuery 2.1.0 | Display elements incrementally

I need to display rows of buttons incrementally from top to bottom in a scrollable parent div. These rows of buttons are created using a replace method applied onto a text string converted to html. Since I can have 500+ more rows of buttons to create and display, the text-to-html conversion can take a few seconds or longer, which freezes the UI during conversion. The code below uses a setInterval which unlocks the UI and provides a cool way of "animating" the gradual display of button rows. Problem is, currently, the whole set of rows is repeated at each setInterval, that's not what I want, I can't figure out what to do next. I need each individual row to be displayed incrementally without repeating, from top to bottom, in the order provided by the string, until the length of array is met. The scrolable parent div is a fixed height of 300px. Maybe a Lazy loading method would be better? Any help solving this issue is appreciated.
DEMO fiddle
var placeholder = $('#placeholder');
placeholder.html(placeholdertohtml(placeholder));
function placeholdertohtml(placeholder){
placeholder.html(placeholder.text().replace(/((\d{2},\d{2}))/g, function(match, number){
var blocks = placeholder.text().split(',').length;
console.log(blocks);
var el = number.substr(0,5).split(',');
var prefix = el[0];
var suffix = el[1];
var t = setInterval(function(){
if (blocks) {
var content = '<p><button>'+prefix+'</button><button>'+suffix+'</button></p>';
$('#placeholder').append(content);
blocks--;
} else {
clearInterval(t);
}
}, 100);
}));
}
So, I reworked your code using for loops instead of the replace function to fix the problem.
Here is a working codepen
I basically made a loop that built up an array of html to add:
var numberOfPairs = placeholder.text().match(/((\d{2},\d{2}))/g).length;
var countdown = numberOfPairs;
var string = placeholder.text();
var elements = [];
for(var i = 0; i < numberOfPairs; i++) {
var pair = string.substring(5 * i + 1, (5 * i) + 6).split(',');
var prefix = pair[0];
var suffix = pair[1];
elements.push('<p><button>'+prefix+'</button><button>'+suffix+'</button></p>');
}
And then looped over the elements with your interval function to get the same "loading" effect:
var elementIndex = countdown;
var t = setInterval(function(){
if (countdown >= 0) {
$('#placeholder').append(elements[(countdown - elementIndex) * -1]);
countdown--;
} else {
clearInterval(t);
}
}, 100);

Handling events with new data before previous event has completed

This code takes two inputs: div, the div (actually a textbox) and target (a number). It'll then try and in/decrement the number in a pseudo-animated way. The problem is that I'm using jQuery sliders as one form of input, which can result in multiple calls before the first call finished. This isn't a problem unless the slider is quickly increased, and then decreased before the increase rollUp finishes, resulting in an eternal decrementing div. I can't figure out what's causing it. Thoughts?
function rollNum(div, target) {
var contentString = $(div).val();
content = parseInt(contentString.substring(1));
if(content === target)
return;
else if(div !== "#costMinusMSP" && div !== "#savingsWithMSP") {
var total = rollNumTotalCost(div, target);
rollNum("#costMinusMSP", total);
rollNum("#savingsWithMSP", total /*- somehow find the cost here*/)
}
if(isNaN(content))
content = 0;
var remainingChange = target - content;
if(remainingChange > 0)
loopUp();
else
loopDown();
function loopUp() {
var length = remainingChange.toString().length;
var incrementBy = 1;
//Find how far away we are from target
for(var i=0;i<length-1;i++)
incrementBy *= 10;
content += incrementBy;
remainingChange -= incrementBy;
$(div).val("$" + (content))
if(content === target)
return;
else if(content > target) {
$(div).val("$" + (target));
return;
}
setTimeout(loopUp, 60);
}
function loopDown() {
remainingChange = Math.abs(remainingChange);
var length = remainingChange.toString().length;
var decrementBy = 1;
//Find how far away we are from target
for(var i=0;i<length-1;i++)
decrementBy *= 10;
content -= decrementBy;
remainingChange -= decrementBy;
if(content < target) {
$(div).val("$" + (target));
return;
}
//This ensures we won't promise our clients negative values.
if(content <= 0) {
$(div).val("$0");
return;
}
$(div).val("$" + (content))
if(content === target)
return;
setTimeout(loopDown, 60);
}
}
Strangely enough, adjusting another slider (that modifies an unrelated div) fixes the eternal decrement.
Things I have tried:
-Creating a boolean "running" that the function sets to true, then false before it returns. If running was true, then the function would wait until it was false to continue executing. This killed the browser or achieved maximum stack.
SomeKittens of years ago: You've learned a lot since you asked this, particularly about managing state & multiple events (not to mention how to properly ask a StackOverflow question). A simple answer would be something like this:
var rolling = false;
function rollNum(div, target) {
if (rolling) { return; }
rolling = true;
// Set rolling to false when done
}
That's all well and good but it ignores any events that are fired while we're rolling. The above won't adjust to changes on the slider made after the first adjustment, but before the numbers have finished rolling. Now, I (we?) would use Angular ($scope.$watch would come in handy here) but that didn't exist when you were working on this. Instead of passing a target number, why don't we check against the live value on the slider? (Note the use of vanilla JS, it's much faster).
var rollNum = function(textarea) {
var content = parseInt(textarea.value.substring(1), 10)
, target = parseInt(document.getElementById('sliderId').value, 10);
if (content === target) {
return;
}
// Roll up/down logic
setTimeout(function() { rollNum(textarea); }, 60);
};
A few other misc changes:
Use brackets after if statements. Waaaay easier to debug
Don't forget the radix param in parseInt
Unfortunately, you didn't think to include a JSFiddle, so I can't provide a live demonstration.

More efficient comparison of numbers

I have an array which is part of a small JS game I am working on I need to check (as often as reasonable) that each of the elements in the array haven't left the "stage" or "playground", so I can remove them and save the script load
I have coded the below and was wondering if anyone knew a faster/more efficient way to calculate this. This is run every 50ms (it deals with the movement).
Where bots[i][1] is movement in X and bots[i][2] is movement in Y (mutually exclusive).
for (var i in bots) {
var left = parseInt($("#" + i).css("left"));
var top = parseInt($("#" + i).css("top"));
var nextleft = left + bots[i][1];
var nexttop = top + bots[i][2];
if(bots[i][1]>0&&nextleft>=PLAYGROUND_WIDTH) { remove_bot(i); }
else if(bots[i][1]<0&&nextleft<=-GRID_SIZE) { remove_bot(i); }
else if(bots[i][2]>0&&nexttop>=PLAYGROUND_HEIGHT) { remove_bot(i); }
else if(bots[i][2]<0&&nexttop<=-GRID_SIZE) { remove_bot(i); }
else {
//alert(nextleft + ":" + nexttop);
$("#" + i).css("left", ""+(nextleft)+"px");
$("#" + i).css("top", ""+(nexttop)+"px");
}
}
On a similar note the remove_bot(i); function is as below, is this correct (I can't splice as it changes all the ID's of the elements in the array.
function remove_bot(i) {
$("#" + i).remove();
bots[i] = false;
}
Many thanks for any advice given!
Cache $("#" + i) in a variable; each time you do this, a new jQuery object is being created.
var self = $('#' + i);
var left = parseInt(self.css("left"));
var top = parseInt(self.css("top"));
Cache bots[i] in a variable:
var current = bots[i];
var nextleft = left + current[1];
var nexttop = top + current[2];
Store (cache) the jQuery object of the DOM element within the bot representation. At the moment it's been created every 50ms.
What I mean by this is that for every iteration of the loop, you're doing $('#' + i). Every time you call this, jQuery is building a jQuery object of the DOM element. This is far from trivial compared to other aspects of JS. DOM traversal/ manipulation is by far the slowest area of JavaScript.
As the result of $('#' + i) never changes for each bot, why not store the result within the bot? This way $('#' + i) gets executed once, instead of every 50ms.
In my example below, I've stored this reference in the element attribute of my Bot objects, but you can add it your bot (i.e in bots[i][3])
Store (cache) the position of the DOM element representing the bot within the bot representation, so the CSS position doesn't have to be calculated all the time.
On a side note, for (.. in ..) should be strictly used for iterating over objects, not arrays. Arrays should be iterated over using for (..;..;..)
Variables are extremely cheap in JavaScript; abuse them.
Here's an implementation I'd choose, which incorporates the suggestions I've made:
function Bot (x, y, movementX, movementY, playground) {
this.x = x;
this.y = y;
this.element = $('<div class="bot"/>').appendTo(playground);
this.movementX = movementX;
this.movementY = movementY;
};
Bot.prototype.update = function () {
this.x += this.movementX,
this.y += this.movementY;
if (this.movementX > 0 && this.x >= PLAYGROUP_WIDTH ||
this.movementX < 0 && this.x <= -GRID_SIZE ||
this.movementY > 0 && this.y >= PLAYGROUND_HEIGHT ||
this.movementY < 0 && this.y <= -GRIDSIZE) {
this.remove();
} else {
this.element.css({
left: this.x,
right: this.y
});
};
};
Bot.prototype.remove = function () {
this.element.remove();
// other stuff?
};
var playground = $('#playground');
var bots = [new Bot(0, 0, 1, 1, playground), new Bot(0, 0, 5, -5, playground), new Bot(10, 10, 10, -10, playground)];
setInterval(function () {
var i = bots.length;
while (i--) {
bots[i].update();
};
}, 50);
You're using parseInt. As far as I know, a bitwise OR 0 is faster than parseInt. So you could write
var left = $("#" + i).css("left") | 0;
instead.
Furthermore, I wouldn't make use of jQuery functions to obtain values like these every 50 ms, as there's always a bit more overhead when using those (the $ function has to parse its arguments, etc.). Just use native JavaScript functions to optimize these lines. Moreover, with your code, the element with id i has to be retrieved several times. Store those elements in a variable:
var item = document.getElementById(i);
var iStyle = item.style;
var left = iStyle.left;
…
(Please note that I'm not a jQuery expert, so I'm not 100% sure this does the same.)
Moreover, decrementing while loops are faster than for loops (reference). If there's no problem with looping through the elements in reverse order, you could rewrite your code to
var i = bots.length;
while (i--) {
…
}
Use offset() or position() depending on if you need coordinates relative to the document or the parent. position() is most likely faster since browsers are efficient at finding offsets relative to the parent. There's no need for parsing the CSS. You also don't need the left and top variables since you only use them once. It may not be as readable but you're going for efficiency:
var left = $("#" + i).position().left + bots[i][1];
var top = $("#" + i).position().top + bots[i][2];
Take a look here for a great comparison of different looping techniques in javascript.
Using for...in has poor performance and isn't recommended on arrays. An alternative to looping backwards and still using a for loop is to cache the length so you don't look it up with each iteration. Something like this:
for(var i, len = bots.length; i < len; i++) { ... }
But there are MANY different ways, as shown in the link above and you might want to test several with your actual application to see what works best in your case.

Categories