Optical illusion with a circle of circles

  • Experiment
  • Vue.js

Hover (click on mobile) the graph to see each circle’s path.

A optical illusion occurs when a series of circles moving in straight lines. After seeing a YouTube video with the same illusion, I wanted to challenge myself to make it in d3.js.

In the code below you’ll see that I’m using d3’s easeSinInOut method to achieve this animation.

import { select, easeSinInOut } from 'd3'

const size = 750
const count = 16
const duration = 3000
const radius = 22
const padding = radius

const data = [...Array(count).keys()]

export default class Graph {
  constructor(svg) {
    this.svg = select(svg).attr('viewBox', [-padding, -padding, size + padding * 2, size + padding * 2].join(' '))
    this.render()
  }

  render() {
    let g = this.svg
      .selectAll('g')
      .data(data)
      .join((enter) => {
        let g = enter.append('g')
        g.append('rect')
        g.append('circle')
        return g
      })

    this.svg
      .on('mouseenter', () => {
        g.select('circle').attr('opacity', 0.25)
        g.select('rect').attr('opacity', 0.1)
        const group = g.filter((d, i) => i === 0 || i === count / 2)

        group.select('circle').attr('opacity', 1)
        group.select('rect').attr('opacity', 1)
      })
      .on('mouseleave', () => {
        g.select('circle').attr('opacity', 1)
        g.select('rect').attr('opacity', 0)
      })

    g.attr('transform', (d) => {
      const degrees = ((d / count) * Math.PI * 180) / Math.PI
      return `translate(${size / 2}, ${size / 2}) rotate(${degrees})`
    })

    g.select('rect')
      .attr('fill', 'white')
      .attr('height', 2)
      .attr('width', size)
      .attr('opacity', 0)
      .attr('transform', `translate(${-size / 2}, 0)`)

    const circle = g
      .select('circle')
      .attr('r', radius)
      .attr('fill', '#FFD650')
      .attr('cx', -size / 2)
      .attr('transform', `translate(0,0)`)

    circle.each(function (d, i) {
      repeat(select(this), i, true)
    })

    function repeat(el, i, first) {
      el.transition()
        .ease(easeSinInOut)
        .duration(duration)
        .attr('transform', `translate(${size},0)`)
        .delay(() => (first ? i * (duration / count) : 0))
        .transition()
        .ease(easeSinInOut)
        .duration(duration)
        .attr('transform', `translate(0,0)`)
        .on('end', () => {
          repeat(el, i, false)
        })
    }
  }
}