Posts tagged ‘graphics’

August 23, 2008

TexPlay: an image manipulation tool for Ruby and Gosu

TexPlay version 0.2.0 has been released!

TexPlay is a C extension specifically designed for Ruby and Gosu. It enables very fast manipulation of Gosu images in the spirit of RMagick.

The Basics

Note the guide below applies only to TexPlay 0.2.2 and above.

Quick Start:

The following code draws a red circle, a green box, and a blue pixel on a Gosu image called image1.

image1.paint {
    circle 20,20,10, :color => :red
    rect 40,40,50,50, :color => :green
    pixel 60,60, :color => :blue
}

And to find the color of the pixel at 60,60:

image1.get_pixel(60,60)

Drawing commands:

TexPlay supports the following drawing commands:

Basic commands:

These commands provide the basic drawing functionality:

circle x, y, rad
line x1, y1, x2, y2
rect x1, y1, x2, y2
pixel x, y
splice x, y, source_image
bezier points
polyline points
ngon x, y, rad, num_sides
fill x, y

All the above drawing actions support an optional list of named parameters (or hash args). The optional parameter list must appear after the mandatory parameters shown above.  Some of the named parameters are: :color (as shown in the first example), :thickness, :alpha, :fill, and so on. A full list of the valid named parameters will be given later.

Color parameter:

The color for the drawing actions can be specified in two ways:

:color => [r, g, b, <alpha>]

OR

:color => symbol

In the first form r, g, b and alpha must all be floats between 0 and 1. If alpha is not given, it defaults to 1.

In the second form ‘symbol’ must be a recognized symbol representing a pre-fab color. Supported symbols are: :red, :green, :blue, :cyan, :yellow, :tyrian, :purple, :black, :white, :alpha and :random (or :rand). The special color :none is also accepted. When the color is set to :none nothing is drawn (this is useful for color control procs, discussed later)

Circle, Line and Rect:

In addition to the :color parameter the circle, line and rect actions also respond to some other parameters:

:thickness => line_thickness

Rect, and line and in fact _all_ drawing actions that are built  on lines respond to this hash parameter. Using it specifies the line thickness to use in pixels. (Circles do not yet respond to thickness, however)
Use it as follows:

line 0, 0, 100, 100, :thickness => 10

The above draws a line with a thickness of 10 pixels.

Circle and rect actions also respond to the :fill (or :filled) hash parameter.  Use as follows:

circle 20, 20, :fill => true

OR

rect 10, 10, 30, 30, :fill => true

The above will draw filled circles and rectangles (applying either the default colour or the colour specified with the :color hash parameter).

Splice:

This function enables you to splice two images together, the syntax is:

splice image2, x, y, <:chroma_key>, <:crop>

Where image2 is the Gosu Image you wish to splice (the source image) and x,y specifies the location in the destination image.

The :crop hash parameter is used to select a sub-section of the source image to be spliced. It looks like this:

:crop => [x1, y1, x2, y2]

Where the x1,y1,x2,y2 parameters delineate a boxed region in the source image.

The :chroma_key parameter specifies a color value that will be excluded from the splice. A common application of this is to exclude all full alpha values. :chroma_key accepts the same parameters as the :color hash parameter.

:chroma_key => color

The :chroma_key parameter also has its opposite, :chroma_key_not. When using :chroma_key_not only the pixels that DO match the given color value are included in the splice.

Here is an example that splices in only non-alpha pixels from the region bounded by 20,20,70,70 from the image: ‘image1′ and saves it in location 100,100 in the destination image:

image1.splice(image2, 100,100, :chroma_key => :alpha, :crop => [20, 20, 70, 70])

Bezier:

Here is how to draw a Bezier curve:

bezier [0,0, 50,50, 100, 0]

Note that in the above there are three points specified (a point is an x and y pair)

A Bezier curve may also be ‘closed’ by providing a :closed (or :close) hash parameter (a closed Bezier curve is also called a ‘Beziergon’)
When a Bezier is closed its first point is connected to its last point.

Here is how we close the Bezier given above:

bezier[0,0, 50,50, 100, 0], :closed => true

A Bezier curve is guaranteed to interpolate its first and last points. The other points work as ‘magnets’ attracting the curve towards them.

Polyline:

A polyline takes exactly the same parameters as a bezier. A ‘closed’ polyline is known as a polygon.

Here is an example of a polygon drawn using polyline:

polyline [0, 50, 50, 100, 0], :closed => true

In the above each successive pair of points is connected by a line to the one before. The last point is also connected to the first as a result of the :closed hash parameter.

Ngon:

An ngon is a regular polygon with a user specified number of sides and a user specified radius.

Here is an example of an ngon with 7 sides (heptagon):

ngon 100, 100, 100, 7

The heptagon above has a radius of 100 pixels and is centered at the point 100, 100.
Note that because an ngon is made of lines it also responds to the :thickness hash argument. Thick circles can be faked using ngons by specifing a large number of sides along with the :thickness hash argument.

The ngon drawing action also supports the hash parameter :start_angle (measured in degrees) that orientates the polygon to a user-defined angle.

Fill:

The fill action performs a flood fill (similar to the paint bucket utility found in some painting programs). It works by reading the color it finds at the  start point and changing the color of any connected pixels that also have that color to the fill color.

Use it as follows:

fill 100, 100, :color => :red

The above sets the fill color to red and begins the filling process at point 100, 100.

You can also perform a texture fill by providing a :texture hash argument. The :texture argument must be a valid Gosu::Image
Here is an example of using a texture fill:

fill 100, 100, :texture => image2

Note that the :texture parameter is not unique to the fill action. Almost all drawing actions respond to the :texture argument and will apply that texture instead of a color.

get_pixel

TexPlay also supports the get_pixel command. It works as follows:

get_pixel(x, y)

The above will return a ruby array containing the color data.

Offset:

The origin for drawing actions can be changed using this function, it has the form:

offset x,y

And a shorthand for restoring the offset to (0,0):

offset :default

Advanced Topics:

Points

TexPlay accepts ‘Points’ as well as x,  y pairs for all the drawing actions. A Point is simply any object that responds to the ‘x’ and ‘y’ methods.

Here is an example using Points to generate a Bezier curve:

points = []

(0..img.width + 50).step(50) { |x|
    p = TexPlay::TPPoint.new
    p.x = x
    p.y = img.height * rand

    points << p
}

img.bezier points, :color => :red

Note, the TexPlay::TPPoint class is simply used for convenience, you do not have to use TPPoint to generate valid points.

The Paint Block

The Paint Block is one of a few ways to invoke the drawing actions. Here is an example of its use:

image1.paint {
    circle 10, 10, 20, :color => :green
    bezier [0, 0, 10, 10, 50, 0], :closed => true
}

The Paint Block may look like an ordinary instance_eval, but it is not.  The problem that plagues the serious use of instance_eval: namely the inability to use local  instance variables in the block does not affect the Paint Block. This means that code like the following is possible in TexPlay (but impossible with an instance_eval):

@x = 20
@y = 30
my_image.paint {
    circle @x, @y, 20
}

How does it work? TexPlay uses an alternative to instance_eval known as gen_eval (http://github.com/banister/gen_eval/tree/master)

Another peculiarity of Paint Blocks is how they sync the drawing actions (syncing will be discussed in greater depth later on). The drawing actions in a Paint Block are not sync’d until the very end of the Paint Block and are then sync’d to video memory all in one go (This style of syncing is called ‘lazy’ syncing in TexPlay)

Direct Invocation and Fluent API

Drawing actions may also be invoked directly on the image:

image1.circle 10, 10, 20, :color => :rand
image1.fill 10, 10, :texture => image3

c = image1.get_pixel(10, 10)

Or, if you prefer, using a fluent api:

image1.
    circle(10, 10, 20, :color => :rand).
    fill(10, 10, :texture =>; image3).
    rect(5, 5, 20, 20, :color => :blue, :fill => true).
    bezier([1,1,100,100,300,0])

c = image1.get_pixel(10, 10)

When using direct invocation each drawing action is sync’d to video memory immediately after its completion (TexPlay calls this ‘eager’ syncing).

Alpha Blending

Alpha blending can be applied to almost all drawing actions in TexPlay:

image1.circle 100, 100, 50, :alpha_blend => true

The effect of the above is to alpha blend the color values of the circle pixels with the pixels already in image1.

Note you can combine the use of :alpha_blend and :color_control to force variable alpha blending even on textures that have full alpha channels:

img1.circle 100, 100, 50, :alpha_blend => true, :fill => true, :texture => img2,
:color_control => proc { |c, c1|
    c1[3] = 0.1
    c1
}

Note that the fill action does not support alpha blending.

The Each Iterator:

You can iterate over all the pixels in an image using the each iterator:

image1.each { |c| c[0] = 1 }

The each iterator works like any other iterator and yields the current pixel (in the form of a color array) to the block. You can then modify the array and these changes directly affect the pixel. In the above example the red component of the pixel color is set to 1.  Note that because the each iterator iterates over all the pixels in the image the effect is to set the red component of all pixels to 1.

The each iterator also accepts a block arity of 3 and a :region parameter:

img.each(:region => [100, 250, 200, 350]) do |c, x, y|
    c[0] = (x - 100) / 100.0
    c[1] = 0
    c[2] = 0
end

The above code generates a red ‘gradient’ across a one hundred pixel interval. It should be clear that :region demarcates a rectangular region within which the each iterator operates. Also it should be obvious that a block with an arity of three passed to each is yielded not only the pixel color but also the x and y values corresponding to the pixel.

In the future I hope to make TexPlay fully enumerable (implement the Enumerable module) and so support other iterators such as: all?, map, any?, and so on.

Creating and duplicating images

Empty images with an arbitrary width and height can be created at runtime in TexPlay:

blank_image = TexPlay.create_blank_image(window, 100, 100)

Where ‘window’ is the standard window parameter required by many Gosu methods. In the above code blank_image stores a reference to a new image of 100 x 100 pixels.

As well as creating new images  TexPlay enables you to duplicate extant images:

dupped_image = image1.dup
cloned_image = image1.clone

These methods make deep copies of the image. and so changing a dupped image has no effect on the original image and vice versa. The clone method also behaves as expected and duplicates not only the object but its singleton class.

Turtle Graphics

The following Turtle functions are supported:

move_to(x, y)
move_rel(dx, dy)
line_to(x, y)
line_rel(dx, dy)
turn_to(theta)
turn(dtheta)
forward(distance, <true>)

Turtle Graphics works by maintaining a a record of the current ‘Turtle’ position and angle,  using that position and angle as a starting point for its drawing actions.

The move_to, and move_rel actions manipulate the Turtle position directly: move_to sets the Turtle position to a given point and move_rel moves the Turtle dx and dy pixels relative to its current position.

Similarly the line_to action draws a line from the current Turtle position to the specified point; line_rel follows the same rule as for move_rel (but draws a line).

The turn_to and turn (think of as turn_rel) actions manipulate the Turtle angle. The angle is measured in degrees.

The forward action moves a distance along the current Turtle angle from the current Turtle position. The forward action is visible if the optional  second parameter is true, and invisible otherwise.

Here is an example drawing a nice a spiral pattern using Turtle Graphics:


(0..100).step(2) { |length|
    img.paint {
        forward(length, true, :color => :red)
        turn(89.5)
        length += 2
    }
}

Here is another example showing how the combination of Turtle Graphics and a Color Control Proc (discussed below) can facilitate some quite sophisticated drawing techniques:

img.move_to(points.first.x, points.first.y)

img.bezier points, :color_control => proc { |c, x, y|
    if((x % 10) == 0)
        img.line_to(x, y + rand * 20 - 10)
    end
    :none
}

The above Color Control Proc is being used to ‘tap’ into the coordinates of the Bezier curve. Turtle Graphics is then used to connected every tenth point of the curve by a line. Combined with a slight randomization of the ‘y’ coordinate.  The resulting ‘Bezier’ has a rough and jagged appearance.

The Color Control Proc

All drawing actions respond to the :color_control parameter. This parameter takes two forms: (1)  the color control proc and (2) the color control hash

Here is how it is used:

image1.bezier [0, 0, 100, 100, 200, 50, 300, 400],
:color_control => proc { |c| c[0] = 1; c }

When a color control proc is specified every pixel that the drawing action manipulates is yielded to the proc. The return value of the proc (which must be a valid colour) is then used to set the color of that pixel.
In the above code the value ‘c’ yielded to the proc corresponds to the pixel the bezier is currently manipulating; the red component of that pixel is set to 1 and returned from the proc. This new color is then used to set that pixel.

Here is another example:

img1.rect 10, 10, 100, 100, :filled => true, :texture => img2,
:color_control => proc { |c1, c2|
    c1[0] = (c1[0] + c2[0] / 2 )
    c1[1] = (c1[1] + c2[1] / 2 )
    c1[2] = (c1[2] + c2[2] / 2 )
    c1[3] = 1
    c1
}

The above is another valid proc format, note there are two parameters. The first parameter, c1, correponds to the ‘c’ in the previous example but the second parameter corresponds to the color that _would_ be used if there were no color control proc. In this case since the rect specifies a :texture parameter the value of c2 is a pixel from the img2 texture. The color control proc then averages the colors between c1 and c2. and returns that average in c1. The effect of this color control proc, therefore, is a kind of 50% alpha blending of img1 and img2 within the bounds of the rect.

Here is one more example of the color control proc, this time using another proc format:

img1.fill 42, 70, :color_control => proc { |c, x, y|
    img2.get_pixel(x % img2.width, y % img2.height)
}

In the three parameter proc, the first parameter is the same as in the first example but the next two are the corresponding x and y values for the pixel. The effect of the color control proc above is to perform a texture fill using img2 as the source texture (note the :texture parameter is the best (and fastest) way to perform a texture fill, this example just illustrates it _could_ also be done with a color control proc)

Here is a list of the valid proc arities (number of parameters) accepted by color_control and the corresponding objects that are yielded (in order):

arity of 0: nothing yielded
arity of 1: yields -> dest pixel
arity of 2: yields -> dest pixel, source pixel
arity of 3: yields -> dest pixel, x,  y
arity of 4: yields -> dest pixel, source pixel, x,  y

Remember, the return value of the proc is used as the new pixel color so it must be a valid color (symbols are allowed).

The :none color symbol can be especially useful here when you simply want to ‘tap’ into the x and y of a drawing action but do not in fact want anything drawn.

The Color Control Hash

The color control hash is significantly less flexible than the color color proc but is also substantially faster.

Here is how it is used:

img1.circle 100, 100, 40, :fill => true,
:color_control => { :mult => [0.5, 0.5, 0.5, 1.0] }

If a color control hash is specified then any color the drawing action may have is ignored. Instead the current color of the destination pixel is transformed (linearly) by the parameters given in the hash. In the example above the red green and blue components of the destination pixel are multiplied by 0.5 but the alpha component is left unchanged.

The color control hash supports two parameter types: :mult and :add. The array given for :mult specifies a factor that each of the color components (rgba) of the destination pixel will be multiplied by. In the case of :add the array specifies a value each component of the desintation pixel will have added. Division and subtraction are not provided since they can be implemented easily in terms of :mult and :add.
(Note that the color control hash is significantly faster than the color control proc because it only requires a simple and fast linear transformation at every pixel and not the invocation of a lambda.)

The color control hash can be used to implement ‘fake’ shadows quite effectively. There is even a parameter, :shadow, that simply darkens the background corresponding to the drawing action, and it is implemented in terms of the color control hash.

Here is how to use the :shadow parameter:

img1.circle 100, 100, 60, :fill => true, :shadow => true

Drawing Modes

TexPlay supports 28 drawing modes. You activate them by using the ‘:mode’ option on any drawing action.

Here is an example using the ‘softlight’ mode applied to a filled rectangle.

img1.rect 100, 100, 200, 200, :color => :blue, :fill => true, :mode => :softlight 

Here is a full list of the accepted modes:
clear, copy, noop, set, copy_inverted, invert, and_reverse, and, or, nand, nor, xor, equiv, and_inverted, or_inverted, additive, multiply, screen, overlay, darken, lighten, colordodge, colorburn, hardlight, softlight, difference, exclusion

Trace (BETA)

The ‘trace’ option is a modifier to a line action. When trace is used a line is not drawn, instead every pixel along the line is checked as to whether it satisfies the rule given in the associated hash. If the condition specified by the rule is triggered then a three element array containing the x and y and color of the last pixel checked is returned. If no correponding pixel is found then the return value is ‘nil’. The supported conditions are :while_color and :until_color.

Here is how they are used:


# returns the first non-red pixel (or nil if none is found)
x, y, c = img1.line 10, 10, 100, 100, :trace => { :while_color => :red }

# returns the first blue pixel (or nil if none is found)
# NOTE: we discard the returned color since it's guaranteed to be :blue in the case of :until_color
x, y = img1.line 10, 10, 100, 100, :trace => { :until_color => :blue }

Linear Interpolation (Lerp)

It is possible to linearly interpolate the source and background pixels when performing any drawing action. Using the :lerp option you can specify a number between 0.0 and 1.0 that represents the amount of source color to use (the amount of background color is always equal to 1 – source)

For example, for a 70% source and 30% background blend while performing a filled rectangle action, use the following code:

img.rect 10, 10, 100, 100, :color => :blue, :fill => true, :lerp => 0.7

Image Caching

An Image is lazily cached by TexPlay at the point a TexPlay drawing action is first invoked on that image. Note that as Gosu may internally store many Gosu::Image(s) on the same OpenGL texture (aka quad), ALL the Gosu::Images that exist on that quad will be cached. Here is a list of the caching related TexPlay methods and constants:

refresh_cache
quad_cached?

refresh_cache_all
TP_MAX_QUAD_SIZE

The first group of two methods are instance methods and are invoked on the Gosu::Image instance. The refresh_cache method will force a caching of a Gosu::Image circumventing lazy caching. The refresh_cache method can be called at any time and will replace the cached quad data with the data in the OpenGL quad. The quad_cached? method simply returns true or false indicating whether or not the quad associated with the Gosu::Image instance has already been cached.

The second group of methods and constants are to be invoked on the TexPlay module itself. The refresh_cache_all method will refresh the cache for all currently cached quads. Note that it will NOT cache uncached images. The TP_MAX_QUAD_SIZE constant evaluates to the maximum quad size supported by your system and is the quad size that Gosu uses to store images.

Syncing

In a sense Syncing is an opposite operation to Caching (discussed above) as image data flows FROM the local cache TO video memory, whereas with caching it is the other way round.

TexPlay has three syncing modes. These modes are eager_sync,  lazy_sync, and no_sync. As stated earlier, eager_sync is the default syncing mode for all drawing actions that are directly invoked on an image. Similarly, lazy_sync is the default syncing mode for drawing actions that appear in a Paint Block. The no_sync mode is not used by default in TexPlay at all, but must be specified by the user.

eager_sync:

img1.rect 100,100, 200, 200
img1.rect 100, 100, 210, 210

The first rectangle in above  is sync’d in eager mode since it was directly invoked  upon the image. This means that immediately after updating the cached image TexPlay will determine which portion of the image has changed and sync that part to video memory. In this case that portion is  the rectangle [100,100, 200, 200]. A similar case is the second rect above. First the cached image is updated and then the changed portion of the image  ([100,100, 210, 210]) is sync’d to video memory.

lazy_sync:

Compare the above scenario to the following:

img1.paint {
    rect 100,100, 200, 200
    rect 100,100, 210, 210
}

In lazy_sync mode the syncing only happens once (unlike twice in the previous example, one time for each rect). And the portion of the image that has changed is calculated by taking all the drawing actions in the Paint Block into account. In the above example, TexPlay is able to identify that the changed region is determined solely by the second rectangle and so it only syncs the region bounded by this rectangle, and only syncs once.

no_sync:

If for some reason you do not want any syncing to occur you can specify the syncing mode to be no_sync. You do this as follows:

img1.rect 100, 100, 200, 200, :sync_mode => :no_sync

In the above no syncing to video memory will take place and the modifications you have made to the image will not be visible to Gosu. The no_sync mode may in some circumstances provide a performance increase especially if you have a large number of drawing actions to perform.

Note that the :sync_mode parameter accepts  any of the three syncing modes as valid parameters, that is: :eager_sync, :lazy_sync, and :no_sync

As a corollary to no_sync TexPlay also provides a method to manually sync a region to video memory:

img1.force_sync [100, 100, 200, 200]

The :sync_mode parameter can also be given as a parameter to the paint method to override the default lazy_sync mode:

img1.paint(:sync_mode => :no_sync)  {
    rect 100,100, 200, 200
    rect 100,100, 210, 210
}

Macros

Users may also define macros. TexPlay macros are built up from the basic drawing commands; here is an example:

TexPlay.create_macro(:example1) { |x,y|
    circle x,y, 10, :color => :red
    circle x, y, 30, :color => :purple
}

Where ‘example1′ is the name of the macro and the associated block the macro definition.

The macro may then be used like any of the basic drawing commands:

image1.paint {
    example1 20,20
}

You may also remove a defined macro using:


TexPlay.remove_macro(:example1)

If your macro needs to access or manipulate ivars of the Image (that is, your macro has state) then you must surround the macro code in a ‘capture’ block:

TexPlay.create_macro(:line_to) do |x, y, *other|
    capture {
        line(@turtle_pos.x, @turtle_pos.y, x, y, *other)

        @turtle_pos.x, @turtle_pos.y = x, y
    }
end

The above is from the Turtle Graphics code (found in texplay-contrib.rb).

The capture block is necessary so that the macro may be invoked both directly upon the image as well as used in a Paint Block.

If your macro requires initial setup to be done, such as setting up ivars, the TexPlay.on_setup() method should be used:

TexPlay.on_setup do
    @turtle_pos = TexPlay::TPPoint.new(width / 2, height / 2 )
    @turtle_angle = 0
end

The above code will be executed immediately after a new Image is instantiated. In the example, the @turtle_pos and @turtle_angle ivars will be initialized for the new image.

Uses of TexPlay

TexPlay began as a side project while designing a worms-style Gosu game in Ruby; such a game required pixel-level collision detection as well as the ability to ‘blow up’ parts of the landscape. TexPlay was therefore designed primarily to bring this functionality to Gosu. However, TexPlay is also designed as a light-weight alternative to RMagick; RMagick is a very powerful tool but it has caused many deployment nightmares and headaches for people who use it. In contrast TexPlay is easy to deploy, easy to setup, and easy to use.

Download Information

Currently TexPlay has been tested on Windows, Linux and MacOsX systems but it should be possible to compile and run it on other platforms too.

The recommended method is to install the cross-platform gem:

sudo gem install texplay

The github page is here: http://github.com/banister/texplay/tree/master

Follow

Get every new post delivered to your Inbox.