Creative Coding
Canvas
2526 · CMIA · Dekens Antoine
Canvas 101
L'élément canvas permet de manipuler des pixels dans notre HTML.
Setup
Première chose à faire: définir notre canvas.
<canvas class='myCanvas'></canvas>
// Soit on récupère une balise canvas existante
const canvas = document.querySelector( ".myCanvas" );
// Soit on le crée
const body = document.querySelector( "body" );
const canvas = document.createElement( "canvas" );
body.append( canvas );
Les dimensions
Par défaut, un canvas a une largeur de 300px et une hauteur 150px. On utilise les propriétés width et height de notre canvas pour changer sa taille.
// On change la largeur et la hauteur.
// Inutile de préciser une unité,
// un canvas interprète les valeurs en pixels.
canvas.width = 500;
canvas.height = 500;
Le contexte
Pour pouvoir dessiner quelque chose dans notre canvas, il nous faut une dernière chose: le contexte. Ce dernier est une boîte à outils, qui contiendra nos méthodes, il en existe 2:
- 2d
- webgl2 (3d)
const canvas = document.querySelector( ".myCanvas" );
// Charge nos méthodes dans une
// constante "ctx", qui pourra être utilisé
// pour dessiner en 2d.
const ctx = canvas.getContext( "2d" );
Formes &
densité
Rectangles
// Dessine un rectangle et ajoute une couleur de fond (noire par défaut)
ctx.fillRect( x, y, width, height );
// Dessine et ajoute un contour (noir par défaut)
ctx.strokeRect( x, y, width, height );
- x: La position horizontale du coin supérieur gauche
- y: La position verticale du coin supérieur gauche
- width: La largeur du rectangle
- height: La hauteur du rectangle
beginPath & closePath
beginPath et closePath permettre d'ouvrir et de fermer le dessin actuel. fillRect() et strokeRect() le fond automatiquement, mais pas la méthode rect() qui permet de les enchainer pour créer des tracés connectées :
fill & stroke
La méthode fill() permet d'appliquer une couleur de remplissage sur une ou plusieurs tracés connectés. Tandis que stroke() appliquera un contour.
// Ouvre un tracé, dessine et rempli la forme et ferme le tracé en une fois.
ctx.fillRect( x, y, width, height );
// La même opération avec rect()
ctx.beginPath();
// Une indentation entre beginPath et closePath
// permet de visualiser les forme dans le tracé actuel.
ctx.rect( x, y, width, height );
ctx.closePath();
ctx.fill();
Densité de pixel
Device Pixel Ratio (ou DPR)
Si votre écran à une densité de pixel plus grande que 1 ( mac, smartphones, ect.), ce que nous avons dessiné est flou, surtout sur les contours.
Il faut voir le canvas comme une balise image: elle est composée de pixels. Et comme une image, si on veut qu'elle soit nette, il faut que sa résolution reflète le nombre de pixel dessiné, multiplié par la densité de pixel de votre écran.
Ex: Sur un écran de DPR 2, un canvas de 200px doit avoir une résolution de 400px pour être net.Cercles…
// Dessine un cercle
ctx.arc( x, y, radius, startAngle, endAngle, counterclockwise = false );
- x: La position horizontale du centre
- y: La position verticale du centre
- radius: Le rayon du cercle
- startAngle: L'angle de début (en radian)
- endAngle: L'angle de fin (en radian)
- counterclockwise: La direction de l'arc de cercle (false par défaut)
Degré vs Radian
//-- Un tour de cercle
// deg = 360 ou rad = 2 * Math.PI
//-- Un quart de cercle
// deg = 90 ou rad = 0.5 * Math.PI
// Fonction pour convertir des radians en degrés
const radToDeg = ( rad ) => {
return rad * ( 180 / Math.PI );
}
// Fonction pour convertir des degrés en radians
const degToRad = ( deg ) => {
return deg * ( Math.PI / 180 );
}
Fonction circle
Utiliser arc() peut vite devenir fatigant pour dessiner des cercles complets. On peut se faciliter la tâche en ajoutant une fonction circle qui prendre 3 paramètres et les passera dans notre méthode arc :
- x
- y
- radius
// Soit on crée une fonction
const circle = ( x, y, radius ) => {
ctx.arc( x, y , radius, 0, 2 * Math.PI );
};
// Soit on ajoute une méthode _circle à notre objet 'ctx'
// Attention au nom que l'on choisit pour ne pas écraser une
// autre propriétés.
ctx._circle = ( x, y, radius ) => {
ctx.arc( x, y , radius, 0, 2 * Math.PI );
};
Template Canvas
v1
Exercice
En partant du template et sur base de l'exemple suivant, réaliser les différentes étapes pour atteindre un résultat similaire :
- créer une particule ronde avec une position aléatoire.
- modifier le radius de manière aléatoire.
- utiliser un tableau de couleurs pour changer la couleur de la particule de manière aléatoire.
- utiliser une boucle for et créer au moins 100 particules.
Les images
drawImage
La méthode drawImage() permet de dessiner une image avec 3 paramètres dans sa version la plus simple :
- image : la référence à l'image
- x : La position horizontale à partir du coin supérieur gauche
- y : La position vertical à partir du coin supérieur gauche
<img src="/path/to/my/image.jpg" alt="…">
// On récupère notre balise <img>
const image = document.querySelector( 'img' );
…
// On dessine notre image à une position x et y
ctx.drawImage( image, x, y );
Redimensionner
On peut passer 2 paramètres supplémentaires pour redimensionner notre image :
- width : indique la largeur de l'image
- height : indique la hauteur de l'image
<img src="/path/to/my/image.jpg" alt="…">
// On récupère notre balise <img>
const image = document.querySelector( 'img' );
…
// On dessine notre image à une position x et y
// avec une certaine largeur et hauteur
ctx.drawImage( image, x, y, width, height );
Respecter le ratio
Le problème c'est qu'en redimensionnant notre image son ratio n'est pas forcément respecté. Pour trouver le ratio vertical, il suffit de savoir combien de fois sa hauteur rentre dans sa largeur :
<img src="/path/to/my/image.jpg" alt="…">
const image = document.querySelector( 'img' );
// En divisant la hauteur par la largeur
// on récupère le ratio vertical de notre image.
const verticalRatio = image.height / image.width;
…
// Le paramètre hauteur peut ensuite être calculé sur base de la largeur
// désirée multipliée par ce ratio vertical.
ctx.drawImage( image, x, y, w, w * verticalRatio );
Le bon ratio
Notre image est horizontale, donc si nous voulons couvrir notre canvas entièrement il va falloir se baser sur sa hauteur. Cela veut dire calculer le ratio horizontal :
<img src="/path/to/my/image.jpg" alt="…">
const image = document.querySelector( 'img' );
// En divisant la largeur par la hauteur
// on récupère le ratio horizontal de notre image.
const horizontalRatio = image.width / image.height;
…
// Le paramètre largeur peut ensuite être calculé sur base de la hauteur
// désirée multipliée par ce ratio horizontal.
ctx.drawImage( image, x, y, h * horizontalRatio, h );
Ratio conditionnel
Pour avoir un code robuste, il va falloir prendre en compte ces 2 scénarios possible :
<img src="/path/to/my/image.jpg" alt="…">
const image = document.querySelector( 'img' );
const verticalRatio = image.height / image.width;
const horizontalRatio = image.width / image.height;
…
// Scénario d'une image horizontale
if( image.width >= image.height ){
ctx.drawImage( image, x, y, h * horizontalRatio, h );
// Scénario d'une image verticale
} else {
ctx.drawImage( image, x, y, w, w * verticalRatio );
}
Charger l'image
Notre méthode de récupération d'image provoque des soucis d'affiche car le JavaScript s'éxécute plus vite que le chargement média. Mettons en place une fonction permettant d'éviter cela :
const loadImage = ( url ) => {
// Instancie une nouvelle image
const image = new Image();
// Règle des problèmes de CORS
image.crossOrigin = "anonymous";
image.addEventListener("load", () => {
// cette fonction sera exécutée une fois
// votre image chargée et prête à être affichée.
});
// On passe le chemin vers notre image
// dans l'attribut src de notre objet
// Image ce qui lance l'évènement 'load'
image.src = url;
};
Centrer l'image
Dernière étape : repositionner notre image qui est toujours aligner sur le coin supérieur gauche. Selon le ratio de notre image, il faudra calculer la différence entre la dimension visible et la dimension totale pour récupérer la dimension non visible de notre image. Une fois obtenue, il suffira de soustraire la moitié à l'axe correspondant :
// Scénario d'une image horizontale
if( image.width >= image.height ){
const offsetX = (w - h * horizontalRatio) * 0.5;
ctx.drawImage( image, x + offsetX, y, h * horizontalRatio, h );
// Scénario d'une image verticale
} else {
const offsetY = (h - w * verticalRatio ) * 0.5;
ctx.drawImage( image, x, y + offsetY, w, w * verticalRatio );
}
Analyser le canvas
getImageData
Il est possible de récupérer les valeurs de chaque pixels sous un format rgba. Pour cela on utilise la méthode getImageData( ), qui permet d'analyser une zone particulière de notre canvas :
// Stocke les données de notre zone d'analyse
const data = ctx.getImageData( x, y, width, height );
Manipuler les données
getImageData va nous retourner un tableau très bizarre (ImageData). Il va falloir mettre en place une boucle pour mieux visualiser nos couleurs :
const datas = ctx.getImageData( 0, 0, 10, 10 ).data;
// Comme un pixel un composé de 4 valeurs (rgba),
// cette boucle passe de 0 -> 4 -> 8 -> 12…
for( let i = 0; i < datas.length; i += 4 ){
// i = 0 ou 4 ou 8 ou 12…
const r = datas[ i ],
// i = 1 ou 5 ou 9 ou 13…
g = datas[ i + 1 ],
// i = 2 ou 6 ou 10 ou 14…
b = datas[ i + 2 ];
const rgb = `rgb(${r},${g},${b})`;
}
Off canvas
Analyser un canvas est très gourmant en performance, c'est pourquoi nous allons utiliser un second canvas qui ne sera pas dans notre HTML mais bien présent en JS. Pour cela nous allons simplement le cloner avec la méthode cloneNode :
Atttention de bien multiplier par le devicePixelRatio vos positions et dimensions passées dans getImageData en offCanvas.const canvas = document.createElement( "canvas" );
const ctx = canvas.getContext('2d');
// Clone notre premier canvas
const offCanvas = canvas.cloneNaode();
// N'oublions pas de récupérer le contexte pour ce canvas
const offCtx = offCanvas.getContext('2d');
Interpréter
Maintenant que nous avons un moyen plus performant de faire des analyses, nous pouvons par exemple calculer la couleur moyenne d'une zone :