DannyDevs logopart of logo

lab008: Enter the Canvas

Aug 1, 2022


window width: Infinitypx
window height: Infinitypx

Ah yes, the famous Matrix screen saver effect. The full screen effect is done using CSS’s absolute positioning and vueUse’s useWindowSize utility, which provides the reactive width and height of the browser window which we can send in to the <canvas> element to tell it to cover the entire window.

I had a bear of a time troubleshooting how to disable vertical scrolling while in the screen saver effect, but in the end it was vanilla JS that worked well for me: document.body.style = 'overflow-y: hidden' to hide the vertical scrollbar and document.body.style = 'overflow-y: auto' to return the vertical scrollbar back to normal.

UX edge case: If users hit the back button while in “screen saver mode”, they will find that the vertical scroll bars have disappeared and they are unable to navigate vertically. Solution: Use an onUnmounted function along with document.body.style = 'overflow-y: auto' to ensure vertical scroll bar behavior remains normal for the rest of the website. I also cleared my setInterval function and removed the window resize event listener in onUnmounted to prevent memory leaks.

You can check out the Matrix digital rain effect code, which is both clever and concise. There are other implementations out there as well.


The pink rectangle you see above is the background of a <canvas> HTML element. This “canvas” provides you with a context for either 2D or WebGL (WebGL allows 3D). The context holds all sorts of properties that can be used to make anything from animations to interactive experiences and games.

Notice that the code snippet below is wrapped within an onMounted function–a Vue 3 lifecycle hook that fires after the .vue component has been mounted to the DOM. Without this hook, any template refs will error out because there is no DOM element to connect with.

We then utilize the context, ctx, provided by the canvas API to create the looping green square animation as well as the row of blue squares.

// template ref--canvas element must have id="myCanvas"
const myCanvas = ref(null)

// onMounted lifecycle hook
onMounted(() => {
  // after mount, it's safe to work with myCanvas--not before!
  myCanvas.value.style.background = 'violet'

  // get the appropriate context, '2d' or 'webgl'
  const ctx = myCanvas.value.getContext('2d')


  ctx.fillStyle = 'blue'

  // creates the row of blue squares
  for (let i = 1; i <= 10; i++) {
    alpha.value = i * 0.1
    ctx.globalAlpha = alpha.value

    ctx.fillRect(i * 50, 20, 40, 40)
  }

  ctx.fillStyle = 'green'
  ctx.fillRect(100, 100, 100, 100)

  // function to create the green square fading in and out
  const fadeOut = () => {
    // notice the recursive loop -- this is the animation loop, using the built-in function `requestAnimationFrame`
    requestAnimationFrame(fadeOut)
    ctx.clearRect(100, 100, myCanvas.value.width, myCanvas.value.height)
    ctx.globalAlpha = Math.sin(alpha.value)
    ctx.fillRect(100, 100, 100, 100)
    alpha.value += -0.05
  }
  fadeOut()
})
// template ref--canvas element must have id="myCanvas"
const myCanvas = ref(null)

// onMounted lifecycle hook
onMounted(() => {
  // after mount, it's safe to work with myCanvas--not before!
  myCanvas.value.style.background = 'violet'

  // get the appropriate context, '2d' or 'webgl'
  const ctx = myCanvas.value.getContext('2d')


  ctx.fillStyle = 'blue'

  // creates the row of blue squares
  for (let i = 1; i <= 10; i++) {
    alpha.value = i * 0.1
    ctx.globalAlpha = alpha.value

    ctx.fillRect(i * 50, 20, 40, 40)
  }

  ctx.fillStyle = 'green'
  ctx.fillRect(100, 100, 100, 100)

  // function to create the green square fading in and out
  const fadeOut = () => {
    // notice the recursive loop -- this is the animation loop, using the built-in function `requestAnimationFrame`
    requestAnimationFrame(fadeOut)
    ctx.clearRect(100, 100, myCanvas.value.width, myCanvas.value.height)
    ctx.globalAlpha = Math.sin(alpha.value)
    ctx.fillRect(100, 100, 100, 100)
    alpha.value += -0.05
  }
  fadeOut()
})

Tip: when working with 3rd-party libraries that require template refs (connecting to the DOM the “Vue way” as opposed to vanilla JS DOM manipulation), be aware that you’ll have to do the onMounted dance and wrangle any scoping issues that may come up when you’re trying to access something that’s inside onMounted and is therefore not accessible from the template.


Click to animate this div

The element animated above was not a <canvas> element–it is just a plain old <div>. You can use requestAnimationFrame to animate an element’s CSS properties. I find this fascinating–there are multiple approaches to animation on the web explore and evaluate.

Be aware of continously running animation/game loops since they can take up a lot of resources. The code snippet below uses an if check to turn off the animation loop after 2 seconds.

// template ref
const myDiv = ref(null)
// boolean flag
const isDone = ref(false)
// init
let start, previousTimeStamp
let scale = 1

// call this function with requestAnimationFrame
function step(timestamp) {
  if (start === undefined)
    start = timestamp

  const elapsed = timestamp - start

  if (previousTimeStamp !== timestamp) {
    const count = Math.min(0.1 * elapsed, 300)

    scale += 0.005
    scale = Math.min(scale, 2)

    myDiv.value.style.transform = `translateX(${count}px) scale(${scale})`

    if (count === 300)
      isDone.value = true
  }

  if (elapsed < 2000) {
    previousTimeStamp = timestamp
    if (!isDone.value)
      window.requestAnimationFrame(step)
  }
}

const animateDiv = () => window.requestAnimationFrame(step)
// template ref
const myDiv = ref(null)
// boolean flag
const isDone = ref(false)
// init
let start, previousTimeStamp
let scale = 1

// call this function with requestAnimationFrame
function step(timestamp) {
  if (start === undefined)
    start = timestamp

  const elapsed = timestamp - start

  if (previousTimeStamp !== timestamp) {
    const count = Math.min(0.1 * elapsed, 300)

    scale += 0.005
    scale = Math.min(scale, 2)

    myDiv.value.style.transform = `translateX(${count}px) scale(${scale})`

    if (count === 300)
      isDone.value = true
  }

  if (elapsed < 2000) {
    previousTimeStamp = timestamp
    if (!isDone.value)
      window.requestAnimationFrame(step)
  }
}

const animateDiv = () => window.requestAnimationFrame(step)

Between .svg and Lottie animations, the <canvas> 2d and webgl APIs, CSS animation, and requestAnimationFrame, animation on the web is multi-faceted and diverse.

I will be continuing my expedition into canvas 2d, which serves as a foundation on top of which many excellent 3rd-party libraries have been written, including pixi.js (2D WebGL renderer), matter.js (2D physics engine), Phaser (2D game engine), and Three.js (3D library). Until then, DannyDevs out!

Home

Lab

Blog

About

Links