RCHowell / rchowell.github.io

https://rchowell.github.io
0 stars 0 forks source link

Moiré Patterns in CMYK Halftone Printing #8

Open RCHowell opened 4 years ago

RCHowell commented 4 years ago

What is a moiré pattern?

Moiré patterns are large-scale interference patterns that can be produced when an opaque ruled pattern with transparent gaps is overlaid on another similar pattern. For the moiré interference pattern to appear, the two patterns must not be completely identical, but rather e.g. displaced, rotated or have slightly different pitch. Wiki

What is CMYK Printing?

The CMYK color model is a subtractive color model, based on the CMY color model, used in color printing ... With CMYK printing, halftoning (also called screening) allows for less than full saturation of the primary colors; tiny dots of each primary color are printed in a pattern small enough that humans perceive a solid color ... To improve print quality and reduce moiré patterns, the screen for each color is set at a different angle. Wiki

How are they related? A CMYK print is made in four layers. Each layer is halftoned with dots. For a given color, the image is filtered and groups of pixels are mapped to a dot with a radius which is representative of the group's intensity. Each dot's center is place on regular grid intervals. It's now obvious why moiré patterns are related. Each layer is an opaque ruled pattern with transparent gaps overlaid on other similar patterns. To reduce the adverse effects of these patterns such as color distortion and ink build-up, each layer is set at a different angle.

Variations of Patterns Here's a visualization of the patterns that can form as you adjust screen angles. You may want to zoom 😄

package main

import (
    "flag"
    "image"
    "image/color"
    "image/gif"
    "math"
    "os"
)

const (
    rate  = 8
    size  = 128
    delay = 10
)

var cmyk = []color.Color{
    color.White, // background
    color.CMYK{C: math.MaxUint8},
    color.CMYK{M: math.MaxUint8},
    color.CMYK{Y: math.MaxUint8},
    color.CMYK{K: math.MaxUint8},
}

var frames = rate * 360

// https://i.imgur.com/k86QK.png
var outerSquareSize = int(math.Ceil(size * 2 / math.Sqrt2))
var radius = size / 2
var center = (outerSquareSize / 2) + 1

func main() {
    gaps := flag.Int("gaps", 4, "gap size between pixels")
    flag.Parse()

    anim := gif.GIF{LoopCount: frames}
    for frame := 0; frame < frames; frame++ {
        anim.Delay = append(anim.Delay, delay)
        anim.Image = append(anim.Image, generateImage(frame, *gaps))
    }
    gif.EncodeAll(os.Stdout, &anim)
}

func generateImage(frame, gaps int) *image.Paletted {
    bounds := image.Rect(0, 0, outerSquareSize, outerSquareSize)
    img := image.NewPaletted(bounds, cmyk)
    for c := 1; c <= 4; c++ {
        a := float64(c*frame) * math.Pi / 180 / rate // deg to rad
        for x := -radius; x < radius; x += gaps {
            fx := float64(x)
            for y := -radius; y < radius; y += gaps {
                fy := float64(y)
                rx := int(fx*math.Cos(a) - fy*math.Sin(a))
                ry := int(fx*math.Sin(a) + fy*math.Cos(a))
                img.SetColorIndex(rx+center, ry+center, uint8(c))
            }
        }
    }
    return img
}