Simple drawing in Ruby with Cairo 2008-03-21


UPDATE: I've just added another howto for Cairo about how to draw a logo with gradients

I've been playing around with Cairo for some time, and have recently been starting to rely on it more and more to avoid having to deal with drawing programs. I'm a command line kind of guy. The very limited graphics on this site, for example, are generated with small Ruby scripts using Cairo (via the rcairo bindings).

After a barrage of StumbleUpon traffic to one of my entries yesterday, I decided I'd like to put up a very in your face panel to point out other recent articles and my feed to Stumbleupon users - I use it a lot myself, and know how quickly people will move on to the next site.

This is what I ended up with: (Note:Cairo installed as well as the Ruby "rcairo" binding that you can find on the Cairo site)

And here is how I did it the hard way, with code:

I dug out a couple of my utility functions - the rcairo API is very verbose, as it's just mirroring the C API. I'd love to have the time to create a cleaner, more Ruby'ish interface, but I simply have better things to do, so at the moment I'm just throwing together bits of code as I need them, and have built up a small selection of utility functions I reuse and gradually clean up:

require 'cairo'

def linear_gradient(x1,y1,x2,y2,extend,*stops)
  g = Cairo::LinearPattern.new(x1,y1,x2,y2)
  g.set_extend(eval("Cairo::EXTEND_#{extend.to_s.upcase}"))
  stops.each {|s| g.add_color_stop_rgba(*s)}
  g
end

def path cr, *pairs
  first = true
  pairs.each do |cmd| 
    if cmd == :c
	cr.close_path
	first = true
    elsif first
	cr.move_to(*cmd)
	first = false
    else
	cr.line_to(*cmd)
    end	
  end
end

def cairo_image_surface(w,h,bg=nil)
  surface = Cairo::ImageSurface.new(w,h)
  cr = Cairo::Context.new(surface)
  if bg
    cr.set_source_rgba(*bg)
    cr.paint
  end
  yield(cr)
end

For the graphics itself, I wanted a simple warning sign, with some subtle borders and gradients.

First some variables to ease life - the dimensions and some colors

h = 100
w = h*1.05
lw = 17       # Line width for the warning sign itself

red    = [1.0,0.0,0.0, 1]
lred   = [1.0,0.3,0.3, 1]
black  = [0.0,0.0,0.0, 1]
white  = [1.0,1.0,1.0, 1]
owhite = [1.0,0.95,0.9, 1]
yellow = [1.0,1.0,0.6, 1]
grey   = [0.5,0.5,0.5,1]

Then the main section. First I create the surface, and then I set up the gradients:

cairo_image_surface(w+(lw*2),h+(lw*2), white) do |cr|

  rg1 = linear_gradient(w*0.4,w*0.8,0,0, :reflect, [0.3,black],[1.0,lred])
  rg2 = linear_gradient(w*0.4,w*0.8,0,0, :reflect, [0.3,red],  [1.0,owhite])
  bg1 = linear_gradient(w*0.3,w*0.3,0,0, :reflect, [0.4,grey], [1.0,black])
  bg2 = linear_gradient(w*0.3,w*0.3,0,0, :reflect, [0.2,grey], [0.8,black])

Gradients can be a bit confusing - if you look back at the linear gradient function and compare with one of these calls, though, it's not that hard to explain.

The first four parameters define a line. The gradient is defines as lines running in parallel with the line you specify. Just experiment with different values and you'll quickly see the difference.

The next parameter sets the "extension" mode. Cairo supports a number of different ways of extending a gradient beyond the limits defined by the lines you provide. They include "none" (don't extend), "reflect" (repeat the pattern in alternating directions), "repeat" (repeat in the same direction) and "pad" (fill the rest with the last color).

The remaining parameters are "stops". Each pair consists of a value between 0.0 and 1.0 and define the color at that percentage into the gradient. The colors between each "stop" is found by mixing the colors at the stop to each side in proportion to how close a point is.

This bit determines what the line will look like wherever the path ends (LINE_CAP_ROUND) or changes direction (LINE_JOIN_ROUND):

  cr.set_line_join(Cairo::LINE_JOIN_ROUND)
  cr.set_line_cap(Cairo::LINE_CAP_ROUND)

Translate changes the coordinate space. Read it as if every x/y coordinate following translate gets lw added. It saves a lot of typing:

  cr.translate(lw,lw)

This creates the path for the sign itself. The :c makes my function call "close_path" which results in a closed polygon so that the edges use the join style insted of drawing endpoints:

  path(cr,[w/2,0],[w,h],[0,h],:c)

Then it's time to set various parameters and actually draw:

  # We want to draw with the gradient "rg1"
  cr.set_source(rg1)               
  cr.set_line_width(lw*1.2)   
  # stroke_preserve draws without clearing the path afterwards
  cr.stroke_preserve              
  # ... because we want to use it again, with gradient 'rg2'
  cr.set_source(rg2)               
  cr.set_line_width(lw)
  cr.stroke

This path is far more complex. It's an exclamation mark in two parts, hence the :c in the middle:

  path(cr,[w*0.45,h*0.5],[w*0.55,h*0.5],[w/2,h*0.68],:c,
	  [w*0.48,h*0.8],[w*0.52,h*0.8],[w*0.515,h*0.82],
          [w*0.485,h*0.82],:c)

  # This does more or less the same as above,
  # but notice how it fills to, to make sure the
  # interior of the exclamation mark is filled in too

  cr.set_line_width(lw*0.6)
  cr.set_source(bg2)
  cr.fill_preserve
  cr.stroke_preserve

  cr.set_source(bg1)
  cr.set_line_width(lw*0.4)
  cr.fill_preserve
  cr.stroke

Finally we write the context to a PNG file, and close the block:

  cr.target.write_to_png("test.png")
end


blog comments powered by Disqus