0
votes

I'm developing my own image editor, and I've run into a bit of a snag. I'm stumped as to how other image editors deal with blending transparent colors together.

Here's how Photoshop and a few other programs do it:

Colors in Photoshop

As you can see, it's not a simple mix of the colors! It varies depending on which one you draw over the other.

This is the closest I've been able to approximate so far in my editor. Transparent red and transparent green are drawn correctly, but when I draw green on red I get a really bright green (0.5, 1, 0, 0.75) and when I draw red on green I get a bright orange (1, 0.5, 0, 0.75).

Colors in my editor

The formula that's gotten me this close is as follows:

pixmap.setBlending(Blending.None);
memtex.pixmap.setBlending(Blending.None);
for(int x = 0; x < width; x++) {
    for(int y = 0; y < height; y++) {
        int src_color = pixmap.getPixel(x, y);
        float src_r = (float)((src_color & 0xff000000) >>> 24) / 255f;
        float src_g = (float)((src_color & 0x00ff0000) >>> 16) / 255f;
        float src_b = (float)((src_color & 0x0000ff00) >>> 8) / 255f;
        float src_a = (float)(src_color & 0x000000ff) / 255f;

        int dst_color = memtex.pixmap.getPixel(x, y);
        float dst_r = (float)((dst_color & 0xff000000) >>> 24) / 255f;
        float dst_g = (float)((dst_color & 0x00ff0000) >>> 16) / 255f;
        float dst_b = (float)((dst_color & 0x0000ff00) >>> 8) / 255f;
        float dst_a = (float)(dst_color & 0x000000ff) / 255f;

        //Blending formula lines! The final_a line is correct.
        float final_r = (src_r * (1f - dst_r)) + (dst_r * (1f - src_a));
        float final_g = (src_g * (1f - dst_g)) + (dst_g * (1f - src_a));
        float final_b = (src_b * (1f - dst_b)) + (dst_b * (1f - src_a));
        float final_a = (src_a * 1) + (dst_a * (1f - src_a));

        memtex.pixmap.drawPixel(x, y, Color.rgba8888(final_r, final_g, final_b, final_a));
    }
}

As you can probably guess, I'm merging one libGDX pixmap down onto another one, pixel by pixel. If you want to blend the pixels, there seems to be no other way to do it, as far as I know.

For the record, the canvas is transparent black (0, 0, 0, 0). Could anyone point out where my formula's gone awry?

1
I think it is more like float final_r = (src_r * src_a + (dst_r * dst_a * (1f - src_a)); and so onMark Setchell
@MarkSetchell The colors sort of mix well using this, but they aren't quite right. Drawing (1, 0, 0, 0.5) results in (0.5, 0, 0, 0.5) getting committed to the canvas, and on successive draw operations it keeps halving the R value for those pixels. Pic: i.imgur.com/eFS5FXt.pngWill Tice
Looking at this again from a HSB perspective, in my second image the hues and saturations of the colors are correct! The only incorrect component is the brightness. Still haven't figured out how to factor that into the formula...Will Tice
Dang, just checked and this isn't actually true for every case. Drawing white at 0.5 alpha over red at 1.0 alpha gives me a totally wrong cyan color.Will Tice
Check under "Alpha Blending" en.m.wikipedia.org/wiki/Alpha_compositingMark Setchell

1 Answers

2
votes

Thanks to Mark Setchell for pointing me in the right direction!

This is the final code, derived from alpha blending formulas on Wikipedia.

pixmap.setBlending(Blending.None);
memtex.pixmap.setBlending(Blending.None);
for(int x = 0; x < width; x++) {
    for(int y = 0; y < height; y++) {
        int src_color = pixmap.getPixel(x, y);
        float src_r = (float)((src_color & 0xff000000) >>> 24) / 255f;
        float src_g = (float)((src_color & 0x00ff0000) >>> 16) / 255f;
        float src_b = (float)((src_color & 0x0000ff00) >>> 8) / 255f;
        float src_a = (float)(src_color & 0x000000ff) / 255f;
        if(src_a == 0) continue; //don't draw if src color is fully transparent. This also prevents a divide by zero if both src_a and dst_a are 0.

        int dst_color = memtex.pixmap.getPixel(x, y);
        float dst_r = (float)((dst_color & 0xff000000) >>> 24) / 255f;
        float dst_g = (float)((dst_color & 0x00ff0000) >>> 16) / 255f;
        float dst_b = (float)((dst_color & 0x0000ff00) >>> 8) / 255f;
        float dst_a = (float)(dst_color & 0x000000ff) / 255f;

        //Blending formula lines! All lines are now correct.

        //we need to calculate the final alpha first.
        float final_a = src_a + dst_a * (1 - src_a);

        float final_r = ((src_r * src_a) + (dst_r * dst_a * (1f - src_a))) / final_a;
        float final_g = ((src_g * src_a) + (dst_g * dst_a * (1f - src_a))) / final_a;
        float final_b = ((src_b * src_a) + (dst_b * dst_a * (1f - src_a))) / final_a;

        memtex.pixmap.drawPixel(x, y, Color.rgba8888(final_r, final_g, final_b, final_a));
    }
}

Without calculating the final alpha first and dividing by it, the colors would get darker and darker on successive draw operations, because even though (0, 0, 0, 0) is being drawn to the pixels they'd make themselves darker by multiplying their RGB by their alpha over and over.

Final result:

Final result