I have a banner that animates through a few background images. The animations are defined in CSS and triggered from JavaScript using the triggerAnimation() function in this fiddle and included below. It works by animating the next-banner div on top of the current-banner div, and then updating the current-banner background to the new image after the animation finishes and resets. The swap from the next-banner to the current-banner should be instantaneous, so that the user doesn't see it.
This works fine in both Firefox and Chrome, but in IE 11 it will occasionally flicker at the end of the animation. It resets back to the beginning state of the animation before the animationend callback makes the swap, causing a noticeable flicker from the new image to the old one, and then back. It happens unpredictably, and doesn't even seem to be consistent in how often it happens. Sometimes it will happen almost every time, while others I'll have to wait through 5 or more transitions before seeing it.
There is evidence that it's caused by the animationend event firing late, because if I set a timeout to 50ms after the animation should have ended, and cancel that timeout in the animationend callback, then my timeout callback only executes when the flicker occurs, and doesn't execute when it behaves normally.
triggerAnimation() code:
window.triggerAnimation = function(element, animation, callback) {
// omitted stuff to determine correct property and event names, which seems to work correctly
element.style[propName] = animation;
element.addEventListener(eventName, function(e) {
element.style[propName] = "";
element.removeEventListener(eventName, arguments.callee);
if (callback) callback(e);
}, false);
};
Is this a problem with my code or with IE, and how can I prevent this from happening?
The closest thing I've found to a solution is to set a timeout for just after when the animation should have finished, which "catches" the cases when IE doesn't fire the callback correctly. Then, the actual event handler cancels the timeout for the cases when the callback is fired correctly.
So, this is what I've ended up with:
window.triggerAnimation = function(element, animation, callback) {
// Omitted stuff to determine correct property and event names
element.style[propName] = animation;
// Extract the duration from the animation CSS and convert it to ms.
var duration = parseFloat(animation.match(/\d+\.\d+/g)[0], 10) * 1000;
var timeout = setTimeout(function() {
element.style[propName] = "";
element.removeEventListener(eventName, arguments.callee);
if (callback) callback(null);
}, duration + 10);
element.addEventListener(eventName, function(e) {
clearTimeout(timeout);
element.style[propName] = "";
element.removeEventListener(eventName, arguments.callee);
if (callback) callback(e);
}, false);
};
The only two downsides to this are: 1) it still will rarely flicker, but much less often and much less noticeably; and 2) any time that the timeout is invoked instead of the event handler, the event object (e) doesn't get passed to callback. In my case that's not a problem.
Add this code to CSS file:
html {
filter: expression(document.execCommand("BackgroundImageCache", false, true));
}
Related
I would like to get one event only per scroll event
I try this code but it produces "wheel" as many times the wheel event is triggered.
Any help? Thank you
window.addEventListener("wheel",
(e)=> {
console.log("wheel");
e.preventDefault();
},
{passive:false}
);
Use case (edit)
I want to allow scrolling from page to page only - with an animation while scrolling. As soon I detect the onwheel event, I would like to stop it before the animation finishes, otherwise the previous onwheel continues to fire and it is seen as new event, so going to the next of the targeted page
My conclusion :
It is not possible to cancel wheel events. In order to identify a new user wheel action while wheeling events (from a former user action) are on going, we need to calculate the speed/acceleration of such events
This is fairly simple problem, store anywhere the last direction and coditionally execute your code:
direction = '';
window.addEventListener('wheel', (e) => {
if (e.deltaY < 0) {
//scroll wheel up
if(direction !== 'up'){
console.log("up");
direction = 'up';
}
}
if (e.deltaY > 0) {
//scroll wheel down
if(direction !== 'down'){
console.log("down");
direction = 'down';
}
}
});
Anyway, the UX context should be defined.
May be that throttling or debouncing your function will give better results in some scenarios.
Throttling
Throttling enforces a maximum number of times a function can be called
over time. As in "execute this function at most once every 100
milliseconds."
Debouncing
Debouncing enforces that a function not be called again until a
certain amount of time has passed without it being called. As in
"execute this function only if 100 milliseconds have passed without it
being called.
In your case, maybe debouncing is the best option.
Temporary lock the browser scroll
$('#test').on('mousewheel DOMMouseScroll wheel', function(e) {
e.preventDefault();
e.stopPropagation();
return false;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="test">
<h1>1</h1>
<h1>2</h1>
<h1>3</h1>
<h1>4</h1>
<h1>5</h1>
<h1>6</h1>
<h1>7</h1>
<h1>8</h1>
<h1>9</h1>
<h1>10</h1>
</div>
Event.preventDefault() tells the browser not to do the default predefined action for that event, such as navigating to a page or submitting the enclosing form, etc. It does not necessarily prevent events from firing.
Also, there is a difference between the wheel event and the scroll event. The wheel event is fired when the user rotates a wheel button, and the scroll event is fired when the target's scrollTop or scrollLeft property is changed due to the scroll position being changed.
When the user rotates the wheel button, the wheel event is fired before any scroll events that could be fired. However, the wheel event might not result in any scroll event simply because the pointer is not hovering on any element or the element is not scrollable at the moment.
To aggregate quickly repeated function calls to the event handler, you can debounce the event handler function. The idea is to wait a certain amount before committing to the action. When a function is debounced, it becomes a new function, when called, sets off a timer that calls the wrapped function inside. The timer is reset and restarted when debounced function is called again. Look at the example diagram below.
© Ilya Kantor(https://github.com/javascript-tutorial/en.javascript.info, licensed under CC-BY-NC)
The function f is a debounced function with a 1000ms timeout duration and is called at time instants 0, 200ms, and 500ms with arguments a, b, and c, respectively. Because f is debounced, calls f(a) and f(b) were "not committed/ignored" because there was another call to f within a 1000ms duration. Still, call f(c) was "committed/accepted" at the time instant 1500ms because no further call followed within 1000ms.
To implement this, you can use the setTimeout and clearTimeout functions. The setTimeout function accepts an action(code or function to execute) and delay in milliseconds, then returns a timer ID in integer. The given action will be executed when the timer expires without being canceled.
const timerId = setTimeout(action, delay)
The clearTimeout function could then be used to destroy the timer with a given ID.
clearTimeout(timerId)
Following simple debounce implementation could be used:
// Default timeout is set to 1000ms
function debounce(func, timeout = 1000) {
// A slot to save timer id for current debounced function
let timer
// Return a function that conditionally calls the original function
return (...args) => {
// Immediately cancel the timer when called
clearTimeout(timer)
// Start another timer that will call the original function
// unless canceled by following call
timer = setTimeout(() => {
// Pass all arguments and `this` value
func.apply(this, args)
}, timeout)
}
}
Read more: Default parameters, Rest parameters, Function.apply(), this keyword
To use is quite simple:
eventTarget.addEventListener('wheel', debounce((e) => {
console.log('wheel', e)
}))
This will limit console.log calls to whenever a wheel event has not been fired in a second.
Live example:
function debounce(f, d = 99, t) {
return (...a) => {
clearTimeout(t)
t = setTimeout(() => {
f.apply(this, a)
}, d)
}
}
document.addEventListener('wheel', debounce((_) => {
console.log('wheel')
}))
A more modern approach uses Promise on top of this idea.
You almost had it But you need to wrap your code in a function.
I added some extra little bits so you can differentiate up and down :)
//scroll wheel manipulation
window.addEventListener('wheel', function (e) {
//TODO add delay
if (e.deltaY < 0) {
//scroll wheel up
console.log("up");
}
if (e.deltaY > 0) {
//scroll wheel down
console.log("down");
}
});
How it works?
(e) = This is just the event, the function is triggered when ever you scroll up and down, but without the function event it just doesn't know what to do! Normally people put "event" but im lazy.
deltaY = This is a function of the wheel scroll it just makes sure you scrolling along the Y axis. Its a standard inbuilt function there is no external variables you need to add.
Extras
setTimeout
You could add this. In the if statements as #Lonnie Best suggested
You could set a minimum amount of time that must pass before you consider an additional scroll event as actionable.
For example, below, 3 seconds must pass between scroll events before console.log("wheel") is fired again:
function createScrollEventHandler(milliseconds)
{
let allowed = true;
return (event)=>
{
event.preventDefault();
if (allowed)
{
console.log("wheel");
allowed = false;
setTimeout(()=>
{
allowed = true;
},milliseconds);
}
}
}
let scrollEventHandler = createScrollEventHandler(3000); // 3 seconds
window.addEventListener("wheel",scrollEventHandler);
I'm currently developing a web page with a scrolling parallax effect (Stellar.js) on the header and three other sections of the site: however, scrolling them causes lag, especially at the top of the page.
I've already tried to reduce the background images' size by using compression, but it hasn't made too much difference; removing the blur effect didn't solve the problem, either (it did reduce the lag, but it still wasn't smooth enough).
The website runs pretty well on Firefox (W10), with almost no frame drops, but there's quite some lag on Chrome (both Windows and OS X) and Safari.
There's a few JS scroll-triggered scripts running, but I don't know if those may be the cause. Any suggestions?
What you're going to want to do is throttle scroll events. Debouncing events means an event can't fire again until after a certain amount of time. Throttling events means that the event can only fire so much per period of time.
Here's function to throttle events (credit: http://sampsonblog.com/749/simple-throttle-function)
// Create the listener function
function throttle (callback, limit) {
var wait = false; // Initially, we're not waiting
return function () { // We return a throttled function
if (!wait) { // If we're not waiting
callback.call(); // Execute users function
wait = true; // Prevent future invocations
setTimeout(function () { // After a period of time
wait = false; // And allow future invocations
}, limit);
}
}
}
To use it just do something like this:
function callback () {
console.count("Throttled");
}
window.addEventListener("scroll", throttle( callback, 200 ));
So I've got a scroll event. It does a load of stuff to work out whether something should be moved on the page. When you scroll down, it fires off. If you wheel down, drag, it fires of bazillions and bazillions of times. As you'd expect, perhaps. Here's some simple dummy code to represent the sequence of events.
function scroller() {
// 1. A really expensive calculation that depends on the scroll position
// 2. Another expensive calculation to work out where should be now
// 3. Stop current animations
// 4. Animate an object to new position based on 1 and 2
}
$(window).on('resize' scroller);
Don't get me wrong, it's usually accurate so there isn't so much a concurrency issue. My animations inside the event call .stop() (as part #3) so the latest version is always* the right one but it's eating up a lot of CPU. I'd like to be a responsible developer here, not expecting every user to have a quad core i7.
So to my question... Can I kill off previous calls to my method from a particular event handler? Is there any way I can interfere with this stack of queued/parallel-running "processes" so that when a new one is added to the stack, the old ones are terminated instantly? I'm sure there's a concurrency-minded way of wording this but I can't think of it.
*At least I think that's the case - if the calculations took longer in an earlier run, their animation could be the last one to be called and could cock up the entire run! Hmm. I hadn't thought about that before thinking about it here. Another reason to stop the previous iterations immediately!
You can ensure the event is fired a maximum of once per x milliseconds. E.g.:
(function ($) {
$.fn.delayEvent = function (event, callback, ms) {
var whichjQuery = parseFloat($().jquery, 10)
, bindMethod = whichjQuery > 1.7 ? "on" : "bind"
, timer = 0;
$(this)[bindMethod](event, function (event) {
clearTimeout (timer);
timer = setTimeout($.proxy(callback, this, event), ms);
});
return $(this);
};
})(jQuery);
$(window).delayEvent("resize", scroller, 1000);
Minimalistic demo: http://jsfiddle.net/karim79/z2Qhz/6/
I've faced the following scenario quite often so I'm wondering if there is a built-in jQuery way of solving the issue.
Imagine the following code:
$(document).click(function() {
paintCanvas();
});
The problem with this code is that if the user clicks on the screen 50 times in rapid succession you are going to overload the browser with 50 calls to paintCanvas.
If paintCanvas is currently executing and a new request is created, we want to queue the new request so that it waits until paintCanvas is finished executing. However, at the same time, we can drop any previously queued calls to paintCanvas as we only care about the final state of the mouse, not all the intermediate states.
Here is some code that solves the problem:
var _isExecuting, _isQueued;
function paintCanvas() {
if (_isExecuting) {
if (!_isQueued) {
_isQueued = true;
setTimeout(function() {
_isQueued = false;
paintCanvas();
}, 150);
}
return;
}
_isExecuting = true;
// ... code goes here
_isExecuting = false;
};
This AJAX queue plugin essentially implements this functionality, but does so only in terms of AJAX. Surely this is a very common problem that can be solved in more generic way?
You shouldn't have to solve this problem with mousemove because the system already does that for you. While paintCanvas is executing, it is not generating hundreds of mousemove events even if the mouse is moving vigorously. Rather, the next event will be the current location of the mouse, not a queue of all the intervening mouse events.
Look at this jsFiddle: http://jsfiddle.net/jfriend00/4ZuMn/.
Wiggle your mouse around in the body (lower, right pane) as fast as you want. Then move the mouse out of the pane and notice that the count stops immediately - there are no more mouse events. It doesn't stack up mouse events ever. Whenever the system is ready for the next mouse event, it gets the latest position of the mouse. Individual mouse moves are NOT queued up - they do not accumulate. You can also see in the listing of mouse events that lots of intervening mouse events are not present (e.g. lots of coordinates are missing) even though the mouse went through more positions. This is because the system wasn't ready to make a mouse event when the mouse was in that position so that position was skipped.
Further, because javascript is single threaded, you will never get a new mouse event while you are currently processing one. The system won't generate a new one until you're done processing the one you're already one. So, you will never, ever see _isExecuting as true in javascript in your code. You simply don't need that check. And, since you don't need that check and it will never be true, none of your queuing code will ever execute. You can see here in this jsFiddle, that you can never catch a mousemove event that was re-entered: http://jsfiddle.net/jfriend00/ngnUT/. The inAction flag is never caught as true, no matter how fast or much you wiggle your mouse around.
Sounds like you want throttle/debounce features.
There are no built in methods that I know of from jQuery, you can use any of these though:
http://benalman.com/projects/jquery-throttle-debounce-plugin/
http://jsperf.com/jquery-throttle-methods
Though #rkw provided a link, I always prefer to show code here on SO. Here's some simple code that kind does what you want. A function that returns a buffered version of another function. This will keep delaying until it stops receiving the event for the given delay. You can tweak this if you don't want to to wait for the delay after the last event. All you'd need to do is keep track of when you first set the timeout and offset the subsequent calls to setTimeout.
Here's a working example http://jsfiddle.net/mendesjuan/qfFjZ/
function createBuffered(handler, delay) {
var timeoutId = null;
return function() {
var me = this;
if (timeoutId) {
window.clearTimeout(timeoutId);
}
timeoutId = setTimeout(function() {
handle.apply(me, arguments);
timeoutId = null;
}, delay);
}
}
I have a jQuery slider on my site and the code going to the next slide is in a function called nextImage. I used setInterval to run my function on a timer, and it does exactly what I want: it runs my slides on a timer. BUT, if I go to the site in Chrome, switch to another tab and return, the slider runs through the slides continuously until it 'catches up'. Does anyone know of a way to fix this. The following is my code.
setInterval(function() {
nextImage();
}, 8000);
How to detect when a tab is focused or not in Chrome with Javascript?
window.addEventListener('focus', function() {
document.title = 'focused';
},false);
window.addEventListener('blur', function() {
document.title = 'not focused';
},false);
To apply to your situation:
var autopager;
function startAutopager() {
autopager = window.setInterval(nextImage, 8000);
}
function stopAutopager() {
window.clearInterval(autopager);
}
window.addEventListener('focus', startAutopager);
window.addEventListener('blur', stopAutopager);
Note that in the latest version of Chromium, there is either a bug or a 'feature' which is making this less reliable, requiring that the user has clicked at least once anywhere in the window. See linked question above for details.
I post an answer here: How can I make setInterval also work when a tab is inactive in Chrome?
Just do this:
setInterval(function() {
$("#your-image-container").stop(true,true);
nextImage();
}, 1000);
inactive browser tabs buffer some of the setInterval or setTimeout functions.
stop(true,true) - will stop all buffered events and execute immadietly only last animation.
The window.setTimeout() method now clamps to send no more than one timeout per second in inactive tabs. In addition, it now clamps nested timeouts to the smallest value allowed by the HTML5 specification: 4 ms (instead of the 10 ms it used to clamp to).
A few ideas comes to mind:
Idea #1
You can make it so that a short burst is idempotent. For example, you could say:
function now() {
return (new Date()).getTime();
}
var autopagerInterval = 8000;
function startAutopager() {
var startImage = getCurrentImageNumber();
var startTime = now();
var autopager = setInterval(
function() {
var timeSinceStart = now() - startTime();
var targetImage = getCurrentImageNumber + Math.ceil(timeSinceStart/autopagerInterval);
if (getCurrentImageNumber() != targetImage)
setImageNumber(targetImage); // trigger animation, etc.
},
autopagerInterval
);
return autopager;
}
This way even if the function runs 1000 times, it will still run in only a few milliseconds and animate only once.
note: If the user leaves the page and comes back, it will have scrolled. This is probably not what the original poster wants, but I leave this solution up since it is sometimes what you want.
Idea #2
Another way to add idempotence (while still keeping your nextImage() function and not having it scroll to the bottom of the page) would be to have the function set a mutex lock which disappears after a second (cleared by another timeout). Thus even if the setInterval function was called 1000 times, only the first instance would run and the others would do nothing.
var locked = false;
var autopager = window.setInterval(function(){
if (!locked) {
locked = true;
window.setTimeout(function(){
locked=false;
}, 1000);
nextImage();
}
}, 8000);
edit: this may not work, see below
Idea #3
I tried the following test:
function f() {
console.log((new Date()) + window.focus());
window.setTimeout(f, 1000);
}
f();
It seems to indicate that the function is being called every second. This is odd... but I think this means that the callbacks are being called, but that the page renderer refuses to update the page in any graphical way while the tab is unfocused, delaying all operations until the user returns, but operations keep piling up.
Also the window.focus() function doesn't say if the window has focus; it GIVES focus to the window, and is thus irrelevant.
What we want is probably this: How to detect when a tab is focused or not in Chrome with Javascript? -- you can unset your interval when the window loses focus (blur), and reset it when it gains focus.
I don't know exactly what is going on in your function nextImage(), but I had a similar issue. I was using animate() with setInterval() on a jQuery image slider that I created, and I was experiencing the same thing as you when I switched to a different tab and back again. In my case the animate() function was being queued, so once the window regained focus the slider would go crazy. To fix this I just stopped the animate() function from queuing.
There are a couple ways you can do this. the easiest is with .stop(), but this issue and ways to fix it are documented in the jQuery docs. Check this page near the bottom under the heading additional notes: http://api.jquery.com/animate/
I had faced similar issue, somehow this code below works fine for me.
var t1= window.setInterval('autoScroll()', 8000);
window.addEventListener('focus', function() {
focused = true;
window.clearInterval(t1);
t1 = window.setInterval('autoScroll()', 8000);
},false);
window.addEventListener('blur', function() {
focused = false;
window.clearInterval(t1);
},false)
function autoScroll()
{
if ( running == true){
if ( focused = true){
forwardSlide();
}
}
else {
running = true;
}
}
If you are using Soh Tanaka's image slider then just add this...to solve your Google Chrome issue:
$(".image_reel").stop(true, true).fadeOut(300).animate({ left: -image_reelPosition}, 500 ).fadeIn(300);
Take note of the .stop() function. Ignore the fading in and out stuff, that's what I used on my version
Thanks
Seconding the comment by jgerstle to use page visibility events instead, see https://www.w3.org/TR/page-visibility/#example-1-visibility-aware-video-playback for more around subscribing to 'visibilitychange' for hidden/visible states.
This seems to be more useful than focus/blur these days as it covers visible-but-not-selected windows if concerned also about multi-window operating systems.