01 November 2013
Tutorial: Paint Brushes, Trailing Object Effects, and More with the Snapshot Canvas
Today, I’m going to show you how to use snapshots to achieve the sorts of tricks you might be used to doing if you did a lot of traditional CPU-based computer graphics.
One of the old school tricks is to be able to touch pixels on the frame buffer directly, or modify the pixels of an image directly. In a GPU world, you cannot do that b/c passing memory between the CPU and GPU is extremely expensive.
Versatility of Snapshots
Snapshots to the rescue!
Normally, you use snapshots as a one-time operation to cache a single rendered result. They work by adding objects to the snapshot’s ‘group’ property, and then calling ‘invalidate()’ each time you want to render the group’s children to the texture. This lets you achieve cool effects like this Mode7 demo.
We recently added some new canvas features to let you manipulate the snapshot texture in new and interesting ways.
Here’s a video showing you a trailing brush effect that renders onto the snapshot. Underneath the snapshot is a background image of the world:
One way to achieve this affect is to keep track of every brush image we draw and then fade it out over time. That’s pretty complex and unwieldy to write code for.
A far easier way approach is to draw the brush image onto the snapshot for every touch event. The trick is to draw a black translucent rectangle in between each touch, thus causing previously drawn images to appear to fade away. It’s a common technique from the CPU days, but now you can achieve it on the GPU!
In Corona, the way to do this is to use the new canvas feature of snapshot, combined with support for Porter-Duff blend modes:
local w = display.viewableContentWidth
local h = display.viewableContentHeight
local background = display.newImage( "world.jpg", w*0.5, h*0.5 )
local snapshot = display.newSnapshot( w,h )
snapshot:translate( w * 0.5, h * 0.5 )
snapshot.canvasMode = "discard"
function listener( event )
local x,y = event.x - snapshot.x, event.y - snapshot.y
if ( event.phase == "began" or event.phase == "moved" ) then
local r = display.newRect( 0, 0, w, h )
r:setFillColor( 0, .98 )
r.blendMode = "dstIn" -- enables snapshot to render over background
local o = display.newImage( "brush.png", x, y )
o:setFillColor( 1, 0, 1 )
snapshot.canvas:insert( r )
snapshot.canvas:insert( o )
snapshot:invalidate( "canvas" ) -- accumulate changes w/o clearing
Runtime:addEventListener( "touch", listener )
Incidentally, if you take out the black rectangles between each touch, you can also build a simple paint brush program (code).
The Snapshot Canvas
snapshot.canvas lets you draw onto the snapshot texture without clearing between invalidates. In order to render these objects to the snapshot, you invalidate with the “canvas” parameter, e.g.
snapshot:invalidate( "canvas" ).
We’ve also added a
snapshot.canvasMode property that lets you control what happens to the children between invalidates. Normally, the canvas group is emptied, and the children are appended to the snapshot’s main group. This ensure that your snapshot texture isn’t lost, which sometimes happens when your app is suspended. If you don’t care to preserve your edits, you can throw away the children via the
Porter-Duff Blend Modes
The Porter-Duff blend modes are something we’ve also added. The normal blend mode that we’re all used to corresponds to “srcOver”, but there are a ton of other modes like
"dst", etc that let you do a lot of amazing things.
In the example above, we could have used the normal blend mode. However, if you do that, the background of the snapshot will become black and you won’t be able to see the image of the world behind the snapshot. The key is to only fade out the portions of the snapshot that are already opaque. And so in the example above, we use the
"dstIn" which multiplies the snapshot texture with the alpha of the black rectangle.
As you can see, snapshots are an incredibly powerful and versatile tool. And yes, there are some obvious next steps for snapshots (e.g. using them as textures for other objects), so we’ve put that on our roadmap. I think what’s really amazing is that this example only used a couple of features, so we’ve only scratched the surface of what’s possible!