01The idea

The print shop's illusion, made interactive

Halftoning is the print industry's oldest rendering method: solid ink dots, sized by tone, that your eye blends back into a photograph. Every newspaper photo, comic panel, and screen-printed poster works this way. The math is simple, the results are beautiful, and it normally lives inside prepress software nobody opens for fun.

Halftones takes that algorithm and exposes every knob. Drop a photo in and it redraws as a live amplitude-modulated dot screen: drag the dot size, spacing, or screen angle and the whole image re-renders in place. It isn't a filter you apply once. It's a print process you get to hold.

Images arrive by drag-and-drop, file browse, or a straight paste from the clipboard, in JPEG, PNG, WebP, or GIF. Adjust, then export the result as a PNG at the source image's full resolution.

Specimen · amplitude modulation

Fixed grid, variable dot. Dark tone draws big dots, light tone draws small ones, and below r = 0.3 the paper stays paper.

  • Circle
  • Square
  • Diamond
  • Ellipse
  • Line Screen
  • Cross
  • Triangle
  • Hexagon
  • Star
  • Heart
  • Square
  • Hexagonal · rows offset a half step
  • Radial · rings out from center

02The pipeline

From dropped file to dot screen in five stations

One exported function, two canvases: a scratch canvas for pixel work and a visible one for ink. Here's the traverse.

  1. An image, however you hand it over

    Drag it in, browse for it, or paste it straight from the clipboard. JPEG, PNG, WebP, and GIF are accepted, and an EXIF pass corrects orientation before anything renders, so phone photos land right-side up.

  2. Adjustments on a scratch canvas

    The source draws to an offscreen canvas where blur runs through the browser's native ctx.filter (a GPU Gaussian, no JS convolution), then a single in-place pixel loop applies brightness, contrast, and invert.

  3. Sample every cell

    For each grid cell, the engine averages luminance over a spacing-by-spacing pixel window using ITU-R BT.709 weights. Dot radius is (1 - luminance) * dotSize, and anything under 0.3 px is skipped entirely.

  4. Draw in rotated space

    The screen angle rotates the whole grid. The engine walks it across the image's diagonal extent so corners stay covered, maps each point back to source pixels, culls anything off-image, and draws in the rotated frame so shapes lean with their screen.

  5. Export at native size

    The canvas holds the image's full native resolution, not the preview size. Export PNG hands it off through canvas.toBlob and a local object URL. There's no round trip because there is nowhere to round-trip to.

Field formula · the halftone in four lines
// per grid cell, simplified
lum    = sampleRegionLuminance(cell)  // BT.709
radius = (1 - lum) * dotSize
if (radius < 0.3) skip                // paper stays paper
drawDot(shape, cell, radius)

The real file, halftoneEngine.js, runs 388 lines: grid geometry, screen rotation, channel passes, and 10 dot shapes. This is its heartbeat.

Feedback loopEvery control change schedules a re-render, debounced at 200 ms, behind an aria-live processing overlay. Sliders feel live, and the engine never queues a render per pixel of thumb travel.

03Channel separation

Four color modes, four kinds of ink math

Grayscale is the baseline. The other three split the image into screens, and screens at angles are where halftones start looking like print.

  • Mode 01 · Grayscale

    One screen, one ink

    BT.709 luminance drives dot size on a single screen. This is the newspaper look, and it's where every preset starts making sense.

  • Mode 02 · RGB

    Three screens, true separation

    Each cell samples its mean red, green, and blue. Three passes run at -15, +15, and 0 degrees, and pure red, green, and blue dots blend with multiply over white, so overlaps darken like layered ink.

  • Mode 03 · CMYK

    Four plates at press angles

    Cyan, magenta, yellow, and black plates at the classic +15, +75, +90, and +45, multiply-blended into the rosette that makes comics look like comics. Dot size derives from luminance on all four plates; the limits section has the honest version.

  • Mode 04 · Duotone

    Two inks, one ramp

    Shadow and highlight pickers. Each dot's fill interpolates between the two inks by tone, t = 1 - luminance, and the background takes the highlight ink. Retro posters live here.

  • C · +15° #00e5ff
  • M · +75° #e040fb
  • Y · +90° #ffd600
  • K · +45° #000000
  • Newspaper
  • Magazine
  • Comic Book
  • Pop Art
  • Retro Poster
  • Silkscreen
  • Thermal Print
  • Line Screen

Touch any slider and the active preset highlight clears, because it's no longer that preset.

04Field notes

The parts I'd walk another developer through

Small app, hand-built engine. These are the pieces under the hood that earn their lines.

  • A 50-line EXIF parser, by hand

    imageUtils.js reads the JPEG APP1/TIFF header from the first 64 KB with a DataView, handles both byte orders, finds orientation tag 0x0112, and corrects all 8 orientations with canvas transforms. Phone photos load right-side up, zero libraries.

  • Rotated screens that reach the corners

    The grid walks the image's diagonal extent in rotated space, a forward rotation matrix maps each point back to source pixels, and off-image points get culled. Dots draw in the rotated frame, so a square dot leans with its screen.

  • Shapes weighted by area

    Squares and diamonds size at radius * 1.77 to match a circle's ink coverage, so switching shapes never shifts the image's tone. The star is a 10-vertex path with a 0.4r inner radius; the heart is four cubic Bezier curves.

  • Two runtime dependencies

    React 18 and Bootstrap's CSS, imported once. No canvas library, no image library, no WebGL. Every dot on screen comes from code in the repo: about 2,550 lines of plain JavaScript and modern CSS across the whole app.

05Privacy survey

Your photo never leaves the tab

Every online image converter asks the same quiet favor: upload your photo and trust us. Halftones never asks. There is no server side, no account, and no analytics, so the trust question never comes up.

That claim is cheap to make and easy to fake, so I verified it the blunt way: search the entire source for anything that can open a connection.

Runtime network requests · 0

06Margin of error

Honest limits

A case study that only lists wins is marketing. The margins, marked on the sheet:

  • One export wired

    The UI exports PNG. exportUtils.js already contains JPEG export and copy-to-clipboard, written and tested against the same canvas, but neither is wired to a button yet.

  • CMYK, honestly

    All four plates derive dot size from luminance, not from per-channel CMYK values. It sells the rosette look; it isn't prepress separation math.

  • Main thread only

    Rendering is synchronous Canvas 2D: no Web Workers, no OffscreenCanvas, no WebGL. The 200 ms debounce keeps the controls responsive, but a very large image still renders in one blocking pass.

  • Desktop first

    There isn't a single media query in the stylesheet. The canvas scales down through max-width, but the fixed-sidebar layout is built for a desktop screen.

Stack

  • React 18
  • Plain JavaScript · JSX
  • Vite 5
  • Canvas 2D
  • Bootstrap 5 · CSS only
  • Native CSS nesting

By the numbers

  • 10 dot shapes
  • 4 color modes
  • 8 presets
  • 388 lines in the engine
  • ~2,550 lines in the whole app
  • 0 network requests at runtime

08Next traverse

Need real computation to feel instant?

Halftones is my favorite kind of frontend work: a real algorithm, live controls, and user data that never leaves the machine. If your product needs canvas rendering, image processing, or an interface that answers on every input, I build that for clients too. I'm in the Chicago area and work remote everywhere.