Baby X logo

The Baby X Graphics Context

The graphics context is used for more complicated 2D graphics. A graphics context is essentially a store of state. This has two purposes, to reduce the number of parameters passed to a graphics drawing function, and to allow code to be reused by calling it in different contexts. For example, if you have code to stamp a tree, you could call it with different transform matrices to create a forest of trees.

The basic use of the graphics context is as follows

unsigned char *rgba;
int width, height;
BBX_Canvas *can; /* create this elsewhere */
BBX_GC *gc; /* the graphics context */

rgba = bbx_canvas_rgba(can, &width, &height);
gc = bbx_graphicscontext(rgba, width, height);
bbx_gc_setfillcolor(gc, bbx_color("white"));
bbx_gc_fillrect(gc, 0, 0, width, height);
bbx_gc_setfillcolor(gc, bbx_color("red"));
bbx_gc_fillcircle(gc, width/2.0, height/2.0 width/2.0 - 5.0);
bbx_graphicscontext_kill(gc);
bbx)canvas_flush(can);

Note that we need to obtain the rgba buffer from the canvas, creare the graphics context off it, draw, then destroy the graphics context. This writes out any remaining graphics commands to the buffer. Then we have to flush the buffer to the canvas. The graphics context and the rest of Baby X are thus looselyy coupled. You can take the graphics context code as is and use it in non-Baby X projects if you desire.

BBX_GC *bbx_graphicscontext(unsigned char *rgba, int width, int height);
void bbx_graphicscontext_kill(BBX_GC *gc);

This is the graphics context constructor and destructor. The constructor takes an rgba buffer which is managed by caller. You shouldn't attempt to free it whilst the graphics context is live. The destructor is important as it does not just free memory, it also writes the cached commands out to the rgba buffer.

Basic usage

The graphics context is based on the concept of lines or curves, having strokes and fills. You begin drawing a shape with bbx_gc_beginpath(), you use bbx_gc_moveto() to move the draw position to a new point, and bbx_gc_lineto() or one of the curve-drawing functions to add geometry to the path. You then use bbx_gc_closepath() to terminate the shape. The shape can conatin several bbx_gc_moveto() commands, it doesn't have to be a single contiguous line. However these commands don't actually draw anything. To draw something, you need to set the stroke or the fill, using bbx_gc_setfillcolor() as the simplest coma nd in this set. Then finally yu need to stroke or fill it with bbx_gc_stroke() or bbx_gc_fill(), which actually causes a drawing command to be issued.

A typical series of commands will looke something like this.

BBX_GC *gc;

bbx_gc_beginpath(gc);
/* draw a triangle */
bbx_gc_moveto(gc, 100, 100);
bbx_gc_lineto(gc, 110, 100);
bbx_gc_lineto(gc, 105, 110);
bbx_gc_lineto(gc, 100, 100);
bbx_gc_closepath(gc);

/* set up our strokes and fills */
bbx_gc_setstrokecolor(gc, bbx_color("black"));
bbx_gc_setstrokewidth(gc, 2.0);
bbx_gc_setlinejoin(gc, "miter");
bbx_gc_setfillcolor(gc, bbx_color("yellow"));
/* now fill first, stroke over the fill */
bbx_gc_fill();
bbx_gc_stroke();

/* we've drawn our triangle, now discard it and move on to our next shape */
bbx_gc_beginpath();

The graphics context caches our path as state for us, and we build it up in small segments.

Path construction commands

void bbx_gc_addcubic(BBX_GC *gc, double x1, double y1, double x2, double y2, double x3, double y3);
void bbx_gc_addquadratic(BBX_GC *gc, double x1, double y1, double x2, double y2);
void bbx_gc_lineto(BBX_GC *gc, double x, double y);
void bbx_gc_beginpath(BBX_GC *gc);
void bbx_gc_moveto(BBX_GC *gc, double x, double y);
void bbx_gc_closepath(BBX_GC *gc);

void bbx_gc_arc(BBX_GC *gc, double cx, double cy, double r, double stheta, double etheta, int ccw);
void bbx_gc_arcto(BBX_GC *gc, double tx1, double ty1, double x2, double y2, double r);

These are the fundamental path-construction functions. Each path is started with a call to gc_beginpath(). However there can be several or no calls to bbx_gc_closepath(). The path is terminated by the next call to bbx_gc_beginpath(), bbx_gc_closepath() simply turns the subpath we are on into a closed curve that can take a fill, and has co-incident start points and end points.

bbx_gc_moveto() effectively starts a new sub-path. bbx_gc_lineto is the simplest of the geometry cnstruction routines, and simply adds a straight line from the current point to the destination point. bbx_gc_addcubic() adds a cubic Bezier curve. The first Bezier point is given by the current point, the last Bezier point is the last two arguments, and the middle four arguments are the two control points. bbx_gc_addquadratic() does the same for a quadratic Bezier. Most commercial vector graphics programs are inf act built from cubic Beziers, though quadratic Beziers are used in fonts.

bbx_gc_arc() and bbx_gc_arcto() are designed to make hand construction of courved object a bit easier. bbx_gc_arc draws an arc centred at cx, cy. Internally it issues a bbx_gc_moveto() the starting point. bbx_gc_arcto() takes the terminating position, the radius, and a point on the arc that the arc should pass through.

Path appearance commands

void bbx_gc_setstrokecolor(BBX_GC *gc, BBX_RGBA col);
void bbx_gc_setstrokewidth(BBX_GC *gc, double width);
void bbx_gc_setfillcolor(BBX_GC *gc, BBX_RGBA col);
void bbx_gc_setfillgradient(BBX_GC *gc, BBX_GRADIENT *g);
void bbx_gc_setfillpattern(BBX_GC *gc, BBX_PATTERN *p);
void bbx_gc_setstrokegradient(BBX_GC *gc, BBX_GRADIENT *g);
void bbx_gc_setstrokepattern(BBX_GC *gc, BBX_PATTERN *p);
void bbx_gc_setlinejoin(BBX_GC *gc, const char *jointype);
void bbx_gc_setlinecap(BBX_GC *gc, const char *capstyle);

A path consists of a stroke and a fill. The stroke is drawn along the path, the fill is drawn in the area it encloses. It is possible and not uncommon to use either only stroke or only fill. It is also possible, though less common, to have more than one stroke or more than one fill on a path. As well as simple coloured strokes and fills, Baby X also supports patterns and gradients.

You set the stroke color wth bbx_gc_setstrokecolor() and set the line width with bbx_gc_setlinewidth(). Line width doesn't have to be a whole number, strokes will be anti-aliased. You can also set the line joining type ("miter", "round" or "bevel") and you can set the cap type ("butt", "round", or "square"). You can also set a gradient on a stroke with a BBX_GRADIENT object constructed via a call to bbx_createlineargradient() or bbx_createradialgradient(). Note that none of these commands actually draw anything, until you issue a bbx_gc_stroke() command.

You can also set the fill colour, the fill gradient, or you can set a bitmap to be used as a fill pattern, via a call to bbx_createpattern(). it is not currently possible to set a pattern on a stroke. Again, none of these commands actually drawe anything, until bbx_gc_fill() is called.

Patterns and gradients

Patterns and gradients are independent Baby X objects. They are not created off the graphics context. So you can cache them for use between contexts. The graphics context makes a local copy of them when it uses them, so you can destroy a BBX_PATTERN or a BBX_GRADIENT any time you have finished with it.

BBX_GRADIENT *bbx_createlineargradient(double x1, double y1, double x2, double y2);
BBX_GRADIENT *bbx_createradialgradient(double x1, double y1, double r1, double x2, double y2, double r2);
BBX_GRADIENT *bbx_gradient_clone(BBX_GRADIENT *grad);
void bbx_gradient_kill(BBX_GRADIENT *g);
int bbx_gradient_addcolorstop(BBX_GRADIENT *g, double offset, BBX_RGBA col);

A colour gradient is a smoothly-varying area of colour, with the values defined by colourstops. The linear gradient pases in a pair of x, y co-ordiantes, by absolute values. The colur of the object then varies accord to its perpendicular distance along the gradient. A radial gradient is similar, but the colour vaies according to distance from two circles. Normally x1, y1 and x2, y2 are the samewith a radial gradient and only the radii differ. A gradient is useless unless you add at least two color stops, of which normally one will be at the gradient start (0.0) and one at the gradient end (1.0). You need to destroy a BBX_GRADIENT after you have finished with it.

BBX_PATTERN *bbx_createpattern(unsigned char *rgba, int width, int height, int filltype);
BBX_PATTERN *bbx_pattern_clone(BBX_PATTERN *pat);
void bbx_pattern_kill(BBX_PATTERN *p);

A pattern is an image, usually of a small repetitive piece of art, which is used to fill shapes. Patterns can only be applied to fills, not to strokes. Though the interface is quite simple, patterns are probably the most powerful operation in the graphics context. Filltype is currently unimplemented, all patterns just repeat. Patterns can contain alpha, however. Patterns with alpha are also a case where you might want more than one fill on a path, one to set the background, one to stamp the image.

Matrix operations

The graphics context comes with a full set of matrix operations.
void bbx_gc_rotate(BBX_GC *gc, double theta);
void bbx_gc_translate(BBX_GC *gc, double x, double y);
void bbx_gc_scale(BBX_GC *gc, double s);
void bbx_gc_scalexy(BBX_GC *gc, double sx, double sy);
void bbx_gc_transform(BBX_GC *gc, double a, double b, double c, double d, double e, double f);
void bbx_gc_settransfrom(BBX_GC *gc, double a, double b, double c, double d, double e, double f);

Matrices allow you to use pre-generated path co-ordinates in the graphics context. Generally, the pre-generated co-orinates center an object on 0,0. The matrix operations are then called to translate, rotate, and scale it to the desired position. Then the path construction functions are called. You will often want to use matrix operations in conjunction with bbx_gc_save() and bbx_gc_restore() to implement a transform stack.

Generally the three operations of rotation, translation, and scale are sufficient. However internaly these are implemented by a 3 x 2 two dimensional transform matrix. bbx_gc_transform lets you apply another matrix to the existing matrix. e and f are the translation component, and a, b, c, d are the rotation and scaling. the matrix is 3 x 2 instead of 3 x 3 because the last row is hard-coded to 0, 0, 1, and the extra value to add to x, y is hard-coded to one. This allows us to implement translation via a matrix multiply operation. bbx_gc_settransform() overwrites the existing matrix instead of multiplying it.

If you are unfamiliar with grahics matrices, you can ignore the matrix fucntions and the graphics context will work as expected. if you are familiar with them, you wil probably already understand what these functions do.

Drawing

There are two functions that actually draw and one that affects drawing.

void bbx_gc_fill(BBX_GC *gc);
void bbx_gc_stroke(BBX_GC *gc);
int bbx_gc_clip(BBX_GC *gc);

None of the graphics commands so far actually draw anything. All they do is set the state of the graphics context, by loading it up with paths, transform matrices, and fill and stroke characteristics. bbx_gc_fill() draws a fill with the curent path, bbx_gc_stroke() draws a stroke. If you need two fills or two strokes on a path, set different fill or stroke parameters,a nd call twice. bbx_gc_clip() tells the graphics cntext to use the current path as a clip mask.

Convenience functions

These functions don't work in the same way as the other graphics context function, and are provided for efficiency and convenience.

void bbx_gc_rect(BBX_GC *gc, double x, double y, double width, double height);
void bbx_gc_fillrect(BBX_GC *gc, double x, double y, double width, double height);
void bbx_gc_strokerect(BBX_GC *gc, double x, double y, double width, double height);
int bbx_gc_clearrect(BBX_GC *gc, double x, double y, double width, double height);
void bbx_gc_circle(BBX_GC *gc, double cx, double cy, double r);
void bbx_gc_fillcircle(BBX_GC *gc, double cx, double cy, double r);

The plain functions bbx_gc_rect() and bbx_gc_circle() are path construction functions. You need to set the stroke and/or fill colour, then call bbx_gc_stroke() or bbx_gc_fill(). bbx_gc_strokerect(), bbx_gc_fillrect(), and bbx_gc_fillcircle() also wrap these calls. However you still need to set the fill color or the stroke color.

void bbx_gc_drawimage(BBX_GC *gc, unsigned char *rgba, int width, int height, int x, int y);
void bbx_gc_drawimagex(BBX_GC *gc, unsigned char *rgba, int width, int height,
	int sx, int sy, int swidth, int sheight, int dx, int dy, int dwidth, int dheight);

These functions are for drawing images directly into the graphics context. The first is for the common situation where you want to draw an axis-aligned, pixel-aligned image, ignoring any matrix transforms. The second is where you want to be matrix-aware,and possibly draw a sub-image. So as well as the image width and height, we take sx, sy, source top left co-ordinates, swidth, sheight, source sub-image width and height, and dx, dy, destination top left, and dwidth, dheight, the width and height of the destination image.

Graphics Context State

A graphics context is a state machine. So sometimes it is desireable to run it as a stack, pushing and popping the state. This is particularly the case if you use complex sequences of matrix transforms.

int bbx_gc_save(BBX_GC *gc);
int bbx_gc_restore(BBX_GC *gc);

This saves the state and restores the state, in a stack-like manner. If you want to make graphics context fucntions re-usable, you should probably save the state at function entry and restore it at function exit. The functions return 0 on success or -1 on error.