Angular의 Twitter 스타일 생일 풍선

Mar 28 2023
이 글을 쓰는 시점에서 OpenAI의 GPT4가 최근에 출시되었으며 잠재적으로 이를 쉽게 구현할 수 있습니다. 그럼에도 불구하고 스스로 작업을 수행하고 싶다면 계속 읽어도 좋습니다.

이 글을 쓰는 시점에서 OpenAI의 GPT4가 최근에 출시되었으며 잠재적으로 이를 쉽게 구현할 수 있습니다. 그럼에도 불구하고 스스로 작업을 수행하고 싶다면 계속 읽어도 좋습니다. 실제로 스스로 작업을 수행하는 것에 대해 만족스러운 것이 있습니다.

약 1년 전, 저는 회사의 애플리케이션에 생일 풍선을 추가할 생각을 했습니다. 나는 영감을 찾기 시작했고 CodePen에서 이 순수한 html/javascript 구현을 발견했습니다. 이것은 실제 풍선의 모양과 그림자에 큰 영감을 주었지만, 그것들을 렌더링하기 위한 논리는 (적어도 나에게는) Angular로 바로 이식하기가 조금 더 어려웠습니다.

이 게시물에서는 Angular CLI만으로 처음부터 시작하여 몇 가지 터지는 작업으로 끝나는 Angular Material CDK의 오버레이를 사용하여 사용자 지정 가능한 생일 풍선을 만드는 방법을 보여줍니다.

아이디어는 다음과 같습니다.

  1. 사용자 지정 지시문을 사용하여 조건부로 "풍선 컨테이너"를 오버레이로 렌더링합니다.
  2. 이 컨테이너 내부의 풍선에 애니메이션을 적용합니다.
  3. 추가 재미: "터지는" 풍선을 추가하여 더욱 축제 분위기를 연출하세요."

Angular CLI를 사용하여 "AngularBalloons"라는 앱을 만드는 것으로 시작합니다.

ng n AngularBalloons

먼저 렌더링하려는 모든 풍선을 포함하는 컨테이너를 만듭니다.

ng g c BalloonContainer. 추적을 위한 ID 속성(나중에 유용함)과 화면에서 애니메이션 속도 및 배치를 무작위화하기 위한 스타일이 있는 풍선 인터페이스를 추가합니다. 또한 이러한 사용자 지정 스타일을 생성하기 위해 몇 가지 도우미 메서드를 추가합니다.

// balloon-container.component.ts
import { Component } from '@angular/core';
import { BalloonComponent } from '../balloon/balloon.component';

export interface Balloon {
  id: number;
  style: string;
}

export function getRandomInt(max: number) {
  return Math.floor(Math.random() * max);
}

// Helper function to set animation duration and some offset 
function getRandomStyles() {
  return `margin: ${getRandomInt(200)}px 0 0 ${getRandomInt(50)}px;
          animation-duration: ${getRandomInt(10) + 7}s`;
}

@Component({
  selector: 'app-balloon-container',
  standalone: true,
  template: `
    <app-balloon 
        *ngFor="let balloon of balloons" 
        [balloon]="balloon">
    </app-balloon>
  `,
  styleUrls: ['./balloon-container.component.scss'],
  imports: [BalloonComponent],
})
export class BalloonContainerComponent {

  readonly nBalloons = 20;
  balloons: Balloon[] = [...new Array(this.nBalloons)].map((_, idx) => ({
    id: idx,
    style: getRandomStyles(),
  }));
}

// balloon.component.ts
/* imports */
@Component({
  selector: 'app-balloon',
  standalone: true,
  template: `
    <div
      *ngIf="balloon"
      class="c-balloon"
      [style]="balloon.style"
      [style.left]="position">
    </div>`,
  styleUrls: ['./balloon.component.scss'],
})
export class BalloonComponent {
  @Input()
  balloon?: Balloon;

  position: string;
  constructor() {
    // place the balloon a random place covering the screen width
    this.position = `${getRandomInt(100)}vw`;
  }
}

// balloon.component.scss

.c-balloon {
  position: absolute;
  width: 105px;
  height: 125px;
  pointer-events: auto;
  border-radius: 75% 75% 70% 70%;
  animation-name: float;
  animation-timing-function: ease-in;
  animation-iteration-count: infinite;

  // You can take color as an input to your balloon as you please
  color: blue; 
  background-color: blue;
  box-shadow: inset -7px -3px 10px darkblue;

  &::before {
    position: absolute;
    top: 125px;
    right: 0;
    left: 0;
    display: block;
    width: 1px;
    height: 75px;
    margin: auto;
    content: "";
  }

  &::after {
    position: relative;
    top: 118px;
    right: 0;
    left: 0;
    display: block;
    margin: auto;
    color: inherit;
    text-align: center;
    content: "▲";
  }
}

@keyframes float {
  from {
    opacity: 0.7;
    transform: translateY(100vh);
  }

  to {
    opacity: 0.2;
    transform: translateY(-100vh);
  }
}

각도 데모 앱의 풍선!

그림에서 눈치채지 못한 경우 풍선이 다른 요소 뒤에 렌더링될 수 있습니다(html에서의 배치로 인해). Angular cdk의 오버레이를 사용하여 문제를 해결할 수 있습니다.

풍선 오버레이 지시문

모든 html 요소를 전달할 수 있는 지시문을 추가해 보겠습니다 appBalloons=”conditionToRenderOrNot”. 먼저 cdk: 를 추가해야 합니다 ng add @angular/cdk. 이 구현은 rxjs와의 일부 반응성도 사용합니다.

// balloon.directive.ts
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { Directive, Input, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { BalloonContainerComponent } from './balloon-container/balloon-container.component';

@Directive({
  selector: '[appBalloon]',
})
export class BalloonDirective implements OnInit, OnDestroy {
  private _overlayRef?: OverlayRef;
  private _onDestroy$ = new Subject<void>();
  private _shouldShow$ = new BehaviorSubject<boolean>(false);

  constructor(private _overlay: Overlay) {}

  @Input()
  set appBalloon(value: boolean) {
    this._shouldShow$.next(value);
  }

  ngOnInit(): void {
    this._shouldShow$
      .pipe(takeUntil(this._onDestroy$))
      .subscribe((shouldShow) => {
        if (shouldShow) {
          this._show();
        } else {
          this._hide();
        }
      });
  }

  ngOnDestroy(): void {
    this._hide();
    this._onDestroy$.next();
    this._onDestroy$.complete();
  }

  private _isVisible(): boolean {
    return this._overlayRef !== undefined && this._overlayRef.hasAttached();
  }

  private _show(): void {
    if (this._isVisible()) {
      return;
    }
    
    this._overlayRef = this._overlay.create({
      panelClass: 'o-overlay--no-pointer-events',
      positionStrategy: this._overlay.position().global().bottom(),
      width: '100vw',
      height: '100vh',
    });

    const balloonPortal = new ComponentPortal(BalloonContainerComponent);
    this._overlayRef.attach(balloonPortal);
  }

  private _hide(): void {
    if (!this._isVisible()) {
      return;
    }
    this._overlayRef?.detach();
  }
}

앱에 간단한 토글을 추가하고 새 지시문을 사용하겠습니다! 매우 간단한 애플리케이션(balloon-container 지시문을 app.module로 가져와야 함):

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <h1 [appBalloon]="showBalloons">Balloons!</h1>
    <input type="checkbox" (change)="onChange()" />` 
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  showBalloons = false;

  onChange(): void {
    this.showBalloons = !this.showBalloons;
  }
}

이제 Angular에서 떠다니는 풍선을 조건부로 표시하기 위해 원하는 논리를 사용할 수 있습니다!

이 방법은 당신이 원하는 어떤 멋진 장면에도 사용할 수 있다는 것을 깨닫기를 바랍니다! 생일 풍선뿐만 아니라 겨울의 눈, 가을의 낙엽 등 계절별 테마도 있습니다.

기타: 풍선 터뜨리기

이제 한 단계 더 나아가 풍선을 터뜨릴 수 있으면 좋을 것입니다.

balloon-component의 — 이벤트 에 바인딩을 추가 click하고 balloon-container로 방출하여 풍선을 클릭 가능하게 만들 것입니다. 그러나 컨테이너는 제거할 풍선을 어떻게 알 수 있습니까? 여기에서 ID가 유용합니다.

// balloon.component.ts
@Component({
  selector: 'app-balloon',
  standalone: true,
  imports: [NgIf],
  template: ` <div
    *ngIf="balloon"
    class="c-balloon"typ
    [style]="balloon.style"
    [style.left]="position"
    (click)="onClick()"
  ></div>`,
  styleUrls: ['./balloon.component.scss'],
})
export class BalloonComponent {
  @Input()
  balloon?: Balloon;

  @Output()
  pop = new EventEmitter<number>();

  position: string;
  constructor() {
    this.position = `${getRandomInt(100)}vw`;
  }

  onClick() {
    if (this.balloon) {
      this.pop.emit(this.balloon.id);
    }
  }
}

// balloon-container.component.ts
@Component({
  selector: 'app-balloon-container',
  standalone: true,
  template: `
    <app-balloon 
        *ngFor="let balloon of balloons" 
        [balloon]="balloon">
        (pop)="onPop($event)"
    </app-balloon>
  `,
  imports: [BalloonComponent],
})
export class BalloonContainerComponent {
  readonly nBalloons = 20;
  balloons: Balloon[] = [...new Array(this.nBalloons)].map((_, idx) => ({
    id: idx,
    style: getRandomStyles(),
  }));
  
  onPop(id: number) {
    this.balloons = this.balloons.filter((b) => b.id !== id);
  }  
}

이제 이를 앱, 회사 등으로 가져올 수 있으며 Angular 에서 풍선으로 기쁨을 더할 수 있습니다.

이 게시물이 영감을 주거나 적어도 다소 재미있었기를 바랍니다. 의견이 있으시면 언제든지 문의해 주세요.