(function(window, document, undefined) { /** * Find the absolute position of an element */ var absPos = function(element) { var offsetLeft, offsetTop; offsetLeft = offsetTop = 0; if (element.offsetParent) { do { offsetLeft += element.offsetLeft; offsetTop += element.offsetTop; } while (element = element.offsetParent); } return [offsetLeft, offsetTop]; }; /** * @constructor Progress Circle class * @param params.canvas Canvas on which the circles will be drawn. * @param params.minRadius Inner radius of the innermost circle, in px. * @param params.arcWidth Width of each circle(to be more accurate, ring). * @param params.gapWidth Space between each circle. * @param params.centerX X coordinate of the center of circles. * @param params.centerY Y coordinate of the center of circles. * @param params.infoLineBaseAngle Base angle of the info line. * @param params.infoLineAngleInterval Angles between the info lines. */ var ProgressCircle = function(params) { this.canvas = params.canvas; this.minRadius = params.minRadius || 15; this.arcWidth = params.arcWidth || 5; this.gapWidth = params.gapWidth || 3; this.centerX = params.centerX || this.canvas.width / 2; this.centerY = params.centerY || this.canvas.height / 2; this.infoLineLength = params.infoLineLength || 60; this.horizLineLength = params.horizLineLength || 10; this.infoLineAngleInterval = params.infoLineAngleInterval || Math.PI / 8; this.infoLineBaseAngle = params.infoLineBaseAngle || Math.PI / 6; this.context = this.canvas.getContext('2d'); this.width = this.canvas.width; this.height = this.canvas.height; this.circles = []; this.runningCount = 0; }; ProgressCircle.prototype = { constructor: ProgressCircle, /** * @method Adds an progress monitor entry. * @param params.fillColor Color to fill in the circle. * @param params.outlineColor Color to outline the circle. * @param params.progressListener Callback function to fetch the progress. * @param params.infoListener Callback function to fetch the info. * @returns this */ addEntry: function(params) { this.circles.push(new Circle({ canvas: this.canvas, context: this.context, centerX: this.centerX, centerY: this.centerY, innerRadius: this.minRadius + this.circles.length * (this.gapWidth + this.arcWidth), arcWidth: this.arcWidth, infoLineLength: this.infoLineLength, horizLineLength: this.horizLineLength, id: this.circles.length, fillColor: params.fillColor, outlineColor: params.outlineColor, progressListener: params.progressListener, infoListener: params.infoListener, infoLineAngle: this.infoLineBaseAngle + this.circles.length * this.infoLineAngleInterval, })); return this; }, /** * @method Starts the monitor and updates with the given interval. * @param interval Interval between updates, in millisecond. * @returns this */ start: function(interval) { var self = this; this.timer = setInterval(function() { self._update(); }, interval || 33); return this; }, /** * @method Stop the animation. */ stop: function() { clearTimeout(this.timer); }, /** * @private * @method Call update on each circle and redraw them. * @returns this */ _update: function() { this._clear(); this.circles.forEach(function(circle, idx, array) { circle.update(); }); return this; }, /** * @private * @method Clear the canvas. * @returns this */ _clear: function() { this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); return this; }, }; /** * @private * @class Individual progress circle. * @param params.canvas Canvas on which the circle will be drawn. * @param params.context Context of the canvas. * @param params.innerRadius Inner radius of the circle, in px. * @param params.arcWidth Width of each arc(circle). * @param params.gapWidth Distance between each arc. * @param params.centerX X coordinate of the center of circles. * @param params.centerY Y coordinate of the center of circles. * @param params.fillColor Color to fill in the circle. * @param params.outlineColor Color to outline the circle. * @param params.progressListener Callback function to fetch the progress. * @param params.infoListener Callback function to fetch the info. * @param params.infoLineAngle Angle of info line. */ var Circle = function(params) { this.id = params.id; this.canvas = params.canvas; this.context = params.context; this.centerX = params.centerX; this.centerY = params.centerY; this.arcWidth = params.arcWidth; this.innerRadius = params.innerRadius || 0; this.fillColor = params.fillColor || '#fff'; this.outlineColor = params.outlineColor || this.fillColor; this.progressListener = params.progressListener; this.infoLineLength = params.infoLineLength || 250; this.horizLineLength = params.horizLineLength || 50; this.infoListener = params.infoListener; this.infoLineAngle = params.infoLineAngle; this.outerRadius = this.innerRadius + this.arcWidth; // If the info listener is not registered, then don't calculate // the related coordinates if (!this.infoListener) return; // calculate the info-line turning points var angle = this.infoLineAngle, arcDistance = (this.innerRadius + this.outerRadius) / 2, sinA = Math.sin(angle), cosA = Math.cos(angle); this.infoLineStartX = this.centerX + sinA * arcDistance; this.infoLineStartY = this.centerY - cosA * arcDistance; this.infoLineMidX = this.centerX + sinA * this.infoLineLength; this.infoLineMidY = this.centerY - cosA * this.infoLineLength; this.infoLineEndX = this.infoLineMidX + (sinA < 0 ? -this.horizLineLength : this.horizLineLength); this.infoLineEndY = this.infoLineMidY; var infoText = document.createElement('div'), style = infoText.style; style.color = this.fillColor; style.position = 'absolute'; style.left = this.infoLineEndX + absPos(this.canvas)[0] + 'px'; // style.top will be calculated in the `drawInfo` method. Since // user may want to change the size of the font, so the top offset // must be updated in each loop. infoText.className = 'ProgressCircleInfo'; // For css styling infoText.id = 'progress_circle_info_' + this.id; document.body.appendChild(infoText); this.infoText = infoText; }; Circle.prototype = { constructor: Circle, update: function() { this.progress = this.progressListener(); this._draw(); if (this.infoListener) { this.info = this.infoListener(); this._drawInfo(); } }, /** * @private * @method Draw the circle on the canvas. * @returns this */ _draw: function() { var ctx = this.context, ANGLE_OFFSET = -Math.PI / 2, startAngle = 0 + ANGLE_OFFSET, endAngle= startAngle + this.progress * Math.PI * 2, x = this.centerX, y = this.centerY, innerRadius = this.innerRadius, outerRadius = this.outerRadius; ctx.fillStyle = this.fillColor; ctx.strokeStyle = this.outlineColor; ctx.beginPath(); ctx.arc(x, y, innerRadius, startAngle, endAngle, false); ctx.arc(x, y, outerRadius, endAngle, startAngle, true); ctx.closePath(); ctx.stroke(); ctx.fill(); return this; }, /** * @private * @method Draw the info lines and info text. * @returns this */ _drawInfo: function() { var pointList, lineHeight; pointList = [ [this.infoLineStartX, this.infoLineStartY], [this.infoLineMidX, this.infoLineMidY], [this.infoLineEndX, this.infoLineEndY], ]; this._drawSegments(pointList, false); this.infoText.innerHTML = this.info; lineHeight = this.infoText.offsetHeight; this.infoText.style.top = this.infoLineEndY + absPos(this.canvas)[1] - lineHeight / 2 + 'px'; return this; }, /** * @private * @method Helper function to draw the segments * @param pointList An array of points in the form of [x, y]. * @param close Whether to connect the first and last point. */ _drawSegments: function(pointList, close) { var ctx = this.context; ctx.beginPath(); ctx.moveTo(pointList[0][0], pointList[0][1]); for (var i = 1; i < pointList.length; ++i) { ctx.lineTo(pointList[i][0], pointList[i][1]); } if (close) { ctx.closePath(); } ctx.stroke(); }, }; window.ProgressCircle = ProgressCircle; })(window, document);