Last active
October 28, 2020 14:17
-
-
Save mopcweb/7805e43014ba6686d8490e69eda6ee5d to your computer and use it in GitHub Desktop.
Angular component for animating provided (via ng-content) svg paths with optional props. Second component is for providing backdrop loader w/ animated svg. In future planned to be rewritten in WC or via Svelte/Stencil
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component, ViewChild, ElementRef, Input, Output, EventEmitter, OnChanges } from '@angular/core'; | |
@Component({ | |
selector: 'animated-svg', | |
template: '<div #container><ng-content></ng-content></div>', | |
}) | |
export class AnimatedSvgComponent implements OnChanges { | |
@Input() public shouldAnimate?: boolean = true; | |
@Input() public duration?: number; | |
@Input() public delay?: number; | |
@Input() public oneByOne?: boolean; | |
@Input() public timingFunction?: string; | |
@Input() public loop?: boolean | number; | |
@Input() public loopDelay?: number; | |
@Input() public stroke?: string; | |
@Input() public strokeWidth?: number; | |
@Input() public width?: number; | |
@Output() public onAnimate = new EventEmitter<{ latestAnimationDuration: number }>(); | |
@ViewChild('container', { static: true }) public container: ElementRef<HTMLElement>; | |
private _deafultConfig: IAnimatedSvgConfig = { | |
shouldAnimate: true, | |
duration: 2000, | |
oneByOne: false, | |
delay: 0, | |
timingFunction: 'ease-in-out', | |
loop: false, | |
loopDelay: 500, | |
stroke: '#000000', | |
strokeWidth: 1, | |
}; | |
public ngOnChanges(): void { | |
this.updateSvgs(); | |
} | |
private updateSvgs(): void { | |
this.svgs.forEach((item) => { | |
this.applySvgConfig(item); | |
this.animate(item); | |
}); | |
} | |
private animate(svg: SVGSVGElement): void { | |
if (!this.defaultConfig.shouldAnimate || !svg) return; | |
const { duration, loopDelay, loop } = this.defaultConfig; | |
// const list = Array.from(this.svg.children); | |
const list = Array.from(svg.querySelectorAll('path')); | |
let latestAnimationDuration: number = duration; | |
list.forEach((item: SVGPathElement, i) => { latestAnimationDuration = this.animateSvgPath(item, i); }); | |
this.onAnimate.emit({ latestAnimationDuration }); | |
if (loop) { | |
const loopDuration = Math.max(typeof loop === 'number' ? loop : duration, latestAnimationDuration); | |
setTimeout(() => { this.animate(svg); }, loopDuration + loopDelay); | |
} | |
} | |
private applySvgConfig(svg: SVGSVGElement): void { | |
if (!svg) return; | |
const { width } = window.getComputedStyle(svg); | |
const viewBox = svg.getAttribute('viewBox'); | |
let cWidth = this.getInt(width); | |
if (!cWidth || cWidth === 0) cWidth = this.getInt(viewBox.split(' ')[2]); | |
if (!cWidth || cWidth === 0) cWidth = this.defaultConfig.width; | |
/* eslint-disable-next-line */ | |
svg.style.width = cWidth ? `${cWidth}px` : 'auto'; | |
} | |
private animateSvgPath(item: SVGPathElement, i: number): number { | |
const { duration, delay, timingFunction, oneByOne, stroke, strokeWidth } = this.defaultConfig; | |
let latestAnimationDuration = duration; | |
const animDelay = i === 0 ? 0 : delay * i || 0; | |
const length = item.getTotalLength(); | |
/* eslint-disable no-param-reassign */ | |
item.style.transition = 'none'; | |
item.style.strokeDasharray = `${length} ${length}`; | |
item.style.strokeDashoffset = `${length}`; | |
const { stroke: oStroke, strokeWidth: oStrokeWidth } = window.getComputedStyle(item); | |
if (oStroke === 'none') item.style.stroke = stroke; | |
if (!oStrokeWidth) item.style.strokeWidth = `${strokeWidth}px`; | |
item.style.fill = 'none'; | |
item.getBoundingClientRect(); // This one is necessary in order to apply styles above and init animation | |
item.style.transitionProperty = 'stroke-dashoffset'; | |
item.style.transitionTimingFunction = timingFunction; | |
item.style.transitionDuration = `${duration}ms`; | |
if (oneByOne) { | |
item.style.transitionDelay = `${duration * i + animDelay}ms`; | |
latestAnimationDuration = duration * i + animDelay + duration; | |
} | |
item.style.strokeDashoffset = '0'; | |
/* eslint-enable no-param-reassign */ | |
return latestAnimationDuration; | |
} | |
private get defaultConfig(): IAnimatedSvgConfig { | |
const { | |
shouldAnimate = this._deafultConfig.shouldAnimate, | |
duration = this._deafultConfig.duration, | |
oneByOne = this._deafultConfig.oneByOne, | |
delay = this._deafultConfig.delay, | |
timingFunction = this._deafultConfig.timingFunction, | |
loop = this._deafultConfig.loop, | |
loopDelay = this._deafultConfig.loopDelay, | |
stroke = this._deafultConfig.stroke, | |
strokeWidth = this._deafultConfig.strokeWidth, | |
width = this._deafultConfig.width, | |
} = this; | |
return { | |
shouldAnimate: this.getBoolean(shouldAnimate), | |
duration: this.getInt(duration), | |
oneByOne: this.getBoolean(oneByOne), | |
delay: this.getInt(delay), | |
timingFunction, | |
loop: this.getBooleanOrInt(loop), | |
loopDelay: this.getInt(loopDelay), | |
stroke, | |
strokeWidth: this.getInt(strokeWidth), | |
width: this.getInt(width), | |
}; | |
} | |
private getBooleanOrInt(target: string | boolean | number): boolean | number { | |
const num = this.getInt(target as number); | |
return Number.isInteger(num) ? num : this.getBoolean(target as boolean); | |
} | |
private getInt(target: string | number): number { | |
return typeof target === 'string' ? Number.parseFloat(target) : target; | |
} | |
private getBoolean(target: string | boolean): boolean { | |
return target === 'true' || target === '' || target === true; | |
} | |
private get svgs(): SVGSVGElement[] { | |
return Array.from(this.container.nativeElement.querySelectorAll('svg')); | |
} | |
private get svg(): SVGSVGElement { | |
return this.container.nativeElement.querySelector('svg'); | |
// const list = Array.from(this.container.nativeElement.children); | |
// return list.find((item: HTMLElement) => item.nodeName === 'svg') as HTMLElement; | |
} | |
} | |
export interface IAnimatedSvgConfig { | |
/** Whether to run animation. Default = true. */ | |
shouldAnimate?: boolean; | |
/** Animation duration in ms. Default = 2000. */ | |
duration?: number; | |
/** Whether to animate paths oneByOne instead of simultaniously. Default = false */ | |
oneByOne?: boolean; | |
/** Animation delay in ms for each path if oneByOne. Default = 0. */ | |
delay?: number; | |
/** Animation timing-function. Default = `ease-in-out`. */ | |
timingFunction?: string; | |
/** Whether to run animation in loop (or loop timeout in ms). Default = false. */ | |
loop?: boolean | number; | |
/** Delay before starting next loop in ms. Default = 500. */ | |
loopDelay?: number; | |
/** Path `stroke` attribute (It is necessary in order to run animation). Default = `#000000`. */ | |
stroke?: string; | |
/** Path `stroke-width` attribute. Default = 1. */ | |
strokeWidth?: number; | |
/** Svg `width` attribute. */ | |
width?: number; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component, ViewChild, ElementRef, Input, OnChanges } from '@angular/core'; | |
@Component({ | |
selector: 'svg-loader', | |
styles: [` | |
.Container { | |
position: absolute; | |
top: 0; | |
left: 0; | |
background: #fffffffa; | |
width: 100%; | |
height: 100%; | |
display: flex; | |
justify-content: center; | |
align-content: center; | |
align-items: center; | |
opacity: 1; | |
z-index: 1234567; | |
} | |
.Container_hidden { | |
opacity: 0; | |
z-index: -1; | |
transition: all 1s; | |
} | |
`], | |
template: ` | |
<div #container [ngClass]="{ Container: true, Container_hidden: !show && (couldBeHidden || !config.shouldWaitAnimationEnd) }"> | |
<animated-svg | |
[shouldAnimate]="show" | |
[duration]="duration" | |
[delay]="delay" | |
[oneByOne]="oneByOne" | |
[timingFunction]="timingFunction" | |
[loop]="loop" | |
[loopDelay]="loopDelay" | |
[stroke]="stroke" | |
[strokeWidth]="strokeWidth" | |
[width]="width" | |
(onAnimate)="handleOnAnimate($event)" | |
> | |
<ng-content></ng-content> | |
</animated-svg> | |
</div> | |
`, | |
}) | |
export class SvgLoaderComponent implements OnChanges { | |
@Input() public show = true; | |
@Input() public duration?: string | number; | |
@Input() public delay?: string | number; | |
@Input() public oneByOne?: boolean; | |
@Input() public timingFunction?: string; | |
@Input() public loop?: boolean | string | number; | |
@Input() public loopDelay?: string | number; | |
@Input() public stroke?: string; | |
@Input() public strokeWidth?: string | number; | |
@Input() public width?: string | number; | |
@Input() public containerBackground?: string; | |
@Input() public shouldWaitAnimationEnd?: boolean; | |
@Input() public delayBeforeHide?: string | number; | |
@Input() public fadeOutDuration?: string | number; | |
@ViewChild('container', { static: true }) public container: ElementRef<HTMLElement>; | |
public config: ISvgLoaderConfig; | |
public couldBeHidden = false; | |
public ngOnChanges(): void { | |
// if (show && show.currentValue !== show.previousValue) this.initSvgAnimation(); | |
this.initSvgAnimation(); | |
} | |
public handleOnAnimate(e: { latestAnimationDuration: number }): void { | |
const { shouldWaitAnimationEnd, delayBeforeHide } = this.config; | |
this.couldBeHidden = !this.show; | |
if (shouldWaitAnimationEnd) setTimeout(() => { this.couldBeHidden = true; }, e.latestAnimationDuration + delayBeforeHide); | |
} | |
private initSvgAnimation(): void { | |
this.config = { ...this.defaultConfig }; | |
this.applyContainerConfig(this.config); | |
} | |
private applyContainerConfig({ containerBackground, fadeOutDuration }: ISvgLoaderConfig): void { | |
this.container.nativeElement.style.background = containerBackground; | |
this.container.nativeElement.style.transition = `all ${fadeOutDuration}ms`; | |
} | |
private get defaultConfig(): ISvgLoaderConfig { | |
const { shouldWaitAnimationEnd = true, containerBackground = '#fffffffa', delayBeforeHide = 0, fadeOutDuration = 1000 } = this; | |
return { | |
containerBackground, | |
shouldWaitAnimationEnd: this.getBoolean(shouldWaitAnimationEnd), | |
delayBeforeHide: this.getInt(delayBeforeHide), | |
fadeOutDuration: this.getInt(fadeOutDuration), | |
}; | |
} | |
private getInt(target: string | number): number { | |
return typeof target === 'string' ? Number.parseFloat(target) : target; | |
} | |
private getBoolean(target: string | boolean): boolean { | |
return target === 'true' || target === '' || target === true; | |
} | |
} | |
export interface ISvgLoaderConfig { | |
/** Whether to wait for animation end before fade out. Default = true. */ | |
shouldWaitAnimationEnd?: boolean; | |
/** Backdrop container background. Default = `#fffffffa`. */ | |
containerBackground?: string; | |
/** Delay after animation end and before fade out in ms. Default = 0. */ | |
delayBeforeHide?: number; | |
/** Backdrop container fade out animation duration in ms. Default = 1000. */ | |
fadeOutDuration?: number; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage