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

tim@collectiveidea.com

Comments

  1. June 08, 2012 at 17:26 PM

    Awesome, I cannot wait to get a free weekend to dive into RubyMotion

  2. redoalf@libero.it
    jma
    June 10, 2012 at 23:04 PM

    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

  3. redoalf@libero.it
    jma
    June 10, 2012 at 23:11 PM

    sorry in previous comment i meant MRI ruby

  4. March 17, 2013 at 13:44 PM

    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.