Как да съставяте анимации на платна в TypeScript

Днес ще създадем платна анимация с доста разцъфнали цветя, стъпка по стъпка. Можете да следвате, като играете StackBlitz проекти в тази публикация в блога и сте добре дошли да разгледате изходния код в това репо за GitHub.

„Снимка от близък план на пчела, опрашваща цветя“ от Лукас Блажек на Unsplash

В моята неотдавнашна публикация в блога описах изглед на високо ниво за съставяне на анимации на платно с помощта на TypeScript. Тук ще ви представя подробен процес как да моделирате обекти и как да ги анимирате върху платно.

Съдържание

  • Начертайте цветя
  • Анимирани цветя
  • Добавете взаимодействия към анимацията

Начертайте цветя

Първо, първо трябва да имаме функция да рисуваме цветя върху платно. Можем да разбием частите на цвете надолу върху венчелистчетата и центъра (плодник и тичинка). Цветният център може да бъде абстрахиран като кръг, изпълнен с някакъв цвят. Венчелистчетата растат около центъра и те могат да бъдат нарисувани чрез въртене на платно с определена степен на симетрия.

Забележете, че смелите съществителни имена (цвете, венчелистче, център) предполагат модели в кода. Ние ще дефинираме тези модели, като идентифицираме техните свойства.

Нека първо се съсредоточим върху рисуването на едно венчелистче с някои абстракции. Вдъхновени от този урок, ние знаем, че формата на венчелистчетата може да бъде представена от две квадратни криви и две криви на Безие. И можем да изчертаем тези криви, използвайки методите quadraticCurveTo () и bezierCurveTo () в HTML canvas API.

Както е показано на фигура 1 (1), квадратичната крива има начална точка, крайна точка и една контролна точка, която определя кривината на кривата. На фигура 1 (2) кривата на Безие има начална точка, крайна точка и две контролни точки.

За да свържем плавно две криви (всяка две криви, квадратична или Безие, или друга), трябва да сме сигурни, че връзката и двете контролни точки наблизо са на една и съща линия, така че тези две криви да имат еднакви кривина в точката на свързване.

Фигура 1. Начертайте цвете стъпка по стъпка. (1) Квадратна крива; (2) крива на Безие; (3) Форма на венчелистчетата, образувана от две квадратични извити (зелени) и две криви на Безие (синьо). Червените точки са венчелистчета на венчелистчетата. Сините точки са контролни точки на кривата на венчелистчетата. (4) Форма на венчелистче, изпълнена с цвят. (5) Форма на цвете, генерирана от центриран кръг и завъртени венчелистчета. (6) Цветна форма със сянка.

Фигура 1 (3) показва основна форма на венчелистчетата, състояща се от две квадратни криви (зелено) и две крива на Безие (синьо). Има 4 червени точки, представляващи венчелистчета на венчелистчетата, и 6 сини точки, представляващи контролни точки на криви.

Долната червена върха е средната точка на цветето, а горната червена върха е върха на цветното венчелистче. Средните два червени върха представляват радиуса на венчелистчето. И ъгълът между тези два върха спрямо централната точка се нарича петален ъгъл. Можете да играете с този StackBlitz проект за формата на венчелистчетата.

След като се определи формата на венчелистчетата, можем да запълним формата с цвят и да получим венчелистче, както е показано на фигура 1 (4). С горепосочената информация е добре да напишем първия си модел на обекта: Венчелистче.

експорт клас Petal {
  частни върхове само за четене: Точка [];
  частни контролни точки само за четене: Точка [] [];
  конструктор (
    обществен център за четенеPoint: Point,
    обществен радиус само за четене: номер,
    обществено само за четене tipSkewRatio: номер,
    ъгъл на обществено четенеСпан: брой,
    цвят за обществено четене: низ
  ) {
    this.vertices = this.getVertices ();
    this.controlPoints = this.getControlPoints (this.vertices);
  }
  рисуване (контекст: CanvasRenderingContext2D) {
    // начертайте криви с помощта на върхове и controlPoints
  }
  частни getVertices () {
    // изчисляване на координатите на върховете
  }
  частни getControlPoints (върхове: Точка []): Точка [] [] {
    // изчисляване на координатите на контролните точки
  }
}

Класът на спомагателните точки в Petal се дефинира по следния начин. Координатите използват цели числа (чрез Math.floor ()), за да спестят някаква изчислителна мощност.

експорт клас Точка {
  конструктор (публично само за четене x = 0, публично само за четене y = 0) {
    this.x = Math.floor (this.x);
    this.y = Math.floor (this.y);
  }
}

Представянето на Цветен център може да бъде параметрирано според неговата централна точка, радиус на окръжност и цвят. По този начин скелетът на класа FlowerCenter е както следва:

експорт клас FlowerCenter {
  конструктор (
    частен център само за четенеPoint: Point,
    частен център за четенеRadius: номер,
    private readonly centerColor: низ
  ) {}
  рисуване (контекст: CanvasRenderingContext2D) {
    // нарисувайте кръга
  }
}

Тъй като имаме венчелистче и център за цветя, сме готови да продължим напред, за да нарисуваме цвете, което съдържа централен кръг и няколко венчелистчета със същата форма.

От обектно-ориентирана гледна точка, Цвете може да бъде конструиран като нов Цвете (център: FlowerCenter, венчелистчета: Венчелистче []) или като нов Цвете (център: FlowerCenter, числоOfPetals: число, венчелистче: Венчелистче). Използвам втория начин, тъй като за този сценарий не е необходим масив.

В конструктора можете да добавите някои валидации, за да гарантирате целостта на данните. Например, хвърлете грешка, ако center.centerPoint не съвпада с petal.centerPoint.

експорт клас Цвете {
  конструктор (
    частен само за четене цветеЦентър: FlowerCenter,
    частен само за четене номерOfPetals: номер,
    частно венчелистче: Венчелистче
  ) {}
  рисуване (контекст: CanvasRenderingContext2D) {
    this.drawPetals (контекст);
    this.flowerCenter.draw (контекст);
  }
  private drawPetals (контекст: CanvasRenderingContext2D) {
    context.save ();
    const cx = this.petal.centerPoint.x;
    const cy = this.petal.centerPoint.y;
    const rotateAngle = (2 * Math.PI) / this.numberOfPetals;
    за (нека i = 0; i 

Обърнете внимание на метода drawPetals (контекст). Тъй като въртенето е около средната точка на цветето, първо трябва да преведем платното, за да преместим произхода в центъра на цветя, след което да завъртим платното. След завъртането трябва да преведем платното обратно, така че първоизточникът да е предишният (0, 0).

Използвайки тези модели (Flower, FlowerCenter, венчелистче), ние успяваме да получим цвете, приличащо на фигура 1 (5). За да направим цветето по-конкретно, добавяме някои сенчести ефекти, така че цветето да изглежда като това на Фигура 1 (6). Можете също да играете с проекта StackBlitz по-долу.

Анимирани цветя

В този раздел ще оживим процеса на цъфтене на цветя. Ще симулираме процеса на цъфтеж с увеличаване на радиуса на венчелистчетата с течение на времето. Фигура 2 показва финалната анимация, в която венчелистчетата на цветята се разширяват на всеки кадър.

Фигура 2. Цъфтящи цветя върху платно.

Преди да направим действителните анимации, може да искаме да добавим някои сортове към цветята, така че да не са скучни. Например, можем да генерираме произволни точки на платното, за да разпръсваме цветя, можем да генерираме произволни форми / размери на цветя и можем да рисуваме произволни цветове за тях. Този вид работа обикновено се извършва в конкретна услуга с цел централизиране на логиката и повторно използване на код. След това поставяме логиката на рандомизация в класа FlowerRandomizationService.

експорт клас FlowerRandomizationService {
  конструктор () {}
  getFlowerAt (точка: Точка): Цвете {
    ... // рандомизация
  }
  ... // други помощни методи
}

След това създаваме клас BloomingFlowers, за да съхраним масив от цветя, генерирани от FlowerRandomizationService.

За да направим анимация, в клас Flower дефинираме метод увеличениеPetalRadius () за актуализиране на цветните обекти. След това, като се обадите на window.requestAnimationFrame (() => this.animateFlowers ()); в клас BloomingFlowers планираме пречертаване на платно на всеки кадър. И цветята се актуализират чрез flower.increasePetalRadius (); по време на всяко преначертаване. Кодният фрагмент по-долу показва минимален клас на анимация.

експорт клас BloomingFlowers {
  частен контекст само за четене: CanvasRenderingContext2D;
  частно платно само за четенеW: номер;
  частно платно само за четенеH: номер;
  частни цветя само за четене: Цвете [] = [];
  конструктор (
    частно платно само за четене: HTMLCanvasElement,
    частно само за четене nFlowers: число = 30
  ) {
    this.context = this.canvas.getContext ('2d');
    this.canvasWidth = this.canvas.width;
    this.canvasHeight = this.canvas.height;
    this.getFlowers ();
  }
  разцвет () {
    window.requestAnimationFrame (() => this.animateFlowers ());
  }
  частни анимираниFlowers () {
    this.context.clearRect (0, 0, this.canvasW, this.canvasH);
    this.flowers.forEach (flower => {
      flower.increasePetalRadius ();
      flower.draw (this.context);
    });
    window.requestAnimationFrame (() => this.animateFlowers ());
  }
  частни getFlowers () {
    за (нека i = 0; i 

Забележете, че функцията за обратно извикване в window.requestAnimationFrame (() => this.animateFlowers ()); използва синтаксиса на функция стрелка, който е необходим за запазване на този контекст на текущия обект клас.

Горният фрагмент на код би довел до увеличаване на дължината на венчелистчетата на цветя непрекъснато, тъй като няма механизъм за спиране на тази анимация. В демонстрационния код използвам setTimeout () обратен сигнал, за да прекратя анимацията след 5 секунди. Какво става, ако искате рекурсивно да играете анимация? Едно просто решение е демонстрирано в проекта StackBlitz по-долу, който използва setInterval () обратен сигнал, за да възпроизвежда анимацията на всеки 8 секунди.

Това е яко. Какво друго можем да направим върху анимации на платно?

Добавете взаимодействия към анимацията

Искаме платното да отговаря на събития на клавиатурата, събития на мишката или събития на допир. Как? Правилно, добавете слушатели на събития.

В тази демонстрация ще създадем интерактивно платно. Когато мишката щракне върху платното, цвете цъфти. Когато щракнете върху друга точка на платното, цъфти друго цвете. При задържане на клавиша CTRL и щракване, платното ще се изчисти. Фигура 3 показва финалната анимация на платното.

Фигура 3. Интерактивно платно.

Както обикновено, ние създаваме клас InteractiveFlowers, за да поберем масив от цветя. Кодовият фрагмент от класа InteractiveFlowers е както следва.

експорт клас InteractiveFlowers {
  частен контекст само за четене: CanvasRenderingContext2D;
  частно платно само за четенеW: номер;
  частно платно само за четенеH: номер;
  частни цветя: Цвете [] = [];
  private readonly randomizationService =
               нова FlowerRandomizationService ();
  частен ctrlIsPress = невярно;
  частна mousePosition = нова точка (-100, -100);
  конструктор (частно платно само за четене: HTMLCanvasElement) {
    this.context = this.canvas.getContext ('2d');
    this.canvasW = this.canvas.width;
    this.canvasH = this.canvas.height;
    this.addInteractions ();
  }
  clearCanvas () {
    this.flowers = [];
    this.context.clearRect (0, 0, this.canvasW, this.canvasH);
  }
  частни анимираниFlowers () {
    ако (this.flowers.every (f => f.stopChanging)) {
      се върне;
    }
    this.context.clearRect (0, 0, this.canvasW, this.canvasH);
    this.flowers.forEach (flower => {
      flower.increasePetalRadiusWithLimit ();
      flower.draw (this.context);
    });
    window.requestAnimationFrame (() => this.animateFlowers ());
  }
  частни addInteractions () {
    this.canvas.addEventListener („щракване“, e => {
      ако (this.ctrlIsPress) {
        this.clearCanvas ();
        се върне;
      }
      this.calculateMouseRelativePositionInCanvas (д);
      const flower = this.randomizationService
                         .getFlowerAt (this.mousePosition);
      this.flowers.push (цвете);
      this.animateFlowers ();
    });
    window.addEventListener ('keydown', (e: KeyboardEvent) => {
      ако (напр. === 17 || e.keyCode === 17) {
        this.ctrlIsPress = вярно;
      }
    });
    window.addEventListener ('keyup', () => {
      this.ctrlIsPress = невярно;
    });
  }
  частен изчислениеMouseRelativePositionInCanvas (e: MouseEvent) {
    this.mousePosition = нова точка (
      e.clientX +
        (document.documentElement.scrollLeft ||
         document.body.scrollLeft) -
        this.canvas.offsetLeft,
      напр. +
        (document.documentElement.scrollTop ||
         document.body.scrollTop) -
        this.canvas.offsetTop
    );
  }
}

Добавяме слушател на събитие, за да проследяваме събитията с щракване на мишката и позициите на мишката. Всяко щракване ще добави цвете към масива с цветя. Тъй като не искаме да оставяме цветята да се разширяват до безкрайност, ние определяме метод увеличениеPetalRadiusWithLimit () в класа Flower за увеличаване на радиуса на венчелистчетата до увеличение на 20. По този начин всяко цвете ще цъфти от само себе си и ще спре да цъфти. след като радиусът на венчелистчетата му се е увеличил 20 единици.

Поставих частен член stopChanging in flower, за да оптимизирам анимацията, така че анимацията да спре, когато всички цветя приключат да цъфтят.

Можем също така да слушаме събития за клавиатура / клавиатура и да добавяме контроли на клавиатурата към платното. В това демонстрационно съдържание съдържанието на платното ще се изчисти, когато потребителят държи клавиша CTRL и кликне върху мишката. Състоянието на натискане на клавиш се проследява от полето ctrlIsPress. По подобен начин можете да добавите други полета за проследяване на други събития на клавиатурата, за да улесните подробните контроли върху платното.

Разбира се, слушателите на събития могат да бъдат оптимизирани с помощта на Observables, особено когато използвате Angular. Можете да играете с проекта StackBlitz по-долу.

Какво следва? Можем да изчертаем интерактивната демонстрация на цветя, като добавим някои звукови ефекти и някои анимационни спрайтове. Можем да проучим как да го накараме да работи безпроблемно във всички платформи и да направим PWA или мобилно приложение от него.

Надявам се тази статия да добави някаква стойност към темата за Canvas Animations. Отново изходният код е в това репо за GitHub и можете също да играете с този StackBlitz проект и да посетите демонстрационен сайт. Чувствайте се свободни да оставяте коментари по-долу. Благодаря ти.

Наздраве!