I am writing a small program where I move DOMs at a specified speed.
When I move it at the rate of 20px per second, the offset that gets added to the elem.style.top is about 0.3px per frame.
The problem is, when this offset is smaller than 0.5px, elem doesn't move!
I constructed a simplified example that can demonstrate the issue in my program:
var requestFrameAnimationId;
function myMove(offset) {
var elem = document.getElementById("animate");
requestFrameAnimationId = animationLoop(frame);
function frame() {
console.log(elem.offsetTop);
if (elem.offsetTop === 350) {
cancelAnimationFrame(requestFrameAnimationId);
} else {
elem.style.top = elem.offsetTop + offset + 'px';
elem.style.left = elem.offsetLeft + offset + 'px';
}
}
}
function animationLoop(render) {
var running, lastFrame = +new Date(); // casting Date to Number
function loop(now) {`enter code here`
requestFrameAnimationId = requestAnimationFrame(loop);
running = render(now - lastFrame);
lastFrame = now;
}
loop(lastFrame);
}
#container {
width: 400px;
height: 400px;
position: relative;
background: yellow;
}
#animate {
width: 50px;
height: 50px;
position: absolute;
background-color: red;
}
<!DOCTYPE html>
<html>
<body>
<p>
<button onclick="myMove(0.3)">Move at 0.3px per frame</button>
<button onclick="myMove(0.5)">Move at 0.5px per frame</button>
</p>
<div id="container">
<div id="animate"></div>
</div>
</body>
</html>
Try clicking on Move at 0.5px per frame. The rectangle should be moving.
Reset it by clicking on Run code snippet.
Now try clicking on Move at 0.3px per frame.
It should be moving the DOM more slowly, but you can see that the DOM is not moving.
It's strange because when I initially kept track of the top position in a javascript variable topPos, and applied ${topPos + offset} to elem.style.top, it worked at even slower speeds!
So my guess is that elem.offsetTop rounds the decimal values, so 0.3 becomes 0, and 0.5 becomes 1.
What can I do to make it so that the DOM moves precisely at the specified speed? I can't use any libraries for this one.
EDIT: I looked more into the problem and I believe it's offsetTop that rounds the numbers to integers.
However, I found out that CSS OM spec changed the type of offsetTop to float, and the Chromium team was working on applying the change on the browser more than 4 years ago, and it seems that it should be fixed by now.
Why is it not working on my program, and how can I make it work?
EDIT2: I found from CSSOM working draft that the type of offsetTop was integer.
readonly attribute long offsetTop;
I think they only changed the type of scrollTop and scrollLeft to a double precision number.
attribute unrestricted double scrollTop;
attribute unrestricted double scrollLeft;
HTMLElement.offset[Left | Top] return long typed value (i.e integer).
Use Element.getBoundingClientRect if you want float values.
var requestFrameAnimationId;
function myMove(offset) {
var elem = document.getElementById("animate");
requestFrameAnimationId = animationLoop(frame);
function frame() {
// build up our own high precision offsetTop
var parentRect = elem.offsetParent && elem.offsetParent.getBoundingClientRect() || {top: 0, left:0};
var elemRect = elem.getBoundingClientRect();
var rect = {
top: elemRect.top - parentRect.top,
left: elemRect.left - parentRect.left
};
if (rect.top >= 350) {
cancelAnimationFrame(requestFrameAnimationId);
} else {
// so we can substract it here
elem.style.top = (rect.top + offset) + 'px';
elem.style.left = (rect.left + offset) + 'px';
}
}
}
function animationLoop(render) {
var running, lastFrame = +new Date(); // casting Date to Number
function loop(now) {
requestFrameAnimationId = requestAnimationFrame(loop);
running = render(now - lastFrame);
lastFrame = now;
}
loop(lastFrame);
}
#container {
width: 400px;
height: 400px;
position: relative;
background: yellow;
}
#animate {
width: 50px;
height: 50px;
position: absolute;
background-color: red;
}
<p>
<button onclick="myMove(0.3)">Move at 0.3px per frame</button>
<button onclick="myMove(0.5)">Move at 0.5px per frame</button>
</p>
<div id="container">
<div id="animate"></div>
</div>
Or simply add up your values to a variable:
var requestFrameAnimationId;
function myMove(offset) {
var elem = document.getElementById("animate");
requestFrameAnimationId = animationLoop(frame);
var pos = 0;
function frame() {
pos += offset;
if (pos >= 350) {
cancelAnimationFrame(requestFrameAnimationId);
} else {
elem.style.top = pos + 'px';
elem.style.left = pos + 'px';
}
}
}
function animationLoop(render) {
var running, lastFrame = +new Date(); // casting Date to Number
function loop(now) {
requestFrameAnimationId = requestAnimationFrame(loop);
running = render(now - lastFrame);
lastFrame = now;
}
loop(lastFrame);
}
#container {
width: 400px;
height: 400px;
position: relative;
background: yellow;
}
#animate {
width: 50px;
height: 50px;
position: absolute;
background-color: red;
}
<p>
<button onclick="myMove(0.3)">Move at 0.3px per frame</button>
<button onclick="myMove(0.5)">Move at 0.5px per frame</button>
</p>
<div id="container">
<div id="animate"></div>
</div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With