ng generate directive MyDirective
import { AnimationPlayer } from '@angular/animations';
import {
coerceBooleanProperty,
coerceNumberProperty,
} from '@angular/cdk/coercion';
import {
AfterContentInit,
AfterViewInit,
Directive,
ElementRef,
Input,
Renderer2,
} from '@angular/core';
import {
ScrollAnimation,
ScrollAnimationService,
} from './scroll-animation.service';
@Directive({
selector: '[scrollAnimation]',
standalone: true,
})
export class ScrollAnimationDirective
implements AfterContentInit, AfterViewInit
{
@Input() scrollAnimation: ScrollAnimation = 'fadeIn';
@Input()
get delay() {
return this._delay;
}
set delay(value: any) {
this._delay = coerceNumberProperty(value);
}
private _delay: number = 0;
@Input()
get stagger() {
return this._stagger;
}
set stagger(value: any) {
this._stagger = coerceBooleanProperty(value);
}
private _stagger: boolean = false;
private intersectionObserver: IntersectionObserver;
private intersectingCache: boolean = false;
private animationPlayer: AnimationPlayer | undefined;
constructor(
private elementRef: ElementRef,
private renderer2: Renderer2,
private scrollAnimationService: ScrollAnimationService
) {
const intersectionObserverInit: IntersectionObserverInit = {
rootMargin: '0px 0px -40% 0px',
};
this.intersectionObserver = new IntersectionObserver(
(entries) => this.onIntersection(entries[0]),
intersectionObserverInit
);
}
ngAfterContentInit() {
const properties = this.scrollAnimationService.build(
this.scrollAnimation
)?.properties;
const animation = this.scrollAnimationService.build(
this.scrollAnimation
)?.animation;
if (properties && animation) {
this.setStyles(properties);
this.animationPlayer = animation.create(this.elementRef.nativeElement);
}
}
ngAfterViewInit() {
this.intersectionObserver.observe(this.elementRef.nativeElement);
}
setStyles(properties: any) {
if (!this.stagger) {
return this.renderer2.setStyle(
this.elementRef.nativeElement,
'opacity',
0
);
}
for (const style in properties) {
this.renderer2.setStyle(
this.elementRef.nativeElement,
style,
properties[style]
);
}
}
onIntersection(entry: IntersectionObserverEntry) {
if (this.intersectingCache) {
return;
}
if (this.intersectingCache == entry.isIntersecting) {
return;
}
this.intersectingCache = entry.isIntersecting;
if (!this.intersectingCache) {
return;
}
if (this.stagger) {
this.renderer2.setStyle(this.elementRef.nativeElement, 'opacity', 1);
}
setTimeout(() => this.animationPlayer?.play(), this.delay);
this.intersectionObserver.disconnect();
}
}
- 디렉티브
ng generate service <name> [options]
- 서비스 생성
import { AnimationBuilder, AnimationFactory } from '@angular/animations';
import { Injectable } from '@angular/core';
import {
fromIntroBottom,
fromIntroBottomStagger,
fromIntroLeftStagger,
fromIntroRight,
fromIntroRightStagger,
fromIntroTop,
fromIntroTopStagger,
fromIntroleft,
toIntroBottom,
toIntroLeft,
toIntroRight,
toIntroTop,
} from './intro.animations';
type Result = {
properties: Object;
animation: AnimationFactory;
};
export type ScrollAnimation =
| 'fadeIn'
| 'fadeOut'
| 'introTop'
| 'introRight'
| 'introBottom'
| 'introLeft'
| 'introBottomStagger'
| 'introLeftStagger'
| 'introRightStagger'
| 'introTopStagger'
| 'focusIn';
@Injectable({
providedIn: 'root',
})
export class ScrollAnimationService {
constructor(private animationBuilder: AnimationBuilder) {}
build(scrollAnimation: ScrollAnimation): Result | null {
if (scrollAnimation === 'introBottomStagger') {
return {
properties: toIntroBottom,
animation: this.animationBuilder.build(fromIntroBottomStagger),
};
} else if (scrollAnimation === 'introLeft') {
return {
properties: toIntroLeft,
animation: this.animationBuilder.build(fromIntroleft),
};
} else if (scrollAnimation === 'introLeftStagger') {
return {
properties: toIntroLeft,
animation: this.animationBuilder.build(fromIntroLeftStagger),
};
}
} else {
return null;
}
}
}
import { animate, keyframes, query, stagger, style } from '@angular/animations';
export const toIntroTop = {
transform: 'translateY(-50px)',
opacity: 0,
};
export const fromIntroTop = [
animate(
'1s cubic-bezier(0.250, 0.460, 0.450, 0.940)',
keyframes([
style({
transform: 'translateY(-50px)',
opacity: '0',
offset: 0,
}),
style({
transform: 'translateY(0)',
opacity: '1',
offset: 1.0,
}),
])
),
];
export const toIntroRight = {
transform: 'translateX(50px)',
opacity: 0,
};
export const fromIntroRight = [
animate(
'1s cubic-bezier(0.250, 0.460, 0.450, 0.940)',
keyframes([
style({
transform: 'translateX(50px)',
opacity: 0,
offset: 0,
}),
style({
transform: 'translateX(0)',
opacity: 1,
offset: 1.0,
}),
])
),
];
export const toIntroBottom = {
transform: 'translateY(0px)',
opacity: 0,
};
export const fromIntroBottom = [
animate(
'1s cubic-bezier(0.250, 0.460, 0.450, 0.940)',
keyframes([
style({
transform: 'translateY(50px)',
opacity: 0,
offset: 0,
}),
style({
transform: 'translateY(0)',
opacity: 1,
offset: 1.0,
}),
])
),
];
export const toIntroLeft = {
transform: 'translateX(-50px)',
opacity: 0,
};
export const fromIntroleft = [
animate(
'1s cubic-bezier(0.250, 0.460, 0.450, 0.940)',
keyframes([
style({
transform: 'translateX(-50px)',
opacity: 0,
offset: 0,
}),
style({
transform: 'translateX(0)',
opacity: 1,
offset: 1.0,
}),
])
),
];
export const fromIntroBottomStagger = [
query('.animation-item', [
style({
transform: 'translateY(50px)',
opacity: 0,
}),
stagger(200, [
animate(
'1s cubic-bezier(0.250, 0.460, 0.450, 0.940)',
style({
transform: 'translateY(0)',
opacity: 1,
})
),
]),
]),
];
export const fromIntroLeftStagger = [
query('.animation-item', [
style({
transform: 'translateX(-50px)',
opacity: '0',
}),
stagger(200, [
animate(
'1s cubic-bezier(0.250, 0.460, 0.450, 0.940)',
style({
transform: 'translateX(0)',
opacity: '1',
})
),
]),
]),
];
export const fromIntroRightStagger = [
query('.animation-item', [
style({
transform: 'translateX(50px)',
opacity: '0',
}),
stagger(200, [
animate(
'1s cubic-bezier(0.250, 0.460, 0.450, 0.940)',
style({
transform: 'translateX(0)',
opacity: '1',
})
),
]),
]),
];
export const fromIntroTopStagger = [
query('.animation-item', [
style({
transform: 'translateY(-50px)',
opacity: '0',
}),
stagger(200, [
animate(
'1s cubic-bezier(0.250, 0.460, 0.450, 0.940)',
style({
transform: 'translateY(0)',
opacity: '1',
})
),
]),
]),
];
export const fadeIn = [
style({
opacity: 1,
}),
animate(
'2000ms cubic-bezier(0.4, 0.0, 0.2, 1)',
style({
opacity: 0,
})
),
];
import { ScrollAnimationDirective } from '/animations/scroll-animation/scroll-animation.directive';
imports: [ScrollAnimationDirective],
<div
scrollAnimation="introBottomStagger"
stagger
>
<div class="animation-item">자식태그1</div>
<div class="animation-item">자식태그2</div>
<div class="animation-item">자식태그3</div>
</div>