Platinum Partner
groovy,java2d,graphics

GraphicsBuilder Tutorial I: Shapes

Drawing graphics with Java2D has never been an easy task, if it was we have probably seen by now a Flex killer app in Java (the fact that applets have a really bad press also contributed to not having that app). Let me introduce you to GraphicsBuilder, a Groovy builder for Java2D, while it alone is not the killer app, it surely can help you build one with less effort than doing it with straight Java.

Introduction

If you are still wondering what is Groovy and what is its relation to Java, I recommend you take a look at From Java to Groovy in a few easy steps and other articles you may find in this site. Back already? let's continue with the topic at hand. GraphicsBuilder is a builder for Java2D, which means that every node in the builder represents an action that will affect a Graphics2D object, the same type of object Swing uses to draw components on the screen. To get you started you should have Groovy installed in your computer as well as the latest GraphicsBuilder distribution (0.4.5 at the time of this writing), if you are on windows using the installer makes it pretty trivial as graphicsbuilder comes bundled with it.

If you have played around with Groovy for a while you surely have come across with a handy tool named groovyConsole, a visual shell that evaluates groovy scripts, well GraphicsBuilder includes a similar tool named graphicsPad that will let you write and run groovy scripts for drawings. All of the code samples you will see on this tutorial can be executed on graphicsPad, the tool also has an option to export your scripts into standalone scripts, which can be run from the command line or with groovyConsole itself.

Basic Shapes

Java2D provides a small set of basic shapes which mostJava based graphic tools support, GraphicsBuilder does the same, the following image shows a sample of those shapes

[img_assist|nid=684|title=|desc=|link=none|align=middle|width=270|height=210]


Let's see, we have rectangles (angular and rounded), circles, ellipses, arcs, polygons and paths. Arcs may have 3 different closing modes and paths may have 2 different winding values, that is why the last star has a hole in the middle. Now let's take a look at the code that produced this image

rect( x: 10, y: 10, width: 50, height: 50, borderColor: 'black', fill:'red' )
rect( x: 70, y: 10, width: 50, height: 50, arcWidth: 20, arcHeight: 20,
borderColor: 'black', fill:'orange' )
circle( cx: 155, cy: 35, radius: 25, borderColor: 'black', fill: 'darkGreen' )
ellipse( cx: 225, cy: 35, radiusx: 35, radiusy: 25, borderColor: 'black', fill: 'blue' )
arc( x: -40, y: 70, width: 100, height: 100, start: 0, extent: 90,
borderColor: 'black', fill: 'cyan', close: 'pie' )
arc( x: 20, y: 70, width: 100, height: 100, start: 0, extent: 90,
borderColor: 'black', fill: 'magenta', close: 'chord' )
arc( x: 80, y: 70, width: 100, height: 100, start: 0, extent: 90,
borderColor: 'black', fill: 'yellow', close: 'open' )
polygon( points: [190,70,225,90,260,70,250,120,225,110,200,120],
borderColor: 'black', fill: 'black' )
path( borderColor: 'black', fill: 'purple', winding: 'nonzero' ){
moveTo( x: 40, y: 130 )
lineTo( x: 20, y: 200 )
lineTo( x: 70, y: 158 )
lineTo( x: 10, y: 158 )
lineTo( x: 60, y: 200 )
close()
}
path( borderColor: 'black', fill: 'lime', winding: 'evenodd' ){
moveTo( x: 120, y: 130 )
lineTo( x: 100, y: 200 )
lineTo( x: 150, y: 158 )
lineTo( x: 90, y: 158 )
lineTo( x: 140, y: 200 )
close()
}

You will notice that all shapes share similar properties, like borderColor and fill, that colors are defined as strings (but there are more powerful ways to get a custom color, we will see that on a later tutorial) following the css2 color set. The most complex shape so far is path because each segment must be constructed with a path operation, all path operations are defined in GeneralPath/Path2D so their names are pretty straightforward.

Extended Path

GraphicsBuilder supports an extended version of path, provided by Batik project (graphicsbuilder-ext-svg and batik libraries are required in the classpath), the main difference is that xpath has an xarcTo operation capable of drawing arcs as opposed to curves only.

[img_assist|nid=685|title=|desc=|link=none|align=middle|width=340|height=90]

Note that you can not mix path and xpath operations in the same path.

xpath( borderColor: 'darkRed', fill: 'red', borderWidth: 4 ){
xmoveTo( x: 10, y: 10 )
xhline( x: 160 )
xvline( y: 60 )
xarcTo( x: 10, y: 60, rx: 150, ry: 150, angle: 90, sweep: true, largeArc: false )
xclose()
}
xpath( borderColor: 'darkRed', fill: 'red', borderWidth: 4 ){
xmoveTo( x: 170, y: 10 )
xhline( x: 320 )
xvline( y: 60 )
xarcTo( x: 170, y: 60, rx: 150, ry: 150, angle: 90, sweep: false, largeArc: false )
xclose()
}

Polygons and Donuts

We have seen that GraphicsBuilder has a polygon shape which can draw polygons, but what about regular polygons? those shapes that have a fixed set of sides and can be inscribed in a circle, like a pentagon or hexagon. With only the basic polygon shape you will have to compute all the points over and over again, its good then that GraphicsBuilder provides a base shape for this type of polygons.

[img_assist|nid=686|title=|desc=|link=none|align=middle|width=430|height=210]

def colors = ['red','darkOrange','blue','darkGreen']
(0..3).each { index ->
regularPolygon( cx: 50 + (index*110), cy: 50, radius: 40,
borderColor: 'black', sides: 3+index, fill: colors[index] )
regularPolygon( cx: 50 + (index*110), cy: 160, radius: 40,
borderColor: 'black', sides: 7+index, fill: colors[index] )
}

Here you can see a great feature of builders in Groovy, you can mix any groovy sentence with your building nodes. What we have here is a range iterated over each element, the value is used to calculate the offset of the next shape and the number of sides the polygon will have. Because these shapes are centered you may be tempted to draw a smaller shape in the same center and create a donut, GraphicsBuilder helps you again with a shape that does that, which will produce a circular donut if you do not specify a sides property.

[img_assist|nid=687|title=|desc=|link=none|align=middle|width=430|height=210]

def colors = ['red','darkOrange','blue','darkGreen']
(0..3).each { index ->
donut( cx: 50 + (index*110), cy: 50, or: 40, ir: 20,
borderColor: 'black', sides: 3+index, fill: colors[index] )
donut( cx: 50 + (index*110), cy: 160, or: 40, ir: 20,
borderColor: 'black', sides: 7+index, fill: colors[index] )
}

The only thing we changed from the previous example was the node name, from regularPolygon to donut and that's it.

Stars and Rays

Having code that can calculate the points for a regular polygon enables drawing other shapes that share similar properties, for example stars which have the same number of points as a polygon donut, but this time the inside shape is rotated in a way where each vertex is pointing to the corresponding middle point in the outer shape segments.

[img_assist|nid=688|title=|desc=|link=none|align=middle|width=430|height=190]

def colors = ['red','darkOrange','blue','darkGreen']
(0..3).each { index ->
star( cx: 50 + (index*110), cy: 50, or: 40, ir: 15, borderColor: 'black',
count: 2+index, fill: colors[index] )
star( cx: 50 + (index*110), cy: 140, or: 40, ir: 15, borderColor: 'black',
count: 7+index, fill: colors[index] )
}

Making a comparison against donuts, stars have a count property instead of sides, aside from that they have the same properties. What would happen if you draw lines from the center to each point of a regular polygon, joining the odd and even points with a segment, and leaving the event to odd joins out? you'll get a ray like shape.

[img_assist|nid=689|title=|desc=|link=none|align=middle|width=430|height=370]

def colors = ['red','darkOrange','blue','darkGreen']
(0..3).each { index ->
rays( cx: 50 + (index*110), cy: 50, radius: 40,
borderColor: 'black', rays: 2+index, fill: colors[index] )
rays( cx: 50 + (index*110), cy: 140, radius: 40, rounded: true,
borderColor: 'black', rays: 2+index, fill: colors[index] )
rays( cx: 50 + (index*110), cy: 230, radius: 40, extent: 0.75,
borderColor: 'black', rays: 2+index, fill: colors[index] )
rays( cx: 50 + (index*110), cy: 320, radius: 40, extent: 0.75, rounded: true,
borderColor: 'black', rays: 2+index, fill: colors[index] )
}

Again shared properties with donuts and stars. Rays can have around segment between points instead of a linear one. The angle of each ray can also be changed with an extent property, whose value must be in the range [0..1].

Triangles

In the last batch of geometrical shapes we find triangles: equilateral, isosceles and rectangle (scalene triangles can be drawn with a polygon operation). Triangles must have at least a set of coordinates (x,y) and a width property defined (will result in an equilateral triangle), if a height property is specified then it may be an isosceles triangle if a rightAngleAt property is not set, which would result in a rectangle triangle.

[img_assist|nid=690|title=|desc=|link=none|align=middle|width=260|height=140]

triangle( x: 10, y: 50, width: 50, borderColor: 'black', fill: 'red' )
triangle( x: 70, y: 50, width: 50, height: 20, borderColor: 'black', fill: 'orange' )
triangle( x: 130, y: 50, width: 50, rightAngleAt: 'start',
borderColor: 'black', fill: 'darkGreen' )
triangle( x: 190, y: 50, width: 50, rightAngleAt: 'end',
borderColor: 'black', fill: 'blue' )
triangle( x: 10, y: 90, width: 50, borderColor: 'black', fill: 'red', angle: 315 )
triangle( x: 70, y: 90, width: 50, height: 20,
borderColor: 'black', fill: 'orange', angle: 315 )
triangle( x: 130, y: 90, width: 50, rightAngleAt: 'start', angle: 315,
borderColor: 'black', fill: 'darkGreen' )
triangle( x: 190, y: 90, width: 50, rightAngleAt: 'end', angle: 315 ,
borderColor: 'black', fill: 'blue' )

All the shapes in this section (from polygons to triangles) accept an angle property to perform quick rotations. All shapes accept transformations like scaling and rotation which we will explore in another tutorial.

Arrows

If you thought we would be finished by now, there are other shapes we can explore! Arrows can be found almost anywhere so it makes sense to have a shape for them. Arrows have a tail and a head, depth controls how much the tail and head share from the total width, rise controls how much the tail takes from the total height, finally arrows can also be quick rotated with an angle property.

[img_assist|nid=691|title=|desc=|link=none|align=middle|width=430|height=220]

def values = [0,0.25,0.5,0.75]
def colors = ['red','darkOrange','blue','darkGreen']
def angles = [0,45,150,240]
(0..3).each { index ->
arrow( x: 10 + (index*110), y: 10, width: 80, height: 50, depth: values[index],
borderColor: 'black', fill: colors[index] )
arrow( x: 10 + (index*110), y: 80, width: 80, height: 50, rise: values[index],
borderColor: 'black', fill: colors[index] )
arrow( x: 10 + (index*110), y: 150, width: 90, height: 50, angle: angles[index],
borderColor: 'black', fill: colors[index] )
}

Crosses

If arrows are common, crosses are too (nothing related to religious ones), you can find them as plus signs or as error indicators. they can have a round finish and can also be rotated with an angle property.

[img_assist|nid=692|title=|desc=|link=none|align=middle|width=320|height=210]

def roundness = [0,0.5,1]
def colors = ['red','blue','darkGreen']
def angles = [0,45,60]
(0..2).each { index ->
cross( cx: 50 + (index*110), cy: 50, radius: 40, borderColor: 'black',
roundness: roundness[index], fill: colors[index] )
cross( cx: 50 + (index*110), cy: 160, radius: 40, borderColor: 'black',
roundness: roundness[index], fill: colors[index], angle: angles[index] )
}

Pins

Pins can be described as a mix between a semi-circle and an isosceles triangle, you may find a similar shape while using an online map provider known for it's "do no evil" mantra. Pins can also be rotated with an angle property (see a trend here :-)) and they can change the height of the triangle.

[img_assist|nid=693|title=|desc=|link=none|align=middle|width=330|height=210]

def heights = [40,10,60]
def colors = ['red','blue','darkGreen']
def angles = [0,45,60]
(0..2).each { index ->
pin( cx: 50 + (index*110), cy: 50, radius: 40, borderColor: 'black',
height: heights[index], fill: colors[index] )
pin( cx: 50 + (index*110), cy: 160, radius: 40, borderColor: 'black',
height: heights[index], fill: colors[index], angle: angles[index] )
}

Multi Rounded Rectangles

The last shape (or is it?) is the multi rounded rectangle. It gives you more control over how each corner will or will not be rounded. Opposed to just arcWidth and arcHeight properties found on the rect shape, roundRect allows 2 round properties for each corner.

[img_assist|nid=694|title=|desc=|link=none|align=middle|width=310|height=120]

roundRect( x: 10, y: 10, width: 140, height: 100, borderColor: 'black',
topLeftWidth: 20, bottomLeftWidth: 20, topRightWidth: 50, bottomRightWidth: 50,
fill: 'red' )
roundRect( x: 160, y: 10, width: 140, height: 100, borderColor: 'black',
topLeftWidth: 20, topLeftHeight: 40, bottomLeftWidth: 40, bottomLeftHeight: 20,
topRightWidth: 0, topRightHeight: 0, bottomRightWidth: 60, bottomRightHeight: 70,
fill: 'blue' )
}

 

Areas

If the previous list of predefined shapes is not enough for your drawings then it will be worth mentioning that you can create complex shapes based on simpler ones by applying any of the following area operations: add, subtract, intersect, xor. Area operations in GraphicsBuilder require at least 2 shapes, as you can stack more than 2 as exemplified by the following images. Let's start with a group of 3 shapes with no area operation applied to them

[img_assist|nid=700|title=|desc=|link=none|align=middle|width=220|height=150]

group( borderColor: 'blue', borderWidth: 4, fill: 'cyan' ){
rect( x: 10, y: 10, width: 200, height: 80, arcWidth: 20, arcHeight: 20 )
circle( cx: 80, cy: 70, radius: 50 )
regularPolygon( cx: 135, cy: 90, radius: 60, sides: 5, angle: 90 )
}

Now onto the operations, to get the desired effect substitute group from the previous code with the operation's name

[img_assist|nid=701|title=|desc=|link=none|align=middle|width=220|height=150]

ADD

[img_assist|nid=702|title=|desc=|link=none|align=middle|width=220|height=150]

SUBTRACT

[img_assist|nid=703|title=|desc=|link=none|align=middle|width=220|height=150]

INTERSECT

[img_assist|nid=704|title=|desc=|link=none|align=middle|width=220|height=150]

XOR

Conclusion

This is all for the first part, there are two more shapes yet to be explored (morph and text) as well as outlines (curves and lines) which we will look further in the next installment of this series. I hope you enjoy playing with the code found on this tutorial, please feel free to drop by the forums or leave a comment, feedback is always appreciated.

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}