The Edge Pixel Is a Contract
The center pixel is boring.
It is opaque. It knows its color. It does not have to explain itself.
The edge pixel is where the contract lives. Half the pixel belongs to the object. Half belongs to whatever is behind it. Maybe the texture was filtered. Maybe the background was unknown when the sprite was authored. Maybe the RGB in a fully transparent texel still contains a color from an old matte. Maybe the blend happened in gamma-encoded sRGB because the pipeline was fast, old, or quietly wrong.
Alpha compositing bugs rarely announce themselves in the middle of the image. They arrive as halos, dark seams, bright fringes, and particles that look right on black but wrong on the actual game level.
So the question for a pixel with alpha is not:
what color are you?
It is:
what color contribution are you already carrying?
The Lab
The lab below has two deliberately small experiments.
The first samples an edge between an opaque orange sprite pixel and a fully transparent texel whose hidden RGB is cyan by default. A straight-alpha texture interpolates RGB and alpha separately, then composites the sampled color. A premultiplied texture stores color already multiplied by alpha, so the fully transparent texel contributes no color when filtered.
The second blends the same orange source over a blue destination. One row blends the displayed sRGB code values directly. The other converts to linear light, blends, and encodes back to sRGB.
With the default settings, the edge sample has alpha 0.5. The
straight-alpha sample over white is #c1b6c2; the premultiplied sample is
#ff9685, exactly the ideal filtered edge. The RMS fringe error from the
straight sample is 0.2108. The premultiplied error is 0.
The sRGB-code blend is not a small bookkeeping difference either. In the default
orange-over-blue blend, the code-value blend has only about 56.4% of the
linear-light luminance.
Source Over Is Not Just Interpolation
Porter and Duff’s 1984 paper gave image compositing an algebra.1 The everyday operator is source over: put a source image over a destination image. In the W3C compositing spec, the operator appears as:
co = alpha_s * Cs + alpha_b * Cb * (1 - alpha_s)
alpha_o = alpha_s + alpha_b * (1 - alpha_s)
where co is the output color contribution and alpha_o is the output
alpha.2
That word “contribution” is the useful mental pivot.
If RGB is stored straight, a texel says:
here is my color
here is my opacity
If RGB is stored premultiplied, it says:
here is how much color I contribute
here is my opacity
For an opaque texel, both statements look the same. For a transparent texel, they are very different. A fully transparent straight-RGBA texel can have any RGB at all. The renderer is not supposed to see it after compositing, but filtering sees it before compositing. That is how a hidden blue matte leaks into an orange edge.
Premultiplied alpha erases that irrelevant color at storage time. If alpha is zero, the stored color contribution is zero. Linear filtering can average contributions without dragging in a color that should not exist.
The Transparent Texel Is Not Empty
Alvy Ray Smith’s history of digital compositing is useful because it separates
several ideas that are easy to collapse: matte creation, matte storage, printing,
and compositing.3 His compositing memo is even more direct about the
software consequence: premultiplied alpha is not a minor encoding preference; it
is what makes a shaped image behave like an image object under over.4
The lab’s first row is the failure case.
Imagine two neighboring texels:
opaque orange: RGB = orange, alpha = 1
transparent cyan: RGB = cyan, alpha = 0
A halfway bilinear sample in straight storage gives:
RGB = average(orange, cyan)
alpha = 0.5
That averaged RGB is already contaminated. Compositing it later cannot know which part came from visible orange and which part came from an invisible matte.
In premultiplied storage, the two texels are:
opaque orange: RGB = orange, alpha = 1
transparent cyan: RGB = black, alpha = 0
The transparent texel is not black as a color claim. It is black as a
contribution claim. A halfway sample stores half an orange contribution and
alpha 0.5; over white, that becomes the expected half-covered orange edge.
This is why premultiplied alpha often feels less like a trick and more like a type system. It refuses to store a color contribution that a pixel does not make.
The Other Contract Is Light
The second lab row is a different mistake. Even with alpha handled correctly, the RGB arithmetic has to happen in a space where addition means adding light.
sRGB code values are nonlinear. CSS Color 4 defines srgb-linear as the same
sRGB primaries and whitepoint, but with a linear-light transfer function rather
than gamma encoding.5 The spec also gives the familiar conversion from
gamma-encoded sRGB components to linear-light components.
If you blend encoded numbers directly, this operation:
0.5 * orange_code + 0.5 * blue_code
does not mean:
half the orange light plus half the blue light
It means arithmetic on display code values. The result is typically too dark.
In the lab’s default pair, the linear-light blend encodes to #bc33bc, while
the direct sRGB-code blend is visibly darker. The luminance ratio tile is the
number to watch.
This is not a demand that every UI engine become a film compositor. There are compatibility, performance, and platform constraints. But the failure mode should be named honestly: if the math is done in code values, the blend is not physically linear.
Practical Checks
When a sprite has halos, I would check in this order:
- Are texture colors premultiplied before filtering and mipmap generation?
- Does the renderer use blend factors compatible with that representation?
- Are transparent texels padded with plausible edge colors if straight-alpha storage is unavoidable?
- Are colors decoded to linear light before operations that are supposed to add light?
- Is the final result encoded exactly once for display?
The mistake can be surprisingly far from the symptom. The halo may be authored into the texture atlas. The dark edge may be born in mipmap generation. The particle may be correct in a linear HDR buffer and wrong only after a UI pass re-blends encoded color.
The edge pixel keeps the receipts.
Audit Notes
The lab exports the same formulas used in the page. Its audit checks:
- the Porter-Duff source-over alpha equation;
- premultiplied filtering equals the ideal edge in the default case;
- straight-alpha filtering leaks hidden matte color;
- changing hidden RGB changes the straight edge but not the premultiplied edge;
- fully opaque and fully transparent edge limits behave correctly;
- opaque color blends have no gamma disagreement;
- the sRGB transfer functions round-trip a midpoint;
- rendered hex colors and summary facts are finite.
Default facts:
{
"edgeAlpha": 0.5,
"fringeError": 0.2108,
"premulError": 0,
"srgbDelta": 0.1692,
"luminanceRatio": 0.564,
"straightHex": "#c1b6c2",
"premulHex": "#ff9685",
"linearHex": "#bc33bc"
}
Alpha is not just transparency. It is a promise about which color is allowed to count.
-
Thomas Porter and Tom Duff, “Compositing Digital Images”, Computer Graphics, 1984. ↩
-
W3C, “Compositing and Blending Level 1”, especially the Porter-Duff source-over operator. ↩
-
Alvy Ray Smith, “Alpha and the History of Digital Compositing”, Microsoft Technical Memo 7, 1995. ↩
-
Alvy Ray Smith, “Image Compositing Fundamentals”, Microsoft Technical Memo 4, 1995. ↩
-
W3C, “CSS Color Module Level 4”, sections on sRGB conversion and the
srgb-linearcolor space. ↩