Développement web

Cours 20

Canvas

Espace de pixels permettant de dessiner à l'aide d'une multitude de méthodes javascript

Compatibilité

http://caniuse.com/#feat=canvas

Démonstrations

Mise en place


<canvas width="800" height="600"></canvas>
                        

const canvas = document.querySelector('canvas')
const context = canvas.getContext('2d')
                        
  • canvas correspond à l'élément dans le DOM
  • context sera utilisé pour dessiner

Ce code ne sera pas répété dans les exemples qui suivent

Lignes et remplissages

Pour dessiner, la méthode classique consiste à

  • Indiquer qu'on commence un tracé
  • Définir le tracé (un trait ici, un cercle là, etc.)
  • Définir le style (trait orange large de 20px)
  • Faire apparaitre le tracé sur le canvas

Lignes


context.beginPath()

context.moveTo(50, 50)
context.lineTo(200, 200)
context.lineTo(50, 200)
context.closePath()

context.stroke()
                        

Lignes


context.beginPath()

context.moveTo(50, 50)
context.lineTo(200, 200)
context.lineTo(50, 200)

context.fill()
                        

Remplissages

Style

Il existe de nombreuses propriétés pour changer le style du dessin

Cela peut concerner les lignes ou le remplissage

Le style s'appliquera sur tous les dessins suivant le changement de propriété

Modifier le style de la ligne


context.beginPath()

context.moveTo(50, 50)
context.lineTo(200, 200)
context.lineTo(50, 200)

context.lineWidth   = 20
context.lineCap     = 'round'  // round | butt | square
context.lineJoin    = 'bevel'  // bevel | round | mitter
context.strokeStyle = 'orange'

context.stroke()
                        

Modifier le style de la ligne

Modifier le style de remplissage


context.beginPath()

context.moveTo(50, 50)
context.lineTo(200, 200)
context.lineTo(50, 200)

context.fillStyle = 'rgba(255, 0, 0, 0.5)'

context.fill()
                        

Modifier le style de remplissage

Ombres


context.beginPath()

context.moveTo(50, 50)
context.lineTo(200, 200)
context.lineTo(50, 200)

context.fillStyle     = 'rgba(255, 0, 0, 1)'
context.shadowColor   = 'blue'
context.shadowBlur    = 50
context.shadowOffsetX = 5
context.shadowOffsetY = 10

context.fill()
                        

Ombres

rect() et arc()

Certaines méthodes permettent de définir des tracés autre que des lignes

rect() permet de tracer un rectangle

arc() permet de tracer un arc de cercle

La variable globale Math possède plusieurs propriétés et méthodes mathématiques

Celle que nous allons utilisé est Math.PI et permet d'obtenir le nombre π utile pour dessiner des arcs de cercle


context.fillStyle   = 'orange'
context.strokeStyle = 'orange'

context.beginPath()
context.rect(50, 50, 200, 100)
context.fill()

context.beginPath()
context.arc(400, 50, 100, 0, Math.PI, false)
context.fill()

context.beginPath()
context.fillStyle = 'orange'
context.rect(50, 200, 200, 100)
context.stroke()

context.beginPath()
context.arc(400, 200, 100, 0, Math.PI, false)
context.stroke()
                        

fillRect() et clearRect()

fillRect() permet de remplir un rectangle sans passer par beginPath() et fill()

clearRect() permet d'effacer un rectangle


context.fillStyle = 'orange'
context.fillRect(50, 50, 300, 160)

context.clearRect(50, 50, 100, 80)

context.fillStyle = 'cyan'
context.fillRect(160, 60, 20, 70)

context.beginPath()
context.fillStyle = 'black'
context.arc(280, 210, 50, 0, Math.PI, false)
context.arc(120, 210, 50, 0, Math.PI, false)
context.fill()
                        

Textes


const text = 'Lorem ipsum dolor sit amet'

context.font = '40px Arial'
context.textAlign = 'center' // left | center | right
context.textBaseline = 'top' // top | bottom | middle | alphabetic | hanging

console.log(context.measureText(text).width)

context.fillText(text, 300, 100)
context.strokeText(text, 300, 160)
                        

Images

Pour dessiner une image, il est nécessaire de l'avoir chargée

Il faut donc créer, en javascript, un objet Image et écouter son événement load


const image = document.createElement('img')

image.addEventListener('load', () =>
{
    context.drawImage(image, 0, 0)
    context.drawImage(image, 0, 0, image.width * 0.5, image.height * 0.5)
})

image.src = 'image.jpg'
                        

Dégradés

Dégradé linéaire


const gradient = context.createLinearGradient(50, 50, 250, 250) // x1, y1, x2, y2

gradient.addColorStop(0, 'rgb(255, 80, 0)')
gradient.addColorStop(0.5, 'rgb(255, 191, 0)')
gradient.addColorStop(1, 'rgb(255, 246, 155)')

context.fillStyle = gradient

context.fillRect(0, 0, 400, 400)
                        

Dégradé linéaire

Dégradé Radial


const gradient = context.createRadialGradient(
    100, 100, 50, // x1, y1, r1
    100, 250, 250 // x2, y2, r2
)

gradient.addColorStop(0, 'rgb(255, 80, 0)')
gradient.addColorStop(0.5, 'rgb(255, 191, 0)')
gradient.addColorStop(1, 'rgb(255, 246, 155)')

context.fillStyle = gradient

context.fillRect(0, 0, 400, 400)
                        

Dégradé Radial

save() et restore()

Les fonctions save() et restore() permettent de sauvegarder l'état du style et de le restaurer

Cela équivaut à un historique de pinceau

Ces méthodes ne permettent pas de sauvegarder ou restaurer l'état du canvas !


context.save()
context.beginPath()
context.moveTo(50, 50)
context.lineTo(50, 100)
context.lineWidth = 1
context.stroke()

context.save()
context.beginPath()
context.moveTo(100, 50)
context.lineTo(100, 100)
context.lineWidth = 5
context.stroke()

context.save()
context.beginPath()
context.moveTo(150, 50)
context.lineTo(150, 100)
context.lineWidth = 10
context.stroke()

context.restore()
context.beginPath()
context.moveTo(200, 50)
context.lineTo(200, 100)
context.stroke()

context.restore()
context.beginPath()
context.moveTo(250, 50)
context.lineTo(250, 100)
context.stroke()
                        

Courbes

Les méthodes bezierCurveTo() et quadraticCurveTo() permettent de dessiner des courbes de bézier en spécifiant chacun des points

Courbe de bézier


context.beginPath()
context.moveTo(50, 50)
context.bezierCurveTo(
    300, 100,
    100, 300,
    300, 300
)
context.stroke()
                        

Courbe de bézier

Courbe de quadratique (de bézier)


context.beginPath()
context.moveTo(50, 50)
context.quadraticCurveTo(
    300, 100,
    300, 300
)
context.stroke()
                        

Courbe de quadratique (de bézier)

globalAlpha


context.globalAlpha = 0.3

context.fillStyle = '#ff0000'
context.fillRect(50, 50, 200, 200)

context.fillStyle = '#00ff00'
context.fillRect(100, 100, 200, 200)

context.fillStyle = '#0000ff'
context.fillRect(150, 150, 200, 200)
                        

globalCompositeOperation

La propriété globalCompositeOperation permet de spécifier comment doivent se comporter les tracés en cours (source) par rapport aux tracés initiaux (destination)

Équivalent au pathfinder d'illustrator


context.globalCompositeOperation = 'lighter'

context.fillStyle = '#ff0000'
context.fillRect(50, 50, 200, 200)

context.fillStyle = '#00ff00'
context.fillRect(100, 100, 200, 200)

context.fillStyle = '#0000ff'
context.fillRect(150, 150, 200, 200)
                        

context.fillStyle = 'red'
context.fillRect(200, 200, 200, 200)

context.globalCompositeOperation = 'destination-out' /* source-over | source-in | source-out | source-atop | destination-over | destination-in | destination-out | desination-atop | lighter | copy | xor */

context.beginPath()
context.fillStyle = 'blue'
context.arc(200, 250, 100, 0, Math.PI, false)
context.fill()
                        

Dans les exemples suivants, on considère que le rond rouge est dessiné après le carré bleu

ImageData

  • getImageData() permet de récupérer les pixels d'une zone du canvas
  • Ces pixels sont stockés dans la propriétés data de l'objet renvoyé
  • Il s'agit d'un tableau
  • Les valeurs R, G, B et A de chaque pixel sont les unes à la suite des autres
  • Il faut donc parcourir ce tableau 4 par 4


const image = document.createElement('img')

image.addEventListener('load', () =>
{
    context.drawImage(image, 0, 0)
    const imageData = context.getImageData(0, 0, image.width, image.height)

    for(let i = 0; i < imageData.data.length; i += 4)
    {
        const gray = (imageData.data[i + 0] + imageData.data[i + 1] + imageData.data[i + 2]) / 3

        imageData.data[i + 0] = gray
        imageData.data[i + 1] = gray
        imageData.data[i + 2] = gray
    }

    context.putImageData(imageData, 0, 0)
})

image.src = 'image.jpg'
                        

Ne fonctionne pas en local !

Canvas cheat sheet (plus visuel)

Animer

Pour animer un canvas, on l'efface complètement et on le redessine à chaque frame (jusqu'à 60 fois par seconde)

Il nous faut donc un fonction qui se déclenche à chaque frame : requestAnimationFrame

Request Animation Frame


const loop = () =>
{
    window.requestAnimationFrame(loop)
    console.log('loop')
}
loop()
                        

La fonction loop sera appelée dès la prochaine frame

Plus l'ordinateur est performant, plus la fréquence sera rapide avec une limite à 60 fps

Exemple : Balle rebondissante


// Coordonnées de base
const ball = { x: 200, y: 200 }

// Fonction déclenchée à chaque frame
const loop = () =>
{
    window.requestAnimationFrame(loop)

    // Mise à jour des coordonnées
    ball.x += 4
    ball.y = 200 - Math.abs(Math.sin(Date.now() * 0.005)) * 100

    // Limite
    if(ball.x > canvas.width + 50)
    {
        ball.x = -50
    }

    // Efface le canvas
    context.globalAlpha = 0.2
    context.fillStyle = '#fff'
    context.fillRect(0, 0, $canvas.width, $canvas.height)

    // Dessine la balle
    context.beginPath()
    context.arc(ball.x, ball.y, 50, 0, Math.PI * 2)
    context.globalAlpha = 1
    context.fillStyle = 'orange'
    context.fill()
}

loop()
                        

Exemple : Balle rebondissante

Exemple : Balle qui suit la souris


// Coordonnées de la souris
const mouse = { x: 0, y: 0 }

window.addEventListener('mousemove', (event) =>
{
    mouse.x = event.clientX
    mouse.y = event.clientY
})

// Coordonnées de la la balle
const ball = { x: 0, y: 0 }

// Fonction déclenchée à chaque frame
const loop = () =>
{
    window.requestAnimationFrame(loop)

    // Met à jour les coordonnées de la balle en appliquant un easing
    ball.x += (mouse.x - ball.x) * 0.1
    ball.y += (mouse.y - ball.y) * 0.1

    // Efface le canvas
    context.globalAlpha = 0.2
    context.fillStyle = '#fff'
    context.fillRect(0, 0, $canvas.width, $canvas.height)

    // Dessine la balle
    context.beginPath()
    context.arc(ball.x, ball.y, 50, 0, Math.PI * 2)
    context.globalAlpha = 1
    context.fillStyle = 'orange'
    context.fill()
}

loop()
                        

Exemple : Balle qui suit la souris

Aller plus loin

Mes anciens élèves :