Angular Router Outlets and Animations with SVGs

Viewed 41

I have been experimenting with Angular and SVGs. After much success (SVG has changed my life), I have encountered an interesting head-scratcher: I have an inline SVG in my root app component that has a router outlet embedded in a foreignObject,

<!-- app.component.html -->
<main>
    <svg viewBox="0 0 900 1600">
        <defs>
            <clipPath 
               id="viewport"
               clipPathUnits="userSpaceOnUse">
                <rect
                   width="178.36444"
                   height="294.26218"
                   ry="49.872025"/>
             </clipPath>
        </defs>

        <!--other stuff: e.g. circles, paths, etc.-->

        <foreignObject 
            width="178.36444"
            height="294.26218"
            x="29.946405"
            y="24.862501"
            clip-path="url(#viewport)"> 
                <router-outlet>
                </router-outlet>
        </foreignObject>
    </svg>
</main>

The router works fine (which is extremely cool), however I am having trouble with animations that are coming from the routed components. The '/home' component, for example, has another inline SVG with an animation attached to it,

<!-- home.component.html -->
<section>
    <svg viewBox="0 0 508 285.75"
         preserveAspectRatio="xMaxYMax meet">
        <!-- other stuff -->
        <path 
           [@cntl_revolve]="revolveCntl.state"
           d="...path..."
           id="revolver-lower"
           class="revolver" />
    </svg>
</section>

Where the cntl_revolve animation trigger is defined through a static factory method,

// animations.ts
import {
  animate,
  animateChild,
  AnimationTriggerMetadata,
  keyframes,
  query,
  state,
  style,
  transition,
  trigger,
} from '@angular/animations';

export enum AnimationTriggers {
  cntl_revolve = 'cntl_revolve',
  // ...
}

export enum AnimationPeriods {
  // ..
  minute = 60,
  five = 300,
  // ..
}

function formatTriggerTag(trigger: string, tag: string | null | undefined) {
  return tag ? `${trigger}_${tag}` : trigger;
}

// ...

export class Animations{

    // ...

    public static getManualRevolveTrigger(
        reversed: boolean = false,
        tag: string | null | undefined = null,
        animateLength: number = AnimationPeriods.minute
      ): AnimationTriggerMetadata {
        let triggerTag: string = formatTriggerTag(
          AnimationTriggers.cntl_revolve, 
          tag
        );
        let reversal: string = reversed ? '-' : '';
        return trigger(triggerTag, [
          state(
            'on',
            style({
               transform: `rotate(${reversal}360deg)`,
            })
          ),
          state(
            'off',
            style({
              transform: 'rotate(0deg)',
            })
          ),
          transition('on <=> off', [
            animate(`${animateLength}s`),
            query('@*', animateChild(), { optional: true }),
          ]),
        ]);
      }

The animation is kicked off in the HomeComponent's AfterViewInit,

// home.component.ts
import { Component } from '@angular/core';
import { 
  AnimationControl, 
  AnimationPeriods, 
  Animations, 
  AnimationTriggers 
} from 'src/animations';
const REVOLVE_DURATION = AnimationPeriods.five;

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css'],
  animations:[
    Animations.getManualRevolveTrigger(false, null, REVOLVE_DURATION),
  ]
})
export class HomeComponent{
    public revolveCntl: AnimationControl = new AnimationControl(
        AnimationTriggers.cntl_revolve
      );

    constructor(){}

    ngAfterViewInit(){
          this.revolve();
      }

    private revolve(): void {
        if(!this.revolveCntl.fired()){
          this.revolveCntl.animate();
        }
        else{
          this.revolveCntl.prime();
        }
        setTimeout(()=>{
          this.revolve();
        }, REVOLVE_DURATION * 1020);
    }
}

Where AnimationControl is a simple wrapper class that has a state that toggles between 'on' and 'off' when animate() and prime() are called, respectively.

When the user is first routed to the component, the animation doesn't fire. However, if I resize the page, or interact with the page in anyway (i.e. clicking on a clickable div, toggling controls, etc), the animation begins. I've tried starting the animation in the OnInit and constructor, but I get the same behavior. I have added console.logs to ngAfterViewInit and the revolve methods, so I know it is definitely being called when the component is created. The accompanying animation just doesn't start for some reason.

I have verified the animation works as intended if it is not rendered through the foreignObject's router-outlet, i.e. if I lay down an <app-home></app-home> then the animation will kick off as soon as the component is created. I assume something funky is happening between Angular's rendering engine and the draw order of the SVG; I have noticed if I put an SVG object on top of the foreignObject it will initially get drawn on top (as expected), but as soon as I jimmy the animation into starting, the nested animation in the HomeComponent gets redrawn on top of everything.

I am not sure I understand enough about SVGs or how Angular actually works to understand this problem currently. I am curious what is going on, though. Can anyone provide an explanation that won't break my brain? How can I get my animation to start when the page loads?

0 Answers
Related