Использование в Angular

Библиотека Vivus работает поверх SVG и управляет анимацией обводки путей через манипуляцию атрибутами stroke-dasharray и stroke-dashoffset. В Angular интеграция строится вокруг жизненного цикла компонентов, поскольку инициализация Vivus требует уже отрендеренного DOM-элемента с SVG.

Установка через npm:

npm install vivus

Библиотека не поставляет Angular-обёрток, поэтому используется прямой импорт:

import Vivus from 'vivus';

SVG-элементы должны быть доступны в DOM к моменту инициализации, иначе Vivus не сможет вычислить длины путей.


Базовая инициализация внутри компонента

Основной способ работы — инициализация в AfterViewInit, когда Angular гарантирует наличие DOM.

import { Component, AfterViewInit, ElementRef, ViewChild, OnDestroy } from '@angular/core';
import Vivus from 'vivus';

@Component({
  selector: 'app-svg-animation',
  template: `
    <div #svgContainer>
      <svg id="logo" viewBox="0 0 200 200">
        <path d="M10 10 L190 10 L190 190 L10 190 Z" stroke="black" fill="none"/>
      </svg>
    </div>
  `
})
export class SvgAnimationComponent implements AfterViewInit, OnDestroy {
  @ViewChild('svgContainer', { static: false }) svgContainer!: ElementRef;
  private vivusInstance?: Vivus;

  ngAfterViewInit(): void {
    this.vivusInstance = new Vivus(
      'logo',
      {
        type: 'delayed',
        duration: 120,
        animTimingFunction: Vivus.EASE
      }
    );
  }

  ngOnDestroy(): void {
    this.vivusInstance = undefined;
  }
}

Ключевым моментом является передача идентификатора SVG-элемента или DOM-ноды. Angular чаще использует id, поскольку это упрощает доступ Vivus к элементу.


Использование ViewChild вместо ID

Работа через ElementRef даёт более строгий контроль над DOM без привязки к глобальным идентификаторам.

@ViewChild('svgRef', { static: false }) svgRef!: ElementRef<SVGElement>;

ngAfterViewInit(): void {
  this.vivusInstance = new Vivus(
    this.svgRef.nativeElement,
    {
      type: 'sync',
      duration: 100
    }
  );
}

HTML:

<svg #svgRef viewBox="0 0 300 300">
  <path d="M50 150 Q150 50 250 150" stroke="black" fill="none"/>
</svg>

Такой подход снижает риск конфликтов идентификаторов при повторном использовании компонентов.


Типы анимации Vivus в Angular-контексте

Vivus поддерживает несколько режимов, которые по-разному ведут себя в рамках Angular change detection.

delayed

Пути анимируются последовательно, с задержкой между сегментами. Подходит для логотипов и декоративных SVG.

new Vivus('logo', { type: 'delayed', duration: 150 });

sync

Все пути анимируются одновременно. Используется для схем и графиков.

new Vivus('diagram', { type: 'sync', duration: 80 });

oneByOne

Каждый сегмент анимируется по очереди внутри одного пути, затем переходит к следующему.

new Vivus('illustration', { type: 'oneByOne', duration: 200 });

Управление повторной анимацией

Angular часто пересоздаёт компоненты при изменении маршрутов или структурных директив. Vivus не всегда автоматически пересчитывает SVG, поэтому требуется ручной контроль.

restartAnimation(): void {
  if (this.vivusInstance) {
    this.vivusInstance.reset();
    this.vivusInstance.play();
  }
}

Методы:

  • reset() — возвращение к начальному состоянию
  • play() — повторный запуск
  • stop() — остановка текущей анимации

Инкапсуляция в Angular Directive

Для повторного использования логики создаётся директива, инкапсулирующая Vivus-инициализацию.

import { Directive, ElementRef, Input, AfterViewInit, OnDestroy } from '@angular/core';
import Vivus from 'vivus';

@Directive({
  selector: '[vivusAnimate]'
})
export class VivusDirective implements AfterViewInit, OnDestroy {
  @Input('vivusAnimate') options: Vivus.Options = {};

  private instance?: Vivus;

  constructor(private el: ElementRef) {}

  ngAfterViewInit(): void {
    this.instance = new Vivus(this.el.nativeElement, {
      type: this.options.type || 'delayed',
      duration: this.options.duration || 100
    });
  }

  ngOnDestroy(): void {
    this.instance = undefined;
  }
}

Использование:

<svg vivusAnimate [vivusAnimate]="{ type: 'sync', duration: 90 }">
  <path d="..." />
</svg>

Такой подход переносит всю логику работы с SVG-анимацией в декларативный слой Angular.


Работа с динамическими SVG

SVG может приходить из API или формироваться через *ngFor. В таких случаях критично дождаться полного рендеринга DOM.

<svg #dynamicSvg>
  <ng-container *ngFor="let p of paths">
    <path [attr.d]="p.d" stroke="black" fill="none"></path>
  </ng-container>
</svg>

Инициализация выполняется после стабилизации view:

import { ChangeDetectorRef, AfterViewChecked } from '@angular/core';

private initialized = false;

ngAfterViewChecked(): void {
  if (!this.initialized) {
    this.initialized = true;

    this.vivusInstance = new Vivus(this.svgRef.nativeElement, {
      type: 'delayed',
      duration: 120
    });
  }
}

Такой способ используется при сложной асинхронной отрисовке SVG-структуры.


Проблемы совместимости с SSR (Angular Universal)

При серверном рендеринге отсутствует DOM, поэтому прямое создание Vivus приводит к ошибкам window is not defined.

Защита выполняется через проверку платформы:

import { isPlatformBrowser } from '@angular/common';
import { Inject, PLATFORM_ID } from '@angular/core';

constructor(@Inject(PLATFORM_ID) private platformId: object) {}

ngAfterViewInit(): void {
  if (!isPlatformBrowser(this.platformId)) {
    return;
  }

  this.vivusInstance = new Vivus('logo', {
    type: 'sync',
    duration: 100
  });
}

Такое разделение позволяет сохранить SSR-совместимость без отключения клиентской анимации.


Управление производительностью

Vivus вычисляет длину каждого SVG-path, что может быть затратным при большом количестве элементов. В Angular это особенно заметно при частых перерендерах.

Практики оптимизации:

  • Изоляция SVG в ChangeDetectionStrategy.OnPush
  • Исключение повторной инициализации Vivus
  • Минимизация количества <path> внутри одного SVG
  • Использование кеширования экземпляра Vivus
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})

Интеграция с Angular Router

При переходах между маршрутами экземпляры Vivus должны корректно уничтожаться, иначе возможны утечки памяти.

ngOnDestroy(): void {
  if (this.vivusInstance) {
    this.vivusInstance.stop();
  }
}

При возврате на страницу возможна повторная инициализация через ngAfterViewInit.


Связка с Angular Animation API

Vivus может использоваться параллельно с @angular/animations, однако важно разграничивать зоны ответственности: Angular управляет контейнером, Vivus — внутренними SVG-path.

trigger('fadeIn', [
  transition(':enter', [
    style({ opacity: 0 }),
    animate('300ms ease-out', style({ opacity: 1 }))
  ])
]);

SVG-анимация запускается после завершения Angular-анимации контейнера, что обеспечивает визуальную согласованность.


Использование нескольких экземпляров Vivus

В сложных интерфейсах может потребоваться независимая анимация нескольких SVG одновременно.

private instances: Vivus[] = [];

ngAfterViewInit(): void {
  const svgs = document.querySelectorAll('svg[data-vivus]');

  svgs.forEach(svg => {
    this.instances.push(
      new Vivus(svg, { type: 'delayed', duration: 100 })
    );
  });
}

Каждый экземпляр управляется отдельно, что позволяет синхронизировать или разносить анимации по времени.


Интеграция с ленивой загрузкой модулей

При lazy-loading модулей Angular Vivus инициализируется только при активации маршрута. Это снижает нагрузку на старт приложения и предотвращает преждевременные вычисления SVG.

const routes: Routes = [
  {
    path: 'animation',
    loadChildren: () =>
      import('./animation/animation.module').then(m => m.AnimationModule)
  }
];

Vivus-инициализация остаётся внутри компонента модуля и не влияет на основной бандл.