A shimmering circle

The inspiration

This animation was first inspired by the circular symmetrical pattern of fruit arranged on pies, but slowly turned into something a bit more esoterically. I realised I wanted to create something that was whimsical and playful, like so I started to play with randomisation as a way to add some unexpectedness into the animation.

How it was created

The circle is made out of a series of SVG paths, each of which is a segment of the circle. Each segment is a path that is a segment of the circle. The segments are animated using CSS keyframes.

The CSS keyframes are used to animate the opacity of the segments. The opacity is animated from 0 to 1 and back to 0, with a delay and duration that is randomised for each segment.

Each segment’s position is calculated using some custom trigonometry maths. Each segment is a slice of the circle, and the position of the slice is calculated based on the angle of the slice. This position is then used to calculate the path dimensions for the SVG element using D3.js.

If you are interested in how it is made in detail, you could have a look at an older version of the source code on github. The newer version is a React component integrated into the website itself.

import * as d3 from "d3"
import type { CSSProperties } from "react"
import { useMemo } from "react"
import "./shimmering-circle.css"

type ArcDatum = {
  startAngle: number
  endAngle: number
}

/**
 * ShimmeringCircle
 *
 * - React renders the SVG structure and the <path> elements.
 * - D3 generates arc geometry (d3.arc) while CSS keyframes drive
 *   shimmering fill transitions via declarative classes.
 */
const ShimmeringCircle = ({
  radius = 200,
  segmentCount = 120,
  maxActiveSegmentsAtOnce = 20,
  timing = 3000,
  cycleTime = 1500,
  glowColour = "--shimmering-circle-glow-red",
}) => {
  // Arc generator (d3 v7: d3.arc, not d3.svg.arc)
  const arcGen = useMemo(
    () =>
      d3
        .arc<ArcDatum>()
        .innerRadius(radius - 0.2 * radius)
        .outerRadius(radius),
    [radius],
  )

  // Static arc data — angles for each segment.
  const arcs = useMemo(() => {
    const circleInRadians = 2 * Math.PI
    return d3.range(0, segmentCount, 1).map(i => ({
      startAngle: (i / segmentCount) * circleInRadians,
      endAngle: ((i + 1) / segmentCount) * circleInRadians,
    }))
  }, [segmentCount])

  const edge = (radius + 0.1 * radius) * 2
  const half = edge / 2

  const shimmerConfig = useMemo(() => {
    const lightUpChance = maxActiveSegmentsAtOnce / Math.max(1, segmentCount)
    return arcs.map(() => {
      const isAnimated = Math.random() < lightUpChance
      const delayMs = Math.random() * Math.max(1, cycleTime)
      const durationMs = timing * (0.75 + Math.random() * 0.5)
      return { isAnimated, delayMs, durationMs }
    })
  }, [arcs, maxActiveSegmentsAtOnce, segmentCount, cycleTime, timing])

  return (
    <svg
      className="shimmering-circle"
      width={edge}
      height={edge}
      viewBox={`${-half} ${-half} ${edge} ${edge}`}
    >
      {arcs.map((arc, i) => {
        const arcAnimation = shimmerConfig[i]
        const arcStyle = {
          "--arc-glow-colour": `var(${glowColour})`,
          "--shimmer-delay": `${arcAnimation.delayMs}ms`,
          "--shimmer-duration": `${arcAnimation.durationMs}ms`,
        } as CSSProperties

        return (
          <path
            key={i}
            className={
              arcAnimation.isAnimated
                ? "shimmering-circle__arc shimmering-circle__arc--animated"
                : "shimmering-circle__arc"
            }
            data-arc-index={i}
            data-animated={arcAnimation.isAnimated ? "true" : "false"}
            d={arcGen(arc) ?? undefined}
            style={arcStyle}
          />
        )
      })}
    </svg>
  )
}

export default ShimmeringCircle