Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What do PorterDuff source and destination refer to when drawing on canvas?

I've been trying to figure this out all night, but answers found on Google relate to very specific problems regarding Android's canvas and I haven't found any 101 explanations on this topic. Even Android documentation uses bitmaps instead of drawing shapes.

Specific problem:

I need to draw an oval and a path on canvas. And according to documentation colour source out with one colour, destination out another colour and overlapping area, either source in or destination in, a third colour. I'm trying to do all this in an offscreen canvas. but problems arise with some of the steps above and get worse when trying to combine them in any way.

  • Code -

        Bitmap bmp = Bitmap.CreateBitmap (720, 720, Bitmap.Config.Argb8888);
        Canvas c = new Canvas (bmp);
    
        Paint paint = new Paint ();
        paint.SetARGB (255, 255, 0, 0);
        c.DrawOval (200, 200, 520, 520, paint); //assumed destination
    
        paint.SetARGB (255, 0, 0, 255);
        paint.SetXfermode (new PorterDuffXfermode (PorterDuff.Mode.*)); //replace mode here
        paint.SetStyle (Paint.Style.Fill);
    
        Path path = new Path ();
        path.MoveTo (c.Width / 2f, c.Height / 2f);
    
        foreach (var m in measurements) {
            //calculations
    
            float x = xCalculatedValue
            float y = yCalculatedValue
    
            path.LineTo (x, y);
        }
    
        path.LineTo (c.Width / 2f, c.Height / 2f);
    
        c.DrawPath (path, paint); //assumed source
    
  • Source out -

This instead draws what XOR is supposed to draw.

  • Destination out -

This works as expected.

  • Source in -

This draws what source atop should.

  • Destination in -

This draws what destination should.

More general question:

What do source and destination refer to in this context? Intuitively I would assume that destination is the current state of the canvas bitmap and source is the matrix added by canvas.Draw* and Paint PortedDuff.Mode. But that doesn't seem to be the case.

EDIT: This is basically the effect I'm after, where the "star" is a dynamic path. Coloured three different colours depending on overlap.

Crude drawing

EDIT 2: York Shen did a great job answering the actual question. But for anyone wanting to get a similar effect here's the final code.

Bitmap DrawGraphBitmapOffscreen ()
{
    Bitmap bmp = Bitmap.CreateBitmap (720, 720, Bitmap.Config.Argb8888);
    Canvas c = new Canvas (bmp);

    // Replace with calculated path
    Path path = new Path ();
    path.MoveTo (c.Width / 2f, c.Height / 2f);
    path.LineTo (263, 288);
    path.LineTo (236, 202);
    path.LineTo (312, 249);
    path.LineTo (331, 162);
    path.LineTo (374, 240);
    path.LineTo (434, 174);
    path.LineTo (431, 263);
    path.LineTo (517, 236);
    path.LineTo (470, 312);
    path.LineTo (557, 331);
    path.LineTo (479, 374);
    path.LineTo (545, 434);
    path.LineTo (456, 431);
    path.LineTo (483, 517);
    path.LineTo (407, 470);
    path.LineTo (388, 557);
    path.LineTo (345, 479);
    path.LineTo (285, 545);
    path.LineTo (288, 456);
    path.LineTo (202, 483);
    path.LineTo (249, 407);
    path.LineTo (162, 388);
    path.LineTo (240, 345);
    path.LineTo (174, 285);
    path.LineTo (263, 288);
    path.Close ();

    Paint paint = new Paint ();

    paint.SetARGB (255, 255, 0, 0);
    paint.SetStyle (Paint.Style.Fill);

    c.DrawPath (path, paint);

    paint.SetARGB (255, 0, 0, 255);
    paint.SetXfermode (new PorterDuffXfermode (PorterDuff.Mode.SrcIn));

    c.DrawOval (200, 200, 520, 520, paint);

    paint.SetARGB (255, 255, 255, 255);
    paint.SetXfermode (new PorterDuffXfermode (PorterDuff.Mode.DstOver));

    c.DrawOval (200, 200, 520, 520, paint);

    return bmp;
}
like image 435
JaanTohver Avatar asked Jan 03 '23 11:01

JaanTohver


1 Answers

What do PorterDuff source and destination refer to when drawing on canvas?

After some in-depth study, I write a few demo to explain this deeply. To help you understand what is source and destination refer to.

First, look at the following code :

protected override void OnDraw(Canvas canvas)
{
    base.OnDraw(canvas);

    Paint paint = new Paint();

    //Set the background color
    canvas.DrawARGB(255, 139, 197, 186);

    int canvasWidth = canvas.Width;
    int r = canvasWidth / 3;

    //Draw a yellow circle
    paint.Color = Color.Yellow;
    canvas.DrawCircle(r, r, r, paint);

    //Draw a blue rectangle
    paint.Color = Color.Blue;
    canvas.DrawRect(r, r, r * 2.7f, r * 2.7f, paint);
} 

I override the OnDraw method, set a green background, then draw a yellow circle and a blue rectangle, effect :

enter image description here

Above is the normal procedure when we draw a Canvas, I didn't use any PorterDuffXfermode,let's analyse its process :

  • First, we call canvas.DrawARGB(255, 139, 197, 186) method draw the whole Canvas with a single color, every pixels in this canvas has the same ARGB value : (255, 139, 197, 186). Since the alpha value in ARGB is 255 instead of 0, so every pixels is opaque.

  • Second, when we execute canvas.DrawCircle(r, r, r, paint) method, Android will draw a yellow circle at the position you have defined. All pixels which ARGB value is (255,139,197,186) in this circle will be replaced with yellow pixels. The yellow pixels is source and the pixels which ARGB value is (255,139,197,186) is destination. I will explain later.

  • Third, after execute the canvas.DrawRect(r, r, r * 2.7f, r * 2.7f, paint) method, Android will draw a blue rectangle, all pixels in this rectangle is blue, and these blue pixels will replac other pixels in the same position. So the blue rectangle can be draw on Canvas.

Second, I use a mode of Xfermode, PorterDuff.Mode.Clear :

 protected override void OnDraw(Canvas canvas)
    {
        base.OnDraw(canvas);

        Paint paint = new Paint();

        //Set the background color
        canvas.DrawARGB(255, 139, 197, 186);

        int canvasWidth = canvas.Width;
        int r = canvasWidth / 3;

        //Draw a yellow circle
        paint.Color = Color.Yellow;
        canvas.DrawCircle(r, r, r, paint);

        //Use Clear as PorterDuffXfermode to draw a blue rectangle
        paint.SetXfermode(new PorterDuffXfermode(PorterDuff.Mode.Clear));

        paint.Color = Color.Blue;
        canvas.DrawRect(r, r, r * 2.7f, r * 2.7f, paint);

        paint.SetXfermode(null);
        this.SetLayerType(LayerType.Software, null);

        //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //I found that PorterDuff.Mode.Clear doesn't work with hardware acceleration, so you have add this code
  }

Effect :

enter image description here

Let's analyse its process :

  • First, we call canvas.DrawARGB(255, 139, 197, 186) method to draw the whole Canvas as a single color, every pixels is opaque.

  • Second, we call canvas.DrawCircle(r, r, r, paint) method to draw a yellow circle in Canvas.

  • Third, execute paint.SetXfermode(new PorterDuffXfermode(PorterDuff.Mode.Clear)), set the paint PorterDuff model to Clear.

  • Forth, call canvas.DrawRect(r, r, r * 2.7f, r * 2.7f, paint) to draw a blue rectangle, and finally it shows a white rectangle.

Why it display a white rectangle? Usually, when we call canvas.DrawXXX() method we will pass a Paint parameter, when Android execute draw method it will check whether the paint has a Xfermode mode. If not, then the graphics will directly covers the pixels that in Canvas at the same position. Otherwise, it will update the pixels in Canvas according to the Xfermode mode.

In my example, when execute canvas.DrawCirlce() method, Paint didn't has a Xfermode model, so the yellow circle directly cover the pixels in Canvas. But when we call canvas.DrawRect() to draw a rectangle, Paint has a Xfermode value PorterDuff.Mode.Clear. Then Android will draw a rectangle in memory, the pixels in this rectangle has a name : Source. The rectangle in memory has a corresponding rectangle in Canvas, the corresponding rectangle is called : destination .

The value of the ARGB of the source pixel and the value of the ARGB of the destination pixel are calculated according to the rules defined by Xfermode, it will calculate the final ARGB value. Then update the ARGB value of the target pixel with the final ARGB value.

In my example, the Xfermode is PorterDuff.Mode.Clear, it require destination pixels ARGB becomes (0,0,0,0), that means it is transparent. So we use canvas.DrawRect() method draw a transparent rectangle in Canvas, since Activity itself has a white background color, so it will show an white rectangle.

EDIT :

To implement the feature you post in the picture, I write a demo :

protected override void OnDraw(Canvas canvas)
{
    base.OnDraw(canvas);

    Paint paint = new Paint();
    paint.SetARGB(255, 255, 0, 0);
    RectF oval2 = new RectF(60, 100, 300, 200);
    canvas.DrawOval(oval2, paint);

    paint.SetXfermode(new PorterDuffXfermode(PorterDuff.Mode.*));

    Path path = new Path();
    paint.SetStyle(Paint.Style.Fill);
    paint.SetARGB(255, 0, 0, 255);

    path.MoveTo(180, 50);
    path.LineTo(95, 240);
    path.LineTo(255, 240);
    path.Close();

    this.SetLayerType(LayerType.Software, null);
    canvas.DrawPath(path, paint);
    paint.SetXfermode(null);
}

When use different Xfermode, their effect :

Xor, SrcOut, Screen, Lighten, Darken, Add.

As you can see, you could use different color and different Xfermode to achieve your effect.

like image 166
York Shen Avatar answered Feb 01 '23 17:02

York Shen