3

I'd like to animate an image swap in an angular 4 app. As the controller replaces the img src property the old image should fade away and the new appear.

In AngularJS this was possible by changing the opacity with ng-animate-swap. However, Angular 4 doesn't seem to support animate-swap. How can this be achieved without a swap trigger?

I've tried adding transitions from void to * and back for the src property but this doesn't work as expected. The first image is animated in, later the exchanges happen without animation.

@Component({
  selector: 'app-play-card',
  template: '<div (click)="loadImg()"><img [@fade]="imgsrc" src="{{ imgsrc }}" /></div>',
  styleUrls: ['./play-card.component.css'],
  animations: [
    trigger('fade', [
      transition('void => *', [
        style({opacity: 0.1}),
        animate(5000, style({opacity: 1}))
      ]),
      transition('* => void', [
        style({opacity: 0.9}),
        animate(5000, style({opacity: 0}))
      ])
    ])
  ]
})
br.julien
  • 3,420
  • 2
  • 23
  • 44
duffy
  • 615
  • 1
  • 9
  • 25
  • https://stackoverflow.com/questions/64848450/angular-crossfade-between-images/64849976#64849976 – Eliseo Jan 23 '22 at 11:18

3 Answers3

7

Here is how I did using Angular animations states:

animations.ts

import { trigger, style, animate, transition, state } from '@angular/animations';

export const fade = [
  trigger('fade', [
    state('in', style({ 'opacity': '1' })),
    state('out', style({ 'opacity': '0' })),
    transition('* <=> *', [
      animate(1000)
    ])
  ])
];

app.component.html

<img [@fade]="state" (@fade.done)="onDone($event)" [src]="imageSource" width="500" (click)="onClick()" />

I used (@fade.done) which is a feature of Angular animations that allows you to call a method once your animation is done.

Here, once the first animation has faded to have an opacity of 0, I changed the image path, and changed the animation state, to fade again and have the opacity value back to 1.

app.component.ts

export class AppComponent implements OnInit {
  choice = 2;
  state = 'in';
  counter = 0;
  enableAnimation = false;
  imageSource = '';
  imgSrc1 = 'firstPath';
  imgSrc2 = 'secondPath';

  ngOnInit() {
    this.imageSource = this.imgSrc1;
  }

  onClick() {
    this.enableAnimation = true;
    this.counter = 0;
    this.toggleState();
  }

  toggleImg() {
    if (this.choice === 1) {
      this.imageSource = this.imgSrc1;
      this.choice = 2;
    } else {
      this.imageSource = this.imgSrc2;
      this.choice = 1;
    }
  }

  onDone($event) {
    if (this.enableAnimation) {
      if (this.counter === 1) {
        this.toggleImg();
      }
      this.toggleState();
    }
  }

  toggleState() {
    if (this.counter < 2) {
      this.state = this.state === 'in' ? 'out' : 'in';
      this.counter++;
    }
  }
}

This is obviously a lot of code for a small animation though.

Here is a StackBlitz example I made for this : https://stackblitz.com/edit/angular-anim-fade-img

br.julien
  • 3,420
  • 2
  • 23
  • 44
  • Unfortunately, this solution is not practical after all. My images are loaded over a slow network and are not in the cache when the fadeout is completed. As a result I continue to see the old image for a short time as toggle image is called and the fadein phase begins. Only after a noticeable delay the new image load completes and replaces the old one. The fadein is in full swing at this point. – duffy Nov 30 '17 at 17:34
  • 2
    You can get around this simply by crawling your Document for 'img' elements, and waiting for 'complete'. Use two image elements (instead of one). Start loading the "toggled" image in the background (leaving its opacity at 0 until it 'completes'), then you can actually enable the fade in animation! – Decoded Mar 29 '20 at 16:32
0

I did filter out as much as possible not to overload the code. I hope the code is simple enough for you to integrate it.

gallery.component.ts:

import { Component, Input } from '@angular/core';
import { trigger, style, animate, transition } from '@angular/animations';

const animations = trigger('imgAnimation', [
    transition(':enter', [
        style({ opacity: 0 }),
        animate('500ms', style({ opacity: 1 })),
    ]),
    transition(':leave', [
        animate('300ms', style({ opacity: 0 }))
    ])
]);

@Component({
    selector: 'app-gallery',
    templateUrl: './gallery.component.html',
    styleUrls: [],
    animations: [animations]
})
export class GalleryComponent {
    @Input() images: string[];

    index: number = 0;
    imageIndex: number = 0;

    onIncIndex(increment: number = 1) {
        this.index = (this.index + this.images.length + increment) % this.images.length;
        this.imageIndex = null;
    }

    onDone() {
        this.imageIndex = this.index;
    }
}

gallery.component.html:

<picture>
    <ng-container *ngFor="let img of images; index as i">
        <img *ngIf="i === imageIndex" 
             @imgAnimation
             (@imgAnimation.done)="onDone()"
             [src]="img">
    </ng-container>
</picture>

<div>
    <button (click)="onIncIndex(-1)">Previous</button>
    <button (click)="onIncIndex()">Next</button>
</div>
Ole Pannier
  • 3,208
  • 9
  • 22
  • 33
user3115030
  • 1
  • 1
  • 1
0

If you are working with 2 images.... You could do something like this..

<mat-icon>
  <img
    [@fadeIn]
    *ngIf="myBool"
    src="assets/svg-icons/back.svg"
    alt=""
  />
  <img
    [@fadeIn]
    *ngIf="!myBool"
    src="assets/svg-icons/close.svg"
    alt=""
  />
</mat-icon>

And you will want to animate only the enter state otherwise for an instance both images will be shown side by side

trigger('fadeIn', [
  transition(':enter', [style({ opacity: 0 }), animate(160)]),
  // transition(':leave', [animate(160, style({ opacity: 0 }))]),
]),

You could work around and make the images absolute and then you can animate both the enter and leave state (Because this time the image will overlap each other)

Ibrahim Ali
  • 2,083
  • 2
  • 15
  • 36