Foundations: pixels, paths, and color#
Before any library, four ideas carry the whole subject: the difference between a raster and a vector, the coordinate system a drawing lives in, what a color actually is once you have to store it, and how two colors combine when one is partly transparent. Every chapter after this one leans on these without re-explaining them. None of it is Perl-specific, but all of the examples are Perl, because the point is to make the ideas concrete in code you can run.
Raster versus vector#
A raster image is a grid of colored cells. It has a fixed width and height in pixels, and every pixel holds a color. Scaling it up past its resolution invents nothing: the cells just get bigger. Photographs, screenshots, and anything with continuous tone are rasters. Imager, GD, and ImageMagick are raster tools.
A vector image is a set of shapes described mathematically: ”a line from here to there“, ”a circle of this radius“, ”fill this path“. There is no resolution until you draw it, so it scales to any size cleanly. Fonts, logos, and diagrams are vectors. Cairo and SVG are vector tools.
The two meet at rasterization: turning a vector description into pixels. A vector library still has to decide which pixels a diagonal line touches, and that decision is the first algorithm in the next chapter. Everything you draw ends up as pixels on a screen; the question is only whether you commit to a resolution early (raster) or late (vector).
A raster in Perl is, at its plainest, an array of rows:
my $w = 4;
my $h = 3;
# each pixel is [r, g, b], each channel 0..255
my @img = map { [ map { [0, 0, 0] } 1 .. $w ] } 1 .. $h;
$img[1][2] = [255, 0, 0]; # row 1, column 2, red
That nested-array picture is all a raster is. Real libraries store the same grid in a packed C buffer for speed, but the mental model does not change: a pixel is addressed by an integer row and column, and it holds a color.
The coordinate system#
Graphics coordinates are not math-class coordinates. The origin (0, 0) is the top-left corner, x increases to the right, and y increases downward. This catches everyone once: a larger y is lower on the screen, not higher.
# (0,0) top-left x ->
# +-----------------
# | (0,0) (w-1, 0)
# y |
# | |
# v | (0, h-1) (w-1, h-1)
A pixel is a little square, and there is a lasting ambiguity about whether the coordinate (3, 5) names the square’s corner or its center. Raster libraries usually treat integer coordinates as pixel centers; vector libraries treat them as the grid lines between pixels, which is why a one-unit-wide vertical line at x = 3 can come out straddling two columns of pixels at half intensity. When a thin line or a rectangle edge looks blurry or one pixel off, this is almost always why, and the fix is a half-pixel offset. The chapters that hit it say so.
Sizes and positions are integers in raster space and real numbers in vector space. That is the practical difference you feel: in Imager you set pixel (120, 47); in Cairo you can move to (120.5, 47.25) and the rasterizer works out the coverage.
Color: what a pixel holds#
A color on a screen is three numbers: how much red, green, and blue light to emit. Add all three at full strength and you get white; leave them all off and you get black. This is the RGB model, and it is additive because you are adding light, not mixing paint.
Each channel is almost always stored in 8 bits, so 0 .. 255 per channel, and a color is three bytes. That is the #RRGGBB hex notation from the web: #ff0000 is (255, 0, 0), pure red.
sub rgb_to_hex {
my ($r, $g, $b) = @_;
return sprintf "#%02x%02x%02x", $r, $g, $b;
}
print rgb_to_hex(255, 128, 0), "\n"; # #ff8000, an orange
RGB is how pixels are stored, but it is a terrible model for a human choosing a color. ”Make this a bit more orange but keep it the same brightness“ is not an obvious move in RGB. HSV (hue, saturation, value) is the model for that: hue is the position on the color wheel in degrees 0 .. 360 (0 red, 120 green, 240 blue), saturation is how vivid versus gray 0 .. 1, and value is how bright 0 .. 1. Rotating the hue while holding saturation and value walks the rainbow at constant vividness and brightness.
Every color-picker and gradient tool works in HSV or a close relative, then converts to RGB to store. The conversion is worth having in plain sight, because you will reimplement it more than once:
# h in 0..360, s and v in 0..1 -> (r, g, b) in 0..255
sub hsv_to_rgb {
my ($h, $s, $v) = @_;
my $c = $v * $s; # chroma
my $hp = $h / 60;
my $x = $c * (1 - abs($hp % 2 - 1 + ($hp - int $hp)));
my ($r, $g, $b) =
$hp < 1 ? ($c, $x, 0) :
$hp < 2 ? ($x, $c, 0) :
$hp < 3 ? (0, $c, $x) :
$hp < 4 ? (0, $x, $c) :
$hp < 5 ? ($x, 0, $c) :
($c, 0, $x);
my $m = $v - $c;
return map { int(($_ + $m) * 255 + 0.5) } ($r, $g, $b);
}
my @rainbow = map { [ hsv_to_rgb($_, 1, 1) ] } 0, 60, 120, 180, 240, 300;
# red, yellow, green, cyan, blue, magenta
There are other spaces you will meet: grayscale (one channel, a brightness), CMYK (subtractive, for print), and perceptually uniform spaces like CIELAB where equal numeric steps look like equal perceptual steps. For screen drawing in two dimensions, RGB for storage and HSV for choosing cover almost everything.
Alpha and compositing#
A fourth channel, alpha, records opacity: 0 fully transparent, 255 (or 1.0) fully opaque. A pixel with alpha is RGBA. Alpha is what lets you draw one image over another and have the top one’s transparent parts show through.
Combining a foreground pixel over a background pixel is compositing, and the standard rule is source-over. With alpha as a fraction 0 .. 1:
# composite one RGBA pixel over another (all channels 0..255,
# alpha treated as 0..1 for the blend)
sub over {
my ($fg, $bg) = @_;
my $a = $fg->[3] / 255; # source alpha
my @out = map {
int($fg->[$_] * $a + $bg->[$_] * (1 - $a) + 0.5)
} 0 .. 2;
return \@out;
}
my $red_half = [255, 0, 0, 128]; # red at 50% opacity
my $white = [255, 255, 255, 255];
my $result = over($red_half, $white);
print "@$result\n"; # 255 128 128, a pink
Red at half opacity over white gives pink, exactly as you would expect paint-thinned-with-water to look. That one formula, applied per pixel, is how every layer, brush stroke, and sprite with soft edges lands on the canvas beneath it. When you stack many semi-transparent layers, you apply it repeatedly, bottom to top.
The subtlety that bites later is premultiplied alpha: storing each color channel already multiplied by its alpha. It makes repeated compositing cheaper and filtering correct at edges, and it is why a library’s raw pixel bytes sometimes are not the RGB you expect. Cairo uses premultiplied alpha internally; the Cairo chapter notes where it shows.
The one 3D idea, named and deferred#
You will hear about the Z-buffer the moment 3D comes up, and it is worth one paragraph here so the term is not a mystery. In 2D, ”what is on top“ is decided by draw order: the last thing drawn wins, which is exactly the source-over compositing above. In 3D, objects have a depth (a z coordinate), and the thing nearest the viewer should win regardless of draw order. A Z-buffer is a second grid, the same size as the image, holding the depth of the closest surface seen so far at each pixel; before drawing a pixel you check whether the new surface is nearer than what is recorded, and skip it if not.
That is the whole concept, and it is as far as this 2D guide goes with it. Depth buffering, projection, and the rest of the third dimension belong to the planned sequel. In two dimensions, draw order is your depth buffer.
Where this goes next#
You now have the vocabulary the rest of the guide speaks: raster and vector, top-left coordinates, RGB and HSV, alpha and source-over compositing. The next chapter spends that vocabulary on the classic drawing algorithms, implemented from scratch, so that when a library draws a line or fills a polygon you know exactly what it is doing on your behalf.