Antes de empezar a escribir, recomendar un canal de youtube de Frank https://www.youtube.com/channel/UCEqc149iR-ALYkGM6TG-7vQ, que contiene muchos e interesantes vídeos sobre Canvas sin librerías, con javascript nativo. Están muy bien explicados desde 0 y aumentando complejidad
See the Pen Componente Web con Partículas en lienzo Canvas by Iván Albizu (@ivan_albizu) on CodePen.
Arrancar proyecto con ParcelJS
Sobre la raiz del proyecto, con ParcelJS instalado (Yo tengo instalada la versión 1.12.5)
parcel index.html
Creación de partículas
La clase de utilidad Particle es encargada de crear un círculo, permitiendo definir su:
- Su posicionamiento mediante
xey - Su color de relleno
Además, le pasamos por parámetro el contexto 2D ctx para usarlo desde la clase CanvasDraw
class Particle {
constructor(ctx, x, y, fillStyle) {
this.ctx = ctx
this.x = x
this.y = y
this.fillStyle = fillStyle
this.size = Math.random() * 16 + 1
this.speedX = Math.random() * 10 - 5
this.speedY = Math.random() * 10 - 5
this.color = fillStyle
}
//...
}
Tenemos otros atributos que se dan valor y actualizan dentro de la misma clase:
size: tamaño inicial de la partículaspeedXyspeedY: dirección del movimiento(*)
(*) Por anticipar lo que veremos más adelante. Lo que realmente se hace es actualizar la posición de x e y de la partícula en función de los valores de speedX y speedY y volver a pintarlo con requestAnimationFrame() dando la sensación de movimiento
Tenemos dos métodos accesibles desde fuera:
update(): actualiza las propiedades de las partículasdraw(): pinta las partículas
class Particle {
//...
update() {
this.x += this.speedX
this.y += this.speedY
if (this.size > 0.2) this.size -= 0.2
}
draw() {
this.ctx.fillStyle = this.fillStyle
this.ctx.beginPath()
this.ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
this.ctx.fill()
}
}
En el método update() modificamos la posición de la partícula y su tamaño
El método draw() sirve para (re) dibujar la partícula tras los cambios de sus propiedades
Creación del WebComponent
Definición de nuestra etiqueta
Creamos nuestra clase tipo PascalCase extendiendo de HTMLElement y se define customElements.define(“canvas-draw”, CanvasDraw);, teniendo el cuenta que el primer parámetro será el nombre de la etiqueta (con al menos un guión medio) y el segundo parámetro será el nombre de la clase (que tendrá toda la lógica)
class CanvasDraw extends HTMLElement {
constructor() {
super()
}
}
customElements.define('canvas-draw', CanvasDraw)
Atributos del WebComponent
Se han usado cuatro atributos.
Dos de ellos, #particlesArray y #animating para almacenar la cantidad de partículas creadas y para bloquear/liberar la animación
Los otros dos atributos particles y maxDistanceJoinParticles para definir cuantas partículas tendrá el canvas y para unir las partículas mediante una línea cuando la distancía entre ellas no supera cierto valor.
Los dos últimos atributos comentados están inicializados con valor por defecto, pero pueden ser definidos otros valores desde la vista HTML. Esta situación, customizable desde HTML, requiere que lo especifiquemos en la implementación de los métodos:
static get observedAttributes(): incluimos en el array aquellos atributos que pudieran ser modificadosattributeChangedCallback(name, oldValue, newValue): asignamos al atributo su nuevo valor
class CanvasDraw extends HTMLElement {
#particlesArray = []
#animating = false
particles = 40
maxDistanceJoinParticles = 80
static get observedAttributes() {
return ['particles', 'max-distance-join-particles']
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'particles') {
this.particles = newValue || 100
} else if (name === 'max-distance-join-particles') {
this.maxDistanceJoinParticles = newValue || 80
}
}
//...
}
Definición del constructor y HTML del WebComponent
En en constructor de clase añadimos shadow en modo abierto this.attachShadow({ mode: "open" }). Iniciamos el canvas y el contexto 2D como null
Seleccionamos todos los elementos del DOM (con clase js-particles) que serán los encargados de iniciar las animaciones canvas
El método render se llama al final del constructor. Este método es el encargado de dar estilos al canvas y de añadirlo al DOM. También sacamos una referencia al contexto canvas para poder usarlo más adelante this.ctx = this.canvas.getContext("2d");
Sobre estilos, comentar que lo único importante es que el canvas está posicionado como fixed ocupando toda la pantalla, como bloque, sin color y anulando eventos click
class CanvasDraw extends HTMLElement {
//...
constructor() {
super()
this.attachShadow({ mode: 'open' })
this.canvas = null
this.ctx = null
this.btns = document.querySelectorAll('.js-particles')
this.render()
}
//...
render() {
const style = document.createElement('style')
style.textContent = `
canvas-draw {
display: block;
overflow: hidden;
position: fixed;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
`
this.appendChild(style)
this.canvas = document.createElement('canvas')
this.shadowRoot.appendChild(this.canvas)
this.ctx = this.canvas.getContext('2d')
}
//...
}
Registro de eventos del WebComponent
La animación se dispara cuando hacemos mousedown sobre aquellos elementos con clase js-particles. Añadimos y quitamos el listener en los métodos connectedCallback() y disconnectedCallback() respectivamente
Hemos creado la función auxiliar _handlerMouseDown(event) para que sea más fácil registrar y eliminar el evento. Este método se dispara si actualmente no existe ninguna animación de partículas. (Se trata de animaciones canvas, si lanzamos muchas animaciones podría consumir muchos recursos el navegador)
Obtenemos aquí tres datos para la definición de las partículas:
-
event.xyevent.y: obtenemos las coordenadas x e y asociadas al evento -
cssObj.getPropertyValue(“background-color”): obtenemos el color de fondo del elementojs-particlessobre el que se hizo click
Creamos tantas partículas como se indicasen en su atributo this.particles y lo guardamos en un array this.#particlesArray para saber cuando todas las partículas desaparecerán
El método _animate() lo dejamos para comentarlo en el siguiente punto
class CanvasDraw extends HTMLElement {
//...
connectedCallback() {
this.btns.forEach((btn) => {
btn.addEventListener('mousedown', this._handlerMouseDown.bind(this))
})
}
disconnectedCallback() {
this.btns.forEach((btn) => {
btn.removeEventListener('mousedown', this._handlerMouseDown.bind(this))
})
}
//...
_handlerMouseDown(event) {
if (this.#animating) return
this.#animating = true
this._calculateCanvasSize()
const cssObj = window.getComputedStyle(event.target, null)
const bgColor = cssObj.getPropertyValue('background-color')
for (let i = 0; i < this.particles; i++) {
this.#particlesArray.push(
new Particle(this.ctx, event.x, event.y, bgColor)
)
}
this._animate()
}
_calculateCanvasSize() {
this.canvas.width = this.clientWidth
this.canvas.height = this.clientHeight
}
//...
}
Si has observado el código de arriba, habrás visto que este método _calculateCanvasSize() no lo he mencionado. Es para asignar el tamaño del canvas, haciéndolo justo en este punto, momento del mousedown, nos ahorramos tener que user eventos resize
Animaciones Canvas del WebComponent
El método _animate() es el encargado de generar la animación, se ejecuta 60fps ya que hace llamada a requestAnimationFrame(). Cada vez que entra se limpia el lienzo y se hace llamada a la función _handleParticles() para actualizar partículas que en seguida veremos. Esta animación no se para hasta que detectamos que el array de partículas this.#particlesArray ha quedado vacío, cuando ha quedado vacío cambiamos this.#animating = false a false para que se pueda iniciar nuevas animaciones y volvemos a limpiar el lienzo
El método _handleParticles recorre el array de partículas creadas y las actualiza haciendo llamada a los métodos update() y draw() de la clase Particles. Si el tamaño de las partículas es menor a uno dado (en nuestro caso es if (this.#particlesArray[i].size <= 0.2)) entonces lo quitamos del array. El bucle for interior es auxiliar para añadir algo más a la. En este caso, lo que añade es una línea que une aquellas partículas próximas entre sí, con el requisito de que su distancia sea menor a una dada this.maxDistanceJoinParticles (recuerda de más arriba, este era uno de los valores personalizables comoa tributos del WebComponent)
class CanvasDraw extends HTMLElement {
//...
_handleParticles() {
for (let i = 0; i < this.#particlesArray.length; i++) {
this.#particlesArray[i].update()
this.#particlesArray[i].draw()
for (let j = i; j < this.#particlesArray.length; j++) {
const dx = this.#particlesArray[i].x - this.#particlesArray[j].x
const dy = this.#particlesArray[i].y - this.#particlesArray[j].y
const distance = Math.sqrt(dx * dx + dy * dy)
if (distance < this.maxDistanceJoinParticles) {
this.ctx.beginPath()
this.ctx.strokeStyle = this.#particlesArray[i].color
this.ctx.lineWidth = 0.2
this.ctx.moveTo(this.#particlesArray[i].x, this.#particlesArray[i].y)
this.ctx.lineTo(this.#particlesArray[j].x, this.#particlesArray[j].y)
this.ctx.stroke()
this.ctx.closePath()
}
}
if (this.#particlesArray[i].size <= 0.2) {
this.#particlesArray.splice(i, 1)
i--
}
}
}
_animate() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this._handleParticles()
if (this.#particlesArray.length > 0) {
requestAnimationFrame(this._animate.bind(this))
} else {
this.#animating = false
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
}
//...
}
Uso del WebComponente
Al inicio comentamos cual sería el nombre de la clases, y que además, podemos usar dos atributos para cambiar la cantidad de partículas y la distancia máxima para que se unan mediante una línea, esto es:
<canvas-draw particles="90" max-distance-join-particles="80"></canvas-draw>
Si vas a cambiar los valores de atributo, ten cuidado, ya que podría consumir muchos recursos. Recomiendo no subir los valores por encima de de 150
Codepen del WebComponent
En este PEN puede verse
See the Pen Componente Web con Partículas en lienzo Canvas by Iván Albizu (@ivan_albizu) on CodePen.
Github del Webcomponent
En este repositorio puede verse el código del Webcomponente