Easy Finger Drawing with Undo Functionality

Posted by jasonschroeder, Posted on January 30, 2012

5 votes

For an app I am soon to release, I needed to allow the user to draw a series of squiggily lines with his or her finger, and to be able to reference or remove specific lines/strokes.

The closest bit of code I could find online was actually provided by Carlos (https://developer.anscamobile.com/forum/2011/05/02/need-some-help), but I found that his solution of appending new points onto a single line got laggy the longer the user kept tracing, and the line would occasionally behave strangely (disappearing, jumping erratically, etc.).

After a little bit of messing about, I've come up with a simple function that consistently works well and doesn't slow down as the user keeps tracing (at least not on a first-gen iPad...I haven't yet tested it on less-capable devices). The line can be set to whatever width you like, and the color is easy to change. Even better, because each "stroke" is saved as a separate display group, it's easy to create an "undo" button for going back one step at a time!

Here's the code. I hope somebody finds it useful. It's free to use or modify as you see fit, under the terms of the MIT License. Let me know if you have any questions or suggestions:

PLEASE NOTE: For this example, I have built this function into a simple main.lua file, but I'm sure it could be easily packaged into a module instead.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
-- MAIN.LUA
 
local widget = require "widget"
    -- This is only included because I used the widget.newButton API for the example "undo" & "erase" buttons. It's not required for the drawing functionality.
 
math.randomseed( os.time() )
    -- This is only included to help generate truly random line colors in this example. It is not required.
 
        
-----------------------------------
-- VARIABLES & LINE TABLE (required)
-----------------------------------
local lineTable = {}
    -- This is a required table that will contain each drawn line as a separate display group, for easy referral and removal
 
local lineWidth = 12
    -- This required variable sets the pixel width for your drawn lines
 
local lineColor = {R=math.random(0,255), G=math.random(0,255), B=math.random(0,255)}
    -- These required variables set the R, G, & B color values for your drawn lines. I've set it up in this example to pick random values.
 
        
-----------------------------------
-- DRAW LINE FUNCTIONALITY (required)
-----------------------------------
local newLine = function(event)
 
        local function drawLine()
                local line = display.newLine(linePoints[#linePoints-1].x,linePoints[#linePoints-1].y,linePoints[#linePoints].x,linePoints[#linePoints].y)
                        line:setColor(lineColor.R, lineColor.G, lineColor.B);
                        line.width=lineWidth;
                        lineTable[i]:insert(line)
 
                local circle = display.newCircle(linePoints[#linePoints].x,linePoints[#linePoints].y,lineWidth/2)
                        circle:setFillColor(lineColor.R, lineColor.G, lineColor.B)
                        lineTable[i]:insert(circle)
        end
 
        if event.phase=="began" then
                i = #lineTable+1
                lineTable[i]=display.newGroup()
                display.getCurrentStage():setFocus(event.target)
                
                local circle = display.newCircle(event.x,event.y,lineWidth/2)
                        circle:setFillColor(lineColor.R, lineColor.G, lineColor.B)
                        lineTable[i]:insert(circle)
                
                linePoints = nil
                linePoints = {};
                
                local pt = {}
                        pt.x = event.x;
                        pt.y = event.y;
                        table.insert(linePoints,pt);
                                        
        elseif event.phase=="moved" then
                local pt = {}
                        pt.x = event.x;
                        pt.y = event.y;
                                
                if not (pt.x==linePoints[#linePoints].x and pt.y==linePoints[#linePoints].y) then
                        table.insert(linePoints,pt)
                        drawLine()
                end
        
        elseif event.phase=="cancelled" or "ended" then
                display.getCurrentStage():setFocus(nil)
                i=nil
        end
        
return true
end     
 
 
-----------------------------------
-- UNDO & ERASE FUNCTIONS (not required)
-----------------------------------
local undo = function()
        if #lineTable>0 then
                lineTable[#lineTable]:removeSelf()
                lineTable[#lineTable]=nil
        end
        return true
end
 
local erase = function()
        for i = 1, #lineTable do
                lineTable[i]:removeSelf()
                lineTable[i] = nil
        end
        return true
end
 
 
-----------------------------------
-- UNDO & ERASE BUTTONS (not required)
-----------------------------------
local undoButton = widget.newButton{
        left = 25,
        top = display.contentHeight - 50,
        label = "Undo",
        width = 100, height = 28,
        cornerRadius = 8,
        onRelease = undo
        }
        
local eraseButton = widget.newButton{
        left = display.contentWidth-125,
        top = display.contentHeight - 50,
        label = "Erase",
        width = 100, height = 28,
        cornerRadius = 8,
        onRelease = erase
        }
 
        
-----------------------------------
-- EVENT LISTENER TO DRAW LINES (required)
-----------------------------------
Runtime:addEventListener("touch",newLine)
        -- NOTE: I set this up as a Runtime listener, but you can certainly add the listener to display objects instead, to control where the user can touch to begin drawing.


Replies

ojnab's picture
ojnab
User offline. Last seen 3 weeks 4 days ago. Offline
Joined: 5 Jan 2011

Hey Jason
I did one of these too, but never got it to work as smooth as this bit of code. Well done and thanks for sharing.

lemsim
User offline. Last seen 4 weeks 1 day ago. Offline
Joined: 30 Mar 2011

I tried too and it works GREAT! Definitively going to use in one my future app. Thanks a lot of sharing:)

Mo

optionniko
User offline. Last seen 2 days 1 hour ago. Offline
Joined: 19 Jul 2011

Thank you sooooo much Jason!!!

doubleslashdesign's picture
doubleslashdesign
User offline. Last seen 14 hours 47 min ago. Offline
Joined: 27 Nov 2010

Great Job, added this to one of my projects :) thanks for sharing :)

Larry
DoubleSlashDesign.com

optionniko
User offline. Last seen 2 days 1 hour ago. Offline
Joined: 19 Jul 2011

Hey Jason, i've got a short question. I'd like to have my strokes transparent (around 0.5) but the problem is that circles overlap the newLines. Even if i set whole lineTable alpha down, same thing happens.
Any suggestions?

Thanks!
Nick

jasonschroeder's picture
jasonschroeder
User offline. Last seen 2 hours 41 min ago. Offline
Joined: 25 Jan 2011

Hi Nick,

Unfortunately there's nothing I can do to help you with that problem at this time. A possible workaround would be to use Carlos' code found here to draw your line as a single display object. This would allow you to set the alpha on the line, but I find the performance of that code to be laggy and unpredictable. So there are definite tradeoffs.

If Ansca adjusted the behavior of the alpha property of display groups to affect the opacity of the flattened display group, instead of each individual child object, then we'd be able to change the alpha of the drawn lines as you hope for. I've submitted a feature request to Ansca asking for just that. Please go to http://developer.anscamobile.com/forum/2012/02/09/requested-change-alpha-behavior-display-groups and add your +1 in the comments. If enough of us ask for it, maybe they'll make it happen!

Thanks for trying out my code, and for your question. Good luck!

-Jason

jasonschroeder's picture
jasonschroeder
User offline. Last seen 2 hours 41 min ago. Offline
Joined: 25 Jan 2011

Hi Everybody,

I just thought I'd share that the app I wrote this code for ("Gordon & Li Li: Learn Animals in Mandarin") is now available for sale on the iOS app store. I'm pretty proud of it, so if you liked this code and wanted to do me a favor, I'd be most appreciative if you'd consider downloading the app and/or recommending it over social media. Especially if you have kids, I think it'd make a good addition to your app library.

Please know that I actually get no money from app sales (I was paid a developer fee, and my client takes the App Store income), but if the app is a success, my client might sign on for more adaptations of her books. So I definitely have an interest in seeing it do well! :)

Anyways, here's a link to my webpage for the app: http://gordonandlili.jasonschroeder.com

And here's the official App Store link: http://itunes.apple.com/us/app/gordon-li-li-learn-animals/id501412183?ls=1&mt=8

Thanks!

doubleslashdesign's picture
doubleslashdesign
User offline. Last seen 14 hours 47 min ago. Offline
Joined: 27 Nov 2010

Enjoyed the code, using it in one of my books

Tweeted and FB'ed :)

Will again tomorrow for ya :)

Larry

hoan
User offline. Last seen 10 hours 16 min ago. Offline
Joined: 20 Jan 2011

Thanks Jason for the good code

On android (HTC EVO), the drawing lags after quite a while. Any workaround to this? Mem is low and TexMem is low. Frame rate goes down a lot, maybe corona can not handle lots of display objects well on Android.

Thanks
Hoan

jasonschroeder's picture
jasonschroeder
User offline. Last seen 2 hours 41 min ago. Offline
Joined: 25 Jan 2011

Hi Hoan,

I don't have an HTC Evo, but if I can find some time I'll try to replicate your experience on my Motorola Atrix - though given that the system and texture memory counts remain low, I suspect you're correct that Corona has a hard time with large numbers of display objects on Android.

Off the top of my head, one possible fix could be to periodically use display.save() or display.captureScreen() to take a snapshot of your current drawing, display that snapshot, and remove all of the display objects except for the snapshot, thereby reducing the number of display objects to one. If you wanted to be especially proactive, you could do this after each stroke. This would eliminate the ability to "undo" your drawing stroke-by-stroke, however.

If I have time, I'll try to mock up some code to this effect, but if you manage to make this work or find another workaround, please share here in the comments!

Thanks,
Jason

si.v.luu
User offline. Last seen 1 week 6 days ago. Offline
Joined: 27 Mar 2012

Jason,

Great code BTW..thanks so much. Since you insert each line in the table...I wonder how to extract/re-draw that on the nextscene using saved table data?

Thanks, Simon