I am translating two divs using setInterval and requestAnimationFrame. Animated using interval, the div translates at a rate of 3px per (1000/60)ms, which equates to 180px per 1000ms. At the same time, the div animated using requestAnimationFrame translates at a rate of 0.18px per 1ms, which equates to 180px per 1000ms.
However, they curiously aren't translating at the speed I want. Look at the example below:
let interval = document.querySelector('.interval')
let raq = document.querySelector('.raq')
function startAnimation() {
let translateValue = 0
setInterval(() => {
translateValue = (translateValue + 3) % 300
interval.style.transform = `translateX(${translateValue}px)`
}, 1000 / 60)
let raqAnimation = (timeElapsed) => {
let translateValue = (timeElapsed * 0.18) % 300
raq.style.transform = `translateX(${translateValue}px)`
window.requestAnimationFrame(raqAnimation)
}
window.requestAnimationFrame(raqAnimation)
}
window.setTimeout(startAnimation, 1000)
.interval,
.raq {
width: 50px;
height: 50px;
border-radius: 5px;
background-color: #121212;
margin: 1rem;
}
<div class="interval"></div>
<div class="raq"></div>
Did I use setInterval or requestAnimationFrame wrong or did I fail at the maths calculation?
There is a absolutely no guarantee that your iterval will run at the requested rate so just adding some constant every callback like the code does for the setInterval case isn't going to match.
you could use performance.now or Date.now as your clock in the setInterval case
let interval = document.querySelector('.interval')
let raq = document.querySelector('.raq')
function startAnimation() {
setInterval(() => {
const translateValue = (performance.now() * 0.18) % 300
interval.style.transform = `translateX(${translateValue}px)`
}, 1000 / 60)
let raqAnimation = (timeElapsed) => {
let translateValue = (timeElapsed * 0.18) % 300
raq.style.transform = `translateX(${translateValue}px)`
window.requestAnimationFrame(raqAnimation)
}
window.requestAnimationFrame(raqAnimation)
}
window.setTimeout(startAnimation, 1000)
.interval,
.raq {
width: 50px;
height: 50px;
border-radius: 5px;
background-color: #121212;
margin: 1rem;
}
<div class="interval"></div>
<div class="raq"></div>
they still may not perfectly align though as (a) they are actually running at different times and so get different time values and (b) the run at different rates. They will be close though since it's effectively the same clock
Related
I tried to understand an example on MDN.
The effect of the current code is a bit confusing to me - after a pause if you wait a while and start again, the position of the arrow is not where it originally stopped.
How to make sure to restart where I left off? I thought of a way as below, but I am not very satisfied with it.
const spinner = document.querySelector('div')
let rotateCount = 0
let startTime = null
let rAF
let spinning = false
let previousRotateCount = 0
let hasStopped = false
let rotateInterval
function draw(timestamp) {
if (hasStopped) {
// The angle of each rotation is constant
rotateCount += rotateInterval
rotateCount %= 360
} else {
if (!startTime) {
startTime = timestamp
}
rotateCount = (timestamp - startTime) / 3
rotateCount %= 360
rotateInterval = rotateCount - previousRotateCount
previousRotateCount = rotateCount
}
console.log(rotateCount)
spinner.style.transform = `rotate(${rotateCount}deg)`
rAF = requestAnimationFrame(draw)
}
document.body.addEventListener('click', () => {
if (spinning) {
hasStopped = true
cancelAnimationFrame(rAF)
} else {
draw()
}
spinning = !spinning
})
html {
background-color: white;
height: 100%;
}
body {
height: inherit;
background-color: #f00;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
div {
display: inline-block;
font-size: 10rem;
}
<div>↻</div>
I hope there is a better way. Thank you very much.
This example code is a bit weird, they define a global rotateCount that's never actually used and their use of the timestamp is ... yes, confusing.
Their let rotateCount = (timestamp - startTime) / 3; makes it that the animation will take 1080ms to perform a full revolution (360deg x 3 = 1080ms).
But this is based on the difference between the current time and the start time. So indeed, even if you pause the animation, when restarting it will just like it never did pause.
To make this, you'd need to actually use the global rotateCount (by not redefining a new variable inside draw), and increment it every time by the amount of rotation that was needed since last draw, and not since the overall beginning.
Then you just need to ensure that the last-drawn timestamp gets updated when you resume the animation and you get your animation to actually pause and resume.
const spinner = document.querySelector('div');
const duration = 1080; // ms to perform a full revolution
const speed = 360 / duration;
let rotateCount = 0;
let rAF;
// we'll set this in the starting code
// (in the click event listener)
let lastTime;
let spinning = false;
// Create a draw() function
function draw(timestamp) {
// get the elapsed time since the last time we did fire
// we directly get the remainder from "duration"
// because we're in a looping animation
const elapsed = (timestamp - lastTime) % duration;
// for next time, lastTime is now
lastTime = timestamp;
// add to the previous rotation how much we did rotate
rotateCount += elapsed * speed;
// Set the rotation of the div to be equal to rotateCount degrees
spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
// Call the next frame in the animation
rAF = requestAnimationFrame(draw);
}
// event listener to start and stop spinner when page is clicked
document.body.addEventListener('click', () => {
if(spinning) {
cancelAnimationFrame(rAF);
spinning = false;
} else {
// reset lastTime with either the current frame time
// or JS time if unavailable
// so that in the next draw
// we see only the time that did elapse
// from now to then
lastTime = document.timeline?.currentTime || performance.now();
// schedule the next draw
requestAnimationFrame( draw )
spinning = true;
}
});
html {
background-color: white;
height: 100%;
}
body {
height: inherit;
background-color: red;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
div {
display: inline-block;
font-size: 10rem;
user-select: none;
}
<div>↻</div>
Today i'm asking myself how to create a duration parameter like GreenSock does with its functions.
To be clear, how to create an animating function that takes a duration in pure vanilla JavaScript ?
Are they doing it by adding a transition-duration with JavaScript ? :)
I would like a solution in plain JS.
I have some ideas, maybe width :
new Date()
Or some :
setInterval()
For example how to handle the duration here ?
HTML :
<div class="box"></div>
CSS
.box {
width: 200px;
height: 200px;
background: orange;
}
JavaScript
const box = document.querySelector('.box');
function move(duration){
box.style.transform = "translateX(50px)";
}
move(2000)
Thank's if someone is passing by to give me a hand ! :)
const box = document.querySelector('.box')
function move(duration){
box.style['transition-property'] = 'transform'
box.style['transition-duration'] = `${duration}ms`
setTimeout(() => box.style.transform = "translate3d(200px,0,0)")
}
move(2000)
.box {
width: 200px;
height: 200px;
background: orange;
}
<div class="box"></div>
If you don't want to leverage CSS for the transition control then you will have to control it manually using requestAnimationFrame and your own easing function:
const EASE_IN_OUT = (t) =>
t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
const ease = (f, easing = EASE_IN_OUT) => (...args) => easing(f(...args))
const limit = (f, limit = 1) => (...args) =>
{ const result = f(...args); return result > limit ? limit : result }
const elapsedFraction = ({ start, duration }) =>
(performance.now() - start) / duration
const asInteger = (f) => (...args) => f(...args).toFixed(0)
const calcX = asInteger(({ start, duration, distance }) =>
ease(limit(elapsedFraction))({ start, duration }) * distance)
function move({ el, duration, distance }) {
const start = performance.now()
const startX = el.getBoundingClientRect().x
const tick = () => {
if (el.getBoundingClientRect().x - startX === distance) return
el.style.transform = `translate3d(${calcX({ start, duration, distance })}px,0,0)`
requestAnimationFrame(tick)
}
tick()
}
const el = document.querySelector(".box")
move({ el, duration: 1000, distance: 200 })
.box {
width: 200px;
height: 200px;
background: orange;
}
<div class="box"></div>
I think the best way is to use an animation library. But, I made an example by using "translate3d". Translate3d runs on the GPU and not in the CPU, because of this is more performant. I put a pure CSS example as well.
const box = document.querySelector('.box');
const blue = document.querySelector('.blue');
function move(elem, duration){
let posx = elem.getBoundingClientRect().x
let counter = duration
const timer = setInterval(() => {
if (counter <= 0) {
clearInterval(timer)
}
posx += 2
elem.style.transform = `translate3d(${posx}px,0,0)`
counter -= 10
}, 10)
}
move(box, 2000)
setTimeout(() => blue.classList.add('act'), 1000)
.box {
width: 60px;
height: 60px;
background: orange;
}
.blue {
width: 60px;
height: 60px;
transform: translate3d(0,0,0);
background: lightblue;
transition: all 0.8s ease;
}
.act {
transform: translate3d(100px,0,0)
}
<div class="box"></div>
<br>
<div class="blue"></div>
I think this function do what you want:
Element.prototype.move = function(duration, distance) {
this.style.transition = duration + 's';
this.style.transform = 'translateX(' + distance + 'px)';;
};
.box {
width: 200px;
height: 200px;
background: orange;
}
<div class="box" onclick="this.move(2, 400);">Click me!</div>
I'm trying to create a Sprite animation using the following image:
To do so I am using it as a background and am trying to manipulate the background's position when animating. Somehow I can't get it working though - it shows the last frame from the very beginning.
Image: https://i.imgur.com/06vjVVj.png - 30800x1398 and 27 frames
Here's a codepen: https://codepen.io/magiix/pen/MWewdYo
#skull {
position: absolute;
border: 1px solid red;
width: 1140px;
height: 1398px;
background: url("https://i.imgur.com/06vjVVj.png") 1140px 0;
}
const animateSkull = () => {
const interval = 50;
let pos = 30800 / 27;
tID = setInterval(() => {
document.getElementById("skull").style.backgroundPosition = `-${pos}px 0`;
if (pos < 30800) {
pos = pos + 1140;
}
}, interval);
};
If you check (with a console for example), you'll see that your animateSkull function is never called, because your addEventListener does not work. Change it to the following so it will be called (but your animateSkull function has another bug (or maybe your css I didn't checked) so it's not fully working after that but you should be able to fix that easily):
document.addEventListener('DOMContentLoaded', () => {
animateSkull();
});
This should do the work, but the frames in your sprite don't have the same width. So the animation looks buggy. (that is one huge image just for the animation)
const animateSkull = () => {
const interval = 1000;
let pos = -1140;
tID = setInterval(() => {
if (pos > -30800) {
pos -= 1140;
}
document.getElementById("skull").style.backgroundPosition = `${pos}px 0`;
}, interval);
};
animateSkull();
#skull {
position: absolute;
border: 1px solid red;
width: 1140px;
height: 1398px;
background-image: url("https://i.imgur.com/06vjVVj.png");
background-position: -1140px 0;
background-size: cover;
}
</style>
<p id="skull"></p>
See the following snippet of code.
It creates a loader on click of a button. But the animation is not smooth.
I have recently read about requestAnimationFrame function which can do this job. But how can I use it to replace setInterval altogether since there is no way to specify time in requestAnimationFrame function.
Can it be used in conjunction with setInterval ?
let idx = 1;
const timetoEnd = 5000;
function ProgressBar(width){
this.width = width || 0;
this.id = `pBar-${idx++}`;
this.create = () => {
let pBar = document.createElement('div');
pBar.id = this.id;
pBar.className = `p-bar`;
pBar.innerHTML = `<div class="loader"></div>`;
return pBar;
};
this.animator = () => {
let element = document.querySelector(`#${(this.id)} div`);
if(this.width < 100){
this.width++;
element.style.width = `${this.width}%`;
} else {
clearInterval(this.interval);
}
};
this.animate = () => {
this.interval = setInterval(this.animator, timetoEnd/100);
}
}
function addLoader (){
let bar1 = new ProgressBar(40);
let container = document.querySelector("#container");
container.appendChild(bar1.create());
bar1.animate();
}
.p-bar{
width: 400px;
height: 20px;
background: 1px solid #ccc;
margin: 10px;
overflow:hidden;
border-radius: 4px;
}
.p-bar .loader{
width: 0;
background: #1565C0;
height: 100%;
}
<input type="button" value="Add loader" onclick="addLoader()" />
<div id="container"></div>
You are right, requestAnimationFrame is the recommended way to avoid UI jam when doing animation.
You can remember the absolute starting time at start instead of trying to do this at each frame. Then it's just a matter of computing a width based on the delta time between start and current time.
Also, document.querySelector is considered a relatively "heavy" operation so I added this.element to avoid doing it at each frame.
Here is how to new width is computed: ((100 - this.startWidth) / timetoEnd) * deltaT + this.startWidth
100 - this.startWidth is the total amount of width we have to animate
(100 - this.startWidth) / timetoEnd is how much width each second must add to (1)
((100 - this.startWidth) / timetoEnd) * deltaT is how much width we have to add to (1)
We just have to shift the whole thing this.startWidth px to have the frame's width
Also notice that some of this computation is constant and do not have to be computed on each frame, which I left as an exercise :)
Here is your slightly adapted code:
let idx = 1;
const timetoEnd = 5000;
function ProgressBar(startWidth){
this.startWidth = startWidth || 0;
this.id = `pBar-${idx++}`;
this.create = () => {
let pBar = document.createElement('div');
pBar.id = this.id;
pBar.className = `p-bar`;
pBar.innerHTML = `<div class="loader"></div>`;
return pBar;
};
this.animator = () => {
const deltaT = Math.min(new Date().getTime() - this.start, timetoEnd);
if(deltaT < timetoEnd){
const width = ((100 - this.startWidth) / timetoEnd) * deltaT + this.startWidth;
this.element.style.width = `${width}%`;
requestAnimationFrame(this.animator.bind(this))
}
};
this.animate = () => {
this.element = document.querySelector(`#${(this.id)} div`);
this.start = new Date().getTime();
this.animator();
}
}
function addLoader (){
let bar1 = new ProgressBar(40);
let container = document.querySelector("#container");
container.appendChild(bar1.create());
bar1.animate();
}
.p-bar{
width: 400px;
height: 20px;
background: 1px solid #ccc;
margin: 10px;
overflow:hidden;
border-radius: 4px;
}
.p-bar .loader{
width: 0;
background: #1565C0;
height: 100%;
}
<input type="button" value="Add loader" onclick="addLoader()" />
<div id="container"></div>
I'm trying to create a simple app that displays the name of the TV show and the duration of the show. What I need to do is change the width of a span according to the time. If the show has already started the user should see a colored progress bar and its width should be of a certain percentage of the timeline.
// this is a very simplified version of the json I'm getting
var data = { "showName":"Batman Begins", "duration":"141", "startTime":"22 January 2016 17:30:00" };
function showProgress(){
var timeline = $('.timeline');
var duration = data.duration; // time in minutes. Getting this from a json
var timelineSection = 100 / duration;
var maxWidth = 100;
var increment = timelineSection;
var now = Math.floor($.now() / 1000); // UNIX timestamp for current date and time
var startTime = Date.parse(data.startTime) / 1000; // UNIX timestamp for showtime
var timer = setInterval(function(){
$('.timeline').css({ 'width' : timelineSection + '%' });
timelineSection = timelineSection + increment; // doing this to keep the incrementation same everytime
if (timelineSection > maxWidth) {
clearInterval(timer);
$('.timeline').css({ 'width' : timelineSection + '%' });
}
}, 1000); // running every second instead of minute for demo purposes
}
$(document).ready(function(){
$('.showName').html(data.showName);
$('.startTime').html(data.startTime);
showProgress();
});
.wrapper {
margin: auto;
width: 500px;
}
.timeline-container {
background: #bbb;
}
.timeline {
background: green;
height: 20px;
margin: 0 0 10px;
max-width: 100%;
transition: all 200ms linear;
width: 0%;
}
.example-timeline {
background: green;
height: 20px;
margin: 0 0 10px;
max-width: 100%;
transition: all 200ms linear;
}
.example-timeline.half { width: 50%; }
.example-timeline.twothirds { width: 66.6666%; }
.example-timeline.onethird { width: 33.3333%; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrapper">
<h1 class="showName"></h1>
<p class="description">Show starting at: <span class="startTime"></span></p>
<div class="timeline-container">
<div class="timeline"></div>
<div class="example-timeline half"></div>
<div class="example-timeline twothirds"></div>
<div class="example-timeline onethird"></div>
</div>
</div>
I can't seem to figure out how to display the timeline correctly if the show has already started. See the example timelines in snippet. I have a countdown clock to the start of the show and it hides when the show starts revealing the timeline and it works perfectly fine.
I really hope someone can help me with this :)
Just perform some calculation to initialize the timeline
// this is a very simplified version of the json I'm getting
var data = { "showName":"Batman Begins", "duration":"141", "startTime":"22 January 2016 17:30:00" };
function showProgress(){
var timeline = $('.timeline');
var duration = data.duration; // time in minutes. Getting this from a json
var timelineSection = 100 / duration;
var maxWidth = 100;
var increment = timelineSection;
//var now = Math.floor($.now() / 1000); // UNIX timestamp for current date and time
var now = Math.floor(Date.parse("22 January 2016 17:39:00") / 1000);
var startTime = Date.parse(data.startTime) / 1000; // UNIX timestamp for showtime
if (now > startTime) {
var elapsed = Math.floor((now-startTime)/60); //timespan converted to minutes
timelineSection = Math.min(maxWidth, elapsed*timelineSection); //set the start width to match the time elapsed but only reach maximum of 100
$('.timeline').css({ 'width' : timelineSection + '%' }); //set the initial width first
timelineSection += increment; //to be used as next width
}
var timer = setInterval(function(){
$('.timeline').css({ 'width' : timelineSection + '%' });
timelineSection = timelineSection + increment; // doing this to keep the incrementation same everytime
if (timelineSection > maxWidth) {
clearInterval(timer);
$('.timeline').css({ 'width' : timelineSection + '%' });
}
}, 1000); // running every second instead of minute for demo purposes
}
$(document).ready(function(){
$('.showName').html(data.showName);
$('.startTime').html(data.startTime);
showProgress();
});
.wrapper {
margin: auto;
width: 500px;
}
.timeline-container {
background: #bbb;
}
.timeline {
background: green;
height: 20px;
margin: 0 0 10px;
max-width: 100%;
transition: all 200ms linear;
width: 0%;
}
.example-timeline {
background: green;
height: 20px;
margin: 0 0 10px;
max-width: 100%;
transition: all 200ms linear;
}
.example-timeline.half { width: 50%; }
.example-timeline.twothirds { width: 66.6666%; }
.example-timeline.onethird { width: 33.3333%; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrapper">
<h1 class="showName"></h1>
<p class="description">Show starting at: <span class="startTime"></span></p>
<div class="timeline-container">
<div class="timeline"></div>
<div class="example-timeline half"></div>
<div class="example-timeline twothirds"></div>
<div class="example-timeline onethird"></div>
</div>
</div>
HTML - I updated your HTML to show you a different way of handling the UX presentation by using CSS against a content wide state provided in the .showInfo class. When the show times are calculated, a data-attr is provided. This allows CSS to show or hide certain text, and will later allow you to build in other state dependant styles (such as a different progress bar colour).
<div class="wrapper showInfo">
<h1 class="showName"></h1>
<p class="description">
<span class="status past">The show has finished</span>
<span class="status present">It started on </span>
<span class="status future">It will start on</span>
<span class="showTimeInfo"></span></p>
<div class="timeline-container">
<div class="timeline"></div>
</div>
<div class="showStatus">
<div class="status past">Oh man, it's over!</div>
<div class="status present">It is ON!</div>
<div class="status future">Dude, the show is on later.</div>
</div>
</div>
Additional CSS - your current CSS should pretty much just fit, and the additional styles are used to update the functionality described.
.showInfo > .showStatus > .status {display: none;}
.showInfo > .description > .status {display: none;}
.showInfo[data-state="past"] .showStatus .past, .showInfo[data-state="past"] .description .past {display: inline-block;}
.showInfo[data-state="present"] .showStatus .present, .showInfo[data-state="present"] .description .present {display: inline-block;}
.showInfo[data-state="future"] .showStatus .future, .showInfo[data-state="future"] .description .future {display: inline-block;}
JavaScript - date/time can be a little tricky in JavaScript so I've not added lots of clever bits or any measurable intelligence to it. Consider Moment.js (or similar) as a library for your project.
// this is a very simplified version of the json I'm getting
var data = { "showName":"Batman Begins", "duration":"5", "startTime":"17 January 2016 20:09:00" };
// Make some jQuery object vars so you only have to call them once
var timeline = $('.timeline');
var showInfo = $('.showInfo');
var showTimeInfo = $('.showTimeInfo');
// Always make a timeout into a global var so you can cancel it later
var progressTimeout = {};
var tickDuration = 1000;
// Self calling function
function showProgress () {
// Set some time related vars
var now = Math.floor($.now() / 1000);
var startTime = Date.parse(data.startTime) / 1000;
// Conver 'duration' into seconds
var durationSec = parseInt(data.duration, 10) * 60
var endTime = (Date.parse(data.startTime) / 1000) + durationSec;
// Help for our conditional statments
var showStart = startTime - now;
var showEnd = + endTime - now;
// Events that are in the past will be minus - thus false
// If both are grater than zero, then the show is on now
if (showStart>0 && showEnd>0) {
// Set a data-attr at the top of our content so we can use CSS as an aide
showInfo.attr('data-state', 'future');
showTimeInfo.html(data.startTime)
} else if (showEnd>-1) {
// If showEnd is zero or more then the show is on, but not over
// Now we can make the progress bar work
// Work out the progress of the show as a percentage
var progress = now - startTime;
var percent = Math.ceil(progress / (durationSec / 1000) / 10);
// Use the percentage to push progress bar
timeline.css('width', percent + "%")
// Set the state to say that the show is on in the 'present'
showInfo.attr('data-state', 'present');
showTimeInfo.html(data.startTime)
} else {
// Both startTime and endTime are now in the past
// Make the bar full
timeline.css('width', '100%')
showInfo.attr('data-state', 'past');
// Clear the time text
showTimeInfo.html("");
// The timeout is no longer needed
clearTimeout(progressTimeout);
}
progressTimeout = setTimeout(showProgress, tickDuration)
}
$('.showName').html(data.showName);
showProgress();
jsFiddle example: https://jsfiddle.net/likestothink/0d7hf2fq/3/