Skip to content

Instantly share code, notes, and snippets.

@mopcweb
Last active October 28, 2020 14:17
Show Gist options
  • Save mopcweb/7805e43014ba6686d8490e69eda6ee5d to your computer and use it in GitHub Desktop.
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
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;
}
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;
}
@mopcweb
Copy link
Author

mopcweb commented Oct 28, 2020

Usage

<animated-svg loop>
  <svg>
    <path d="M 10 10, v 100, v -50, h 50, v -50, v 100" />
  
    <path d="M 80 110, v -80, m 0 -10, v -10" />
  </svg>
</animated-svg>

<!-- Or -->

<svg-loader loop>
  <svg>
    <path d="M 10 10, v 100, v -50, h 50, v -50, v 100" />
  
    <path d="M 80 110, v -80, m 0 -10, v -10" />
  </svg>
</svg-loader>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment