Reputation: 454
I'm currently working on a website when you scroll down a line will animate into a square/rectangle, works perfectly but there's one thing that's bugging me a bit might be a hard one to solve but here it is:
When the animation is playing and you scroll up and down rapidly (with the canvas in the viewport), you can see it slowing down a little bit. No idea what's causing this, i'm sending only 1 call to animate function each time the element reaches a certain point in the Window
. Any ideas?
Edit: Updated the link, just scroll down and it'll be more clear by what i mean by slowing down.
See working example in this fiddle or below.
function createBorderAnimation(elm) {
console.log(elm);
var canvas = elm.get(0),
ctx = canvas.getContext('2d');
canvas.width = $(window).width() / 4 * 3;
canvas.height = $(window).height() / 2;
var Point = function(x, y) {
this.x = x;
this.y = y;
};
var points = [
new Point(0, 0),
new Point(canvas.width, 0),
new Point(canvas.width, canvas.height),
new Point(0, canvas.height),
new Point(0, -10),
];
function calcWaypoints(vertices) {
var waypoints = [];
for (var i = 1; i < vertices.length; i++) {
var pt0 = vertices[i - 1];
var pt1 = vertices[i];
var dx = pt1.x - pt0.x;
var dy = pt1.y - pt0.y;
for (var j = 0; j < 50; j++) {
var x = pt0.x + dx * j / 50;
var y = pt0.y + dy * j / 50;
waypoints.push({
x: x,
y: y
});
}
}
return (waypoints);
}
var wayPoints = calcWaypoints(points);
ctx.strokeStyle = "rgba(0,0,0,0.5)";
ctx.lineWidth = 5;
ctx.moveTo(points[0].x, points[0].y);
ctx.globalCompositeOperation = 'destination-atop';
var counter = 1,
inter = setInterval(function() {
var point = wayPoints[counter++];
ctx.lineTo(point.x, point.y);
ctx.stroke();
if (counter >= wayPoints.length) {
clearInterval(inter);
$(canvas).parent().addClass('active');
}
}, 1);
ctx.stroke();
}
$(window).scroll(function() {
var st = $(window).scrollTop();
$('canvas').each(function(key, elm) {
var _this = $(elm);
if (st > _this.offset().top - $(window).height()) {
if (!_this.hasClass('animating')) {
_this.addClass('animating');
createBorderAnimation(_this);
}
TweenLite.set(_this, {
y: -(st - _this.offset().top) / (1.5 * 10)
});
}
});
});
canvas {
width: 35vw;
height: 50vh;
display: inline-block;
vertical-align: top;
}
canvas:first-child {
margin: 0 5vw 0 0;
}
div {
width: 75vw;
height: 50vh;
margin: 100px auto;
font-size: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/latest/TweenMax.min.js"></script>
<div>
<canvas></canvas>
<canvas></canvas>
</div>
<div>
<canvas></canvas>
<canvas></canvas>
</div>
<div>
<canvas></canvas>
<canvas></canvas>
</div>
<div>
<canvas></canvas>
<canvas></canvas>
</div>
<div>
<canvas></canvas>
<canvas></canvas>
</div>
Upvotes: 0
Views: 1276
Reputation: 136608
You got some good advises from @Blindman67 and @markE (his answer is now deleted), about
setInterval
for an animationI would also add that you should avoid initializing things as much as possible, (initialize once, then reuse), and be careful with jQuery.
About setInterval
, it's just bad, imprecise, and may lead to browser crash when call on too small intervals (it's just a reminder to the browser that it has to do something when the interval has elapsed, so if something takes longer than the interval, it will just keep adding things to do, and will try to execute them all, blocking all other resources and finally just crashes...)
It should be noted that the scroll event may fire really fast depending on the device used, and usually, just faster than you screen refresh rate, so it's one of the events that needs the most optimization.
Here you are doing some extensive use of jQuery's methods, inside the scroll event. The $(selector)
method is a more complicated way to call document.querySelectorAll()
, which in itself is already kind of a loud operation.
Moreover, for all these canvases, you are again calling jQuery's methods to get their size and position. To understand why it's bad at high rate, simply keep in mind that .height
is calling window.getComputedStyle(elem)
every time, which does return every styles assigned to your element, it may be fine occasionally, but not in your case (> 300 calls per seconds).
A simple workaround : call all of these once and store what won't change somewhere.
You can also use requestAnimationFrame
to throttle it, here is a simple example using a flag :
// our flag
var scrolling;
window.onscroll=function(){
// only if we haven't already stacked our function
if(!scrolling){
// raise our flag
scrolling = true
requestAnimationFrame(function(){
// add the callback function
doSomething();
// release the flag for next frame
scrolling = false;
})
}
}
So here is an annotated cleanup of your code, using requestAnimationFrame
, and avoiding too much calls to jQuery's method. It can certainly be optimized and cleaned further, but I hope you'll get the main idea.
var BorderAnimation = function() {
// a global array for all our animations
var list = [];
// init one animation per canvas
var initAnim = function() {
// our canvas
var c = this;
// our animation object
var a = {};
a.ctx = c.getContext('2d');
// a function to check if our canvas is visible in the screen
a.isVisible = function(min, max) {
return min < c.offsetTop + a.height && max > c.offsetTop;
};
// a trigger
a.play = function() {
// fire only if we're not already animating, and if we're not already finished
if (!a.playing && !a.ended) {
a.playing = true;
loop();
}
};
// reverse trigger
a.pause = function() {
a.playing = false;
}
// our looping function
var loop = function() {
// perform our drawing operations
a.draw(a.ctx);
// and only if we should still be playing...
if (a.playing) {
//...loop
requestAnimationFrame(loop);
}
};
// init our canvas' size and store it in our anim object
a.width = c.width = $(window).width() / 4 * 3;
a.height = c.height = $(window).height() / 2;
// initialize the drawings for this animation
initDrawing(a);
// push our animation in the global array
list.push(a);
};
// this does need to be made outside, but I feel it's cleaner to separate it,
// and if I'm not wrong, it should avoid declaring these functions in the loop.
var initDrawing = function(anim) {
var ctx = anim.ctx;
var Point = function(x, y) {
this.x = x;
this.y = y;
};
var points = [
new Point(0, 0),
new Point(anim.width, 0),
new Point(anim.width, anim.height),
new Point(0, anim.height),
new Point(0, -10),
];
function calcWaypoints(vertices) {
var waypointsList = [];
for (var i = 1; i < vertices.length; i++) {
var pt0 = vertices[i - 1];
var pt1 = vertices[i];
var dx = pt1.x - pt0.x;
var dy = pt1.y - pt0.y;
for (var j = 0; j < 50; j++) {
var x = pt0.x + dx * j / 50;
var y = pt0.y + dy * j / 50;
waypointsList.push({
x: x,
y: y
});
}
}
return (waypointsList);
}
var wayPoints = calcWaypoints(points);
ctx.strokeStyle = "rgba(0,0,0,0.5)";
ctx.lineWidth = 5;
ctx.globalCompositeOperation = 'destination-atop';
// always better to add this, e.g if you plan to make a `reset` function
anim.ctx.beginPath();
anim.ctx.moveTo(points[0].x, points[0].y);
var counter = 1;
// store into our drawing object the drawing operations
anim.draw = function() {
// we reached the end
if (counter >= wayPoints.length) {
anim.playing = false;
anim.ended = true;
return;
}
var point = wayPoints[counter++];
ctx.lineTo(point.x, point.y);
// ctx.clearRect(0,0,anim.width, anim.height); // don't you need it ???
ctx.stroke();
}
};
// a single call to the DOM
$('canvas').each(initAnim);
var scrolling;
var checkPos = function() {
// release the flag
scrolling = false;
var st = pageYOffset; // it's just the same as $(window).scrollTop();
// loop over our list of animations
list.forEach(function(a) {
// the canvas is in the screen
if (a.isVisible(st, st + window.innerHeight)) {
// it will be blocked if already playing
a.play();
} else {
// stop the loop
a.pause();
}
});
};
$(window).scroll(function() {
if (!scrolling) {
// set the flag
scrolling = true;
requestAnimationFrame(checkPos);
}
});
// if you want to do something with these animations, e.g debugging
return list;
}();
canvas {
width: 35vw;
height: 50vh;
display: inline-block;
vertical-align: top;
}
canvas:first-child {
margin: 0 5vw 0 0;
}
div {
width: 75vw;
height: 50vh;
margin: 100px auto;
font-size: 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<canvas></canvas>
<canvas></canvas>
</div>
<div>
<canvas></canvas>
<canvas></canvas>
</div>
<div>
<canvas></canvas>
<canvas></canvas>
</div>
<div>
<canvas></canvas>
<canvas></canvas>
</div>
<div>
<canvas></canvas>
<canvas></canvas>
</div>
Upvotes: 1