Module

Puregrain.Cells

Package
purescript-puregrain
Repository
roman-mkh/puregrain

Dithering with cells: the image is divided into cells of width × height pixels, and each cell may use only one or two colors, chosen for it from a list of candidates. That's how the attribute cells of 8-bit home computers work, such as the ZX Spectrum's 8 × 8 cells with an ink and a paper color (Puregrain.Machine.ZxSpectrum). A CellMode holds the cell size and the candidates; a machine module exports one per screen mode. How it works and which rule suits which picture: docs/machine-screens/.

The image is processed in bands, one row of cells at a time. In each band:

  1. Every cell gets the candidate that fits its original pixels best. A candidate's score is the sum, over the cell's pixels, of each pixel's squared distance (in r, g and b) to the nearest color the candidate can produce: the nearest point on the segment between its two colors, since dithering can mix any share of them, or its one color. The lowest score wins; on a tie, the earlier candidate. A middle gray, for example, fits black and white perfectly, blue and white poorly.
  2. The band's rows are dithered: a CellQuantizer turns each pixel into one of its cell's colors, and the kernel passes the error on, across cell and band borders.

ditherCells works on an image in memory, ditherBands on a lazy stream of rows, with the same result:

-- error diffusion inside the cells
ditherCells floydSteinberg nearestInCell mode image

-- pure ordered dithering inside the cells, with a threshold map m
ditherCells noDiffusion (orderedInCell m) mode image

#CellSize Source

type CellSize = { height :: Int, width :: Int }

The size of a cell in pixels, for CellMode. A width or height below 1 counts as 1.

#CellPalette Source

newtype CellPalette

The colors one cell may use: one color, or two that dithering mixes. Build it with singleColor or colorPair. The order of two colors doesn't change which cells choose them, nor what nearestInCell gives; with orderedInCell it sets the pattern's direction.

#singleColor Source

singleColor :: RGB -> CellPalette

A cell palette of one color.

#colorPair Source

colorPair :: RGB -> RGB -> CellPalette

A cell palette of two colors. The same color twice is a single color.

  • RGB: the first color.
  • RGB: the second color; the order matters only for orderedInCell.

#cellColors Source

cellColors :: CellPalette -> NonEmptyArray RGB

The palette's colors: one, or two in the order they were given.

#CellMode Source

type CellMode c = { candidates :: NonEmptyArray c, palette :: c -> CellPalette, size :: CellSize }

A screen mode for cells: how big a cell is, and which palettes a cell may use. A machine module exports one per screen mode, such as Puregrain.Machine.ZxSpectrum.screen.

  • c: the candidate type, anything that stands for a cell's palette (for the ZX Spectrum, Attribute).
  • size: the cell's width and height in pixels.
  • palette: a candidate's colors.
  • candidates: what a cell may use, in the order they're tried; on a tie the earlier wins.

For cells of your own, write the record; the candidates can be the palettes themselves:

mode :: CellMode CellPalette
mode =
  { size: { width: 4, height: 4 }
  , palette: identity
  , candidates: NEA.cons' (colorPair black white) [ colorPair blue yellow ]
  }

To change one part of a machine's mode, update its record, as in Zx.screen { candidates = fewer }.

#CellQuantizer Source

data CellQuantizer

How a pixel becomes one of its cell's colors. Build it with nearestInCell or orderedInCell.

#nearestInCell Source

nearestInCell :: CellQuantizer

Each pixel becomes the nearer of its cell's colors, by squared RGB distance (the first on a tie). With a kernel, that's error diffusion inside the cells.

#orderedInCell Source

orderedInCell :: ThresholdMap -> CellQuantizer

Ordered dithering inside the cells. A pixel lies some fraction of the way from its cell's first color to the second, measured along the line between them; it becomes the second color where that fraction is above the map's threshold at the pixel's position, else the first. A single-color cell keeps its color.

  • ThresholdMap: the thresholds, from Puregrain.Ordered.bayer or compileThresholdMap. Positions count from the image's corner, so any size works: a map whose side divides the cell's repeats in every cell, a larger one spreads more tone steps over several cells.

With noDiffusion, that's pure ordered dithering. With another kernel it's the hybrid: the difference between a pixel's value and its color is passed on.

#ditherCells Source

ditherCells :: forall c. Kernel -> CellQuantizer -> CellMode c -> Array (Array RGB) -> { cells :: Array (Array c), image :: Array (Array RGB) }

Dithers an image in memory with cells.

  • c: the mode's candidate type (see CellMode).
  • Kernel: where each pixel's error goes; noDiffusion for nowhere.
  • CellQuantizer: how each pixel picks one of its cell's colors.
  • CellMode c: the cell size, and the candidates with their colors.
  • Array (Array RGB): the image, rows top to bottom, all of the same length.

Returns the candidate chosen for each cell, as rows of cells (top to bottom, each left to right), and the dithered image. An image whose size isn't a multiple of the cell size gets partial cells at the right and bottom edges. Stops with an error naming a row whose length differs from the first row's.

#ditherBands Source

ditherBands :: forall c. Kernel -> CellQuantizer -> CellMode c -> List (Array RGB) -> List { cells :: Array c, rows :: Array (Array RGB) }

Dithers a stream of rows with cells, one band (row of cells) at a time.

  • c, Kernel, CellQuantizer and CellMode c: as for ditherCells.
  • LL.List (Array RGB): the image as a lazy list of rows, top to bottom, all of the same length; it may be endless.

Returns a lazy list of bands, top to bottom, each with the candidates chosen for its cells (left to right) and its dithered rows: the same result as ditherCells. A band's rows are read and dithered only when the band is first needed, so only one band is in memory. When a band holds a row whose length differs from the first row's, ditherBands stops there with an error naming it; the bands before it have come out.