Getting Artistic w/ RubyMotion
A coworker has a habit of leaning over to me and mentioning something that would be neat to have on an iPhone. I’m a sucker for it every time.
What he was looking for was an app that can draw over the top of another picture, namely a layout of his backyard, and be able to clear off any changes with a shake of the device. Having never done a drawing app before, I thought this would be a lot of fun and set off to write it in RubyMotion.
Some quick google searching turned up a few forum and blog posts that provide instruction if you are using Objective-C. Here’s how to do it in RubyMotion.
For the purposes of this blog post we are going to be building an app that allows you to drag your finger around the screen to create an image.
We are going to start out by creating a custom view which we can draw to as well as track touches.
class PaintView < UIView
def initWithFrame(frame)
if super
@hue = 0.5
end
self
end
def touchesMoved(touches, withEvent:event)
@touch = touches.anyObject
self.setNeedsDisplay()
end
def drawRect(rect)
context = UIGraphicsGetCurrentContext()
color = UIColor.colorWithHue(@hue, saturation:0.7, brightness:1.0, alpha:1.0)
CGContextSetStrokeColorWithColor(context, color.CGColor)
CGContextSetLineCap(context, KCGLineCapRound)
CGContextSetLineWidth(context, 15)
last_point = @touch.previousLocationInView(self)
new_point = @touch.locationInView(self)
CGContextMoveToPoint(context, last_point.x, last_point.y)
CGContextAddLineToPoint(context, new_point.x, new_point.y)
CGContextStrokePath(context)
end
end
We instantiate this view inside a basic UIViewController and add it to the display.
class MainViewController < UIViewController
def viewDidLoad
paint_view = PaintView.alloc.initWithFrame(self.bounds)
view.addSubview(paint_view)
end
end
Upon running this app, you will discover that it flickers REALLY badly. This is because the view is double-buffered by default. We can solve this by caching our draw calls in a new context.
def init_cache_context
bitmap_bytes_per_row = size.width * 4
bitmap_byte_count = bitmap_bytes_per_row * size.height
cache_bitmap = Pointer.new(:char, bitmap_byte_count)
@cached_context = CGBitmapContextCreate(cache_bitmap, size.width, size.height, 8, bitmap_bytes_per_row, CGColorSpaceCreateDeviceRGB(), KCGImageAlphaNoneSkipFirst)
true
end
There are a couple of interesting things to note here about the differences between Objective-C development and RubyMotion. The first is the pointer. Here we are generating a new character pointer with a size equal to that of the buffer we need. The second thing of note is the KCGImageAlphaNoneSkipFirst
constant. The Objective-C equivalent is kCGImageAlphaNoneSkipFirst
. Ruby requires its constants to start with a capital letter. This is a gotcha that can send you on a wild google hunt.
Add a call to init_cache_context
to the PaintView initWithFrame
method.
def initWithFrame(frame)
if super
@hue = 0.5
self.init_cache_context(frame.size)
end
self
end
Now that we have our cache context setup, we need to use it when someone touches the screen. This is a good time to extract that code into a new method.
def touchesMoved(touches, withEvent:event)
@touch = touches.anyObject
self.draw_to_cache(@touch)
end
def draw_to_cache(touch)
color = UIColor.colorWithHue(@hue, saturation:0.7, brightness:1.0, alpha:1.0)
CGContextSetStrokeColorWithColor(@cached_context, color.CGColor)
CGContextSetLineCap(@cached_context, KCGLineCapRound)
CGContextSetLineWidth(@cached_context, 15)
last_point = @touch.previousLocationInView(self)
new_point = @touch.locationInView(self)
CGContextMoveToPoint(@cached_context, last_point.x, last_point.y)
CGContextAddLineToPoint(@cached_context, new_point.x, new_point.y)
CGContextStrokePath(@cached_context)
self.setNeedsDisplay()
end
Nothing new here, just using our cached context instead of the current one. If you are following closely along, you will by now have realized that the call to setNeedsDisplay
causes the view to redraw itself.
Because we are using a cached context, we need to have our drawRect
method behave a little differently. We need to have it copy our cached context in the form of an image onto the screen.
def drawRect(rect)
context = UIGraphicsGetCurrentContext()
cache_image = CGBitmapContextCreateImage(@cached_context)
CGContextDrawImage(context, self.bounds, cache_image)
end
With any luck, you should now have an app that is flicker free while dragging your finger around the screen. I’ll leave tweaks and additions going forward up to your imagination. You can get the code from github here.
Resources I used to help me get this far…
Drawing to the screen
How to build a Simple Paint App for iOS
GLPaint example from Apple Developer Program
Comments
Awesome, I cannot wait to get a free weekend to dive into RubyMotion
Hello, thank you for your post, which gave me the occasion to learn about RubyMotion. However while i was watching the video-introduction on rubymotion site i started questioning myself about this:
they say rubymotion is a ruby port written in objective-c and that it can compile code to machine code through a static compiler.
What I don’t get is this: could this also be possible for mit ruby? aren’t many part of it still written in c (what if it was all coded in c?) And..anyway.. does compiling with a static compiler mean that all the dynamic features of the language are gone?? I don’t know if you’ll find this the right place to ask, but I just felt like it and maybe other people will be interested in this :)
Thanks, jma
sorry in previous comment i meant MRI ruby
Hi, Neat post. There is a problem with your website in internet explorer, would check this… IE still is the market leader and a good portion of people will miss your wonderful writing due to this problem.