Understanding Pictures with Masks

I’m in a situation where I need an image with a .Mask with a valid .Graphics, but I’m not understanding how pictures with masks work.

Without a mask, if I create a picture and draw a circle into it, I get what I expect: A black circle with a transparent background (light gray indicates transparent pixels):

image

  Dim p As New Picture(100, 100)
  Dim gg As Graphics = p.Graphics
  gg.FillOval(0, 0, 100, 100)
  g.DrawPicture(p, 0, 0)

But if I explicitly create a picture with 32 bits (the above image is also 32 bits, but this will create an image with a .Mask property. But the default state of the image has a white (opaque) background:

image

  Dim p As New Picture(100, 100, 32)
  Dim gg As Graphics = p.Graphics
  gg.FillOval(0, 0, 100, 100)
  g.DrawPicture(p, 0, 0)

So, how do I clear the background and draw my opaque circle? Clearing the mask removes the white background, but drawing into the picture’s graphics doesn’t affect the alpha channel? Is this the expected behavior?

image

  Dim p As New Picture(100, 100, 32) // (updated post to fix missing bit depth parameter)
  Dim gg As Graphics = p.Graphics

  Dim mg As Graphics = p.mask.Graphics
  mg.ClearRectangle(0, 0, p.Width, p.Height)

  gg.FillOval(0, 0, 100, 100)
  g.DrawPicture(p, 0, 0)

I’m in a situation where I need to draw—as quickly as possilble–into the alpha channel of a picture. I’m aware of the ApplyMask method, but I don’t want to do a lot of copying of pixel data. I’d like to issue a few drawing commands that would only affect the alpha, like this text example, where I’ve written text in the alpha channel and it erases the circle:

image
(This is just a simplified illustration)

I would say it is the expected behavior. The whole point of the mask after all is to control which portion of your image should display and which should not.

If your image is created with the ,32) last parameter, you get
image

There is no mask yet.

If you create a mask image, anything that is black will allow pixels from the main image to be drawn, anything white will be transparent.
So if you apply this same image as the Mask, the white bits become transparent.

All you need to do to get your cutout text is to draw text on the mask graphics, in white.

(grey in the mask allows for semi transparency)

1 Like

Sorry, I fixed that third chunk of code to add the bit depth parameter.

Unfortunately, that doesn’t seem to work: It looks like the picture and the mask are separated.

Intuitively, I’d think (based on my experience with other 32 bit pixels maps) that if you started with a transparent mask, adding opaque pixels to the color components, in the form of circles, etc., would set the mask pixels to be opaque—which is not what’s happening in XOJO, I guess.

I believe this is what you’re trying for. This is a Canvas.Paint event:

Sub Paint(g As Graphics, areas() As Rect) Handles Paint
  '// Setup a multicolor background to verify opacity
  g.DrawingColor = &cff0000
  g.FillRectangle( 0, 0, g.Width, g.Height )
  g.DrawingColor = &c00ff00
  g.FillRectangle( g.Width / 2, 0, g.Width / 2, g.Height / 2 )
  g.DrawingColor = &c0000ff
  g.FillRectangle( 0, g.Height / 2, g.Width / 2, g.Height / 2 )
  g.DrawingColor = &cff00ff
  g.FillRectangle( g.Width / 2, g.Height / 2, g.Width / 2, g.Height / 2 )
  
  '// Create our 32-bit picture with mask.
  var p as new Picture( 100, 100, 32 )
  var pg as Graphics = p.Graphics
  var pm as Graphics = p.Mask.Graphics
  
  '// Fill our picture's graphics context with the desired foreground color.
  '   Black, in this case.
  pg.DrawingColor = &c000000
  pg.FillRectangle( 0, 0, pg.Width, pg.Height )
  
  '// Prepare the mask graphics context by painting with white.
  pm.DrawingColor = &cffffff
  pm.FillRectangle( 0, 0, pm.Width, pm.Height )
  
  '// Fill in the opaque portion of the mask.
  pm.DrawingColor = &c000000
  pm.FillOval( 0, 0, pm.Width, pm.Height )
  
  '// Now draw our text in white over the opaque portion to get the punchthrough.
  pm.FontSize = 40
  pm.DrawingColor = &cffffff
  pm.DrawText( "Hello", 0, pm.FontAscent, pm.Width, false )
  
  '// Draw to the Canvas graphics context.
  g.DrawPicture( p, 0, 0 )
End Sub

Result:
image

Here’s the equivalent without using a 32-bit Picture, using ApplyMask:

Sub Paint(g As Graphics, areas() As Rect) Handles Paint
  '// Setup a multicolor background to verify opacity
  g.DrawingColor = &cff0000
  g.FillRectangle( 0, 0, g.Width, g.Height )
  g.DrawingColor = &c00ff00
  g.FillRectangle( g.Width / 2, 0, g.Width / 2, g.Height / 2 )
  g.DrawingColor = &c0000ff
  g.FillRectangle( 0, g.Height / 2, g.Width / 2, g.Height / 2 )
  g.DrawingColor = &cff00ff
  g.FillRectangle( g.Width / 2, g.Height / 2, g.Width / 2, g.Height / 2 )
  
  '// Create our visible surface
  var p as new Picture( 100, 100 )
  var pg as Graphics = p.Graphics
  
  '// Fill our picture's graphics context with the desired foreground color.
  '   Black, in this case.
  pg.DrawingColor = &c000000
  pg.FillRectangle( 0, 0, pg.Width, pg.Height )
  
  '// Create our mask surface/
  var mask as new Picture( 100, 100 )
  var pm as Graphics = mask.Graphics
  
  '// Prepare the mask graphics context by painting with white.
  pm.DrawingColor = &cffffff
  pm.FillRectangle( 0, 0, pm.Width, pm.Height )
  
  '// Fill in the opaque portion of the mask.
  pm.DrawingColor = &c000000
  pm.FillOval( 0, 0, pm.Width, pm.Height )
  
  '// Now draw our text in white over the opaque portion to get the punchthrough.
  pm.FontSize = 40
  pm.DrawingColor = &cffffff
  pm.DrawText( "Hello", 0, pm.FontAscent, pm.Width, false )
  
  p.ApplyMask( mask )
  
  '// Draw to the Canvas graphics context.
  g.DrawPicture( p, 0, 0 )
End Sub

Thanks, yes, that solution is good if you’re only drawing to the mask, but I have a path that’s rendered to the picture’s RGB values, and that’s where I discovered the issue I asked about.

The problem I’m trying to solve: I’d like to create the effect of “stroked wide path.” Right now, I’m trying to draw it twice: First in the RGB, then a second time—with a thinner line—in the mask. In this screenshot, I’m cheating by drawing the narrower path with the background color, but I actually need transparency for the next part of my project.

I’ve got various half-working solutions, with plugins, with declares, but there’s all sorts of trade offs and I haven’t found a good solution for macOS and iOS. This version shows a method where I use a CGContext via MBS, but unfortuantely, I can’t mix drawing to both Graphics, and a CGContext based on the Graphics, in my scene graph without it causing render glitches.

I detail the journey so far, here: John Balestrieri: "I need a stroked path that I can then draw as a s…" - Mastodon

This is for a sprite engine, so it needs to be fast—that’s why I’m avoiding ApplyMask, because of the pixel copy.

Using the same method I provided above, fill the visible surface with the color you wish to use for the outline, then draw the path unfilled in the mask?

1 Like

Yes—I acknowledge that’s a valid solution, outside my project constraints.

I didn’t want to go too far into the weeds because I came here only to ask about .Mask behavior :sweat_smile: but there are API limitations in my framework that I’m trying to avoid adding complexity to deal with: My sprite framework is based on the Graphics class. It’s been extended to arbitrary renderers.

Right now, I have SpriteKit implemented. However, certain sprite types aren’t going to render well in SpriteKit (text sprites—text looks terrible in SK) or at all (path-based sprites, because GraphicsPath is opaque, and I can’t translate the path commands to SK path geometry).

So right now, these sprites are rendered to picture buffers, and then SK handles them like regular old bitmap sprites. The buffers are created without mask pictures but render RGBA images. Adding support for masks-or-no-masks is going to make a mess of things—but your solution is valid, yes, and may ultimately be what I have to do.

Hello John,
I still program in the old basic style and am interested in your new tools.
For the transparent graphics path, I don’t have a demo of my approach
I took the liberty of reprogramming your numbers game in normal xojo code for the desktop - it also runs on my window pc
Examples:

https://www.dropbox.com/scl/fi/13flmyqeani3rlui07dri/Pfadmaske.xojo_binary_project?rlkey=r46oiq4f23cykkrp5z8zqtd0m&dl=1

https://www.dropbox.com/scl/fi/b2mb09k4fu175n9aylawh/my-Number-Game-API2.xojo_binary_project?rlkey=wxbiisxap3k0x5ptw01l5thjc&dl=1

Yes, that apply mask method is very good!

The numbers game is quite advanced—I would only ask that you not publish it, as it reflects the original work I do to make my livelihood, and I haven’t published it yet. :sweat_smile:

For my game, I was able to use MBS plugins to work with a CGPath and CGContext to use a special blend mode to clear the alpha of the inner path, without the need for a mask.

I am happy to do a screencast presentation for you, or anyone else, of my sprite framework. It’s getting a lot of my attention now because I have two apps in progress.

John

That’s actually the case. You should draw the same thing twice: once in the regular picture, with the colour and attributes you need and another time in the mask, with either solid black for an opaque zone or solid white for transparent (use gray to intermediate transparency).
The whole difference of using masks rather than alpha transparency is that you get two pictures (the regular one and the mask). You should then draw to both if you want to stay synced (or use plain black to have no transparency at all).

Hint: it may help to have a method to draw to both pictures, if your drawing code is complex, so that the same set of commands is applied to both. E.g.:

Sub DrawToPicture(Pict as picture,IsMask as boolean)
if Pict=nil then return

Var g as graphics=Pict.Graphics
if g=nil then return

g.DrawingColor=if(not IsMask,MyDrawingColor,Color.Black) 'Or Color.White to add transparency rather than opacity

g.DrawLine 0,0,g.width,g.height
'And other complex drawings
End Sub

And call it that way:

DrawToPicture MyPicture,False
DrawToPicture MyPicture.Mask,True

Exactly.
What would be useful would be the ability to draw ‘on the alphachannel’. Without that, I stick to masks.

1 Like