Как создать анимацию холста в TypeScript

kak sozdat animacziyu holsta v typescript?v=1656571455

от Changhui Xu

Сегодня мы создадим анимацию на холсте с красивыми цветами, шаг за шагом. Вы можете продолжить, играя в проекты StackBlitz в этой публикации блога, и вы можете ознакомиться с исходным кодом в этом репозитории GitHub.

0*pJTkQHXgr6hKmpXZ
«Фото пчелы, опыляющие цветы крупным планом» от Лукаса Блазека на Unsplash

В своей недавней публикации в блоге я описал высокоуровневый взгляд на создание анимации холста с помощью TypeScript. Здесь я покажу подробный процесс моделирования объектов и их анимации на холсте.

Содержание

Нарисуйте цветы

Прежде всего, нам нужно иметь функцию для рисования цветов на холсте. Мы можем разбить части a цветок вниз в лепестки и центр (песочка и тычинка). Центр цветка можно абстрагировать в виде круга, заполненного каким-либо цветом. Лепестки растут вокруг центра, и их можно рисовать, поворачивая полотно с определенной степенью симметрии.

Обратите внимание, что жирные существительные (цветок, лепесток, центр) иметь в виду модели в коде Мы собираемся определить эти модели, определив их свойства.

Сначала остановимся на рисовании одного лепестка с некоторыми абстракциями. Благодаря этому учебнику мы знаем, что форму лепестка можно представить двумя квадратичными кривыми и двумя кривыми Безье. И мы можем нарисовать эти кривые с помощью quadraticCurveTo() и bezierCurveTo() методы в HTML canvas API.

Как показано на рисунке 1 (1), квадратическая кривая имеет начальную, конечную и одну контрольную точку, определяющую кривизну кривой. На рисунке 1 (2) кривая Безье имеет исходную, конечную и две контрольные точки.

Чтобы плавно соединяются две кривые (любые две кривые, квадратические или Безье, или другие), нам нужно убедиться, что точка соединения и две ближайшие контрольные точки находятся на одной прямой, чтобы эти две кривые имеют одинаковую кривизну в точке соединения.

1*XwdZt1n54qbsXLaP67d3Ww
Фигура 1. Нарисуйте цветок поэтапно. (1) Квадратная кривая; (2) кривая Безье; (3) Форма лепестка, образованная двумя квадратичными кривыми (зеленая) и двумя кривыми Безье (синие). Красные точки – это вершины лепестков. Синие точки являются контрольными точками кривой лепестки. (4) Форма лепестка, заполненная цветом. (5) Форма цветка, созданная центрированным кругом и возвращенными лепестками. (6) Форма цветка с тенью.

На рисунке 1 (3) показана основная форма лепестка, состоящая из двух квадратических кривых (зеленая) и двух кривых Безье (синяя). Есть 4 красных точек, представляющих вершины лепестков, и 6 синих точек, представляющих контрольные точки кривых.

Нижняя красная вершина – это центральная точка цветка, а верхняя красная вершина – кончик лепестка цветка. Средние две красные вершины представляют радиус лепестка. А угол между этими двумя вершинами против центральной точки называется размахом угла лепестка. Вы можете поиграть с этим проектом StackBlitz о форме лепестков.

После определения формы лепестка мы можем заполнить форму цветом и получить лепесток, как показано на рисунке 1 (4). Имея приведенную выше информацию, мы можем написать нашу первую объектную модель: Лепесток.

export class Petal {
  private readonly vertices: Point[];
  private readonly controlPoints: Point[][];
  
  constructor(
    public readonly centerPoint: Point,
    public readonly radius: number,
    public readonly tipSkewRatio: number,
    public readonly angleSpan: number,
    public readonly color: string
  ) {
    this.vertices = this.getVertices();
    this.controlPoints = this.getControlPoints(this.vertices);
  }
  
  draw(context: CanvasRenderingContext2D) {
    // draw curves using vertices and controlPoints  
  }
  
  private getVertices() {
    // compute vertices' coordinates 
  }
  private getControlPoints(vertices: Point[]): Point[][] {
    // compute control points' coordinates
  }
}

Вспомогательный Point класс в Petal определяется следующим образом. Координаты используют целые числа (через Math.floor()), чтобы сэкономить вычислительную мощность.

export class Point {
  constructor(public readonly x = 0, public readonly y = 0) {
    this.x = Math.floor(this.x);
    this.y = Math.floor(this.y);
  }
}

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

export class FlowerCenter {
  constructor(
    private readonly centerPoint: Point,
    private readonly centerRadius: number,
    private readonly centerColor: string
  ) {}
  
  draw(context: CanvasRenderingContext2D) {
    // draw the circle
  }
}

Поскольку у нас есть лепесток и центр цветка, мы готовы двигаться вперед, чтобы нарисовать цветок, содержащий центральный круг и несколько лепестков одинаковой формы.

С объектно-ориентированной точки зрения, Flower можно построить как new Flower(center: FlowerCenter, petals: Petal[]) или как new Flower(center: FlowerCenter, numberOfPetals: number, petal: Petal). Я использую второй способ, потому что для этого сценария не требуется массив.

В конструкторе можно добавить некоторые проверки, чтобы обеспечить целостность данных. К примеру, вывести ошибку if center.centerPoint не совпадает petal.centerPoint.

export class Flower {
  constructor(
    private readonly flowerCenter: FlowerCenter,
    private readonly numberOfPetals: number,
    private petal: Petal
  ) {}
  
  draw(context: CanvasRenderingContext2D) {
    this.drawPetals(context);
    this.flowerCenter.draw(context);
  }
  
  private drawPetals(context: CanvasRenderingContext2D) {
    context.save();
    const cx = this.petal.centerPoint.x;
    const cy = this.petal.centerPoint.y;
    const rotateAngle = (2 * Math.PI) / this.numberOfPetals;
    for (let i = 0; i < this.numberOfPetals; i++) {
      context.translate(cx, cy);
      context.rotate(rotateAngle);
      context.translate(-cx, -cy);
      this.petal.draw(context);
    }
    context.restore();
  }
}

Обратите внимание на drawPetals(context) метод. Поскольку вращение происходит вокруг центральной точки цветка, нам нужно сначала переложить полотно, чтобы переместить начало координат в центр цветка, а затем повернуть полотно. После вращения нам нужно перевести полотно назад, чтобы начало координат было предварительным (0, 0).

Использование этих моделей (Flower, FlowerCenter, Petal), мы можем получить цветок, который выглядит как на рисунке 1 (5). Чтобы сделать цветок более конкретным, мы добавляем некоторые эффекты тени, чтобы цветок выглядел как на рисунке 1(6). Вы также можете играть с проектом StackBlitz ниже.

Анимированные цветы

В этой главе мы собираемся анимировать процесс цветения цветов. Мы будем моделировать процесс цветения как увеличение радиуса лепестка с течением времени. На рисунке 2 показана окончательная анимация, в которой лепестки цветов расширяются по каждому кадру.

1*tJ5xB9d4xcN3alxmqgEGMA
Рисунок 2. Цветущие цветы на холсте.

Прежде чем делать анимацию, мы можем захотеть добавить несколько разновидностей в цветы, чтобы они не были скучными. К примеру, мы можем генерировать случайные точки на холсте, чтобы разбрасывать цветы, мы можем генерировать случайные формы/размеры цветов и мы можем рисовать для них случайные цвета. Такая работа обычно производится в определенном сервисе с целью централизации логики и повторного использования кода. Затем мы вкладываем логику рандомизации в FlowerRandomizationService класс.

export class FlowerRandomizationService {
  constructor(){}
  getFlowerAt(point: Point): Flower {
    ... // randomization
  }
  ...  // other helper methods
}

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

Чтобы создать анимацию, мы определяем метод increasePetalRadius() в Flower класс для обновления цветочных объектов. Затем позвонив window.requestAnimationFrame(() => this.animateFlowers()); in BloomingFlowВ классе мы планируем повторное рисование на холсте для каждого кадра. И цветы обновлены вia flower.increasePetalRadius(); при каждом перерисовании. Фрагмент кода ниже показывает минимальный класс анимации.

export class BloomingFlowers {
  private readonly context: CanvasRenderingContext2D;
  private readonly canvasW: number;
  private readonly canvasH: number;
  private readonly flowers: Flower[] = [];
  
  constructor(
    private readonly canvas: HTMLCanvasElement,
    private readonly nFlowers: number = 30
  ) {
    this.context = this.canvas.getContext('2d');
    this.canvasWidth = this.canvas.width;
    this.canvasHeight = this.canvas.height;
    this.getFlowers();
  }
  
  bloom() {
    window.requestAnimationFrame(() => this.animateFlowers());
  }
  
  private animateFlowers() {
    this.context.clearRect(0, 0, this.canvasW, this.canvasH);
    this.flowers.forEach(flower => {
      flower.increasePetalRadius();
      flower.draw(this.context);
    });
    window.requestAnimationFrame(() => this.animateFlowers());
  }
  
  private getFlowers() {
    for (let i = 0; i < this.nFlowers; i++) {
      const flower = ... // get a randomized flower
      this.flowers.push(flower);
    }
  }
}

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

Приведенный выше фрагмент кода приведет к постоянному увеличению длины лепестка цветка, поскольку у него нет механизма, чтобы остановить эту анимацию. В демонстрационном коде я использую a setTimeout() обратный вызов для завершения анимации через 5 секунд. Что делать, если вы хотите рекурсивно воспроизвести анимацию? Простое решение демонстрируется в проекте StackBlitz ниже, который использует a setInterval() обратный вызов для воспроизведения анимации каждые 8 ​​секунд.

Круто. Что еще мы можем сделать с анимацией холста?

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

Мы хотим, чтобы полотно реагировало на события клавиатуры, мыши или события соприкосновения. Как? Верно, добавьте слушателей событий.

В этой демонстрации мы намерены создать интерактивное полотно. Когда мышка щелкает по холсту, распускается цветок. Когда вы щелкнете в другой точке на холсте, расцветает другой цветок. Если удерживать клавишу CTRL и щелкнуть, холст очистится. На рисунке 3 показана окончательная анимация холста.

1*SsPGDDNaxiQHHQnzT4YCvg
Рисунок 3. Интерактивное полотно.

Как обычно, создаем класс InteractiveFlowers держать массив цветов. Фрагмент кода InteractiveFlowers класс смотрится следующим образом.

export class InteractiveFlowers {
  private readonly context: CanvasRenderingContext2D;
  private readonly canvasW: number;
  private readonly canvasH: number;
  private flowers: Flower[] = [];
  private readonly randomizationService = 
               new FlowerRandomizationService();
  private ctrlIsPressed = false;
  private mousePosition = new Point(-100, -100);
  
  constructor(private readonly canvas: 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);
  }
  
  private animateFlowers() {
    if (this.flowers.every(f => f.stopChanging)) {
      return;
    }
    this.context.clearRect(0, 0, this.canvasW, this.canvasH);
    this.flowers.forEach(flower => {
      flower.increasePetalRadiusWithLimit();
      flower.draw(this.context);
    });
    window.requestAnimationFrame(() => this.animateFlowers());
  }
  
  private addInteractions() {
    this.canvas.addEventListener('click', e => {
      if (this.ctrlIsPressed) {
        this.clearCanvas();
        return;
      }
      this.calculateMouseRelativePositionInCanvas(e);
      const flower = this.randomizationService
                         .getFlowerAt(this.mousePosition);
      this.flowers.push(flower);
      this.animateFlowers();
    });
    
    window.addEventListener('keydown', (e: KeyboardEvent) => {
      if (e.which === 17 || e.keyCode === 17) {
        this.ctrlIsPressed = true;
      }
    });
    window.addEventListener('keyup', () => {
      this.ctrlIsPressed = false;
    });
  }
  
  private calculateMouseRelativePositionInCanvas(e: MouseEvent) {
    this.mousePosition = new Point(
      e.clientX +
        (document.documentElement.scrollLeft || 
         document.body.scrollLeft) -
        this.canvas.offsetLeft,
      e.clientY +
        (document.documentElement.scrollTop || 
         document.body.scrollTop) -
        this.canvas.offsetTop
    );
  }
}

Мы добавляем прослушивающий события, чтобы отслеживать события щелчка мыши и положение мыши. Каждый клик прибавит цветок к массиву цветов. Поскольку мы не хотим позволять цветам расширяться до бесконечности, мы определяем метод increasePetalRadiusWithLimit() в Flower класса, чтобы увеличить радиус лепестка до увеличения на 20. Таким образом, каждый цветок расцветет сам по себе и перестанет цвести после того, как его радиус лепестка увеличится на 20 единиц.

Я установил частного члена stopChanging в цветке, чтобы оптимизировать анимацию, чтобы анимация прекратилась, когда все цветы перестали распускаться.

Мы также можем послушать keyup/keydown события и добавьте элементы управления клавиатурой на холст. В этой демонстрации содержимое полотна будет очищено, когда пользователь удерживает клавишу CTRL и щелкнет мышью. Условие нажатия клавиши отслеживает ctrlIsPressed поле. Аналогично, вы можете добавить другие поля для отслеживания других событий клавиатуры, чтобы облегчить детальное управление на холсте.

Конечно, прослушивание событий можно оптимизировать с помощью Observables, особенно если вы используете Angular. Вы можете играть с проектом StackBlitz ниже.

Что дальше? Мы можем улучшить демонстрацию интерактивных цветов, добавив некоторые звуковые эффекты и некоторые анимационные попытки. Мы можем изучить, как обеспечить бесперебойную работу на всех платформах и создать из этого PWA или мобильное приложение.

Я надеюсь, что эта статья придаст определенную ценность теме Canvas Animations. Опять же исходный код находится в этом репозитории GitHub, и вы также можете играть с этим проектом StackBlitz и посетить демонстрационный сайт. Не стесняйтесь оставлять комментарии ниже. Спасибо.

На здоровье!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *