9am / 9am.github.io

9am Blog 🕘 ㍡
https://9am.github.io/
MIT License
3 stars 0 forks source link

CMYK Halftone - The Art of Downsampling #14

Open 9am opened 1 year ago

9am commented 1 year ago
Explore the painting art of dots and 4 colors.
halftone-char hits
9am commented 1 year ago

This is the printing method we use for 1.5 centuries. And you still see it in newspapers, magazines, and wrappers. If look closely, there're just layered dots in only 4 colors, it's amazing and I gotta know how it works. So let's code a CMYK Halftone (it's harder than I thought).

halftone_closeup halftone_under_a_microscope

CMYK halftone paint under a microscope

Mono color

The idea is pretty straightforward for mono-color:

  1. Grayscale the origin image.
  2. Divide it into tiles.
  3. Add one dot per tile.
  4. The size of dots is the avg color of the tile.
mono

This is easy to do since we have canvas APIs to get the RGBA data of the image.

  const cells = [];
  const cellSize = 8;
  const [cw, ch] = [cellSize, cellSize];
  const column = Math.ceil(w / cw);
  const row = Math.ceil(h / ch);
  for (let j = 0; j < row; j++) {
    for (let i = 0; i < column; i++) {
        const cell = greyCtx.getImageData(i * cw, j * ch, cw, ch);
        const avg = getAvg(cell.data);
        const size = 1 - avg / 255;
        cells.push(size);
    }
  }

mono-sandbox

Edit mono-color

CMYK

1. Separate colors

We know that the CMYK stands for 4 colors: cyan, magenta, yellow, key(black). If we find a way to separate those colors from the origin image like the greyscale process, then we could use the mono methods to halftone the separated color layer.

The image data we have from canvas is in RGBA color space. The equation to turn it to CMYK is this:

key = 1 - max(r, g, b)
cyan = (1 - r - key) / (1 - key)
magenta = (1 - g - key) / (1 - key)
yellow = (1 - b - key) / (1 - key)

cmyk-sandbox

Edit cymk-color

2. Blend colors

Now we have the CMYK layers, simply stacking them together will not get us there, cuz to simulate the print effect, we need to find a way to blend colors, or the dots on top will cover lower ones. We'll lose a lot of details of that.

blend

There are 2 ways to do this in the browser.

  1. mix-blend-mode if we render in HTML elements.
  2. globalCompositeOperation if we render in canvas.

We'll try canvas first:

outCtx.globalCompositeOperation = 'multiply'

blend-sandbox

Edit blend-color

3. Angle color layers

It's not done because the tiles for each color layer are the same size, so if we look closely, the dots in each color are stacked together, and not putting their max effort into the final results. The way to resolve this is to angle different layers.

why-angle

dots overlayed

halftone-angles

Typical CMYK halftone screen angles

To be honest, this cost me some time to figure out (partly because of my poor geometry knowledge). First, I thought of a 2d matrix converting for each pixelData(i, j), but if go this way, the origin points need to be pre-calculated, and I still want to take advantage of the getImageData which only works in (x, y, width, height) way.

At last, I took a way less complex for me. Rotate the origin image before dividing tiles, and draw it into a larger hidden canvas, after that, everything else goes as usual. Then rotate and translate the output canvas to fit the hidden canvas and draw it.

angles

Pros:

  • The codes are less complex to understand.
  • No need to implement a customized version of getImageData.

Cons:

  • Calculation waste for the space outside the origin image size.
    // calculate the viewport size after rotation
    const cos = Math.cos(this.angle);
    const sin = Math.sin(this.angle);
    const [vw, vh] = [Math.ceil(w * cos + h * sin), Math.ceil(w * sin + h * cos)];
    ...
    ...
    // rotate and translate the origin before drawing
    ctx.translate(h * sin, 0);
    ctx.rotate(this.angle);
    ctx.drawImage(source, 0, 0, w, h);
    ctx.resetTransform();
angle-after

not overlayed

angle-sandbox

after angled

final

with smaller cell size, we'll get a vivid picture

Edit angle-color

Closing thoughts

After the experiments, I made a custom element <img-halftone>. It's not a perfect implementation. I have to re-sample the image to limit calculation, and still couldn't find a way to solve the Moiré pattern problem. And due to the resolution of our display screen, it'll never be as sophisticated as the real print. But I really enjoy the coding! Give it a try.

Well, hope you enjoy it. I'll see you next time.

img-tissue

<img-halftone>

A web component turns <img> into halftone.🥑

GitHub npm npm npm bundle size

Features

Demo

Description Live demo
Default setting with a zoom-in-out animation codepen
Render with different varient codepen
Render with different cellsize codepen
Render with different shape codepen
Concurrent processing codepen

@9am 🕘