<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<canvas class="canvas" data-color="lightblue" data-bg-color="#222" data-percent="35" width="300" height="300"></canvas>
<canvas class="canvas" data-color="lightblue" data-bg-color="#222" data-percent="99" width="300" height="300"></canvas>
<canvas class="canvas" data-color="lightblue" data-bg-color="#222" data-percent="62" width="300" height="300"></canvas>
<canvas class="canvas" data-color="lightblue" data-bg-color="#222" data-percent="22" width="300" height="300"></canvas>
<canvas class="canvas" data-color="lightblue" data-bg-color="#222" data-percent="72" width="300" height="300"></canvas>
<canvas class="canvas" data-color="lightblue" data-bg-color="#222" data-percent="12" width="300" height="300"></canvas>
<canvas class="canvas" data-color="lightblue" data-bg-color="#222" data-percent="82" width="300" height="300"></canvas>
<canvas class="canvas" data-color="lightblue" data-bg-color="#222" data-percent="55" width="300" height="300"></canvas>
<canvas class="canvas" data-color="lightblue" data-bg-color="#222" data-percent="45" width="300" height="300"></canvas>
<script>
/**
* @class Circle
* @constructor
*/
var Circle = function (canvas, callback) {
/**
* @type {HTMLCanvasElement}
* @private
*/
this._canvas = canvas;
/**
* @type {CanvasRenderingContext2D}
* @private
*/
this._ctx = canvas.getContext('2d');
/**
* @type {number}
* @private
*/
this._animateSpeed = 20;
/**
* @type {number}
* @private
*/
this._endProgress = Number(this._canvas.getAttribute('data-percent'));
/**
* @type {string}
* @private
*/
this._bgColor = this._canvas.getAttribute('data-bg-color');
/**
* @type {string}
* @private
*/
this._color = this._canvas.getAttribute('data-color');
/**
* @type {Array<function>}
* @private
*/
this._onReadyCallbacks = [];
/**
* @type {boolean}
* @private
*/
this._isReady = false;
/**
* @type {function}
* @private
*/
this._onEndAnimation = callback;
/**
* @type {number}
* @private
*/
this._radius = this._canvas.width / 2 - 3;
/**
* @type {number}
* @private
*/
this._width = this._canvas.width;
/**
* @type {number}
* @private
*/
this._height = this._canvas.height;
this._onScroll = this._onScroll.bind(this);
this._initialize();
};
/**
* @lends Circle#
*/
Circle.prototype = {
setReady: function () {
this._isReady = true;
this._onReadyCallbacks.forEach(function (callback) {
callback.call(this);
}, this);
this._onReadyCallbacks = [];
},
/**
* @param {function} callback
* @private
*/
_onReady: function (callback) {
if (this._isReady) {
callback.call(this);
} else {
this._onReadyCallbacks.push(callback);
}
},
/**
* @returns {boolean}
* @private
*/
_isVisible: function () {
var scroll = this._getScrollTop();
var offset = this._canvas.offsetTop;
return (scroll < offset) && (scroll + innerHeight > offset + this._canvas.height);
},
/**
* @returns {number}
* @private
*/
_getScrollTop: function () {
return document.body.scrollTop || document.documentElement.scrollTop;
},
/**
* @private
*/
_animate: function () {
var time = this._endProgress * this._animateSpeed;
Circle._animate({
duration: time,
step: this._redraw.bind(this),
complete: this._onEndAnimation
});
window.removeEventListener('scroll', this._onScroll, false);
},
/**
* @private
*/
_clear: function () {
this._canvas.width = this._canvas.width;
},
/**
* @param {number} progress
* @private
*/
_draw: function (progress) {
var angleStart = - Math.PI / 2;
var angleEnd = angleStart + (2 * Math.PI * this._endProgress / 100) * progress;
this._ctx.beginPath();
this._ctx.strokeStyle = this._bgColor;
this._ctx.lineWidth = 4;
this._ctx.arc(this._width / 2, this._height / 2, this._radius, 0, Math.PI * 2, false);
this._ctx.stroke();
this._ctx.beginPath();
this._ctx.strokeStyle = this._color;
this._ctx.lineWidth = 4;
this._ctx.arc(this._width / 2, this._height / 2, this._radius, angleStart, angleEnd, false);
this._ctx.stroke();
this._ctx.fillStyle = this._color;
this._ctx.font = "45px bebas";
var text = Math.floor(this._endProgress * progress) + "%";
var text_width = this._ctx.measureText(text).width;
this._ctx.fillText(text, this._width / 2 - text_width / 2, this._height / 2 + 15);
},
/**
* @param {number} progress
* @private
*/
_redraw: function (progress) {
this._clear();
this._draw(progress);
},
_setHandlers: function () {
window.addEventListener('scroll', this._onScroll, false);
},
_onScroll: function () {
if (this._isVisible()) {
this._animate();
}
},
/**
* @private
*/
_initialize: function () {
this._onReady(function () {
if (this._isVisible()) {
this._animate();
} else {
this._setHandlers();
}
});
this._draw(0);
}
};
/**
* @param {Object} options
* @param {function} options.step
* @param {number} options.duration
* @param {function} options.complete
* @param {function} [options.timeFunction]
* @private
* @static
*/
Circle._animate = function (options) {
var start = Date.now(); // сохранить время начала
requestAnimationFrame(function tick() {
var timePassed = Date.now() - start;
var progress = timePassed / options.duration;
var timeFunction = options.timeFunction || function (progress) {
return progress;
};
progress = progress > 1 ? 1 : progress;
options.step(timeFunction(progress));
if (progress === 1) {
options.complete();
} else {
requestAnimationFrame(tick);
}
});
};
window.onload = function () {
var canvases = Array.prototype.slice.call(document.querySelectorAll('.canvas'));
var circles = [];
var animateEndCount = 0;
var onEnd = function () {
animateEndCount++;
if (circles[animateEndCount]) {
circles[animateEndCount].setReady();
}
};
canvases.forEach(function (canvas) {
circles.push(new Circle(canvas, onEnd));
});
circles[0].setReady();
};
</script>
</body>
</html>