Creating the Morphing Effect of the Luma Dream Machine Website

Learn how to create this powerful morphing effect with only JavaScript using the Canvas2D API.

Today I’m going to share some technical insights about the morphing effect I did for the Luma Dream Machine website.

I’ll show you how this effect boils down to simple principles: loading a sequence of images, mapping them to transitions, and rendering them dynamically on a canvas. Let’s see step by step how it works.

I’m going to explain the core concept behind this effect, feel free to use it however you like.

First thing, the effect is achieved by playing a sequence of images according to an index, to generate this images I’m using the Dream machine App. The effect you saw in the website have 5 states, each state corresponds to a persona.

I have a total of 5 image sequences : 1-2, 2-3, 3-4, 4-5 and 5-1, each sequence is a batch of 24 images:

The images are painted in a canvas element using the Canvas2D API.

First things first: I start by loading all the images of the 5 sequences, (5×24 = 120 images in total) and store them in an array:

import { EventEmitter } from "events"

export const imagesSequenceEmitter = new EventEmitter()

let loadedImages:HTMLImageElement[] = []

export const loadSequenceImages = () => {
    const tr1_2 = []
    for (let i = 0; i <= 23; i++) {
      const fileName = `/morphing/1-2/1-2${i
        .toString()
        .padStart(2, "0")}.jpg`
      tr1_2.push(fileName)
    }
    const tr2_3 = []
    for (let i = 0; i <= 23; i++) {
      const fileName = `/morphing/2-3/2-3${i
        .toString()
        .padStart(2, "0")}.jpg`
      tr2_3.push(fileName)
    }
    const tr3_4 = []
    for (let i = 0; i <= 23; i++) {
      const fileName = `/morphing/3-4/3-4${i
        .toString()
        .padStart(2, "0")}.jpg`
      tr3_4.push(fileName)
    }
    const tr4_5 = []
    for (let i = 0; i <= 23; i++) {
      const fileName = `/morphing/4-5/4-5${i
        .toString()
        .padStart(2, "0")}.jpg`
      tr4_5.push(fileName)
    }
  
    const tr5_1 = []
    for (let i = 0; i <= 23; i++) {
      const fileName = `/morphing/5-1/5-1${i
        .toString()
        .padStart(2, "0")}.jpg`
      tr5_1.push(fileName)
    }
  
    const images = [...tr1_2, ...tr2_3, ...tr3_4, ...tr4_5, ...tr5_1]
  
    const imagePromises = images.map((src) => {
      return new Promise<HTMLImageElement>((resolve) => {
        const img = new Image()
        img.src = src
        img.onload = () => resolve(img)
      })
    })
  
    Promise.all(imagePromises).then((imagesLoaded) => {
      loadedImages = [...(imagesLoaded as HTMLImageElement[])]
      imagesSequenceEmitter.emit("sequence-loaded")
    })
  }

Now I have an array of 120 images containing the sequences in their logical order. The morphing effect is achieved by iterating through the array.

To achieve this, the switcher element has 5 buttons, with each button triggering a transition to a new state (new persona). I use a variable called progress that takes floating values between 1 and 6 (resetting to 1 as soon as it reaches 6). This progress value is then converted to an index between 0 and 119.

I initialize the canvas and run a function that draws an image from the array based on the value of progress. The progress (a floating-point number) is converted to an index (an integer) using the normalize function, which interpolates the range of float values (1–6) to integer values (0–119).

let progress = 1

export const normalize = (value: number, min: number, max: number) => {
  return Math.max(0, Math.min(1, (value - min) / (max - min)))
}

const canvas = document.querySelector('#personas-canvas') as HTMLCanvasElement

canvas.width = 720
canvas.height = 720
const ctx = canvas.getContext('2d')

imagesSequenceEmitter.on('sequence-loaded', () => {
  requestAnimationFrame(render)
})

let currentIndex = -1

function render() {
  let index = Math.round(normalize(progress, 1, 6) * (loadedImages.length - 1))

  if (index !== currentIndex) {
    currentIndex = index
    if (!ctx || !canvas) return

    ctx.drawImage(
      loadedImages[index] as HTMLImageElement,
      0,
      0,
      canvas.width,
      canvas.height
    )
  }

  requestAnimationFrame(render)
}

And that’s it! Thanks for reading!

Jean Mazouni

Creative developer - Freelancer - Threejs enjoyer

Stay in the loop: Get your dose of frontend twice a week

Fresh news, inspo, code demos, and UI animations—zero fluff, all quality. Make your Mondays and Thursdays creative!