UNPKG

angular-svg-round-progressbar

Version:

Angular module that uses SVG to create a circular progressbar

558 lines (550 loc) 24.7 kB
import * as i0 from '@angular/core'; import { DOCUMENT, Injectable, Optional, Inject, InjectionToken, EventEmitter, Component, ChangeDetectionStrategy, ViewChild, Input, Output, NgModule } from '@angular/core'; const DEGREE_IN_RADIANS = Math.PI / 180; class RoundProgressService { constructor(document) { this.supportsSvg = !!(document && document.createElementNS && document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect); this.base = document?.head?.querySelector('base'); this.hasPerf = typeof window !== 'undefined' && window.performance && window.performance.now && typeof window.performance.now() === 'number'; } /** * Resolves a SVG color against the page's `base` tag. */ resolveColor(color) { if (this.base && this.base.href) { const hashIndex = color.indexOf('#'); if (hashIndex > -1 && color.indexOf('url') > -1) { return color.slice(0, hashIndex) + location.href + color.slice(hashIndex); } } return color; } /** * Generates a timestamp. */ getTimestamp() { return this.hasPerf ? window.performance.now() : Date.now(); } /** * Generates the value for an SVG arc. * * @param current Current value. * @param total Maximum value. * @param pathRadius Radius of the SVG path. * @param elementRadius Radius of the SVG container. * @param isSemicircle Whether the element should be a semicircle. */ getArc(current, total, pathRadius, elementRadius, isSemicircle = false) { const value = Math.max(0, Math.min(current || 0, total)); const maxAngle = isSemicircle ? 180 : 359.9999; const percentage = total === 0 ? maxAngle : (value / total) * maxAngle; const start = this._polarToCartesian(elementRadius, pathRadius, percentage); const end = this._polarToCartesian(elementRadius, pathRadius, 0); const arcSweep = percentage <= 180 ? 0 : 1; return `M ${start} A ${pathRadius} ${pathRadius} 0 ${arcSweep} 0 ${end}`; } /** * Converts polar cooradinates to Cartesian. * * @param elementRadius Radius of the wrapper element. * @param pathRadius Radius of the path being described. * @param angleInDegrees Degree to be converted. */ _polarToCartesian(elementRadius, pathRadius, angleInDegrees) { const angleInRadians = (angleInDegrees - 90) * DEGREE_IN_RADIANS; const x = elementRadius + pathRadius * Math.cos(angleInRadians); const y = elementRadius + pathRadius * Math.sin(angleInRadians); return x + ' ' + y; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressService, deps: [{ token: DOCUMENT, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: Document, decorators: [{ type: Optional }, { type: Inject, args: [DOCUMENT] }] }] }); const DEFAULTS = { radius: 125, animation: 'easeOutCubic', animationDelay: undefined, duration: 500, stroke: 15, color: '#45CCCE', background: '#EAEAEA', responsive: false, clockwise: true, semicircle: false, rounded: false, innerCircleFill: 'transparent', }; const ROUND_PROGRESS_DEFAULTS = new InjectionToken('ROUND_PROGRESS_DEFAULTS', { providedIn: 'root', factory: () => DEFAULTS }); const ROUND_PROGRESS_DEFAULTS_PROVIDER = { provide: ROUND_PROGRESS_DEFAULTS, useValue: DEFAULTS, }; /** * TERMS OF USE - EASING EQUATIONS * Open source under the BSD License. * * Copyright © 2001 Robert Penner * All rights reserved. * Redistribution and use in source and binary forms, with or without modification, are permitted * provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this list of conditions * and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions * and the following disclaimer in the documentation and/or other materials provided with the * distribution. * * Neither the name of the author nor the names of contributors may be used to endorse or promote * products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ class RoundProgressEase { // t: current time (or position) of the neonate. This can be seconds or frames, steps, // seconds, ms, whatever – as long as the unit is the same as is used for the total time. // b: beginning value of the property. // c: change between the beginning and destination value of the property. // d: total time of the neonate. linearEase(t, b, c, d) { return (c * t) / d + b; } easeInQuad(t, b, c, d) { return c * (t /= d) * t + b; } easeOutQuad(t, b, c, d) { return -c * (t /= d) * (t - 2) + b; } easeInOutQuad(t, b, c, d) { // eslint-disable-next-line no-cond-assign if ((t /= d / 2) < 1) { return (c / 2) * t * t + b; } return (-c / 2) * (--t * (t - 2) - 1) + b; } easeInCubic(t, b, c, d) { return c * (t /= d) * t * t + b; } easeOutCubic(t, b, c, d) { return c * ((t = t / d - 1) * t * t + 1) + b; } easeInOutCubic(t, b, c, d) { // eslint-disable-next-line no-cond-assign if ((t /= d / 2) < 1) { return (c / 2) * t * t * t + b; } return (c / 2) * ((t -= 2) * t * t + 2) + b; } easeInQuart(t, b, c, d) { return c * (t /= d) * t * t * t + b; } easeOutQuart(t, b, c, d) { return -c * ((t = t / d - 1) * t * t * t - 1) + b; } easeInOutQuart(t, b, c, d) { // eslint-disable-next-line no-cond-assign if ((t /= d / 2) < 1) { return (c / 2) * t * t * t * t + b; } return (-c / 2) * ((t -= 2) * t * t * t - 2) + b; } easeInQuint(t, b, c, d) { return c * (t /= d) * t * t * t * t + b; } easeOutQuint(t, b, c, d) { return c * ((t = t / d - 1) * t * t * t * t + 1) + b; } easeInOutQuint(t, b, c, d) { // eslint-disable-next-line no-cond-assign if ((t /= d / 2) < 1) { return (c / 2) * t * t * t * t * t + b; } return (c / 2) * ((t -= 2) * t * t * t * t + 2) + b; } easeInSine(t, b, c, d) { return -c * Math.cos((t / d) * (Math.PI / 2)) + c + b; } easeOutSine(t, b, c, d) { return c * Math.sin((t / d) * (Math.PI / 2)) + b; } easeInOutSine(t, b, c, d) { return (-c / 2) * (Math.cos((Math.PI * t) / d) - 1) + b; } easeInExpo(t, b, c, d) { return t === 0 ? b : c * Math.pow(2, 10 * (t / d - 1)) + b; } easeOutExpo(t, b, c, d) { return t === d ? b + c : c * (-Math.pow(2, (-10 * t) / d) + 1) + b; } easeInOutExpo(t, b, c, d) { if (t === 0) { return b; } if (t === d) { return b + c; } // eslint-disable-next-line no-cond-assign if ((t /= d / 2) < 1) { return (c / 2) * Math.pow(2, 10 * (t - 1)) + b; } return (c / 2) * (-Math.pow(2, -10 * --t) + 2) + b; } easeInCirc(t, b, c, d) { return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b; } easeOutCirc(t, b, c, d) { return c * Math.sqrt(1 - (t = t / d - 1) * t) + b; } easeInOutCirc(t, b, c, d) { // eslint-disable-next-line no-cond-assign if ((t /= d / 2) < 1) { return (-c / 2) * (Math.sqrt(1 - t * t) - 1) + b; } return (c / 2) * (Math.sqrt(1 - (t -= 2) * t) + 1) + b; } easeInElastic(t, b, c, d) { const p = d * 0.3; let s = 1.70158; let a = c; if (t === 0) { return b; } // eslint-disable-next-line no-cond-assign if ((t /= d) === 1) { return b + c; } if (a < Math.abs(c)) { a = c; s = p / 4; } else { s = (p / (2 * Math.PI)) * Math.asin(c / a); } return -(a * Math.pow(2, 10 * t--) * Math.sin(((t * d - s) * (2 * Math.PI)) / p)) + b; } easeOutElastic(t, b, c, d) { const p = d * 0.3; let s = 1.70158; let a = c; if (t === 0) { return b; } // eslint-disable-next-line no-cond-assign if ((t /= d) === 1) { return b + c; } if (a < Math.abs(c)) { a = c; s = p / 4; } else { s = (p / (2 * Math.PI)) * Math.asin(c / a); } return a * Math.pow(2, -10 * t) * Math.sin(((t * d - s) * (2 * Math.PI)) / p) + c + b; } easeInOutElastic(t, b, c, d) { const p = d * (0.3 * 1.5); let s = 1.70158; let a = c; if (t === 0) { return b; } // eslint-disable-next-line no-cond-assign if ((t /= d / 2) === 2) { return b + c; } if (a < Math.abs(c)) { a = c; s = p / 4; } else { s = (p / (2 * Math.PI)) * Math.asin(c / a); } if (t < 1) { return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin(((t * d - s) * (2 * Math.PI)) / p)) + b; } return a * Math.pow(2, -10 * (t -= 1)) * Math.sin(((t * d - s) * (2 * Math.PI)) / p) * 0.5 + c + b; } easeInBack(t, b, c, d, s = 1.70158) { return c * (t /= d) * t * ((s + 1) * t - s) + b; } easeOutBack(t, b, c, d, s = 1.70158) { return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b; } easeInOutBack(t, b, c, d, s = 1.70158) { // eslint-disable-next-line no-cond-assign if ((t /= d / 2) < 1) { return (c / 2) * (t * t * (((s *= 1.525) + 1) * t - s)) + b; } return (c / 2) * ((t -= 2) * t * (((s *= 1.525) + 1) * t + s) + 2) + b; } easeInBounce(t, b, c, d) { return c - this.easeOutBounce(d - t, 0, c, d) + b; } easeOutBounce(t, b, c, d) { // eslint-disable-next-line no-cond-assign if ((t /= d) < 1 / 2.75) { return c * (7.5625 * t * t) + b; } else if (t < 2 / 2.75) { return c * (7.5625 * (t -= 1.5 / 2.75) * t + 0.75) + b; } else if (t < 2.5 / 2.75) { return c * (7.5625 * (t -= 2.25 / 2.75) * t + 0.9375) + b; } return c * (7.5625 * (t -= 2.625 / 2.75) * t + 0.984375) + b; } easeInOutBounce(t, b, c, d) { if (t < d / 2) { return this.easeInBounce(t * 2, 0, c, d) * 0.5 + b; } return this.easeOutBounce(t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressEase, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressEase, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressEase, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class RoundProgressComponent { constructor(service, easing, defaults, ngZone) { this.service = service; this.easing = easing; this.defaults = defaults; this.ngZone = ngZone; this.currentLinecap = ''; /** Current value of the progress bar. */ this.current = 0; /** Maximum value of the progress bar. */ this.max = 0; /** Radius of the circle. */ this.radius = this.defaults.radius; /** Name of the easing function to use when animating. */ this.animation = this.defaults.animation; /** Time in millisconds by which to delay the animation. */ this.animationDelay = this.defaults.animationDelay; /** Duration of the animation. */ this.duration = this.defaults.duration; /** Width of the circle's stroke. */ this.stroke = this.defaults.stroke; /** Color of the circle. */ this.color = this.defaults.color; /** Background color of the circle. */ this.background = this.defaults.background; /** Whether the circle should take up the width of its parent. */ this.responsive = this.defaults.responsive; /** Whether the circle is filling up clockwise. */ this.clockwise = this.defaults.clockwise; /** Whether to render a semicircle. */ this.semicircle = this.defaults.semicircle; /** Whether the tip of the progress should be rounded off. */ this.rounded = this.defaults.rounded; /** Fill color of the circle inside the progress bar */ this.innerCircleFill = this.defaults.innerCircleFill; /** Emits when a new value has been rendered. */ this.onRender = new EventEmitter(); this.lastAnimationId = 0; } /** Animates a change in the current value. */ _animateChange(from, to) { if (typeof from !== 'number') { from = 0; } to = this._clamp(to); from = this._clamp(from); const changeInValue = to - from; const duration = this.duration; // Avoid firing change detection for each of the animation frames. this.ngZone.runOutsideAngular(() => { const start = () => { const startTime = this.service.getTimestamp(); const id = ++this.lastAnimationId; const animation = () => { const currentTime = Math.min(this.service.getTimestamp() - startTime, duration); const easingFn = this.easing[this.animation]; const value = easingFn(currentTime, from, changeInValue, duration); this._updatePath(value); if (this.onRender.observers.length > 0) { this.onRender.emit(value); } if (id === this.lastAnimationId && currentTime < duration) { requestAnimationFrame(animation); } }; requestAnimationFrame(animation); }; if (this.animationDelay > 0) { setTimeout(start, this.animationDelay); } else { start(); } }); } /** Updates the path apperance. */ _updatePath(value) { if (this.path) { const arc = this.service.getArc(value, this.max, this.radius - this.stroke / 2, this.radius, this.semicircle); const path = this.path.nativeElement; // Remove the rounded line cap when the value is zero, // because SVG won't allow it to disappear completely. const linecap = this.rounded && value > 0 ? 'round' : ''; // This is called on each animation frame so avoid // updating the line cap unless it has changed. if (linecap !== this.currentLinecap) { this.currentLinecap = linecap; path.style.strokeLinecap = linecap; } path.setAttribute('d', arc); } } /** Clamps a value between the maximum and 0. */ _clamp(value) { return Math.max(0, Math.min(value || 0, this.max)); } /** Determines the SVG transforms for the <path> node. */ getPathTransform() { const diameter = this._getDiameter(); if (this.semicircle) { return this.clockwise ? `translate(0, ${diameter}) rotate(-90)` : `translate(${diameter + ',' + diameter}) rotate(90) scale(-1, 1)`; } else if (!this.clockwise) { return `scale(-1, 1) translate(-${diameter} 0)`; } return null; } /** Resolves a color through the service. */ resolveColor(color) { return this.service.resolveColor(color); } /** Change detection callback. */ ngOnChanges(changes) { if (changes.current) { this._animateChange(changes.current.previousValue, changes.current.currentValue); } else { this._updatePath(this.current); } } /** Diameter of the circle. */ _getDiameter() { return this.radius * 2; } /** The CSS height of the wrapper element. */ _getElementHeight() { if (!this.responsive) { return (this.semicircle ? this.radius : this._getDiameter()) + 'px'; } return null; } /** Viewbox for the SVG element. */ _getViewBox() { const diameter = this._getDiameter(); return `0 0 ${diameter} ${this.semicircle ? this.radius : diameter}`; } /** Bottom padding for the wrapper element. */ _getPaddingBottom() { if (this.responsive) { return this.semicircle ? '50%' : '100%'; } return null; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressComponent, deps: [{ token: RoundProgressService }, { token: RoundProgressEase }, { token: ROUND_PROGRESS_DEFAULTS }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.1", type: RoundProgressComponent, isStandalone: true, selector: "round-progress", inputs: { current: "current", max: "max", radius: "radius", animation: "animation", animationDelay: "animationDelay", duration: "duration", stroke: "stroke", color: "color", background: "background", responsive: "responsive", clockwise: "clockwise", semicircle: "semicircle", rounded: "rounded", innerCircleFill: "innerCircleFill" }, outputs: { onRender: "onRender" }, host: { attributes: { "role": "progressbar" }, properties: { "attr.aria-valuemin": "0", "attr.aria-valuemax": "max", "attr.aria-valuenow": "current", "style.width": "responsive ? \"\" : _getDiameter() + \"px\"", "style.height": "_getElementHeight()", "style.padding-bottom": "_getPaddingBottom()", "class.responsive": "responsive" } }, viewQueries: [{ propertyName: "path", first: true, predicate: ["path"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<svg xmlns=\"http://www.w3.org/2000/svg\" [attr.viewBox]=\"_getViewBox()\">\n <circle\n [attr.fill]=\"innerCircleFill\"\n [attr.cx]=\"radius\"\n [attr.cy]=\"radius\"\n [attr.r]=\"radius - stroke / 2\"\n [style.stroke]=\"resolveColor(background)\"\n [style.stroke-width]=\"stroke\"/>\n\n <path\n #path\n fill=\"none\"\n [style.stroke-width]=\"stroke\"\n [style.stroke]=\"resolveColor(color)\"\n [attr.transform]=\"getPathTransform()\"/>\n</svg>\n", styles: [":host{display:block;position:relative;overflow:hidden}:host(.responsive){width:100%;padding-bottom:100%}:host(.responsive)>svg{position:absolute;width:100%;height:100%;top:0;left:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressComponent, decorators: [{ type: Component, args: [{ selector: 'round-progress', changeDetection: ChangeDetectionStrategy.OnPush, host: { 'role': 'progressbar', '[attr.aria-valuemin]': '0', '[attr.aria-valuemax]': 'max', '[attr.aria-valuenow]': 'current', '[style.width]': 'responsive ? "" : _getDiameter() + "px"', '[style.height]': '_getElementHeight()', '[style.padding-bottom]': '_getPaddingBottom()', '[class.responsive]': 'responsive', }, template: "<svg xmlns=\"http://www.w3.org/2000/svg\" [attr.viewBox]=\"_getViewBox()\">\n <circle\n [attr.fill]=\"innerCircleFill\"\n [attr.cx]=\"radius\"\n [attr.cy]=\"radius\"\n [attr.r]=\"radius - stroke / 2\"\n [style.stroke]=\"resolveColor(background)\"\n [style.stroke-width]=\"stroke\"/>\n\n <path\n #path\n fill=\"none\"\n [style.stroke-width]=\"stroke\"\n [style.stroke]=\"resolveColor(color)\"\n [attr.transform]=\"getPathTransform()\"/>\n</svg>\n", styles: [":host{display:block;position:relative;overflow:hidden}:host(.responsive){width:100%;padding-bottom:100%}:host(.responsive)>svg{position:absolute;width:100%;height:100%;top:0;left:0}\n"] }] }], ctorParameters: () => [{ type: RoundProgressService }, { type: RoundProgressEase }, { type: undefined, decorators: [{ type: Inject, args: [ROUND_PROGRESS_DEFAULTS] }] }, { type: i0.NgZone }], propDecorators: { path: [{ type: ViewChild, args: ['path'] }], current: [{ type: Input }], max: [{ type: Input }], radius: [{ type: Input }], animation: [{ type: Input }], animationDelay: [{ type: Input }], duration: [{ type: Input }], stroke: [{ type: Input }], color: [{ type: Input }], background: [{ type: Input }], responsive: [{ type: Input }], clockwise: [{ type: Input }], semicircle: [{ type: Input }], rounded: [{ type: Input }], innerCircleFill: [{ type: Input }], onRender: [{ type: Output }] } }); class RoundProgressModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressModule, imports: [RoundProgressComponent], exports: [RoundProgressComponent] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressModule, providers: [ROUND_PROGRESS_DEFAULTS_PROVIDER] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RoundProgressModule, decorators: [{ type: NgModule, args: [{ imports: [RoundProgressComponent], exports: [RoundProgressComponent], providers: [ROUND_PROGRESS_DEFAULTS_PROVIDER], }] }] }); /** * Generated bundle index. Do not edit. */ export { ROUND_PROGRESS_DEFAULTS, ROUND_PROGRESS_DEFAULTS_PROVIDER, RoundProgressComponent, RoundProgressEase, RoundProgressModule, RoundProgressService, RoundProgressModule as RoundprogressModule }; //# sourceMappingURL=angular-svg-round-progressbar.mjs.map