RonkLogan

Creating a Moiré Pattern with SVG

Mr. Spock at his science station
Mr. Spock at his science station on the bridge of the USS Enterprise

Have you ever noticed that groovy growing/shrinking pattern on the console of Mr. Spock’s science station on the bridge of the USS Enterprise (and on the communicators, if you look close enough) and wondered, “What is that, and how can I make one?” It’s a cool interference effect called a Moiré pattern! Let’s walk through how to create one using SVG (Scalable Vector Graphics) step-by-step.

Moiré Pattern

Note: if you are using an older browser that doesn’t support inline SVG elements, the images on this page probably won’t render properly and you may experience a little difficulty visualizing the examples. I would recommend a recent version of Google Chrome for best results.

Building a Moiré

The base image for this interference effect is pretty simple: it’s just a series of lines radiating from a central point:

The basic building block of a Moiré pattern

The trick here is that the Moiré is two of these images stacked on top of each other, one offset a little from the other, and one of them rotating. TOS set designers had these patterns printed on a couple transparency sheets with a light behind them, a diffuser in front, and a cheap motor slowly turning the bottom one. Low-tech, but it got the job done.

I wanted to recreate the effect digitally, so I decided to render and animate it all with SVG and CSS. I used a <circle> element if the appropriate radius, with a dashed stroke twice the radius thick. Using a dashed stroke on the circle like this causes the radiating lines to draw way outside the radius of the circle, but we’ll be adding a clip region later on, so all the stuff outside the circle doesn’t really matter. When using this method, however, I noticed that the dash array for the stroke is measured along the circumference of the circle, so in order for the lines to match up seamlessly at the beginning and end of the pattern, I had to calculate the radius such that the circumference is as close to an even integer number as possible. Remember that the circumference of a circle, c, is equal to twice the radius, r, times π. Let’s say I want the above image to be used to generate a 300-pixel diameter circle. A radius of 150 would give a circumference of 942.48 – not good. We therefore picker a slightly larger, but even circumference of 1,000 and calculate the radius for that to be 159.15, and double that for a stroke width of 318.3:

<svg width="300" height="300" viewBox="-150 -150 300 300">
  <circle r="159.15" fill="white" stroke="black" stroke-width="318.3" stroke-dasharray="5 5"/>
</svg>

That makes the spokes line up pretty well. Notice that I’ve set the viewBox attribute on the <svg> element so that the center of the SVG is at the point (0, 0). This will save us later from having to specify the center point for centered items and eliminate the need for some arithmetic when determining offsets from that center.

Now, to create the interference pattern, we need two of these circles. I use a <g> element so I can share the common stroke settings and embed the two <circle> elements inside. The first one is rendered on the bottom, so we use a fill color of white for that one; the second one is rendered on top, so we use a fill of “none” so that the bottom pattern shows through the top:

<g stroke="black" stroke-width="318.3" stroke-dasharray="5 5">
  <circle r="159.15" fill="white"/>
  <circle r="159.15" fill="none"/>
</g>

That doesn’t do anything – the two spoked images are right on top of each other so there’s no interference effect at all. To get that nifty pattern to manifest, you need to offset one of the images a little bit. I find that using a 10-pixel offset on the top image seems to work well:

<g stroke="black" stroke-width="318.3" stroke-dasharray="5 5">
  <circle r="159.15" fill="white"/>
  <circle r="159.15" fill="none" cx="10" cy="10"/>
</g>
Overlaying two of the basic patterns at an offset: (10, 10)

Now we’re cookin’ with gas! The amount of offset we add to one of the circles does two things:

  1. It determines the angle of those static lobes, and
  2. It determines the number of nested arcs making up each lobe.

Here are a few examples:

Overlaying two of the basic patterns at an offset: (1, 1)
Overlaying two of the basic patterns at an offset: (-5, 5)
Overlaying two of the basic patterns at an offset: (0, 10)
Overlaying two of the basic patterns at an offset: (20, 20)

Now to add the animation. We’re going to use CSS animations and hope for some good hardware acceleration to kick in. First, we need to create a couple style rules that define a full-circle rotation of a given duration (we’ll do a full rotation in one minute):

  • A @keyframes directive that says we start with a rotation of 0º and end with a rotation of 360º, and
  • A rule we can apply to one of the patterns (we’ll use an HTML ID) to trigger the animation as well as specify the duration, that it should never stop, and that it should be a nice, steady rotation without any easing in or out (linear):
@keyframes rot {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
#r {
  animation-duration: 60s;
  animation-iteration-count: infinite;
  animation-name: rot;
  animation-timing-function: linear;
}

Because there are two patterns, we can choose to rotate either one for different effect. If we rotate the centered pattern (the bottom one in this example), the lobes will animate but stay at the same overall angle position:

Rotating the bottom centered pattern

But if we rotate the offset pattern (the top one in this example), not only will the lobes animate, but the whole pattern will also slowly rotate around the center of the image. I personally prefer this method because I feel it gives it that little touch of extra motion:

Rotating the top offset pattern

And finally, we’ll clip this bad-boy to a nice, round circle of the desired diameter using a <clipPath> element we define in a <defs> block, applied to the parent <g> element using the clip-path attribute:

Clipped and all together now

The final code:

<svg width="300" height="300" viewBox="-150 -150 300 300">
  <style>
    @keyframes rot {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
    #r {
      animation-duration: 50s;
      animation-iteration-count: infinite;
      animation-name: rot;
      animation-timing-function: linear;
    }
  </style>
  <defs>
    <clipPath id="port">
      <circle r="150"/>
    </clipPath>
  </defs>
  <g clip-path="url(#port)" stroke="black" stroke-width="318.3" stroke-dasharray="5 5">
    <circle r="159.15" fill="white"/>
    <circle r="159.15" fill="none" cx="10" cy="10" id="r"/>
  </g>
</svg>

Exercise for the Reader

If we really wanted to try and match the display seen on TOS more closely, we could try to soften out the spoke lines, leaving just the fuzzy animating arcs. I believe the set designers left a bit of space between the top-most pattern and the panel-level diffuser, which was some sort of frosted glass to blur out the hard edges. I tried to apply a blur filter to the SVG, but that seems to tax the poor browser rendering just a little too much – especially if there wasn’t much hardware acceleration available. Plus, it was just a little too hard to get just the right amount of blur to provide the desired effect. If you’d like to try, be my guest.

Notes on Browsers

First a little note on mobile iOS Safari and inline SVGs. The clipping mask for this animation is applied on the <g> element using a clip-path attribute containing a value that is a URL path to the ID of the clip geometry to apply – typically just a hash-reference to the ID of the <clipPath> element in the SVG’s <defs> block. Most browsers don’t seem to have the slightest problem with the URL just being that hash: clip-path="url(#port)". Mobile Safari however…. If the webpage uses a <base> element in the head of the document (as this page does), the URL value of the clip geometry needs to be relative to the href of the <base> element and can’t just be “#port.” So, if you look at the source code of this page, rather than the code in the above listing, you’ll see that I needed to have a path relative from the base to get it to work on my iPhone. Go figure.

Because this effect is an interference pattern, every little pixel is significant. How good the animation looks for you will depend entirely upon the scaling of the animation, the resolution of your monitor, the rendering engine of your browser, whether any GPU hardware acceleration us applied to the animation, etc. Even the tiniest rendering artifact or ill-placed merging of pixels in those spokes makes a huge difference in the resulting visual. Best results seem to be with Chrome on a Retina display; Firefox and Safari both tend to generate some oddly misshapen lobes; Edge, well… Edge looks like a sort of nightmare fuzzy cyber-tarantula or something. Internet Explorer looks like it has the same rendering issues as Edge, but on top of that won’t even animate (probably requires some sort of CSS-animation polyfill or something). Good luck!