Quartz 2D implementing html5 canvas globalCompositeOperation

681 views Asked by At

I am trying to implement html5 canvas globalCompositeOperation using CGContextSetBlendMode and translating the html5 canvas operators (source-in, source-atop, etc.) to its CGBlendMode counterparts (kCGBlendModeSourceIn, kCGBlendModeSourceAtop, etc.).

Here's the expected results according to the specs:

globalCompositeOperation

With CGContextSetBlendMode, I get this instead:

enter image description here

Some of the results are wrong. For example, source-out (kCGBlendModeSourceOut in Quartz 2D), does not clip the blue rectangle.

Which implementation is correct, I am not sure. But my question is, is there a workaround? After some tinkering, I came up with this solution that preprocesses the destination before applying the operation:

  1. Clip everything except the source (i.e the red circle)
  2. Erase the destination (leaving only the image in the circle)

Here's the preprocessing step that does that (assuming the source path is already set):

auto save = CGContextCopyPath(ctx);
CGContextSaveGState(ctx);
CGContextAddRect(ctx, CGRectInfinite);
CGContextEOClip(ctx);
CGContextSetBlendMode(ctx, kCGBlendModeClear);
CGContextAddRect(ctx, CGRectInfinite);
CGContextFillPath(ctx);
CGContextRestoreGState(ctx);
CGContextAddPath(ctx, save);
CGPathRelease(save);

With the workaround, I get almost what I want:

enter image description here

except for that artifact in source-out with some (anti-aliasing?) fringes in the circle's outline.

Is there a better way? Am I doing it wrong? Am I missing something?

Thank you very much in advance, and please pardon this longish question.

2

There are 2 answers

3
Michael Radionov On

Blending modes are implemented by a function which basically accepts two colors (each from one source) as an input and gives you resulting color as an output.

f(a,b) = c

In your example we can assume that blue rectangle comes from one source a, and red circle comes from the other source b. I am not familiar with Quartz 2D, but if it has an API to directly access colors of each pixel, then you could walk over the pixels from both sources, call a blending function per pixel and get a resulting image.

c[1][1] = f(a[1][1], b[1][1])
c[1][2] = f(a[1][2], b[1][2])
...
c[n][m] = f(a[n][m], b[n][m])

where n and m - image width and height in pixels respectively.

Be aware that walking over pixels might be an expensive operation, especially if you work with images of high resolution.

Regarding source-out - it is one of the Porter/Duff compositing operators, which is somewhat similar to blending modes. Besides two colors it also needs an alpha channel values for these colors. Alpha value represents how much of each source should be represented in the final result, and this "participation" value is either used or discarded depending on the operator you want to use.

function sourceOut(aColor, bColor, aAlpha, bAlpha) {
  return bAlpha * (1 - aAlpha) * bColor;
}

See JSBin with full example

Reference:

0
Joel de Guzman On

Ok, so it turns out that both Skia and Quartz-2D behave the same way when compositing vector shapes (paths). So it seems that html canvas is the odd one here.

https://skia.org/user/api/SkBlendMode_Overview#Blend_Mode notes that:

Draw geometry with transparency using Porter_Duff compositing does not combine transparent source pixels, leaving the destination outside the geometry untouched. But: Drawing a bitmap with transparency using Porter_Duff compositing is free to clear the destination.

And so the proper solution is to compose the source using an offscreen bitmap (at least for the problematic operations: source-in, source-out, destination-atop, destination-in) before compositing into the target surface. Doing so, the alpha channel is also respected, unlike my initial hack using clip-paths.

Doing it this way produces the expected results:

enter image description here