cgPaint: Classic CG Algorithms, Pixel by Pixel
For my computer graphics mini project I built a paint app where every primitive
is implemented from scratch. No ctx.lineTo for lines. No ctx.arc for circles.
Just the raw algorithms, plotting one pixel at a time.
The most useful part turned out to be the speed slider: slow an algorithm down to 100ms per step and you can watch Bresenham decide whether to step right or diagonally. That made the theory click in a way lecture notes didn’t.
DDA vs Bresenham: two ways to draw a line
DDA (Digital Differential Analyzer) is the intuitive approach — divide the total change in x and y by the number of steps, then increment:
Bresenham eliminates the floating point entirely using an integer error term. At each step, the error tells you whether the next pixel should be directly right or diagonally up-right:
Run both on the same line: they produce identical pixels. The difference shows up at scale — Bresenham avoids floating-point rounding that accumulates across millions of lines in a real renderer.
Bresenham’s circle: computing one octant, mirroring eight
A circle has 8-fold symmetry. Bresenham’s algorithm exploits this: compute
one octant (45°), then mirror it to all eight positions simultaneously.
For a radius of r, you only do ~r iterations to draw the full circle:
Scan-line polygon fill
Given an arbitrary polygon, the scan-line algorithm sweeps horizontal lines
top to bottom. For each scan line it finds every edge intersection, sorts them,
and fills between pairs. The tricky part is the edge table — you only activate
edges whose yMin the current scan line has reached:
The █ blocks in the output show the actual fill span per scan line. You can
see the triangle widen as y increases.
2D transformations and why order matters
Translate, rotate, and scale are applied via the canvas transform stack. The important thing: the order you apply them changes the result. Rotate-then-translate is not the same as translate-then-rotate:
In the actual app, each shape stores its own transform state and
ctx.save()/ctx.restore() isolates it so one object’s rotation doesn’t
bleed into the next.