de Changhui Xu

Cum să compuneți animații de tip pânză în TypeScript

Astăzi vom crea o animație de pânză cu flori destul de înflorite, pas cu pas. Puteți continua jucând proiectele StackBlitz în această postare de blog și sunteți binevenit (ă) să verificați codul sursă această repo GitHub.

0*pJTkQHXgr6hKmpXZ
„O fotografie de aproape a unei albine care polenizează flori” de Lukas Blazek pe Unsplash

În recenta mea postare pe blog, Am descris o vedere de nivel înalt a compunerii animațiilor de tip canvas folosind TypeScript. Aici voi prezenta un proces detaliat al modului în care se modelează obiecte și cum se animă pe pânză.

Cuprins

  • Desenați flori
  • Animează Flori
  • Adăugați interacțiuni la animație

Desenați flori

În primul rând, trebuie să avem o funcție pentru a desena flori pe pânză. Putem sparge părțile unui floare jos în petale și centru (pistil și stamină). Centrul florilor poate fi abstractizat ca un cerc umplut cu o anumită culoare. Petalele cresc în jurul centrului și pot fi desenate prin rotirea pânzei cu un anumit grad de simetrie.

Observați că substantivele aldine (floare, petală, centru) implică modele în cod. Vom defini aceste modele identificându-le proprietățile.

Să ne concentrăm mai întâi pe desenarea unei petale cu câteva abstractizări. Inspirat de acest tutorial, știm că forma petalelor poate fi reprezentată de două curbe pătratice si doi Curbele Bézier. Și putem desena aceste curbe folosind quadraticCurveTo() și bezierCurveTo() metodele din API-ul HTML canvas.

Așa cum se arată în Figura 1 (1), o curbă pătratică are un punct de pornire, un punct final și un punct de control care determină curbura curbei. În Figura 1 (2), o curbă Bézier are un punct de plecare, un punct final și două puncte de control.

Pentru a conectați ușor două curbe (oricare două curbe, fie pătratice, fie Bézier, sau altele), trebuie să ne asigurăm că punctul de conectare și cele două puncte de control din apropiere sunt pe aceeași linie, astfel încât acestea două curbe au aceeași curbură la punctul de legătură.

Cum se compun animatii de tip panza in TypeScript
Figura 1. Desenați o floare pas cu pas. (1) Curba quadratică; (2) curba Bézier; (3) Forma petalei formată din două curbe pătratice (verde) și două curbe Bézier (albastru). Punctele roșii sunt vârfurile petalelor. Punctele albastre sunt puncte de control ale curbei petalelor. (4) Forma de petală umplută cu culoare. (5) O formă de floare generată de un cerc centrat și de petale rotite. (6) O formă de floare cu umbră.

Figura 1 (3) prezintă o formă de bază a petalelor formată din două curbe pătratice (verde) și două curbe Bézier (albastru). Există 4 puncte roșii care reprezintă vârfurile petalelor și 6 puncte albastre care reprezintă punctele de control ale curbelor.

Vârful roșu inferior este punctul central al florii, iar vârful roșu superior este vârful petalei florii. Cele două vârfuri roșii din mijloc reprezintă raza petalei. Și unghiul dintre aceste două vârfuri față de punctul central se numește petal angle span. Vă puteți juca cu acest proiect StackBlitz despre forma petalelor.

După ce forma petalei este definită, putem umple forma cu o culoare și putem obține o petală, așa cum se arată în Figura 1 (4). Cu informațiile de mai sus, suntem buni să scriem primul nostru model de obiect: 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
  }
}

Auxiliarul Point clasa în Petal este definit după cum urmează. Coordonatele folosesc numere întregi (via Math.floor()) pentru a economisi puterea de calcul.

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

Reprezentarea unui Centrul Florilor poate fi parametrizat prin punctul său central, raza cercului și culoarea. Astfel, scheletul FlowerCenter clasa este după cum urmează:

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

Deoarece avem o petală și un centru de flori, suntem gata să mergem înainte pentru a desena o floare, care conține un cerc central și mai multe petale cu aceeași formă.

Dintr-o perspectivă orientată pe obiecte, Flower poate fi construit ca new Flower(center: FlowerCenter, petals: Petal[]) sau ca new Flower(center: FlowerCenter, numberOfPetals: number, petal: Petal). Folosesc a doua cale, pentru că nu este nevoie de matrice pentru acest scenariu.

În constructor, puteți adăuga câteva validări pentru a asigura integritatea datelor. De exemplu, aruncați o eroare dacă center.centerPoint nu se potrivește 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();
  }
}

Fii atent la drawPetals(context) metodă. Deoarece rotația este în jurul punctului central al florii, trebuie mai întâi să traducem pânza pentru a muta originea în centrul florii, apoi să rotim pânza. După rotație, trebuie să traducem pânza înapoi, astfel încât originea să fie precedenta (0, 0).

Folosind aceste modele (Flower, FlowerCenter, Petal), putem obține o floare care arată ca Figura 1 (5). Pentru a face floarea mai concretă, adăugăm câteva efecte de umbră, astfel încât floarea să arate ca cea din Figura 1 (6). Vă puteți juca și cu proiectul StackBlitz de mai jos.

Animează Flori

În această secțiune, vom anima procesul de înflorire a florilor. Vom simula procesul de înflorire ca raza creșterii petalelor pe măsură ce trece timpul. Figura 2 arată animația finală în care petalele florilor se extind la fiecare cadru.

Cum se compun animatii de tip panza in TypeScript
Figura 2. Flori înflorite pe pânză.

Înainte de a face animațiile propriu-zise, ​​este posibil să dorim să adăugăm câteva soiuri florilor, astfel încât să nu fie plictisitoare. De exemplu, putem genera puncte aleatorii pe pânză pentru a împrăștia flori, putem genera forme / dimensiuni aleatorii de flori și le putem picta culori aleatorii. Acest tip de muncă se face de obicei într-un serviciu specific în scopul centralizării logicii și reutilizării codului. Apoi punem logica randomizării în FlowerRandomizationService clasă.

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

Apoi creăm un BloomingFlowers clasă pentru a stoca o serie de flori generate de FlowerRandomizationService.

Pentru a realiza o animație, definim o metodă increasePetalRadius() în Flower clasa pentru a actualiza obiectele de flori. Apoi, sunând window.requestAnimationFrame(() => this.animateFlowers()); in BloomingFlowÎn clasa noastră, programăm o redesenare pe pânză la fiecare cadru. Iar florile sunt actualizate via flower.increasePetalRadius(); în timpul fiecărei redesenări. Fragmentul de cod de mai jos prezintă o clasă minimă de animație.

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

Observați că funcția de apel înapoi în window.requestAnimationFrame(() => this.animateFlowers()); folosește sintaxa funcției săgeată, care este necesară pentru a păstra this contextul clasei de obiecte curente.

Fragmentul de cod de mai sus ar duce la creșterea continuă a lungimii petalei florii, deoarece nu are un mecanism care să oprească animația respectivă. În codul demonstrativ, folosesc un setTimeout() apel invers pentru a termina animația după 5 secunde. Ce se întâmplă dacă doriți să redați recursiv o animație? O soluție simplă este prezentată în proiectul StackBlitz de mai jos, care utilizează un setInterval() apel invers pentru a reda animația la fiecare 8 secunde.

Asta e tare. Ce altceva putem face pe animațiile de pânză?

Adăugați interacțiuni la animație

Vrem ca panza să răspundă la evenimentele de pe tastatură, evenimentele mouse-ului sau evenimentele tactile. Cum? Bine, adăugați ascultători de evenimente.

În această demonstrație, vom crea o pânză interactivă. Când mouse-ul dă clic pe pânză, înflorește o floare. Când faceți clic într-un alt punct de pe pânză, o altă floare înflorește. Când țineți apăsată tasta CTRL și faceți clic, pânza se va șterge. Figura 3 prezintă animația finală a pânzei.

1611518291 496 Cum se compun animatii de tip panza in TypeScript
Figura 3. Pânză interactivă.

Ca de obicei, creăm o clasă InteractiveFlowers să dețină o serie de flori. Fragmentul de cod al fișierului InteractiveFlowers clasa este după cum urmează.

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

Adăugăm un ascultător de evenimente pentru a urmări evenimentele care fac clic pe mouse și poziția (pozițiile) mouse-ului. Fiecare clic va adăuga o floare la matricea de flori. Deoarece nu vrem să lăsăm florile să se extindă la infinit, definim o metodă increasePetalRadiusWithLimit() în Flower clasa pentru a crește raza petalelor până la o creștere de 20. În acest fel, fiecare floare va înflori de la sine și va înceta să înflorească după ce raza sa de petale a crescut 20 de unități.

Am stabilit un membru privat stopChanging în floare pentru a optimiza animația, astfel încât animația să se oprească când toate florile au terminat de înflorit.

De asemenea, putem asculta keyup/keydown evenimente și adăugați controale de tastatură pe pânză. În această demonstrație, conținutul pânzei va fi șters când utilizatorul ține tasta CTRL și face clic pe mouse. Starea apăsării tastei este urmărită de ctrlIsPressed camp. În mod similar, puteți adăuga alte câmpuri pentru a urmări alte evenimente de la tastatură pentru a facilita controalele granulare pe pânză.

Desigur, ascultătorii de evenimente pot fi optimizați folosind Observables, mai ales atunci când utilizați Angular. Vă puteți juca cu proiectul StackBlitz de mai jos.

Ce urmeaza? Putem reda demonstrația interactivă a florilor prin adăugarea unor efecte sonore și a unor sprite de animație. Putem studia cum să-l facem să ruleze fără probleme pe toate platformele și să facem din ea o aplicație PWA sau mobilă.

Sper că acest articol adaugă o oarecare valoare subiectului Canvas Animations. Din nou, codul sursă este în această repo GitHub și te poți juca și cu acest proiect StackBlitz și vizitați un site demo. Nu ezitați să lăsați comentarii mai jos. Mulțumesc.

Noroc!