Create “Troxler's Fading” with plain canvas api

Artjom Löbsack
Feb 2021

Abstract

Playing with canvas and some optical illusions.

~ 6 min
#canvas, #js, #optical-illusion
cover
Effect in action

About

If you like optical illusions1 as much as I do, then you also once thought about creating your own.

Although I will not create something new, I will implement a generator of a popular optical illusion with my own hands.

Today we will talk about the so-called “The Troxler's Effect”2.

Principle of operation

“What is this color smudge that I have to see here?” - ask the impatient. Do not rush, now I will explain everything.

“The Troxler's Effect” or “The Troxler's Phenomenon”, as explained, is the cessation of the eye to perceive a visible stimulus that occupies a fixed position on the retina of the eye.

And now in simple words: the fact is that when our eyes are fixed and motionless for a while, the brain stops responding to some visible objects, thereby making it possible to hide these objects from visibility at all.

Try it yourself

Open the picture attached at the beginning of the article, fix your eyes on the cross in the center and look at it motionlessly for 10-20 seconds.

As a result, you should notice how the colored spots gradually dissolve into a solid gray background.

Done! It was as if there were no spots at all.

Implementation

The illusion inspired me so much, and I wanted even more of these images, so I thought: “Why don't I write my own generator for them…?”.

Looking ahead, I will say: I still wrote this generator, I also added a few settings there for more fine-tuning, all the code can be found here.

Tools

So, where to start? As is customary for rendering any “programmable” graphics on the Web, a <canvas>3 element is used, so you don't have to choose…

Also, I would like not to use any libraries at all (in the spirit of real asceticism), so we are armed with only pure ES6+.

Template

To begin with, we need to render the image somewhere, create html markup:

<!-- index.html -->
<html>
  <body>
    <canvas width='300' height='300'></canvas>
  </body>
</html>

Script

Let's create a function that will draw on our canvas.

// index.js
function draw (canvas) {
  // …draw
}

Here we accept canvas element in canvas argument.

All further pieces of code should be considered as parts of the draw function. (To simplify).

The basic principle is to draw a lot of rectangles of random color - in a random place, and then blur all the image to get a blending.

  • Size and place

    Let's agree that the number of rectangles to draw, as well as their size, will depend on the size of the canvas, let's say it will be a large side divided by four:

    const { width: w, height: h } = canvas
    const side = Math.floor(Math.max(w, h) / 4)
    
  • Color

    To determine the color, I will write a helper function rndColor:

    const rndColor = (alpha = 1) =>
      `rgba(${rndByte()}, ${rndByte()}, ${rndByte()}, ${alpha})`
    

    As we know4, colors are encoded with three values from zero to two hundred and fifty-five (three bytes) (the red component, the green component, and the blue component), and as you can see, our function also takes color transparency values (the alpha component), it can be from zero to one.

    The question arises - what is hidden behind the other helper function rndByte?

    const rndTo = to => Math.floor(Math.random() * to)
    const rndByte = () => rndTo(255)
    

    The roundto(to) function returns a random value from 0 to the to argument.

    (We'll need it soon. Many have already guessed why…)

Let's at least draw5 something!

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

// random rectangles
for (let i = side; i--;) {
  ctx.fillStyle = rndColor()
  ctx.fillRect(rndTo(w), rndTo(h), side, side)
}

This loop draws a side-by-side rectangle of random color, at a random location on x (w) and at a random location on y (h), but no further than our canvas, side times.

This is where we also needed the rndTo function.

Strange rectangles on canvas (?)
Strange rectangles on canvas (?)

Pretty simple!

  • Blend

    Wait… Now it just looks like a bunch of rectangles scattered on the canvas, how do we achieve the blur effect?

    To do this, apply the blur filter6 to the canvas context:

    // blur with canvas
    ctx.filter = 'blur(1.5rem)'
    

    Filtering directly on the canvas context can sometimes affect performance, so alternatively you can blur the canvas itself using css:

    // blur with css
    canvas.style.filter = 'blur(1.5rem)'
    

    To apply this effect on our rectangles you must put this code before drawing loop.

    Awesome blending
    Awesome blending
  • Marker

    To make it easier to focus the center of the image, let's add a marker:

    First remove our previous blur filter:

    ctx.filter = 'none'
    

    (Otherwise, we won't see our marker.)

    // draw marker
    const markerSide = Math.round(Math.max(w, h) / 100)
    const markerSideHalf = Math.round(markerSide / 2)
    const markerX = Math.round(w / 2 - markerSideHalf)
    const markerY = Math.round(h / 2 - markerSideHalf)
    ctx.fillStyle = '#000'
    ctx.fillRect(markerX, markerY, markerSide, markerSide)
    
    With marker
    With marker
  • Background

    As you can see, for now our image is pretty transparent, this is because of blur.

    To fix this, we need to fill our canvas before drawing:

    // fill bg
    ctx.fillStyle = '#fff'
    ctx.fillRect(0, 0, w, h)
    
    With background
    With background

In the end, we have this code:

// helpers
const rndTo = to => Math.floor(Math.random() * to)
const rndByte = () => rndTo(255)
const rndColor = (alpha = 1) =>
  `rgba(${rndByte()}, ${rndByte()}, ${rndByte()}, ${alpha})`

function draw (canvas) {
  const { width: w, height: h } = canvas
  const side = Math.floor(Math.max(w, h) / 4)
  const ctx = canvas.getContext('2d')

  // fill bg
  ctx.fillStyle = '#fff'
  ctx.fillRect(0, 0, w, h)

  // for blending
  // blur with canvas
  ctx.filter = 'blur(1.5rem)'
  /*
  // blur with css
  canvas.style.filter = 'blur(1.5rem)'
  */

  // random rectangles
  for (let i = side; i--;) {
    ctx.fillStyle = rndColor()
    ctx.fillRect(rndTo(w), rndTo(h), side, side)
  }

  // reset filter
  ctx.filter = 'none'

  // draw marker
  const markerSide = Math.round(Math.max(w, h) / 100)
  const markerSideHalf = Math.round(markerSide / 2)
  const markerX = Math.round(w / 2 - markerSideHalf)
  const markerY = Math.round(h / 2 - markerSideHalf)
  ctx.fillStyle = '#000'
  ctx.fillRect(markerX, markerY, markerSide, markerSide)
}

Conclusion

So, we wrote a “Troxler's Effect” generator.

I hope it was easy, you liked it and you learned something new.

Also in addition to this, I wrote an npm module7 where you can use and configure this function.

You can see it in action on my homepage. Just reload the page couple of times.

All the code can be found here8 or here9.

See you again :)

Article reference.

NotesDemosCodeGitHubEmailRSS

ceigh.com