I'm trying to get the position of a div being translated using CSS3 inside another div with a overflow: hidden property.
Here's a jsFiddle that demonstrates it: https://jsfiddle.net/meeuz3w9
The top position of the element is not updated when trying to get it with jQuery's position().top.
I have no idea why this is happening, does anyone have a solution?
Updated: This doesn't work on Chrome 44 on OSX, but works on other browsers
After playing with it a while, I guess I have an answer:
This is a performance issue
Chrome uses the resources to make the animation more fluid making the script to get less callbacks called and less positions updated.
While Firefox uses the resources to keep both updated, wich makes the animation not so fluid, but the script gets more updated positions.
In Chrome the profile of the calls is also very irregular (between 2 and 100 calls per position), while in Firefox keeps under 4 calls per position.
In this fiddle I tried to get better performance, using a native calculus in a global variable:
var position = function() {
return this.getBoundingClientRect().top -
this.offsetParent.getBoundingClientRect().top;
};
and avoiding the use of console.log ...
var callback = function(){
var top = position.call(callback.target);
if(benchmark[top] === undefined){
benchmark[top] = 0;
} else {
benchmark[top] += 1;
}
};
then I found this perfomance difference.
How to solve it
You can get more info here about the differences between JavaScript animations and CSS animations:
https://css-tricks.com/myth-busting-css-animations-vs-javascript/
So after this reading, I propose the following solution:
Do it with JavaScript
The animation you describe is as easy as:
$('#bar').animate({
'translate3d': '-2000'
}, {
step: function (now, fx) {
console.log(now);
$(this).css({"transform": "translate3d(0px, " + now + "px, 0px)"});
},
duration: 1000,
easing: 'linear',
queue: false
},
'linear');
this way you will be able to handle the position on javascript on every tick instead of asking it to the CSS.
I hope it helps.
Related
I have a section of code which is supposed to move this image ( id = 'background' ), which I have downloaded locally, and is quite large. It is supposed to move when I hover over top of a certain div(s). This then changes the opacity CSS value, which in turn is detected by the js, which then makes the image move. The js code looks like this:
setInterval(function(){
var element = document.getElementById('background'),
style = window.getComputedStyle(element),
left = style.getPropertyValue('left');
left = left.replace(/px/g, "")
left = parseInt(left,10)
if(getOpacity('rightbar') == .5){
document.getElementById('background').style.left = left - 8 + 'px'
}
if(getOpacity('leftbar') == .5){
document.getElementById('background').style.left = left + 8 + 'px'
}
},10)
The getOpacity(idName) function looks like this:
function getOpacity(param){
var element = document.getElementById(param),
style = window.getComputedStyle(element),
opacity = style.getPropertyValue('opacity');
return opacity
}
So the problem is that, no matter what movement values or setInteveral time I use, the animation always makes out to be laggy. Is there a way to make this smooth with vanilla js, or better yet, to scrap the opacity detection and do it all with CSS?
It works fine when I put the above code in a fiddle, but when it actually runs full browser (on my personal chrome window), it looks like this.
Note: I am running this full browser window on a 4k monitor, is this just too much for chrome to handle?
1. Use requestAnimationFrame instead of setInterval
This signals the browser you want to do something before the next redraw. The callback you provide is executed exactly once per frame.
(In case this matters: requestAnimationFrame does not work in IE9 and below.)
2. Don't increase by a fixed value per frame, tween between values
Both when using requestAnimationFrame and using setInterval, the time difference between frames vary.
You can verify that yourself by using something like this in the developer toolbar:
var last = new Date();
function onFrame(){
var now = new Date();
console.log(new Date() - last);
last = now;
requestAnimationFrame(onFrame);
}
onFrame();
The developer console will output the frame times in ms, like this:
16
17
17
15
19
...
If you increase position (not so noticeable on e.g. opacity) by a fixed amount on vaying intervals, the animation will look jagged. So Instead of doing left = left + 8;, calculate at which position in the animation you are, based on the current time, something like this:
var myElement = ...;
var initialOpacity = 1.0;
var targetOpacity = 0.5;
var duration = 2000;
var startTime = new Date();
function animation() {
var delta = Math.min(1, (new Date() - startTime) / duration);
// delta is now a number in the range [0 ... 1]
myElement.style.opacity = initialOpacity + delta * (targetOpacity - initialOpacity);
if (delta < 1) requestAnimationFrame(animation);
}
requestAnimationFrame(animation);
Yes, this example tweens opacity and not position, but you get the idea - and your teacher can't claim you copy-pasted ;-)
3. Don't read and write back-and-forth between JS and CSS
Assuming the initial position of your image is not viewport-related (say, left: -10%), there is no need to read the position on every frame.
When your JavaScript is the only thing changing the left property, why read it from CSS? Save it in a variable and set it to CSS from your JavaScript.
var initialX = ComputeCssXPosition(myElement);
...
function animate() {
...
myElement.style.left = computedNewXPosition;
}
If you want to change the postition when the user hovers an element, use mouse events in your JS.
myElement.addEventListener('mouseover', function (ev) { ... });
myElement.addEventListener('mouseout', function (ev) { ... });
Alternative: Use CSS transitions
Already covered in the answer by Shomz.
The best approach would be to use requestAnimationFrame instead of setInterval and not to check for the style changes, but use mouseover listeners (too much communication: CSS->JS->CSS->JS...). See here: https://jsfiddle.net/yqpun3eb/4/
However, if you still want to use setInterval, you can simply put the transition CSS rule on your background element. For example:
#background {transition: left 0.2s linear}
That will smooth out all the value changes because CSS performs way better, so it should be fine even on the 4K screens. The problem was your changes can jump by 8 pixels.
Seems to work nice with 0.2s on my machine: https://jsfiddle.net/yqpun3eb/3/
Oh, and btw. you want good performance, but why are you raping the system with this:
function getOpacity(param){
var element = document.getElementById(param),
style = window.getComputedStyle(element),
opacity = style.getPropertyValue('opacity');
return opacity
}
This doesn't create extra variables (which you don't need anyway):
function getOpacity(param){
return window.getComputedStyle(document.getElementById(param))
.getPropertyValue('opacity');
}
Finally, here's a slightly optimized version using requestAnimationFrame (that's how I would do it + use listeners instead of reading the style values): https://jsfiddle.net/yqpun3eb/4/
I'm trying to improve an animation that I've been working on where things move across the screen.
Currently the object moves at a set speed and has no variance.
I'm trying to include two features that will ultimately end up doing the same thing; changing the speed of the animated object.
I'd like the user to be able to change the speed and also for the object to slow down or speed up depending on where it is on the screen.
I'm not sure if I'm looking in the right place as currently I've been unable to update the duration once the animation loop has started. I first thought I could replace the number with a function that would return an int. This works in that the value of 'speed' changes but the animate loop is not updated.
Any help is hugely appreciated, thanks.
Code snippets below.
function moveObj () {
//initially the duration was set here. I understand that will not work as the animation is only
//being called once.
//animation process
obj.animate('top', '+=' + canvas.height, {
duration: speedOfObj(0),
abort: function () {
},//end abort callback
onChange: function () {
//testing only//
speedOfObj(1000);
}
//test function to see what the results would be. speed changes when called within the on change but the animation is not affected.
function speedOfObj(modifier){
var speed = 10000 / (new Number(speedControl.value));
if(modifier == 0){
console.log("speed: "+speed);
return speed;
}else{
speed *= modifier;
console.log("speedBBBB: "+speed);
return speed;
}
}
Once a jQuery animation is off and running, it's pretty much off on its own. If you want to change how it works, you can .stop(true) it and then start up a new animation that starts again from where it is now at your new speed.
It's also possible to implement a custom step function in the animation that might takes some queues for how to work from outside influences that can change during the animation, but I think that would end being much more complicated than just stopping the original animation and starting a new one that moves at the newly desired speed.
Working demo: http://jsfiddle.net/jfriend00/tzxca/
some time ago I started to write some code in JavaScript to learn it a little bit. I picked a rollin/rollout animation as 'project'. (I know about JQuery's slideDown/slideUp, but I wanted to work with pure JavaScript.)
I finished my effect, and the result looks pretty good in all major browsers except Firefox (tested versions 22.x to the latest (25.0.1)). In Firefox, the rolling (in and out) stutters while it rolls smoothly in Chrome, Opera, and Internet Explorer.
The general approach is (unsurprisingly) to have an element's style.height (or width) attribute increased/decreased several times by some pixels over a given time. To avoid calculating sizes every time the effect takes place, I calculate them one time and place them in an array (first item (0 + stepSize), last item wanted height/width). The decrease of the element's height is done by this function:
var verticalRollInWorker = function(step) {
if (step > 0) {
$(btt).style.height = stepSizes[step - 1];
setTimeout(function() { verticalRollInWorker(step - 1); }, delay);
} else {
$(btt).style.display = "none";
$(btt).style.height = 0;
// Enable roll out effect:
stateChange(false);
if (afterFullRollIn != null) {
afterFullRollIn();
}
}
}
In the particular example, I'm using 20 steps over 400ms. The step sizes in the array are rounded to integers, that's why the last step just sets 0 - to handle rounding differences.
(For convenience, I wrote my own $(element) helper, there's no JQuery involved here.)
I tested Firefox without Add-Ons, no difference.
I highly appreciate any help you can provide :)
One problem that I noticed in the above code is that you used $(btt). So, every 20s when the function is iterated, the browser needs to obtain the jQuery object. You could rather store it into a variable say 'var BTT=$(btt);' and use this BTT. Fetching jQuery object is a time consuming task.
Since you are using setTimeout(), the function will be executed every 20ms regardless of the completion of the current execution, this may also cause a drag. As Dagg Nabbit said, you could use setInterval() instad of setTimeout.
Another possible reason might be browser-reflow. I made a personalised scrollbar, and found browser reflow was noticeably greater in my FF than Chrome or IE. This depends on the size of the element, DOM tree depth, overflow property, and more...
And again use this code and see if it is fixed. reduces the subtraction into 1 code.
var BTT=$(btt).get(0);
var verticalRollInWorker = function(step) {
if (step > 0) {
step--;
BTT.style.height = stepSizes[step];
setTimeout(function() { verticalRollInWorker(step); }, delay);
}
else {
BTT.style.display = "none";
BTT.style.height = 0;
// Enable roll out effect:
stateChange(false);
if (afterFullRollIn != null) {
afterFullRollIn();
}
}
}
Further Comments can be made only after seeing a live example.
Regards.
Here is the problem, I've got a tree structure of html blocks, global container is of a fixed width(X) and height(Y). When i click one of the blocks on a level, all other blocks shrink to some size, while the clicked one gets enlarged to the leftover space, and the sublevels show up on it's place.
For all the shrinking i'm using default animate function with easing effect, when shrinking 1 level, to avoid enlargement bugs i have to do something like this:
$tabs.not($obj).animate({height:32<<$obj.getVerUp().length+"px"},{duration:300,
step:function() {
$obj.height(function(){
var sum = 0;
$tabs.not($obj).each(function(){
sum += $(this).height();
});
return $obj.getCont().height()-sum+"px";
});
}
});
$tabs are all the tabs of current level, $obj - is the one tab that i want to enlarge
The main problem is:
When i open up a tab that is on a deep level, i have to animate all the tabs of higher levels to shrink a little bit more, thus the $obj X and Y would change, so the current animation has to use new values, but if i call 3 different animations on different levels i'm bound to get a bug, when one of the animations on a deeper level finishes 1 step earlier, while the one on the level above, would enlarge the object by 5-10 more pixels and that space wouldn't be used up.
The second problem is that there has to be about 50 object animating with easing at the same time, which is a little bit overkill.
And the last problem is when i call step callback on animation as shown above, i have a strange feeling that it calls the step separately for each animation of the $tabs collection, while i need 1 step for all the tabs in the list (to avoid unnecessary scripts)
There might be some other way to fix all that, but i have yet to discover all jQuery functions, so from what i see the only way is to simulate easing, and do everything in one single animation.
I don't really want to use setInterval and determining when do i need to clear it plus calculating each of the easing values, if there is a simple way doing it.
Does jQuery has some sort of empty animation easing, e.g.
$().css("height":starth+"px").animate({height:endh},{duration:300,
step:function(fn) {
// all the animation actions here via fn end value
}
});
Thanks in advance.
What I need - is not a completely working solution in code, just some enlightenment in those subjects:
Is there a legal way to call one step function for a collection of animated elements, or, maybe, it does call step once when I use one .animate on collection.
I'd be really appreciated if someone would shed some light over how does jquery handle multiple .animate, would they be used in one global function that works on .setInterval? or would they be having massive number of those .setIntervals that are equivalent to setTimeout (which most browsers can't handle in large amounts);
Is there a way to simulate 'animate' easing, some function name maybe, or a special trick to achieve that (the only thing I see is a hidden element or 'window' property to change maybe)
Or some directed pushes with functions I should study, that could help me achieve my goals
Guess i pretty much found the answer to my questions:
http://james.padolsey.com/javascript/fun-with-jquerys-animate/
Here's the empty animation from the link above with 1 step function with desired values, going to post the result later on if it all works out.
var from = {property: 0};
var to = {property: 100};
jQuery(from).animate(to, {
duration: 100,
step: function() {
console.log( 'Currently # ' + this.property );
}
});
Yes it all worked great, no desynch, and a good speed, since only 1 animate, found making one universal function for the animation - waste of resourses, so it is pretty specific, but still, here it is:
animate: function($obj) {
var T = this;
...
T.arr = new Array();
// gathering the array
$obj.each(function(i){
var size;
T.arr[i] = {obj:$(this), rest:$(this).getSibl(), cont:$(this).getCont()}
if($(this).hasClass("vert"))
{
size = "height";
T.arr[i].to = yto;
}
else
{
size = "width";
T.arr[i].to = xto;
T.arr[i].children = $(this).getChld();
}
T.arr[i].rest.each(function(){
$(this).attr("from",$(this)[size]());
});
});
// animating prop
jQuery({prop:0}).animate({prop:1}, {
duration: 300,
step: function() {
var i;
var P = this;
var newval;
var sum;
var size;
for(i = 0; i < T.arr.length; i++)
{
size = T.arr[i].obj.hasClass("vert") ? "height":"width";
sum = 0;
T.arr[i].rest.each(function(){
// new value of width/height, determined by the animation percentage
newval = parseInt($(this).attr("from")) + (T.arr[i].to-$(this).attr("from"))*P.prop;
$(this)[size](newval);
sum += newval;
});
T.arr[i].obj[size](T.arr[i].cont[size]()-sum);
}
}
});
},
I've run into a mysterious problem. I'm using jQuery's animate function and the object I'm animating is not moving to the 'left' coordinate I provide; rather the left coordinate is always 0. However, the 'top' coordinate does move as expected. I had this working fine at one point and I rearranged some things around and it has stopped.
var new_x,
new_y,
player_tranistion_holder;
new_x = bg_app.boardModel[id_num].x+'px';
new_y = (bg_app.boardModel[id_num].chips.length) * bg_app.game_utils.parse_css_dimension(selected_chip.chip.css('height'));
console.log(new_x) //outputs 300px for example - the expected value;
selected_chip.chip.animate({'left': new_x, 'top': new_y }, 500, 'easeInOutQuad', function () {
console.log(selected_chip.chip.css('left'); //output is 0
});
Any idea what could cause this to screw up? The stuff I changed around it was merely in how the function came to be called, nothing in the function itself. Do I have some syntax error that I'm missing?
I've put the broken site here:
http://www.warunicorn.com/bg_show_busted/
here is an example with it working:
http://www.warunicorn.com/bg_show/
Any advice would be appreciated.
I found the problem. When using the animate function if the new value passed in is the same as the current value of the object jQuery gets confused and sets the value to 0. I haven't done extensive testing on this and so there may be exceptions where it does work, but in this case, that was the problem.