I know that CSS transitions do not work with auto keyword, so if I want to apply transition: left 2s, right 2s; have to vary both right and left from (let's say) 0 to 100.
I have a script which might change an object's position either towards left or right, but I cannot set right: 100 if left is set to 0 (it has to be auto).
So I came out with this solution.
function moveObject(direction) {
direction ? myobject.style.right = 0 : myobject.style.left = 0; //same position as auto but can be transitioned
direction ? myobject.style.right = "50vw" : myobject.style.left = "50vw"; //actual transition
}
function resetObject() {
myObject.style.left = myObject.style.right = "auto"; //now I can move to either right or left again
//I don't need an animation on reset
}
#myobject {
position: absolute;
left: auto;
right: auto;
/*width, height...*/
}
<div id="myObject"></div>
This didn't work (the assign to 0 was completely ignored).
So I came out with this solution instead (same as above but using setTimeout(..., 0);
function moveObject(direction) {
direction ? myobject.style.right = 0 : myobject.style.left = 0;
setTimeout(function() {
direction ? myobject.style.right = "50vw" : myobject.style.left = "50vw"; //this is executed async
}, 0);
}
function resetObject() {
myObject.style.left = myObject.style.right = "auto";
}
#myobject {
position: absolute;
left: auto;
right: auto;
/*width, height...*/
}
<div id="myObject"></div>
This improved the result, but sometimes (it seems to be completely random, but more frequently on small screens), the transition still does not occur.
I supposed that this was because of the fact that sometimes the asynchronous function is executed too soon, so I tried to increase the delay to 10ms, but the problem still occurs sometimes (less frequently).
Now, I could increase the value furthermore, but:
I can never be sure that the error will not occur anyway sooner or later, maybe on a faster device (unless I set the timeout to a very large number)
I cannot set the timeout to a very large number, since I want it to be imperceptible
So, is there a minimum number that can assure a successful output?
If not, how can accomplish the same result?
As #Akxe suggested, the solution was reflowing the page. In other words, browsers flush multiple changes of the DOM all together. This speeds up the page by a bit but leads to some problems like the one I described (further information here)
As suggested here, the solution was a function like this one:
function reflow() { //this will be called between the two elements
void(document.documentElement.offsetWidth);
}
function moveObject(direction) {
direction ? myobject.style.right = 0 : myobject.style.left = 0;
reflow(); //the line above is now flushed
direction ? myobject.style.right = "50vw" : myobject.style.left = "50vw";
}
function resetObject() {
myObject.style.left = myObject.style.right = "auto";
}
#myobject {
position: absolute;
left: auto;
right: auto;
/*width, height...*/
}
<div id="myObject"></div>
The function actually worked for me even without using void, but I preferred keeping it since in the referenced question someone pointed out that it didn't work for him, and also for more clarity.
This could also be useful: it is an (unofficial) list of everything that causes page reflow.
Related
I'm developing a game engine in HTML5. Characters are div elements using an animated sprite for background. As sprite animation have fluid parameters and must be set by code, they can't be predefined in a static CSS definition, thus I use element.animate to set sprite animations to a given row at a given speed knowing my scales and frame counts.
// Applies the given frame and animation to the sprite
// Frame is an angle, clockwise direction: 0 = up, 1 = right, 2 = down, 3 = left
set_animation(frame, duration) {
const scale_x = this.settings.sprite.scale_x * this.settings.sprite.frames_x;
const pos_y = this.settings.sprite.scale_y * -frame;
// Cancel the existing animation
if(this.data_actors_self.anim) {
this.data_actors_self.anim.cancel();
this.data_actors_self.anim = null;
}
// Play the animation for this row or show the first frame if static
if(duration > 0) {
this.data_actors_self.anim = this.element.animate([
{
backgroundPosition: px([0, pos_y])
}, {
backgroundPosition: px([scale_x, pos_y])
}
], {
duration: duration * 1000,
direction: "normal",
easing: "steps(" + this.settings.sprite.frames_x + ")",
iterations: Infinity
});
this.data_actors_self.anim.play();
} else {
this.element.style.backgroundPosition = px([0, pos_y]);
}
}
Obviously that's a snippet from an actor class function: this.element is the div, this.settings is an object with parameters to be used who's names should make sense in this context, the px() function is a simple converter to turn arrays into pixel strings for HTML (eg: [0, 0] to "0px 0px").
The issue I'm having: While I can always run this function to set a new animation, I want the ability to change the speed of the animation without resetting it. It doesn't need to be a smooth transition, for all I care the new speed can be applied at the next iteration... I only want to avoid a visual snap or any kind of reset upon applying the change. Once an animation is set, I have no idea how to access and update its duration parameter. Does anyone have any suggestions?
When using console.log on this.data.anim I'm rightfully told it's an animation object. I tried using JSON.stringify to get more information but nothing relevant is printed. this.data.anim.duration returns undefined so the setting must be stored under some other property. Even if I know that property, I'd like to be sure web browsers will agree with me changing it like this.data.anim.options.duration = new_duration.
You can wait for the end of an iteration before changing the animation duration if that is what is required.
This snippet only sets an event listener for animationiteration event when you click the button to increase the speed.
function upthespeed() {
const div = document.querySelector('div');
div.addEventListener('animationiteration', function() {
div.style.animationDuration = '1s';
});
document.querySelector('button').style.display = 'none';
}
div {
width: 10vmin;
height: 10vmin;
background-color: magenta;
animation: move 10s linear infinite;
}
#keyframes move {
0% {
transform: translateX(50vw);
}
50% {
transform: translateX(0);
}
100% {
transform: translateX(50vw);
}
}
<div></div>
<button onclick="upthespeed()">Click me to increase the speed at the end of the next iteration (you may have to wait!)</button>
The value for the animation duration isn't in the Animation object itself but in the CSS animation-duration property for the Element: so this.data_actors_self.style.animationDuration = new_duration will do the job. It will however restart the animation if it is being played, but if I understand correctly that isn't a problem for you.
Edit: To change the animation's duration without restarting it, all you have to do is set the value of anim.startTime to what it was before. For example:
const startTime = anim.startTime;
this.data_actors_self.style.animationDuration = new_duration
anim.startTime = startTime;
I'm making a very basic space invaders game, but having some trouble with the collisionDetection. enemy are the ones I need to hit, they are inside "fiende". "missile" is the div for the missiles.
I haven't tried much, since I'm very new to js, hence I don't have enough knowledge to improve on it.
EDIT: After some testing, I've found out that the collisionDetection breaks when the animations is running.
function collisionDetection() {
for (var enemy = 0; enemy < fiende.length; enemy++) {
for (var missile = 0; missile < missiles.length; missile++) {
if (
missiles[missile].left >= fiende[enemy].left &&
missiles[missile].left <= (fiende[enemy].left + 50) &&
missiles[missile].top <= (fiende[enemy].top + 50) &&
missiles[missile].top >= fiende[enemy].top
) {
fiende.splice(enemy, 1);
missiles.splice(missile, 1);
}
}
}
}
HTML
<div id="background">
<div id="missiles"></div>
<div id="fiende"></div>
</div>
CSS
div.missile1 {
width: 10px;
height: 28px;
background-image: url('missile1.png');
position: absolute;
}
div.enemy {
width: 50px;
height: 50px;
background-image: url('enemy.png');
position: absolute;
}
Some of the animation, it's basically the same after that.
#keyframes bevegelse {
0% {
left: -230px;
top: 0%;
}
5% {
left: 250px;
top: 0%;
}
10% {
left: 250px;
top: 40px;
}
15% {
left: -230px;
top: 40px;
}
When a missile hits the enemy, it destroys some other enemies, which the missile didn't hit. Sometimes it doesn't even destroy the randoms ones.
You have a problem in your loop. You are stepping through the indices into the array, but also modifying the array as you go, so you may get confusing results. This may or may not be the cause of your other issues (clearly this is not all of your code) however fixing this may help.
There are a number of ways to fix this. My usual method is to count backwards through the array so that removing elements does not impact the locations of indices the loop is yet to visit.
function collisionDetection() {
//because this loop is continued once the array is modified, count from the end
for (var enemy = fiende.length-1; enemy >=0 ; enemy--) {
//this loop exits when a clash is found, so count as normal
for (var missile = 0; missile < missiles.length; missile++) {
if (
missiles[missile].left >= fiende[enemy].left &&
missiles[missile].left <= (fiende[enemy].left + 50) &&
missiles[missile].top <= (fiende[enemy].top + 50) &&
missiles[missile].top >= fiende[enemy].top
) {
fiende.splice(enemy, 1);
missiles.splice(missile, 1);
//the enemy has already been hit, exit and dont consider other missiles
break;
}
}
}
}
I've made the assumption here that it is one missile gone per enemy. If you want more than one missile to be destroyed, then remove the break and also count backwards in the inner loop.
Thank you for supplying a reference to your git code (see the comments below).
The above is an error in the code, however not one which is typically causing any problem. The issue seems to be that when the enemies are animated the bullets don't hit them. The problem here is that you are detecting collisions with your js code's view of the enemy location, but you are enhancing the animation with css animation. If you wish to detect the collisions then you need to animate entirely in JS.
Try removing the css animation to verify you can hit the enemies. Then if that works you need to do all the movement in js.
I have some code to slide out a menu which works:
Vue.set(this.filterStyles, filterIndex, {
display: "block",
height: "auto",
});
let filterValuesElHeight;
Vue.nextTick().then(() => {
let filterValuesEl = document.getElementById('parent-filter-values-' + filterIndex);
filterValuesElHeight = filterValuesEl.clientHeight;
Vue.set(this.filterStyles, filterIndex, {
height: 0,
display: "block"
});
return Vue.nextTick();
}).then(() => {
setTimeout(() => {
Vue.set(this.filterStyles, filterIndex, {
height: filterValuesElHeight + "px",
display: "block"
});
}, 10);
});
Initially the menu is setup with the following rules:
display: none;
height: 0;
transition: all 500ms;
The first Vue.set sets the height to auto so a accurate height can be taken with filterValuesEl.clientHeight
On the next tick the height is returned back to 0 then finally on the last tick it's set to its natural height.
However, it seems Vue.nextTick() isn't enough although I noticed adding an extremely small timeout seems to do the job. This works but feels quite messy. I was hoping someone might have a better solution?
I'm guessing that you're using filterStyles in a template, something like: :style="filterStyles". If that's the case, then
Vue.set(this.filterStyles, filterIndex, {
display: "block",
height: "auto",
});
won't actually set the height to auto immediately. Rather, it updates the component's data, and that update will be reflected in the DOM after a nextTick(). So, in effect, your code is off by one tick.
Stacking a series of nextTick calls one after the other is probably a bad practice, however. If, for example, the component is removed during the series, you'll be left with a dangling reference.
You could avoid all those ticks by just setting the style directly in order to measure its natural height. If the element has ref="filter" attribute, for example, then something like:
this.$refs.filter.style.height = "auto";
filterValuesElHeight = this.$refs.filter.clientHeight;
this.$refs.filter.style.height = "0";
You may have to work out conflicts between the template and the inline JavaScript (I can't tell because you don't show your template.), but that should get you started.
I'm trying to create a marquee (yes, I've done LOTS of searching on that topic first) using animated text-indent. I prefer this solution over others I've tried, like using translation 100%, which causes text to leak out beyond the boundaries of my marquee.
I've been trying to follow this example here: https://www.jonathan-petitcolas.com/2013/05/06/simulate-marquee-tag-in-css-and-javascript.html
...which I've updated a bit, doing it in TypeScript, using API updates (appendRule instead of insertRule) and dropping concerns about old browser support.
The problem is that the animation restarts using the old keyframe rules -- the step described by the comment "re-assign the animation (to make it run)" doesn't work.
I've looked at what's going on in a debugger, and the rules are definitely being changed -- old rules deleted, new rules added. But it's as if the old rules are cached somewhere, and they aren't being cleared out.
Here's my current CSS:
#marquee {
position: fixed;
left: 0;
right: 170px;
bottom: 0;
background-color: midnightblue;
font-size: 14px;
padding: 2px 1em;
overflow: hidden;
white-space: nowrap;
animation: none;
}
#marquee:hover {
animation-play-state: paused;
}
#keyframes marquee-0 {
0% {
text-indent: 450px;
}
100% {
text-indent: -500px;
}
}
And the relevant section of my TypeScript:
function updateMarqueeAnimation() {
const marqueeRule = getKeyframesRule('marquee-0');
if (!marqueeRule)
return;
marquee.css('animation', 'unset');
const element = marquee[0];
const textWidth = getTextWidth(marquee.text(), element);
const padding = Number(window.getComputedStyle(element).getPropertyValue('padding-left').replace('px', '')) +
Number(window.getComputedStyle(element).getPropertyValue('padding-right').replace('px', ''));
const offsetWidth = element.offsetWidth;
if (textWidth + padding <= offsetWidth)
return;
marqueeRule.deleteRule('0%');
marqueeRule.deleteRule('100%');
marqueeRule.appendRule('0% { text-indent: ' + offsetWidth + 'px; }');
marqueeRule.appendRule('100% { text-indent: -' + textWidth + 'px; }');
setTimeout(() => marquee.css('animation', 'marquee-0 15s linear infinite'));
}
I've tried a number of tricks so far to get around this problem, including things like cloning the marquee element and replacing it with its own clone, and none of that has helped -- the animation continues to run as if the original stylesheet values are in effect, so the scrolling of the marquee doesn't adapt to different widths of text.
The next thing I'll probably try is dynamically creating new keyframes objects instead of editing the rules inside of an existing keyframes object, but that's a messy solution I'd rather avoid if anyone has a better solution.
I found a way to get my marquee working, and it did involved dynamically adding and removing keyframes rules from a stylesheet, but that wasn't as painful or ugly as I thought it might be.
let animationStyleSheet: CSSStyleSheet;
let keyframesIndex = 0;
let lastMarqueeText = '';
function updateMarqueeAnimation(event?: Event) {
const newText = marquee.text();
if (event === null && lastMarqueeText === newText)
return;
lastMarqueeText = newText;
marquee.css('animation', 'none');
const element = marquee[0];
const textWidth = getTextWidth(newText, element);
const padding = Number(window.getComputedStyle(element).getPropertyValue('padding-left').replace('px', '')) +
Number(window.getComputedStyle(element).getPropertyValue('padding-right').replace('px', ''));
const offsetWidth = element.offsetWidth;
if (textWidth + padding <= offsetWidth)
return;
if (!animationStyleSheet) {
$('head').append('<style id="marquee-animations" type="text/css"></style>');
animationStyleSheet = ($('#marquee-animations').get(0) as HTMLStyleElement).sheet as CSSStyleSheet;
}
if (animationStyleSheet.cssRules.length > 0)
animationStyleSheet.deleteRule(0);
const keyframesName = 'marquee-' + keyframesIndex++;
const keyframesRule = `#keyframes ${keyframesName} { 0% { text-indent: ${offsetWidth}px } 100% { text-indent: -${textWidth}px; } }`;
const seconds = (textWidth + offsetWidth) / 100;
animationStyleSheet.insertRule(keyframesRule, 0);
marquee.css('animation', `${keyframesName} ${seconds}s linear infinite`);
}
There's other stuff going on here not needed for a general solution. One thing is that this method is called for two reasons: The window is being resized, or an update to the marquee text has been made. I always want to update when the window is resized, but otherwise I don't want to update the animation if the text hasn't changed, otherwise it could unnecessarily reset when someone is trying to read it.
The other thing is that I don't want text to scroll at all if it happens to fit nicely without scrolling.
How would I go about adjusting the time manually based on the scroll position? What might that look like? To basically 'scroll' the tween? So that the tween reacts to the scrolling mouse's Y position rather than just trigger and execute based on a preset time?
IMHO, here is what you'll need to do:
You will need TimelineMax for sequencing your animations. Place
your animations in TimelineMax as you like them to be.
You'll need to figure out the maximum scroll position your window can scroll up to, beforehand. (This can also be re-calculated on browser resize as well but I haven't taken this into account in my example below). You can figure out with the
help of this answer. Also read the comments on that answer.
Upon scroll, you'll need to convert the current scroll position of
your window object into percentage that is: var currentScrollProgress=window.scrollY/maxScroll; such that your currentScrollProgress should always be between 0 and 1.
TimelineMax has a progress() method which takes values ranging
from 0 and 1 where 0 being the initial state of the animations
and 1 being the final state. Feed this currentScrollProgress
into it and you're done.
OR, you can tween the timeline itself that is: TweenMax.to(timeline,scrollTweenDuration,{progress:currentScrollProgress,ease:ease});.
Code used in my example is as follows:
HTML:
<div> </div>
<div> </div>
...
CSS:
html, body { margin: 0; padding: 0; }
div { width: 100%; height: 60px; margin: 2px 0; }
div:nth-child(odd) { background: #cc0; }
div:nth-child(even) { background: #0cc; }
JavaScript:
/*global TweenMax, TimelineMax,Power2*/
var myDIVs=document.querySelectorAll('div'),numDIVs=myDIVs.length;
var timeline=new TimelineMax({paused:true}),duration=.4,ease=Power2.easeOut,staggerFactor=.1,scrollTweenDuration=.4;
var scrollTimeout=null,scrollTimeoutDelay=20,currentScrollProgress=0;
var maxScroll=Math.max(document.body.scrollHeight,document.body.offsetHeight,document.documentElement.clientHeight,document.documentElement.scrollHeight,document.documentElement.offsetHeight)-window.innerHeight; //see [https://stackoverflow.com/a/17698713/3344111]
function init(){
initTimeline();
listenToScrollEvent();
onScroll();
}
function initTimeline(){
for(var i=0; i<numDIVs; i+=1){ timeline.fromTo(myDIVs[i],duration,{opacity:0},{opacity:1,ease:ease},i*staggerFactor); }
}
function listenToScrollEvent(){
(window.addEventListener)?window.addEventListener('scroll',debounceScroll,false):window.attachEvent('onscroll',debounceScroll);
}
function debounceScroll(){
clearTimeout(scrollTimeout);
scrollTimeout=setTimeout(onScroll,scrollTimeoutDelay);
}
function onScroll(){
currentScrollProgress=roundDecimal(window.scrollY/maxScroll,4);
//timeline.progress(currentScrollProgress); // either directly set the [progress] of the timeline which may produce a rather jumpy result
TweenMax.to(timeline,scrollTweenDuration,{progress:currentScrollProgress,ease:ease}); // or tween the [timeline] itself to produce a transition from one state to another i.e. it looks smooth
}
function roundDecimal(value,place){ return Math.round(value*Math.pow(10,place))/Math.pow(10,place); }
//
init();
Here is the resulting jsFiddle. Hope it helps.
T
While Tahir's answer is correct and sufficient, there's a lot of unnecessary code to show the example.
A more concise snippet is:
var max_scroll = document.body.offsetHeight - window.innerHeight;
win.addEventListener('scroll', function(){
var scroll_perc = parseFloat(Math.min(window.pageYOffset / max_scroll, 1).toFixed(2));
TweenMax.to(tl, 0, {
progress: scroll_perc
});
});
var tl = new TimelineMax({paused: true});
// the rest of your timeline....