Skip to content
downsample

downsample

Mojo function 🡭

downsample

fn def downsample(source: Canvas, factor: Int) -> Canvas

Shrink source by factor, which must evenly divide both source.width and source.height – raises rather than truncating a partial edge block away. factor=1 is a valid no-op copy, so a caller sweeping factor across several values needn’t branch around 1.

Alpha is the rounded mean of the source block. Color is averaged with alpha weights and returned as straight RGBA, so transparent samples do not darken a shape’s edge or contribute hidden color. A block whose alpha rounds to zero becomes transparent black. Factor 1 copies every byte, including color under zero alpha.

Supersampling with it: pixel (px, py) is centered at (px, py), and output pixel p averages source pixels factor * p through factor * p + factor - 1, whose center is factor * p + (factor - 1) / 2. A drawing scaled by factor alone therefore shows up (factor - 1) / (2 * factor) px early after the shrink – 3/8 px at 4 – for every primitive. Translate by (factor - 1) / 2 before the scale and it lands where a scale-1 drawing does:

big.translate((f - 1) / 2, (f - 1) / 2)
big.scale(f, f)
... draw ...
var out = downsample(big, f)

The recipe places every primitive, Int rects included, where a factor-1 drawing places it (within about 0.01 px for circles, ellipses, lines, paths, and text). Output is not byte-identical across factors: antialiased edges legitimately differ between a supersampled render and a plain one.

The recipe moves geometry, not snapping. fill_rect snaps its edges in device space after the transform, so a fractional logical edge that resolves to one gray column at factor 1 resolves to factor finer steps instead, and that edge stops matching the factor-1 output. A caller who wants hard edges under supersampling snaps each edge to the nearest half-integer in logical space before drawing, and derives the size from the two snapped edges rather than snapping a position and a size separately, which lets two rounding errors accumulate in the width. Every factor then produces the same crisp edge in the same place. Lines, paths, and text are never snapped and need nothing.

Args:

  • source (Canvas): Canvas to shrink.
  • factor (Int): Integer shrink factor; must evenly divide both source.width and source.height.

Returns:

Canvas: A new canvas, source.width // factor by source.height // factor.

Raises:

Error: factor isn’t positive, or doesn’t evenly divide source’s dimensions.