2D animation in canvas

Manipulation de canvas en JavaScript

  • objectifs du cours
strong skills soft skills
canvas try and error
trigonometry embrace serendipity
easing vectors, sinus and cosinus

start with cloning https://github.com/gobelins/minimalist-starter

git clone git@github.com:gobelins/minimalist-starter.git 01_canvas

create a context

const ctx = canvas.getContext('2d')

simple canvas stuff

const canvas = document.createElement('canvas')
canvas.width = 800
canvas.height = 600
const ctx = canvas.getContext('2d')

function gameLoop() {
  ctx.fillStyle = 'green'
  ctx.fillRect(0, 0, 800, 600)
  window.requestAnimationFrame(gameLoop)
}

document.addEventListener('DOMContentLoaded', () => {
  const app = document.getElementById('app')
  app.append(canvas)
  gameLoop()
})

create a path

ctx.strokeStyle = '#000'
ctx.beginPath()
ctx.moveTo(75, 50)
ctx.lineTo(100, 75)
ctx.lineTo(100, 25)
ctx.stroke()
ctx.fill()

mdn: CanvasRenderingContext2D/lineTo

stroke and fill properties

ctx.fillStyle = 'blue'
ctx.strokeStyle = 'orange'
ctx.lineWidth = 10

simple forms

ctx.rect(10, 10, 50, 50)
ctx.arc(100, 35, 25, 0, 2 / 3 * Math.PI, true)
ctx.fill()

mdn: CanvasRenderingContext2D/arc, CanvasRenderingContext2D/rect

context pollution

ctx.lineWidth = 10
ctx.fillStyle = 'blue'
ctx.strokeStyle = 'orange'
ctx.rect(10, 10, 50, 50)
ctx.stroke()
ctx.arc(100, 35, 25, 0, 2 * Math.PI)
ctx.fill()

path pollution

ctx.rect(10, 10, 50, 50)
ctx.arc(100, 35, 25, 0, 2 * Math.PI)
ctx.stroke()

Path2D

var ctx = canvas.getContext('2d')

const rectangle = new Path2D();
rectangle.rect(10, 10, 50, 50);

const circle = new Path2D();
circle.moveTo(125, 35);
circle.arc(100, 35, 25, 0, 2 * Math.PI);

ctx.stroke(rectangle);
ctx.fill(circle);

try yourself 1

draw eath and moon

interact with input

simple drawing app

some trigonometry


don't panic

Math.cos, Math.sin

angles in radians, ranged [0, 2π]

const orbit = (centerx, centery, angle, distance) => {
  const x = centerx + Math.sin(angle) * distance
  const y = centery + Math.cos(angle) * distance
  return { x, y }
}

try yourself 2

moon shall circle around earth

(alt version)

const orbit = ([cx, cy], angle, d) =>
  [cx + Math.sin(angle) * d, cy + Math.cos(angle) * d]

try yourself 3 (optional)

the couple earth moon shall circle around sun

what to do with that?

width:200px width:200px width:200px
width:200px width:200px width:200px width:200px

Math.atan2

what is the angle of a vector?

in radians, ranged [0, 2π]

const trigoangle = ([cx,cy], [mousex, mousey]) =>
  Math.atan2(mousex - cx, mousey - cy)

try yourself 4

rotate the solar system aligned with your mouse

what to do with that?

width:200

Math.sqrt

what is the distance between two points?
(pythagore)

const distance = ([cx, cy], [dx, dy]) =>
  Math.sqrt(
    Math.pow(cx - dx, 2) + Math.pow(cy - dy, 2)
  )

try yourself

Create a mirror of your mouse

what to do with that?

a pool game?

easing

easing is a value between 0 and 1.
0 being the start and 1 the arrival

const position.x = startpos.x - easing(time) * distance

linear easing

the most boring one

const easingLinear = n => n

implementation


const easeLinear = x => x

let i = 0;
const center = { x: 400, y: 300 }

function gameLoop() {

  ctx.fillStyle = 'white'
  ctx.fillRect(0, 0, 800, 600)

  const floor = new Path2D()
  floor.moveTo(0, center.y)
  floor.lineTo(800, center.y)
  ctx.strokeStyle = '#ccc'
  ctx.stroke(floor)

  const time = i/100
  const positionx = center.x - easeLinear(time) * 200
  const dot = new Path2D()
  dot.arc(center.x, positionx, 5, 0, 2 * Math.PI, true)
  ctx.fillStyle = 'red'
  ctx.fill(dot)

  i ++;
  window.requestAnimationFrame(gameLoop)
}

inCubic easing

strong acceleration

const easeInCubic = n => n * n * n

outBounce easing

a ball falling

const easeOutBounce = n => // see on easings.net

try yourself

go on https://easings.net/ and try various easing formulaes

fibo and golden ratio

const goldenAngle = () => {
  // golden ratio formula https://en.wikipedia.org/wiki/Golden_ratio
  const phi = (1 + Math.sqrt(5)) / 2; // 1.618033988749895
  // https://en.wikipedia.org/wiki/Golden_angle
  return Math.PI * 2 / (phi * phi);
};

what do I do with that

it's usage gives a very organic result

like sun flowers when using fermat spiral
https://simosavonen.github.io/seedpattern/

or applied to boids

linked lists

https://en.wikipedia.org/wiki/Linked_list

what do I do with that

https://github.com/fahadhaidari/chain-simulation/blob/master/chain.js

http://paperjs.org/examples/chain/

events loops

don't correlate your code to events,

create a publisher and a consumer

steps

  • make a class
  • draw a grid
  • draw your stuff

paperjs

## soluce import config from './config.js' const canvas = document.createElement('canvas') canvas.width = 800 canvas.height = 600 const ctx = canvas.getContext('2d') let i = 0; function gameLoop() { ctx.fillStyle = 'black' ctx.fillRect(0, 0, 800, 600) const earth = new Path2D() earth.arc(100, 100, 25, 0, 2 * Math.PI, true) ctx.fillStyle = 'blue' ctx.fill(earth) const moon = new Path2D() moon.arc(200, 100, 15, 0, 2 * Math.PI, true) ctx.fillStyle = 'white' ctx.fill(moon) i ++; window.requestAnimationFrame(gameLoop) } document.addEventListener('DOMContentLoaded', () => { const app = document.getElementById('app') app.append(canvas) gameLoop() })

const canvas = document.createElement('canvas') canvas.width = 400 canvas.height = 400 canvas.style.width = '200px' canvas.style.height = '200px' const ctx = canvas.getContext('2d') ctx.lineWidth = 3 ctx.lineCap = 'round' ctx.fillStyle = '#222' ctx.fillRect(0, 0, 400, 400) ctx.strokeStyle = '#ccc' ctx.moveTo(0, 0) const mouse = {x: 0, y:0} function gameLoop() { ctx.lineTo(mouse.x, mouse.y) ctx.stroke() window.requestAnimationFrame(gameLoop) } document.addEventListener('DOMContentLoaded', () => { const app = document.getElementById('app') app.append(canvas) gameLoop() }) canvas.addEventListener('mousemove', (e) => { mouse.x = e.offsetX * 2 mouse.y = e.offsetY * 2 })

https://www.educastream.com/formules-trigonometriques-calcul-angles-3eme

trigo-orbit2.gif import config from './config.js' const canvas = document.createElement('canvas') canvas.width = 800 canvas.height = 600 const ctx = canvas.getContext('2d') let mouse = { x: 0, y: 0 } let i = 2; const orbit = ([cx, cy], angle, distance) => [cx + Math.sin(angle) * distance, cy + Math.cos(angle) * distance] function gameLoop() { ctx.fillStyle = 'white' ctx.fillRect(0, 0, 800, 600) const earth = new Path2D() earth.arc(200, 200, 5, 0, 2 * Math.PI, true) ctx.fillStyle = 'blue' ctx.fill(earth) const moon = new Path2D() const [xmoon, ymoon ] = orbit([200, 200], i / 100, 100) moon.arc(xmoon, ymoon, 5, 0, 2 * Math.PI, true) ctx.fillStyle = 'red' ctx.fill(moon) const orbitalpath = new Path2D() orbitalpath.arc(200, 200, 100, 0, 2 * Math.PI, true) ctx.strokeStyle = '#ccc' ctx.stroke(orbitalpath) const horiz = new Path2D() horiz.moveTo(200, 200) horiz.lineTo(xmoon, 200) ctx.strokeStyle = '#ccc' ctx.stroke(horiz) const vert = new Path2D() vert.moveTo(xmoon, 200) vert.lineTo(xmoon, ymoon) ctx.strokeStyle = '#ccc' ctx.stroke(vert) const diag = new Path2D() diag.moveTo(200, 200) diag.lineTo(xmoon, ymoon) ctx.strokeStyle = '#ccc' ctx.stroke(diag) i ++; window.requestAnimationFrame(gameLoop) } document.addEventListener('DOMContentLoaded', () => { const app = document.getElementById('app') app.append(canvas) gameLoop() })

import config from './config.js' const canvas = document.createElement('canvas') canvas.width = 800 canvas.height = 600 const ctx = canvas.getContext('2d') let mouse = { x: 0, y: 0 } let i = 2; const moonPos = (cx, cy, angle, distance) => { const x = cx + Math.sin(angle / 100) * distance const y = cy + Math.cos(angle / 100) * distance return {x, y} } function gameLoop() { ctx.fillStyle = 'black' ctx.fillRect(0, 0, 800, 600) const earth = new Path2D() earth.arc(100, 100, 25, 0, 2 * Math.PI, true) ctx.fillStyle = 'blue' ctx.fill(earth) const moon = new Path2D() const { x, y} = moonPos(100, 100, i, 100) moon.arc(x, y, 15, 0, 2 * Math.PI, true) ctx.fillStyle = '#e7e8d4' ctx.fill(moon) i ++; window.requestAnimationFrame(gameLoop) } document.addEventListener('DOMContentLoaded', () => { const app = document.getElementById('app') document.addEventListener('mousemove', mouseMove); app.append(canvas) gameLoop() })

import config from './config.js' const canvas = document.createElement('canvas') canvas.width = 800 canvas.height = 600 const ctx = canvas.getContext('2d') let mouse = { x: 0, y: 0 } let i = 2; const orbite = (cx, cy, angle, distance) => { const x = cx + Math.sin(angle / 100) * distance const y = cy + Math.cos(angle / 100) * distance return [x, y] } function gameLoop() { ctx.fillStyle = 'black' ctx.fillRect(0, 0, 800, 600) const sun = new Path2D() sun.arc(300, 300, 30, 0, 2 * Math.PI, true) ctx.fillStyle = 'yellow' ctx.fill(sun) const earth = new Path2D() const [xearth, yearth ] = orbite(300, 300, i, 200) earth.arc(xearth, yearth, 15, 0, 2 * Math.PI, true) ctx.fillStyle = 'blue' ctx.fill(earth) const moon = new Path2D() const [xmoon, ymoon ] = orbite(xearth, yearth, i * 365/29, 30) moon.arc(xmoon, ymoon, 5, 0, 2 * Math.PI, true) ctx.fillStyle = '#e7e8d4' ctx.fill(moon) i ++; window.requestAnimationFrame(gameLoop) } document.addEventListener('DOMContentLoaded', () => { const app = document.getElementById('app') app.append(canvas) gameLoop() })

import config from './config.js' const canvas = document.createElement('canvas') canvas.width = 800 canvas.height = 600 const ctx = canvas.getContext('2d') let mouse = { x: 0, y: 0 } let i = 2; const center = { x: 400, y: 300 } const orbite = ([cx, cy], angle, d) => [cx + Math.sin(angle) * d, cy + Math.cos(angle) * d] // returns the current rotation in radians, ranged [0, 2π] const trigoangle = ([cx,cy], [dx,dy]) => { let rad = Math.atan2(dx - cx, dy - cy); if (rad < 0) { // angle is > Math.PI rad += Math.PI * 2; } return rad; } function gameLoop() { ctx.fillStyle = 'white' ctx.fillRect(0, 0, 800, 600) const horiz = new Path2D() horiz.moveTo(center.x, center.y) const horizx = mouse.x > center.x ? Math.max(mouse.x, center.x + 100) : Math.min(mouse.x, center.x - 100) horiz.lineTo(horizx, center.y) ctx.strokeStyle = '#ccc' ctx.stroke(horiz) const vert = new Path2D() vert.moveTo(center.x, center.y) const verty = mouse.y > center.y ? Math.max(mouse.y, center.y + 100) : Math.min(mouse.y, center.y - 100) vert.lineTo(center.x, verty) ctx.strokeStyle = '#ccc' ctx.stroke(vert) const direct = new Path2D() direct.moveTo(center.x, center.y) direct.lineTo(mouse.x, mouse.y) ctx.strokeStyle = '#ccc' ctx.stroke(direct) const angle = trigoangle([center.x, center.y], [mouse.x, mouse.y]) ctx.fillStyle = '#000' ctx.font = '15px sans-serif' ctx.fillText(`${angle.toFixed(2)} rad`, center.x + 5, center.y + 15) ctx.fillText(`${(angle / Math.PI * 180).toFixed(2)}°`, center.x + 5, center.y + 30) const [xdot, ydot] = orbite([center.x, center.y], angle, 100) const startAngle = Math.PI * ( mouse.x > center.x ? mouse.y > center.y ? 0 : 1.5 : mouse.y > center.y ? 0.5 : 1 ) const endAngle = startAngle + Math.PI * 0.5 const arc = new Path2D() arc.arc(center.x, center.y, 100, startAngle, endAngle, false) ctx.stroke(arc) const dot = new Path2D() dot.arc(xdot, ydot, 5, 0, 2 * Math.PI, true) ctx.fillStyle = 'red' ctx.fill(dot) // const [xaltdot, yaltdot] = orbite([center.x, center.y], angle + Math.PI * 2 / 3, 100) // const altdot = new Path2D() // altdot.arc(xaltdot, yaltdot, 5, 0, 2 * Math.PI, true) // ctx.fillStyle = 'blue' // ctx.fill(altdot) i ++; window.requestAnimationFrame(gameLoop) } const mouseMove = (ev) => { mouse.x = ev.pageX mouse.y = ev.pageY } document.addEventListener('DOMContentLoaded', () => { const app = document.getElementById('app') document.addEventListener('mousemove', mouseMove); app.append(canvas) gameLoop() })

import config from './config.js' const canvas = document.createElement('canvas') canvas.width = 800 canvas.height = 600 const ctx = canvas.getContext('2d') let mouse = { x: 0, y: 0 } let i = 2; const center = { x: 400, y: 300 } const distance = ([cx, cy], [dx, dy]) => Math.sqrt( Math.pow(cx - dx, 2) + Math.pow(cy - dy, 2) ) function gameLoop() { ctx.fillStyle = 'white' ctx.fillRect(0, 0, 800, 600) const horiz = new Path2D() horiz.moveTo(center.x, center.y) horiz.lineTo(mouse.x, center.y) ctx.strokeStyle = '#ccc' ctx.stroke(horiz) const vert = new Path2D() vert.moveTo(mouse.x, center.y) vert.lineTo(mouse.x, mouse.y) ctx.strokeStyle = '#ccc' ctx.stroke(vert) const direct = new Path2D() direct.moveTo(center.x, center.y) direct.lineTo(mouse.x, mouse.y) ctx.strokeStyle = '#ccc' ctx.stroke(direct) ctx.fillStyle = '#000' ctx.font = '15px sans-serif' ctx.fillText(`x:${mouse.x - center.x}px`, center.x + 5, center.y + 15) ctx.fillText(`y:${center.y - mouse.y}px`, mouse.x + 5, mouse.y + 15) ctx.fillStyle = '#ccc' ctx.fillText(Math.pow(mouse.x - center.x, 2), center.x + 5, center.y + 30) ctx.fillText(Math.pow(mouse.y - center.y, 2), mouse.x + 5, mouse.y + 30) const hypotenuse = distance([center.x, center.y], [mouse.x, mouse.y]) ctx.fillStyle = 'purple' ctx.fillText( hypotenuse.toFixed(2), center.x + (mouse.x - center.x) / 2, center.y + (mouse.y - center.y) / 2, ) const dot = new Path2D() dot.arc(mouse.x, mouse.y, 5, 0, 2 * Math.PI, true) ctx.fillStyle = 'red' ctx.fill(dot) const altdot = new Path2D() altdot.arc(center.x, center.y, 5, 0, 2 * Math.PI, true) ctx.fillStyle = 'blue' ctx.fill(altdot) i ++; window.requestAnimationFrame(gameLoop) } const mouseMove = (ev) => { mouse.x = ev.pageX mouse.y = ev.pageY } document.addEventListener('DOMContentLoaded', () => { const app = document.getElementById('app') document.addEventListener('mousemove', mouseMove); app.append(canvas) gameLoop() })

import config from './config.js' const canvas = document.createElement('canvas') canvas.width = 800 canvas.height = 600 const ctx = canvas.getContext('2d') const easeLinear = n => n const easeInSquare = n => n * n const easeInCubic = n => n * n * n const easeInOutSine = n => -(Math.cos(Math.PI * n) - 1) / 2 const easeOutBounce = n => { const n1 = 7.5625; const d1 = 2.75; if (n < 1 / d1) { return n1 * n * n; } else if (n < 2 / d1) { return n1 * (n -= 1.5 / d1) * n + 0.75; } else if (n < 2.5 / d1) { return n1 * (n -= 2.25 / d1) * n + 0.9375; } else { return n1 * (n -= 2.625 / d1) * n + 0.984375; } } const easing = easeOutBounce let i = 0; const center = { x: 400, y: 300 } function gameLoop() { ctx.fillStyle = 'white' ctx.fillRect(0, 0, 800, 600) const floor = new Path2D() floor.moveTo(0, center.y) floor.lineTo(800, center.y) ctx.strokeStyle = '#ccc' ctx.stroke(floor) if (i > 100) i = 0 const distance = 100 const bar = new Path2D() bar.moveTo(center.x, center.y) for (let j = 0; j < i; j++) { bar.lineTo(center.x + j, center.y + easing((j)/100) * distance) } ctx.strokeStyle = '#333' ctx.stroke(bar) const dot = new Path2D() dot.arc(center.x, center.y - easing(i/100) * 200, 5, 0, 2 * Math.PI, true) ctx.fillStyle = 'red' ctx.fill(dot) i ++; window.requestAnimationFrame(gameLoop) } document.addEventListener('DOMContentLoaded', () => { const app = document.getElementById('app') app.append(canvas) gameLoop() })

file:///Users/gaspard/Projects/gravure/spiral.html