I created a simple catalog item component using stencil.js. In the component there is canvas tag that on it I painted animating curved lines. At the componentDidLoad function, I define the canvas, initialize it and call the animate function. This is the code of the component itself:
import { Component, Host, h, Prop, Element } from "@stencil/core"; import { Item } from "../hotb-catalog-container/data"; import { initCanvas, init, animate } from "./wave.js"; import axios from "axios"; @Component({ tag: "hotb-catalog-item", styleUrl: "hotb-catalog-item.scss", shadow: false, }) export class HotbCatalogItem { @Prop() item: Item; @Element() el: HTMLElement; // @Event({ bubbles: true, composed: true }) itemSelect: EventEmitter<Item>; itemSelected(event: Event) { event.preventDefault(); //sessionStorage.setItem("item", JSON.stringify(this.item)); axios.post("/item", { item: this.item }).then(() => { window.location.href = "http://localhost:3000/item"; }); } componentDidLoad() { let canvas = this.el.querySelector('canvas'); initCanvas(canvas); init(); animate(); } render() { return ( <Host> <div class="heading"> <h1>{this.item.name}</h1> <span> מ{this.item.origin} · {this.item.taste}</span> </div> <div class="center-part"> <div class="center-part__img"></div> <div class="center-part__icon center-part__icon--temp"> {this.item.temp}°C </div> <div class="center-part__icon center-part__icon--time"> {this.item.time} </div> </div> <a href="/item" onClick={this.itemSelected.bind(this)} class="primary-btn homepage__tea" > לצפיה </a> <canvas></canvas> </Host> ); } }
As you can see, I import the canvas code, here it is:
let line1; let line2; var ctx; var canvasElem; function initCanvas(canvas) { canvasElem = canvas; ctx = canvas.getContext("2d"); const parent = canvas.parentElement; canvas.width = parent.clientWidth; canvas.height = parent.scrollHeight; } class LineGen { constructor(x, y, directionY, cpX, directionCPX, cpY, directionCPY, size, color) { this.x = x; this.y = y; this.directionY = directionY; this.cpX = cpX; this.directionCPX = directionCPX; this.cpY = cpY; this.directionCPY = directionCPY; this.size = size; this.color = color; } draw() { ctx.beginPath(); ctx.moveTo(this.x, this.y); ctx.quadraticCurveTo(this.cpX, this.cpY, 400, this.y); ctx.strokeStyle = this.color; ctx.lineWidth = this.size; ctx.stroke(); } update() { if (this.y > 490 || this.y < 400) { this.directionY = -this.directionY; } if (this.color === '#E07D31') { if (this.cpX > 390 || this.cpX < 10) { this.directionCPX = -this.directionCPX; } if (this.cpY > 590 || this.cpY < 150) { this.directionCPY = -this.directionCPY; } } else if (this.color === '#49E048') { if (this.cpX > 390 || this.cpX < 10) { this.directionCPX = -this.directionCPX; } if (this.cpY > 560 || this.cpY < 240) { this.directionCPY = -this.directionCPY; } } //this.y += this.directionY; this.cpX += this.directionCPX; this.cpY += this.directionCPY; this.draw(); } } class Line extends LineGen { constructor(x, y, directionY, cpX, directionCPX, cpY, directionCPY, size, color) { super(x, y, directionY, cpX, directionCPX, cpY, directionCPY, size, color); } } function init() { let x = 0; let y = 400; let y2 = 380; let directionY = -.2; let cpX = 100; let cpX2 = 100; let directionCPX = .15; let cpY = 300; let cpY2 = 300; let directionCPY = .15; let directionCPY2 = .178125; let size = 2; let color = '#E07D31'; let color2 = '#49E048'; line1 = new Line(x, y, directionY, cpX, directionCPX, cpY, directionCPY2, size, color); line2 = new Line(x, y2, directionY, cpX, directionCPX, cpY, directionCPY, size, color2); } function animate() { requestAnimationFrame(animate); ctx.clearRect(0, 0, canvasElem.width, canvasElem.height); line1.update(); line2.update(); } export { initCanvas, init, animate };
Now, the catalog item component is inside of a container component like so:
let items; if (this.newItemsArr.length == 0) { items = <div class="no-items">אין משקאות שתואמים לחיפוש שלך</div>; } else { items = this.newItemsArr.map((item) => ( <hotb-catalog-item item={item}></hotb-catalog-item> )); } return ( <Host> {items} </Host> );
The end result is that the components are showing the canvas and the lines however animation ocurres only in one of them. The rest are static in theire initial state. Please tell me why it happens and what i can do to fix it so that all of the components will animate. It is important to note that when i refresh the code and the browser refreshes via hot reload, another component starts animate and so on with each refresh.
Thank you for your help.
Advertisement
Answer
The problem is that some variables are defined outside the initCanvas
function and therefore shared between all the components (line1
, line2
, ctx
and canvasElem
). So each time you call initCanvas
they are overwritten.
A quick solution would be to wrap it in a class:
export class WaveCanvas { constructor(canvas) { this.canvasElem = canvas; this.ctx = canvas.getContext("2d"); const parent = canvas.parentElement; canvas.width = parent.clientWidth; canvas.height = parent.scrollHeight; } init() { // ... this.line1 = new Line(...); } animate() { requestAnimationFrame(() => this.animate()); this.ctx.clearRect(0, 0, this.canvasElem.width, this.canvasElem.height); this.line1.update(); this.line2.update(); } }
And then instantiate it in the component:
componentDidLoad() { let canvas = this.el.querySelector('canvas'); const waveCanvas = new WaveCanvas(canvas); waveCanvas.init(); waveCanvas.animate(); }
This way each instance of WaveCanvas
will hold its own reference to the correct <canvas>
element.