Initiation au canvas

Nous allons voir dans cet article un exemple d'utilisation de la balise canvas dans le cadre d'une animation sur un visuel.
L'animation consistera à faire disparaître au survol l'image bloc par bloc avec une animation d'opacité et de translation vers le haut.

Dans un premier temps, nous allons créer le canvas à l'intérieur d'une div container.

const container = document.getElementById('container')
const img = new Image()
img.src = 'http://lorempicsum.com/up/750/1050/5'
const canvas = document.createElement('canvas')
canvas.setAttribute('width', 375)
canvas.setAttribute('height', 675)
container.appendChild(canvas)
const ctx = canvas.getContext('2d')

L'image choisie fait 750 pixels de large sur 1050 pixels de haut.
La taille du canvas est réduite par deux soit 375 x 525.
On y insérera des blocs carrés de 75 pixels de côté.
En prévision de l'animation de translation, on ajoute 75 pixels sur le haut, et 75 pixels sur le bas pour garder le canvas parfaitement centré sur la hauteur.

Nous allons maintenant dessiner l'image à l'intérieur du canvas.
Pour se faire, nous utiliserons une classe Javascript.

class Canvas {
	constructor(ctx, img, timeAnim){
		this.ctx = ctx
		this.img = img
		this.timeAnim = timeAnim
		this.translateY = 150
		this.blocks = []
		this.img.onload = () => {
			this.init()
		}
	}

	init(){
		for (let i=0; i<35; i++){
			this.blocks[i] = 0
			this.drawBlock(i, this.blocks[i]);
		}
	}
}

Le constructeur prend en paramètre 3 attributs, le contexte, l'image et un temps qui correspond à la vitesse de disparition des blocs.
L'attribut blocks correspond à l’état d'avancement de l'animation.
On lance la méthode init une fois l'image chargée.

La méthode init parcoure l'ensemble des 35 blocs pour initialiser l'attribut blocks et utiliser la méthode drawBlock qui va dessiner l'image.

drawBlock(pos, easing){
	let x = pos % 5
	let y = Math.floor(pos / 5)
	this.ctx.globalAlpha = 1 - easing
	this.ctx.drawImage(this.img, x*150, y*150, 150, 150, x*75, ((y+1)*75 - this.translateY*easing), 75, 75)
}

Avec le paramètre pos, nous pouvons récupérer les coordonnées en x et y, la fonction drawImage permettant de copié l'image téléchargée pour la placer dans un canvas.
Le lien pour voir le fonctionnement de drawImage

L'easing servira pour le calcul des animations.

Nous pouvons désormais initialiser notre canvas

class Canvas {...}

const container = document.getElementById('container')
const img = new Image()
img.src = 'http://lorempicsum.com/up/750/1050/5'
const canvas = document.createElement('canvas')
canvas.setAttribute('width', 375)
canvas.setAttribute('height', 675)
container.appendChild(canvas)
const ctx = canvas.getContext('2d')
const timeAnim = 300

const monCanvas = new Canvas(ctx, img, timeAnim)

 À présent, nous allons construire dans la classe Canvas les méthodes pour permettre l'animation des blocs.

easing(t){
	t = t/this.timeAnim
	if(t < 0){
		t = 0
	} else if( t > 1){
		t = 1
	}
	return t*(2-t)
}

draw(currentPos, t){
	for(let i = 0; i < 35; i++){
		if (i == currentPos){
		 	this.blocks[i] = this.easing(t)
		}
		this.drawBlock(i, this.blocks[i]);
	}
}

La méthode easing va nous permettre d'avoir l'équivalence d'un ease sur une transition css, avec t le temps passé.
J'ai choisi un simple ease-out pour l'exemple.

Pour draw, il s'agit de la méthode qui dessinera l’entièreté de l'image durant l'animation.
Cette méthode ressemble à init, la différence étant la variation du contenu blocks via la méthode easing vu ci-dessus.
Nous avons en paramètre currentPos qui est l'identifiant du bloc en train d’être animé, et t qui représente le temps de l'animation.

Animer un canvas est différent d'une simple animation css, il nous faudra redessiner continuellement ce dernier pour voir l'animation.

 

window.requestAnimationFrame = window.requestAnimationFrame || 
window.mozRequestAnimationFrame || 
window.webkitRequestAnimationFrame ||
 window.msRequestAnimationFrame;

let animPlay = false
let start = null
let currentPos = 0
let progress
const step = (timestamp) => {
	if (start === null) start = timestamp
	progress = timestamp - start

	ctx.clearRect(0, 0, canvas.width, canvas.height)
	monCanvas.draw(currentPos, progress)
	if(progress >= timeAnim){
		currentPos++
		start = timestamp
		if(currentPos / 5 > 7){
			animPlay = false
			start = null
			currentPos = 0
			ctx.clearRect(0, 0, canvas.width, canvas.height)
			monCanvas.init()
			return
		}
	}
	requestAnimationFrame(step)
} 

container.addEventListener('mouseover', () => {
	if(!animPlay){
		animPlay = true
		requestAnimationFrame(step);
	}
})

Nous utilisons requestAnimationFrame dans une fonction récursive pour appeler notre méthode draw.
La variable animPlay force à attendre la fin de l'animation avant de la relancer.
Les variables start et progress permette de calculer le temps passé au lancement de l'animation.
Pour finir currentPos représente l'identifiant du bloc étant en train d'être animé.

A chaque itération on efface le context grâce à clearRect pour redessiner notre image.
Lorsque progress atteint ou dépasse la valeur du temps d'animation défini, c'est que le bloc en cours à fini d'être animé.
On incrémente donc le currentPos et réinitialise start et progress pour animer le prochain bloc.
La condition de sortie étant lancée lorsque la variable currentPos dépasse le nombre de blocs à animer, auquel cas on réinitialise les variable et le canvas.

Voici le code final également disponible sur codePen

<div id="container"></div>
body{
	display: flex;
	align-items: center;
	justify-content: center;
	min-height: 100vh;
}
#container{
	display: flex;	
	flex-wrap: wrap;	
	width: 375px;
	height: 675px;
	cursor: pointer;
}
class Canvas {
	constructor(ctx, img, timeAnim){
		this.ctx = ctx
		this.img = img
		this.timeAnim = timeAnim
		this.translateY = 150
		this.blocks = []
		this.img.onload = () => {
			this.init()
		}
	}

	init(){
		for (let i=0; i<35; i++){
			this.blocks[i] = 0
			this.drawBlock(i, this.blocks[i]);
		}
	}

	easing(t){
		t = t/this.timeAnim
		if(t < 0){
			t = 0
		} else if( t > 1){
			t = 1
		}
		return t*(2-t)
	}

	drawBlock(pos, easing){
		let x = pos % 5
		let y = Math.floor(pos / 5)
		this.ctx.globalAlpha = 1 - easing
		this.ctx.drawImage(this.img, x*150, y*150, 150, 150, x*75, ((y+1)*75 - this.translateY*easing), 75, 75)
	}

	draw(currentPos, t){
		for(let i = 0; i < 35; i++){
			if (i == currentPos){
			 	this.blocks[i] = this.easing(t)
			}
			this.drawBlock(i, this.blocks[i]);
		}
	}
}

const container = document.getElementById('container')
const img = new Image()
img.src = 'http://lorempicsum.com/up/750/1050/5'
const canvas = document.createElement('canvas')
canvas.setAttribute('width', 375)
canvas.setAttribute('height', 675)
container.appendChild(canvas)
const ctx = canvas.getContext('2d')
const timeAnim = 300

const monCanvas = new Canvas(ctx, img, timeAnim)


window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;

let animPlay = false
let start = null
let currentPos = 0
let progress
const step = (timestamp) => {
	if (start === null) start = timestamp
	progress = timestamp - start

	ctx.clearRect(0, 0, canvas.width, canvas.height)
	monCanvas.draw(currentPos, progress)
	if(progress >= timeAnim){
		currentPos++
		start = timestamp
		if(currentPos / 5 > 7){
			animPlay = false
			start = null
			currentPos = 0
			ctx.clearRect(0, 0, canvas.width, canvas.height)
			monCanvas.init()
			return
		}
	}
	requestAnimationFrame(step)
} 

container.addEventListener('mouseover', () => {
	if(!animPlay){
		animPlay = true
		requestAnimationFrame(step);
	}
})

Il s'agit d'un exemple, il est possible d'optimiser le code, beaucoup de valeurs étant écrites en dur.
On peut imaginer à partir de cet exemple plusieurs possibilités exploitable en production, en imaginant des transitions d'image à partir de bandes horizontales ou verticales.

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.