typescript中函数_如何在TypeScript中合成Canvas动画

typescript中函数

by Changhui Xu

徐昌辉

如何在TypeScript中合成Canvas动画 (How to Compose Canvas Animations in TypeScript)

Today we are going to create a canvas animation with pretty blooming flowers, step by step. You can follow along by playing StackBlitz projects in this blog post, and you are welcome to check out the source code in this GitHub repo.

今天,我们要创建一个漂亮的鲜花盛开, 一步步画布动画。 您可以通过在此博客文章中玩StackBlitz项目来进行后续操作,也欢迎您在GitHub存储库中查看源代码。

In my recent blog post, I described a high level view of composing canvas animations using TypeScript. Here I will present a detailed process of how to model objects and how to animate them on canvas.

在最近的博客文章中 ,我描述了使用TypeScript组成画布动画的高级视图。 在这里,我将详细介绍如何对对象进行建模以及如何在画布上对其进行动画处理。

目录 (Table of Contents)

画花 (Draw Flowers)

First things first, we need to have a function to draw flowers on canvas. We can break the parts of a flower down into petals and center (pistil and stamen). The flower center can be abstracted as a circle filled with some color. The petals grow around the center, and they can be drawn by rotating canvas with a certain degree of symmetry.

首先,我们需要具有在画布上绘制花朵的功能。 我们可以将花朵的一部分分解为花瓣中心 (雌蕊和雄蕊)。 花中心可以抽象为一个充满某种颜色的圆圈。 花瓣围绕中心生长,可以通过旋转画布以一定程度的对称性来绘制它们。

Notice that the bold nouns (flower, petal, center) imply models in the code. We are going to define these models by identifying their properties.

注意,粗体名词( flower花瓣center )在代码中暗含模型 。 我们将通过识别它们的属性来定义这些模型。

Let’s first focus on drawing one petal with some abstractions. Inspired by this tutorial, we know that petal shape can be represented by two quadratic curves and two Bézier curves. And we can draw these curves using the quadraticCurveTo() and bezierCurveTo() methods in the HTML canvas API.

让我们首先集中精力绘制具有抽象的花瓣。 受本教程的启发,我们知道花瓣的形状可以用两条二次曲线和两条贝塞尔曲线来表示。 我们可以使用HTML canvas API中的quadraticCurveTo()bezierCurveTo()方法绘制这些曲线。

As shown in Figure 1 (1), a quadratic curve has a starting point, an end point, and one control point which determines the curve’s curvature. In Figure 1 (2), a Bézier curve has a starting point, an end point, and two control points.

如图1(1)所示,二次曲线具有起点,终点和一个确定曲线曲率的控制点。 在图1(2)中,贝塞尔曲线具有起点,终点和两个控制点。

In order to smoothly connect two curves (any two curves, either quadratic or Bézier, or other), we need to make sure that the connection point and the two nearby control points are on the same line, so that these two curves have the same curvature at the connection point.

为了平滑地连接两条曲线(任意两条曲线,无论是二次曲线还是贝塞尔曲线,或其他曲线),我们需要确保连接点和附近的两个控制点在同一条线上,以便这两条曲线具有相同的连接点的曲率

Figure 1 (3) shows a basic petal shape consisting of two quadratic curves (green) and two Bézier curve (blue). There are 4 red points representing petal vertices and 6 blue points representing control points of curves.

图1(3)显示了基本的花瓣形状,该形状由两个二次曲线(绿色)和两个贝塞尔曲线(蓝色)组成。 有4个红色点代表花瓣顶点,有6个蓝色点代表曲线的控制点。

The bottom red vertex is the flower’s center point and the top red vertex is the flower petal tip. The middle two red vertices represent the petal’s radius. And the angle between these two vertices against the center point is named petal angle span. You can play with this StackBlitz project about petal shape.

底部的红色顶点是花朵的中心点,顶部的红色顶点是花瓣的尖端。 中间的两个红色顶点代表花瓣的半径。 并将这两个顶点相对于中心点的角度称为花瓣角跨度。 您可以玩这个有关花瓣形状的StackBlitz项目

After the petal shape is defined, we can fill the shape with a color and get a petal, as shown in Figure 1 (4). With the information above, we are good to write up our first object model: Petal.

定义花瓣形状后,我们可以用一种颜色填充形状并得到一个花瓣,如图1(4)所示。 根据以上信息,我们很高兴编写出第一个对象模型: Petal

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
  }
}

The auxiliary Point class in Petal is defined as follows. Coordinates are using integers (via Math.floor()) to save some computing power.

Petal的辅助Point类定义如下。 坐标使用整数(通过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);
  }
}

The representation of a Flower Center can be parameterized by its center point, circle radius, and color. Thus, the skeleton of the FlowerCenter class is as follows:

花中心的表示可以通过其中心点,圆半径和颜色进行参数设置。 因此, FlowerCenter类的框架如下:

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

Since we have a petal and a flower center, we are ready to move forward to draw a flower, which contains a center circle and several petals with the same shape.

由于我们有一个花瓣和一个花中心,因此我们可以继续前进以绘制花朵,该花包含一个中心圆和几个具有相同形状的花瓣。

From an Object Oriented perspective, Flower can be constructed as new Flower(center: FlowerCenter, petals: Petal[]) or as new Flower(center: FlowerCenter, numberOfPetals: number, petal: Petal). I use the second way, because no array is needed for this scenario.

从面向对象的角度来看, Flower可以构造为new Flower(center: FlowerCenter, petals: Petal[]) ,也可以构造为new Flower(center: FlowerCenter, petals: Petal[]) new Flower(center: FlowerCenter, numberOfPetals: number, petal: Petal) 。 我使用第二种方法,因为这种情况下不需要数组。

In the constructor, you can add some validations to ensure data integrity. For example, throw an error if center.centerPoint doesn’t match petal.centerPoint.

在构造函数中,您可以添加一些验证以确保数据完整性。 例如,如果center.centerPointpetal.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();
  }
}

Pay attention to the drawPetals(context) method. Since the rotation is around the flower’s center point, we need to first translate the canvas to move the origin to flower center, then rotate the canvas. After rotation, we need to translate the canvas back so that the origin is the previous (0, 0).

请注意drawPetals(context)方法。 由于旋转是围绕花的中心点进行的,因此我们首先需要平移画布以将原点移动到花的中心,然后旋转画布。 旋转后,我们需要将画布向后平移,以便原点是前一个(0,0)。

Using these models (Flower, FlowerCenter, Petal), we are able to obtain a flower looks like Figure 1 (5). To make the flower more concrete, we add some shadow effects so that the flower looks like the one in Figure 1 (6). You can also play with the StackBlitz project below.

使用这些模型( FlowerFlowerCenterPetal ),我们可以获得图1(5)所示的花朵。 为了使花朵更具体,我们添加了一些阴影效果,使花朵看起来像图1中的花朵(6)。 您也可以使用下面的StackBlitz项目

动画花 (Animate Flowers)

In this section, we are going to animate the flower blooming process. We will simulate the blooming process as increasing petal radius as time passes. Figure 2 shows the final animation in which the flowers’ petals are expanding at each frame.

在本节中,我们将为花朵盛开过程制作动画。 随着时间的流逝,随着花瓣半径的增加,我们将模拟开花过程。 图2显示了最终的动画,其中花朵的花瓣在每一帧处都在扩展。

Before we do the actual animations, we may want to add some varieties to the flowers so that they are not boring. For example, we can generate random points on the canvas to scatter flowers, we can generate random shapes/sizes of flowers, and we can paint random colors for them. This kind of work usually is done in a specific service for the purpose of centralizing logic and reusing code. We then put randomization logic into the FlowerRandomizationService class.

在进行实际动画制作之前,我们可能需要为花朵添加一些变种,以免枯燥。 例如,我们可以在画布上生成随机点以散布花朵,可以生成花朵的随机形状/大小,还可以为其绘制随机颜色。 为了集中逻辑和重用代码,通常在特定的服务中完成这种工作。 然后,我们将随机化逻辑放入FlowerRandomizationService类中。

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

Then we create a BloomingFlowers class to store an array of flowers generated by FlowerRandomizationService.

然后,我们创建一个BloomingFlowers类来存储由FlowerRandomizationService生成的花朵数组。

To make an animation, we define a method increasePetalRadius() in Flower class to update the flower objects. Then by calling window.requestAnimationFrame(() => this.animateFlowers()); in BloomingFlowers class, we schedule a redraw on canvas at each frame. And flowers are updated via flower.increasePetalRadius(); during each redraw. The code snippet below shows a bare minimum animation class.

为了制作动画,我们在Flower类中定义了一个increasePetalRadius()方法来更新花朵对象。 然后通过调用window.requestAnimationFrame(() => this.animateFlowers( )); in BloomingFlow ers类中,我们计划在每一帧的画布上进行重绘。 并通过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);
    }
  }
}

Notice that the call back function in window.requestAnimationFrame(() => this.animateFlowers()); is using Arrow Function syntax, which is needed to preserve this context of the current object class.

注意, window.requestAnimationFrame(() => this.animateFlowers());回调函数window.requestAnimationFrame(() => this.animateFlowers()); 使用箭头函数语法,这是保留当前对象类的this上下文所必需的。

The above code snippet would result in the flower petal length increasing continually, because it doesn’t have a mechanism to stop that animation. In the demo code, I use a setTimeout() callback to terminate animation after 5 seconds. What if you want to recursively play an animation? A simple solution is demoed in the StackBlitz project below, which utilizes a setInterval() callback to replay the animation every 8 seconds.

上面的代码片段将导致花瓣长度不断增加,因为它没有停止该动画的机制。 在演示代码中,我使用setTimeout()回调在5秒后终止动画。 如果要递归播放动画怎么办? 下面的StackBlitz项目演示了一个简单的解决方案, 该项目利用setInterval()回调每8秒重播一次动画。

That’s cool. What else can we do on canvas animations?

这很酷。 我们还能在画布动画上做什么?

将交互添加到动画 (Add Interactions to Animation)

We want the canvas to be responsive to keyboard events, mouse events, or touch events. How? Right, add event listeners.

我们希望画布能够响应键盘事件,鼠标事件或触摸事件。 怎么样? 正确,添加事件监听器。

In this demo, we are going to create an interactive canvas. When the mouse clicks on the canvas, a flower blooms. When you click at another point on the canvas, another flower blooms. When holding the CTRL key and clicking, the canvas will clear. Figure 3 shows the final canvas animation.

在此演示中,我们将创建一个交互式画布。 当鼠标单击画布时,就会开花。 当您单击画布上的另一点时,另一朵花盛开。 按住CTRL键并单击时,画布将清除。 图3显示了最终的画布动画。

As usual, we create a class InteractiveFlowers to hold an array of flowers. The code snippet of the InteractiveFlowers class is as follows.

像往常一样,我们创建一个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
    );
  }
}

We add an event listener to track the mouse click events and mouse position(s). Every click will add a flower to the flowers array. Since we don’t want to let the flowers expand to infinity, we define a method increasePetalRadiusWithLimit() in the Flower class to increase the petal radius until an increment of 20. In this way, each flower will bloom by itself and will stop blooming after its petal radius has increased 20 units.

我们添加了一个事件侦听器,以跟踪鼠标单击事件和鼠标位置。 每次单击都会在花朵数组中添加花朵。 因为我们不想让花朵扩展到无限大,我们定义了一个方法increasePetalRadiusWithLimit()Flower类增加花瓣半径,直到20的增量这样,每朵花将绽放自己,并会停止绽放花瓣半径增加20个单位后

I set a private member stopChanging in flower to optimize the animation, so that the animation will stop when all flowers have finished blooming.

我在花中设置了一个私有成员stopChanging以优化动画,以便当所有花都开花完后动画将停止。

We can also listen to keyup/keydown events and add keyboard controls to the canvas. In this demo, the canvas content will be cleared when the user holds the CTRL key and clicks the mouse. The key press condition is tracked by the ctrlIsPressed field. Similarly, you can add other fields to track other keyboard events to facilitate granular controls on the canvas.

我们还可以侦听keyup / keydown事件,并将键盘控件添加到画布。 在此演示中,当用户按住CTRL键并单击鼠标时,画布内容将被清除。 按键状态由ctrlIsPressed字段跟踪。 同样,您可以添加其他字段来跟踪其他键盘事件,以方便在画布上进行精细控制。

Of course, the event listeners can be optimized using Observables, especially when you’re using Angular. You can play with the StackBlitz project below.

当然,可以使用Observables优化事件侦听器,尤其是在使用Angular时。 您可以使用下面的StackBlitz项目

What’s next? We can brush up the interactive flowers demo by adding some sound effects and some animation sprites. We can study how to make it run smoothly across all platforms and make a PWA or mobile app out of it.

下一步是什么? 我们可以通过添加一些声音效果和一些动画精灵来修饰交互式花朵演示。 我们可以研究如何使其在所有平台上平稳运行,并利用它来制作PWA或移动应用。

I hope this article adds some value to the topic of Canvas Animations. Again, the source code is in this GitHub repo and you can also play with this StackBlitz project and visit a demo site. Feel free to leave comments below. Thank you.

我希望本文能为“画布动画”主题增添一些价值。 同样,源代码位于该GitHub存储库中 ,您也可以使用此StackBlitz项目并访问演示站点 。 随时在下面发表评论。 谢谢。

Cheers!

干杯!

翻译自: https://www.freecodecamp.org/news/how-to-compose-canvas-animations-in-typescript-9368dfa29028/

typescript中函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值