My first Stackoverflow question. The tween seems to run because it calls the brute function at the end. However, there's no tween happening.
window.onload=init();
function init() {
testImg = document.getElementById("testImg");
createjs.Tween.get(testImg).wait(2000).to({alpha: 1}, 600).call(brute);
}
function brute() {
// why this function get called if there's no visible tween?
console.log("testImg alpha is " + testImg.alpha)
testImg.style.opacity=1;
}
#testImg {
opacity: .3;
background: url("http://oyos.org/oyosbtn_466x621.jpg");
}
<script src="https://code.createjs.com/tweenjs-0.6.2.min.js"></script>
<body>
<div id="testImg">
here is the div
</div>
</body>
TweenJS isn't really optimized to tween styles on HTML elements, since it was developed to tween properties directly on objects. There is a CSS plugin that can help, particularly when dealing with properties that have suffixes (like "px" on width/height, etc)
However, it can definitely be done. There are a few issues with your code:
As you mentioned in your comment above, you have to target the "opacity" instead. The alpha property is what EaselJS DisplayObjects use.
You have to target the testImg.style instead, since the opacity lives on that element, and not on the testImg directly. Setting opacity/alpha on the #testImg will do nothing
Unfortunately, TweenJS doesn't know how to read CSS properties set on elements using CSS classes or selectors. The getComputedStyle is very expensive to look up, and is required to determine what the current style of the element is.
You can totally make your demo work, but you have to consider these things. Here is an updated snippet (from this pen):
createjs.Tween.get(testImg.style) // Target the style
.to({opacity:0.3}) // Set the property initially
.wait(2000)
.to({opacity: 1}, 600)
.call(brute); // Tween the opacity instead
You could also use the change event to update the opacity yourself:
createjs.Tween.get(testImg)
.set({alpha:0}) // Still requires an initial set
.wait(2000)
.to({alpha:1})
.call(brute)
.on("change", function(event) {
// Every time the tween is updated, set the opacity
testImg.style.opacity = testImg.alpha;
});
Note that the CSS plugin I mentioned above can handle the computedStyle lookup now (a fairly recent addition).
Hope that sheds some light on the behaviour.
Cheers,
Related
I'm having some major headache trying to apply CSS3 transitions to a slideshow trough JavaScript.
Basically the JavaScript gets all of the slides in the slideshow and applies CSS classes to the correct elements to give a nice animated effect, if there is no CSS3 transitions support it will just apply the styles without a transition.
Now, my 'little' problem. All works as expected, all slides get the correct styles, the code runs without bugs (so far). But the specified transitions do not work, even though the correct styles where applied. Also, styles and transitions work when I apply them myself trough the inspector.
Since I couldn't find a logical explanation myself I thought someone here could answer it, pretty please?
I've put together a little example of what the code is right now: http://g2f.nl/38rvma
Or use JSfiddle (no images): http://jsfiddle.net/5RgGV/1/
To make transition work, three things have to happen.
the element has to have the property explicitly defined, in this case: opacity: 0;
the element must have the transition defined: transition: opacity 2s;
the new property must be set: opacity: 1
If you are assigning 1 and 2 dynamically, like you are in your example, there needs to be a delay before 3 so the browser can process the request. The reason it works when you are debugging it is that you are creating this delay by stepping through it, giving the browser time to process. Give a delay to assigning .target-fadein:
window.setTimeout(function() {
slides[targetIndex].className += " target-fadein";
}, 100);
Or put .target-fadein-begin into your HTML directly so it's parsed on load and will be ready for the transition.
Adding transition to an element is not what triggers the animation, changing the property does.
// Works
document.getElementById('fade1').className += ' fade-in'
// Doesn't work
document.getElementById('fade2').className = 'fadeable'
document.getElementById('fade2').className += ' fade-in'
// Works
document.getElementById('fade3').className = 'fadeable'
window.setTimeout(function() {
document.getElementById('fade3').className += ' fade-in'
}, 50)
.fadeable {
opacity: 0;
}
.fade-in {
opacity: 1;
transition: opacity 2s;
}
<div id="fade1" class="fadeable">fade 1 - works</div>
<div id="fade2">fade 2 - doesn't work</div>
<div id="fade3">fade 3 - works</div>
Trick the layout engine!
function finalizeAndCleanUp (event) {
if (event.propertyName == 'opacity') {
this.style.opacity = '0'
this.removeEventListener('transitionend', finalizeAndCleanUp)
}
}
element.style.transition = 'opacity 1s'
element.style.opacity = '0'
element.addEventListener('transitionend', finalizeAndCleanUp)
// next line's important but there's no need to store the value
element.offsetHeight
element.style.opacity = '1'
As already mentioned, transitions work by interpolating from state A to state B. If your script makes changes in the same function, layout engine cannot separate where state A ends and B begins. Unless you give it a hint.
Since there is no official way to make the hint, you must rely on side effects of some functions. In this case .offsetHeight getter which implicitly makes the layout engine to stop, evaluate and calculate all properties that are set, and return a value. Typically, this should be avoided for performance implications, but in our case this is exactly what's needed: state consolidation.
Cleanup code added for completeness.
Some people have asked about why there is a delay. The standard wants to allow multiple transitions, known as a style change event, to happen at once (such as an element fading in at the same time it rotates into view). Unfortunately it does not define an explicit way to group which transitions you want to occur at the same time. Instead it lets the browsers arbitrarily choose which transitions occur at the same time by how far apart they are called. Most browsers seem to use their refresh rate to define this time.
Here is the standard if you want more details:
http://dev.w3.org/csswg/css-transitions/#starting
I have a stylesheet which sets a css transition property like so (prefixed versions omitted for brevity):
transition: opacity 1s;
Then I have a number of elements on the page, and I wish to modify the transition-delay property of each element via JavaScript, to give a stagger effect. I am using jQuery like so:
$(element).css('transition-delay', delay + 's');
However, the above code does not add an inline transition-delay: Xs to the element. Instead, it results in:
<div style="transition: Xs;">
But that's fine, because it works as expected. Somehow, the browser knows that transition: Xs really means to just set the transition-delay to Xs and leave the rest intact.
However:
If I now get the inline style of that element via $(element).attr('style'), and then re-apply it to the element, $(element).attr('style', style), the HTML looks exactly the same, but now the transition has totally overwritten the other properties and essentially sets the element's transition value to all Xs ease 0s.
// HTML before - working
<div style="transition: Xs">
// then I do this
var style = $(el).attr('style');
$(el).attr('style', style);
// HTML after - broken!
<div style="transition: Xs">
Demo
A JSFiddle of exactly what I have described: http://jsfiddle.net/7vp8m/4/
What is going on?
I think just writing out the question and coding that demo really helped me to find the answer:
The HTML style attribute is not the actual style. We need to use the CSSStyleDeclaration object
Although it seems that the inline style is as simple as whatever is contained in the style="..." HTML attribute (as I had assumed), it turns out that this is not the case. Behind the scenes, inline styles (and all other styles) are actually defined by an object called CSSStyleDeclaration. The string contained in the style attribute only represents this object, but does not contain all the information needed to define a style.
This is why setting `el.style = "width: 100px;" does not work. From the MDN article on HTMLElement.style:
Except in Opera, styles can not be set by assigning a string to the (read only) style property, as in elt.style = "color: blue;". This is because the style attribute returns a CSSStyleDeclaration object. Instead, you can set style properties like this:
elt.style.color = "blue"; // Directly
var st = elt.style;
st.color = "blue"; // Indirectly
So this shows us why doing $(el).attr('style', 'transition: Xs'); will not work as expected - and this is exactly what I was running into. Doing so will modify the underlying CSSStyleDeclaration object, but not always in the way we want it to (hence my original question).
The solution is therefore to use the API provided by CSSStyleDeclaration. The following SO question proved crucial to me understanding this issue: JavaScript & copy style
Copying a CSSStyleDeclaration:
var originalStyle = el.cloneNode().style;
Here we are using the cloneNode() method because otherwise (if we just get el.style) the CSSStyleDeclaration object is copied by reference, which is not what I want since I will be changing the element's inline style and then I want to restore the original style later. Cloning the element first allows us to get a "fresh" copy of the CSSStleDeclaration that will not change when we alter the inline styles of our element el.
Replacing current inline style with the saved CSSStyleDeclaration
// first we need to delete all the style rules
// currently defined on the element
for (var i = el.style.length; i > 0; i--) {
var name = el.style[i];
el.style.removeProperty(name);
}
// now we loop through the original CSSStyleDeclaration
// object and set each property to its original value
for (var i = originalStyle.length; i > 0; i--) {
var name = originalStyle[i];
el.style.setProperty(name,
originalStyle.getPropertyValue(name),
priority = originalStyle.getPropertyPriority(name));
}
Demo
Here is an update of my original demo that implements the above methods: http://jsfiddle.net/7vp8m/11/
It breaks in chrome and the "new" opera but no in ff. In Maxthon it stops and restarts the animation the first time, then it works well.
As you said in http://jsfiddle.net/7vp8m/5 (fortunately you solved it) it is due to setting the transition delays through a inline style.
But if you force the engine to refresh the css it works somehow (stops the animation at first but then it continues, playing the animation slower): http://jsfiddle.net/7vp8m/7/
function tick() {
[...]
$.each($('.test'), function(i, e){
e.style.marginLeft = x + 'px'; // Trying with vanilla js but it is the same
e.offsetHeight; // force the refresh. It moves again but bad
});
[...]
}
This doesn't work too: http://jsfiddle.net/7vp8m/8/
$.each($('.test'), function (index, el) {
var style = $(el).attr('style');
style += '; transition-delay: '+delay + 's;'+
'-webkit-transition-delay'+delay + 's;'
$(el).attr('style', style);
delay += 0.2;
});
It seems to be a webkit bug related to transition-delay, but Maxthon stopped the animation in a similar way so it probably is a more generalized bug.
So, if it is a bug, the best option is to not use the property transition-delay through js.
I've grown used to code my jquery animation by doing the following.
Set the initial state of the element (things like width, height, top, left, opacity, etc...) using either css or javascript. This initial state is the state at the end of the animation.
Use .animate with 0 duration to move the element to the state in which elements are at the beginning of the animation.
Use a regular .animate with the appropriate duration to do the animation in which at the end, the elements are back to state in step 1.
Here is an example.
/* css file */
.animatedObject
{
width:60%;
}
// javascript file
$('.animatedObject')
.animate({width: '-=20%'}, 0)
.animate({width: '+=20%'}, 1000);
There are several advantage using this code at least for me. First, it looks clear to me what I'm trying to animate. Second, if javascript is disabled, the object would be at the end state of the animation which is often where I want it to be for graceful degradation. Third, objects can change position slightly for adjustments using css and the animation would still look largely the same.
The reason I'm not using the .css is that it if I try to replace the animate with 0 duration with the .css method, I would get differing animation starting point. I don't think it support += or -=, or if it does, it behaves differently.
So, is this a good way to do this? What is the industry standard way?
Well you could still use .css():
var anObj = $('.animatedObject');
anObj.css("width", anObj.css("width") - (20 / anObj.css("width"))).animate({
width: '+=20%'
}, 1000);
I believe this would work faster.
Edit:
I did a little benchmark for you, using jsperf.com. Here are my results (using Google Chrome):
.animate({}, 0)
Code:
$('.animatedObject')
.animate({width: '-=20%'}, 0)
.animate({width: '+=20%'}, 1000);
End Results:
10,013 operations per second
±7.48%
fastest
.css();
Code:
var anObj = $('.animatedObject');
anObj.css("width", anObj.css("width") - (20 / anObj.css("width"))).animate({
width: '+=20%'
}, 1000);
End Results:
2,477 operations per second
±6.39%
75% slower
Conclusion
Keep the animation function. It turns out using .css() is actually slower. I guess it's the fact that you have 2 extra functions. It's not worth it. This was actually a surprise to me as I thought .css() would function faster than it did. Test this yourself in your browser!
I want to change the background color of in-viewport elements (using overflow: scroll)
So here was my first attempt:
http://jsfiddle.net/2YeZG/
As you see, there is a brief flicker of the previous color before the new color is painted. Others have had similar problems.
Following the HTML5 rocks instructions, I tried to introduce requestAnimationFrame to fix this problem to no avail:
http://jsfiddle.net/RETbF/
What am I doing wrong here?
Here is a simpler example showing the same problem: http://jsfiddle.net/HJ9ng/
Filed bug with Chromium here: http://code.google.com/p/chromium/issues/detail?id=151880
if it is only the background color, well why don't you just change the parent background color to red and once it scroll just change it to pink?
I change your CSS to that
#dad
{
overflow-y: scroll;
overflow-x: hidden;
width: 100px;
height: 600px;
background-color:red;
}
I remove some of you Jquery and change it to this
dad.bind('scroll', function() {
dad.css('background-color', 'pink');
});
And I remove this line
iChild.css('backgroundColor', 'red');
But is the Red color it is important that won't work for sure http://jsfiddle.net/2YeZG/5/
I like Manuel's Solution.
But even though I don't get what you're exactly trying to do, I want to point out a few things.
In your fiddle code, I saw that you included Paul Irish's Shim for requestAnimationFrame.
But you never use it.
(It's basically a reliable setTimeOut, nothing else) it's from frame based animations.)
So since you just want to change some CSS properties, I don't see why you would need it. Even if you want transitions, you should rely on CSS transitions.
Other than that your code could look something like
dad.bind('scroll', function() {
dad.css('background-color', 'pink');
eachElemNameHere.css('background-color','randomColor');
});
Also you should ideally not use something like that if you can help it. You should just add and remove class names and add all these properties in your CSS. Makes it work faster.
Also, again I don't quite get it, but you could use the jQuery function to find out each elements' position from the top to have better control.
Your problem seems to be that you only change the background color of the elements which have already been scrolled into view. Your code expects that the browser waits for your code to handle the scroll event before the browser redraws its view. This is most probably not a guarantee given by the HTML spec. That's why it flickers.
What you should do instead is to change the elements which are going to be scrolled into view. This is related to off screen rendering or double buffering as it is called in computer games programming. You build your scene off screen and copy the finished scene to the visible frame buffer.
I modified your first JSFiddle to include a multiplier for the height of the scroll area: http://jsfiddle.net/2YeZG/13/.
dad.bind('scroll', function() {
// new: query multiplier from input field (for demonstration only) and print message
var multiplier = +($("#multiplier")[0].value);
$("#message")[0].innerHTML=(multiplier*100)-100 + "% of screen rendering";
// your original code
var newScrollY = newScrollY = dad.scrollTop();
var isForward = newScrollY > oldScrollY;
var minVal = bSearch(bots, newScrollY, true);
// new: expand covered height by the given multiplier
// multiplier = 1 is similar to your code
// multiplier = 2 would be complete off screen rendering
var newScrollYHt = newScrollY + multiplier * dadHeight;
// your original code (continued)
var maxVal;
for (maxVal = minVal; maxVal < botsLen; maxVal++) {
var nxtTopSide = tops[maxVal];
if (nxtTopSide >= newScrollYHt) {
break;
}
}
maxVal = Math.min(maxVal, botsLen);
$(dadKids.slice(minVal, maxVal)).css('background', 'pink');
});
Your code had a multiplier of 1, meaning that you update the elements which are currently visible (100% of scroll area height). If you set the multiplier to 2, you get complete off screen updates for all your elements. The browser updates enough elements to the new background color so that even a 100% scroll would show updated elements. Since the browser seldom scrolls 100% of the area in one step (depends of the operating system and the scroll method!), it may be sufficient to reduce the multiplier to e.g. 1.5 (meaning 50% off screen rendering). On my machine (Google Chrome, Mac OS X with touch pad) I cannot produce any flicker if the multiplier is 1.7 or above.
BTW: If you do something more complicated than just changing the background color, you should not do it again and again. Instead you should check whether the element has already been updated and perform the change only afterwards.
I am using JavaScript to dynamically add an element to the DOM. I want to use CSS3 transitions to "fade in" the element as it is added.
I am using something like the following to achieve this:
function add(el) {
el.className += ' fader';
el.style.opacity = 0;
document.getElementById('parent-element').appendChild(el);
//setTimeout(function () { el.style.opacity = 1; }, 5);
el.style.opacity = 1;
}
And the CSS:
.fader {
-webkit-transition: opacity 0.5s;
}
This does not work as expected - the element does not fade in. If I replace the line el.style.opacity = 1; with setTimeout(function () { el.style.opacity = 1; }, 5);, as seen commented-out above, it does work as expected.
I am guessing that the first case does not work as there is some delay between adding the element and the appropriate CSS rules being applied to it. The 5ms delay created by the setTimeout in the second case gives enough time for these rules to be applied, therefore the fade takes place as expected.
Firstly, is this a correct assumption? Secondly, is there a better way to solve this? The setTimout feels like a hack. Is there perhaps some event that is fired once the element has had all its styles applied?
For a CSS3 transition to work, the object has to exist in a particular state and then you have to make a change to the object that triggers the transition.
For a variety of reasons, all of my experience with CSS3 transitions has shown me that a state that counts for this is only a state that it exists in when your javascript returns and the browser goes back to its event loop. It's as if, the only way you can tell the browser to loop at your object now and remember it's state for future transitions is to go back to the browser event loop. There are some programming reasons why this may be the case (so it's not trying to execute transitions as you're programmatically building your object and changing it), but those issues could have been solved a different way (like with a specific method call to codify the object now), but it wasn't done that way.
As such, your solution is the way I've found to do it. Create the object in it's initial state. Set a timer for a very short duration. Return from all your javascript so the object will get codified in its initial state and so the timer can fire. In the timer event, add a class to the object that triggers the CSS3 transition.
I don't honestly know if CSS3 transitions are specified this way in the specification, but my experience in Safari, Firefox and Chrome has been that this is how they work.