Vector Graphics with Dojo's GFX

Vector graphics can have many advantages, including flawless scaling with maximum portability. The problem with vector graphic creation is that it can be difficult—but not so with Dojo's GFX library. GFX provides a simple and flexible API (along with other utilities) for creating, animating, and managing amazing vector graphics.

Getting Started

Vector graphics—the use of geometric "primitives" or shapes—is a time-honored way of creating images by using mathematical formulae to describe how to render something; unlike raster-based images (such as PNG and JPG files), which use a two dimensional array of colors. Often vector-based images (such as those made with a program like Illustrator or InkScape) are more efficient because they are not rendered until the viewing device interprets the math behind it.

There are several advantages to using vector graphics as opposed to fixed JPG/GIF images:

  • Seamless Scalability: no loss of quality when enlarging or shrinking;
  • Portability: vector graphics are easily portable and may be rendered in many formats (SVG, Canvas, VML, etc.);
  • Programmable: you don't need to be a Photoshop or Illustrator expert to quickly create vector graphics.

Vector graphics have been in use for a long time; one of the most common examples is the use of PostScript to describe how to print something.

The Dojo Toolkit features dojox/gfx (GFX), a vector graphic creation library capable of creating extremely powerful vector graphics. Features of GFX include:

  • Works on almost all devices
  • Can render images (including charts) with SVG, VML, Silverlight, or Canvas.
  • Evaluates the client and uses whichever renderer will work most efficiently
  • Allows for the developer to decide which renderer to use
  • Allows for linear and radial gradients within shapes (and even works in Internet Explorer!)

dojox/gfx also includes experimental SVG rendering for older versions of Internet Explorer through the use of the SVGWeb project.

GFX was created to accomplish visual tasks that are not easily accomplished with basic CSS and HTML, all while avoiding Flash and keeping the API simple.

Creating an Image using dojox/gfx

The following is a general timeline for the creation of most vector graphics:

  1. Create the surface (or "canvas")
  2. Create the shapes (paths, lines, rectangles, text, etc.)
  3. Create groups of shapes (grouping shapes together)
  4. Animate shapes or groups of shapes (transform, scale, etc.)
  5. Add shape events

To use the GFX library, there's one simple resource to require:

require("dojox/gfx", function(gfx) {

});

If a specific rendering priority is preferred, it may be added to the dojoConfig object that is created before loading Dojo:

<script>
    dojoConfig = {
        async: true,
        gfxRenderer: "svg,silverlight,vml" // svg gets priority
    };
</script>
<script src="/path/to/dojo/dojo.js"></script>

With GFX available within the page, let's explore each part of a GFX graphic timeline, focusing on both the concepts and the syntax.

Creating the Surface

We consider the surface to be the "canvas" of the graphic; it hosts all of the graphic's shapes. To create a surface, simply code:

<script>
    // Create a GFX surface
    // Arguments:  node, width, height
    require(["dojox/gfx", "dojo/domReady!"], function(gfx) {
        gfx.createSurface("surfaceElement", 400, 400);
    });
</script>

<!-- DOM element which will become the surface -->
<div id="surfaceElement"></div>

That's all that's needed to create the surface! Each rendering engine (SVG, VML) will generate its own code. For example, the SVG renderer will output:

<svg width="400" height="400"><defs></defs></svg>

...while the VML rendering engine will output:

<group style="position: absolute; width: 400px; display: inline-block; height: 400px">
    <rect style="width: 400px; height: 400px; top: 0px; left: 0px"/>
</group>

...and the Canvas engine will render:

<canvas width="400" height="400"></canvas>

View Demo

Creating Shapes

With a surface created, the next step is creating shapes. GFX provides many shapes, including:

  • Rect: A basic rectangle
  • Circle: A basic circle
  • Ellipse: A basic ellipse, more flexible than a circle
  • Line: A basic line
  • PolyLine: A multi-point line
  • Image: The Image shape allows for loading of bitmap images
  • Text: Allows for creation of text
  • TextPath: A shape that flows text along an arbitrary path. TextPath properties are based on the text shape properties
  • Path: Most versatile geometric shape, which can emulate all other geometric shapes

Text support within the Canvas rendering engine was added in Dojo 1.6—which is especially useful, since Android does not presently support SVG.

dojox/gfx has implemented a factory pattern for shape creation. Creating a shape is as easy as gfx.create_ShapeName_(properties). For example, creating a rectangle would look like:

// Create a basic 200px wide, 100px tall rectangle at position 100x, 50y
var rectangle = surface.createRect({ x: 100, y: 50, width: 200, height: 100 });

When a shape is created, Dojo generates the the necessary objects within the rendering environment and provides references to them for future modification and management. The method from above returns the following object:

Dojo GFX

Rectangle shape properties

Any number of shapes can be created on a given surface. Let's create a series of shapes:

// Create a GFX surface
// Arguments:  node, width, height
var surface = gfx.createSurface("surfaceElement", 400, 400);

// Create a rectangle
surface.createRect({ x: 100, y: 50, width: 200, height: 100 })
    .setFill("yellow")
    .setStroke("blue");

// Add a circle
surface.createCircle({ cx: 100, cy: 300, r:50 })
    .setFill("green")
    .setStroke("pink");

// Now an ellipse
surface.createEllipse({ cx: 300, cy: 200, rx:50, r:25 })
    .setFill("#fff")
    .setStroke("#999");

// And a line
surface.createLine({ x1: 10, y1: 50, x2:400, y2:400 })
    .setStroke("green");

// How about a polyline?
surface.createPolyline([
    {x: 250, y: 250},
    {x: 300, y: 300},
    {x: 250, y: 350},
    {x: 200, y: 300},
    {x: 110, y: 250}
]).setStroke("blue");

// Add in an image
surface.createImage({
    x:100, y:300, width: 123, height: 56, src: "../images/logo.png"
});

// With some text
surface.createText({ x: 64, y: 220, text: "Vector Graphics Rock!", align: "start" })
    .setFont({ family: "Arial", size: "20pt", weight: "bold" }) //set font
    .setFill("blue");

// And an advanced textpath
var textShape = surface.createTextPath({ text: "TextPath!" })
    .moveTo(125, 70)
    .lineTo(285, 20)
    .setFont({ family: "Verdana", size: "2em" })
    .setFill("black");

// And a simple path
surface.createPath("m100 100 100 0 0 100c0 50-50 50-100 0s-50-100 0-100z")
    .setStroke("black");

View Demo

Each shape type has its own creation properties; visit the dojox/gfx reference guide to see options for your specific shape. Note also that Path shapes use the SVG Path syntax when using a string as the main argument.

Shapes generated by dojox/gfx also include numerous methods for modification. A few key methods include:

  • applyTransform: Allows for transforming of a shape (scaling and skewing, for example)
  • getFill/setFill: Get and set fill colors
  • getStroke/setStroke: Get and set stroke colors
  • moveToBack/moveToFront: Moves shapes based on "z-indexing"

More details about these methods will be provided later within this tutorial.

Moving shapes from back to front (and vice-versa) is not quite the same as the z-index in CSS; it depends on the rendering engine being used to draw the shapes.

Styling Shapes

Creating shapes is easy, but more important than creating the shape is making it look good. The shape objects created by dojox/gfx provides a number of methods to change fill, stroke, and font properties. These methods allow the developer to style a shape to their heart's content.

Filling a Shape

The setFill method allows for a named color, hex color, linear gradient, or radial gradient to color (or fill) a shape.

// Create a circle with a set "blue" color
surface.createCircle({ cx: 50, cy: 50, rx:50, r:25 }).setFill("blue");

// Crate a circle with a set hex color
surface.createCircle({ cx: 300, cy: 300, rx:50, r:25 }).setFill("#f00");

// Create a circle with a linear gradient
surface.createRect({ x: 180, y: 40, width: 200, height: 100 }).
setFill({ type:"linear",
    x1:0,
    y1:0,   //x: 0=&gt;0, consistent gradient horizontally
    x2:0,   //y: 0=&gt;420, changing gradient vertically
    y2:420,
    colors: [
        { offset: 0,   color: "#003b80" },
        { offset: 0.5, color: "#0072e5" },
        { offset: 1,   color: "#4ea1fc" }
    ]
});

// Create a circle with a radial gradient
surface.createEllipse({
    cx: 120,
    cy: 260,
    rx: 100,
    ry: 100
}).setFill({
    type: "radial",
    cx: 150,
    cy: 200,
    colors: [
        { offset: 0,   color: "#4ea1fc" },
        { offset: 0.5, color: "#0072e5" },
        { offset: 1,   color: "#003b80" }
    ]
});

View Demo

The colors array accepts objects with offset and color keys. The offset property represents a number between 0 and 1, and the color property represents the color at that offset. You may provide any number of colors objects.

Setting a Stroke on a Shape

The setStroke method styles the shape's stroke (like a border or outline). The setStroke method accepts a color string (hex, named color, rgb, etc.) or an object with more specific stroke properties:

// Create a GFX surface
// Arguments:  node, width, height
var surface = gfx.createSurface("surfaceElement", 400, 400);

// Create a rectangle with a basic black border
surface.createRect({x: 100, y: 50, width: 200, height: 100 }).setStroke("#000");

// Create a circle with a 3-pixel dotted red border
surface.createCircle({ cx: 300, cy: 300, rx: 50, r: 25 }).setStroke({
    style: "Dot", width: 3, cap: "round", color: "#f00"
});

// Create a circle with a 3-pixel dotted red border
surface.createCircle({ cx: 150, cy: 250, rx: 100, r: 50 }).setStroke({
    style: "Dash", width: 3, cap: "butt", color: "#00f"
});

Properties may include:

  • style: the style of the line (solid, dotted, dashed)
  • width: the width of stroke in pixels
  • color: the stroke's color
  • cap: the shape of the end of the stroke

View Demo

Choosing a Font

Both the Text and TextPath shapes allow for a specific font family, size, and weight. Usage of setFont is easy:

// Create the initial text, set the font to 20pt Arial Bold, and fill it blue
surface.createText({ x: 64, y: 220, text: "This is my text", align: "start"}).
setFont({ family: "Arial", size: "20pt", weight: "bold" }).
setFill("blue");

The font properties are formatted and work very much like CSS properties you use every day!

Grouping Shapes Together

Individual shapes may be "glued" together in groups, so that the shapes within a group can be treated like they are a single shape. Groups are especially important when animating related shapes and attaching events to said shapes. The best thing about using groups is that they feature the same animation methods as individual shapes:

// Create a GFX surface
// Arguments:  node, width, height
var surface = gfx.createSurface("surfaceElement", 400, 400);

// Create a group
var group = surface.createGroup();

// Add a shape directly to the group instead of the surface
var rectShape = group.createRect({ x: 0, y: 0, width: 200, height: 100 })
    .setFill("#0000ae");

Shapes can also be added to the group at any time:

// Create the shape on the surface
var rectShape = shape.createRect({ x: 0, y: 0, width: 200, height: 100 })
    .setFill("#0000ae");

// Move it to the group!
group.add(rectShape);

Groups are especially useful when creating moveable shapes:

// Require the resource
require("dojox/gfx", "dojox/gfx/Moveable", function(gfx, Moveable) {

    // Make all shapes within the group move together!
    new Moveable(group);

});

The above snippet allows users to click and hold any shape within the group to move every shape in the group around.

View Demo

Animations and Transformations

The real power of the GFX library lies within its animation capabilities. GFX's animations are extremely powerful and smooth, and capable of many animations—including simple stroke and fill animations, scaling, rotating, and skewing. The dojox/gfx/fx resource was created to allow for both simple and complex animations.

The first step in creating GFX animations is requiring the resource:

// Require the powerful gfx.fx resource
require(["dojox/gfx", "dojox/gfx/fx"], function(gfx, gfxFx) {

});

Transform capabilities are hosted by each individual shape, so no additional resources are required for scaling, skewing, etc.

Fill, Stroke, and Font Animations

The dojox/gfx/fx resource provides animateFill, animateFont, and animateStroke methods for easy animation of each shape property:

// Create a circle, a dojox/gfx.fx instance, play it immediately
var circle = surface.createCircle({ cx: 50, cy: 50, rx: 50, r: 25 })
    .setFill("blue");
gfxFx.animateFill({
    shape: circle,
    duration: 500,
    color: { start: "blue", end: "green" }
}).play();

// Create a rectangle, animate its stroke
var rectangle = surface.createRect({ x: 100, y: 50, width: 200, height: 100 })
    .setStroke("yellow");
gfxFx.animateStroke({
    shape: rectangle,
    duration: 500,
    color: { start: "yellow", end: "pink" },
    width: { end: 15 },
    join:  { values: ["miter", "bevel", "round"] }
}).play();

// Create text, animate it
var text = surface.createText({
    x: 64, y: 220, text: "Vector Graphics Rock!", align: "start"
}).setFont({ family: "Arial", size: "20pt", weight: "bold" })
    .setFill("red");
gfxFx.animateFont({
    shape: text,
    duration: 500,
    variant: { values: ["normal", "small-caps"] },
    size:  { end: 10, units: "pt" },
    color: "green"
}).play();

Each method has its own properties relative to the property (stroke, fill, text) being changed. Also note that gradient backgrounds cannot be animated; solid colors are animated flawlessly.

View Demo

Rotating a Shape

Shape rotation is also incredibly easy with GFX's animation API. The rotateAt and rotategAt animations allow for rotating shapes or groups of shapes around a given center-point. The gfxFx.animateTransform method will assemble the animation, and the play method will start it.

// Create a group
var group = surface.createGroup();

// Create a circle
var circle1 = group.createCircle({ cx: 100, cy: 300, r: 50 }).setFill("green");
var circle2 = group.createCircle({ cx: 100, cy: 100, r: 50 }).setFill("blue");
var circle3 = group.createCircle({ cx: 300, cy: 300, r: 50 }).setFill("black");
var circle4 = group.createCircle({ cx: 300, cy: 100, r: 50 }).setFill("yellow");

// Create an animation of the group
var animation = new gfxFx.animateTransform({
    duration: 700,
    shape: group,
    transform: [{
        name: "rotategAt",
        start: [0, 200, 200], // Starts at 0 degree rotation at center-point 200x200
        end: [360, 200, 200]  // Ends at 360 degrees
    }]
});

// Showtime!
animation.play();

The transform property passed to gfxFx.animateTransform allows for any number of animations to be added.

View Demo

If you are wondering why there are rotateAt and rotategAt methods, it is because geometry with JavaScript is usually radian-based; but most developers find it easier to think in terms of degrees. Both rotate a shape around a specific point on the surface.

Scaling and Skewing

The process of shrinking, enlarging, and skewing GFX graphics is simple. Use the gfx.matrix.scale method to scale the image, providing x and y values for the amount to scale on each axis:

// Double the size of the shape
shapeGroup.applyTransform(gfx.matrix.scale({ x: 2, y: 2 }));

// Shrink the shape to half size
shapeGroup.applyTransform(gfx.matrix.scale({ x: 2, y: 0.5 }));

The applyTransform method of a shape is used to perform the transformation. The graphic will be flawlessly resized! Skewing (transforming or moving points of a shape along a single axis) is just as easy:

// Skews the group at -20 degrees
shapeGroup.applyTransform(gfx.matrix.skewYg(-20));

View Demo

The gfx.matrix resource contains numerous helpers to invert, rotate, scale, and skew shapes so that you don't need to know the complex math equations behind setting up any shape modifications or transformations.

Events

Events are particularly important in dojox/gfx as they allow for triggering movement and display changes within groups. Many of the charting plugins are triggered by events on GFX-created shapes. You can add events to GFX-created nodes or groups using shape.on and group.on, which extends dojo/on.

shape.on

The shape.on method works very much like a native event handler. Provide a shape and event type:

// Add a circle
var circle = group.createCircle({ cx: 100, cy: 300, r: 50 })
    .setFill("green").setStroke("pink");

// Add a click event to the circle to change its fill!
circle.on("click", function(e) {
    circle.setFill("red");
});

group.on

The group.on method of GFX groups allows for dojo/on-style event listeners:

// Get the eventSource to find out what element the event occurred on
group.on("click", function(e) {
    //our shape was clicked, now do something!
}, true);

The event object is very much like a standard DOM event object. The target property provides the GFX-generated DOM element which was clicked.

View Demo

> The Silverlight rendering engine supports the following events: onclick, onmouseenter, onmouseleave, onmousedown, onmouseup, onmousemove, onkeydown, and onkeyup. If you want to target the broadest range of renderers, you are advised to restrict yourself to this list of events.

Create the Dojo Logo with GFX

If we have the Dojo Toolkit logo in an SVG format, the path information describing the shapes of that logo can be extracted and used to create a GFX-based image. The following is a portion of that SVG file:

<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="1100px" height="700px" viewBox="0 0 1100 700" enable-background="new 0 0 1100 700" xml:space="preserve">
    <g>
        <g>
            <path fill="#010101" d="M826.698,536.736v11.722h12.71v6.758h-12.71v26.25c0,6.065,1.718,9.483,6.659,9.483    c2.427,0,3.834-0.203,5.147-0.61l0.406,6.759c-1.721,0.621-4.439,1.211-7.876,1.211c-4.145,0-7.466-1.404-9.575-3.729    c-2.429-2.729-3.442-7.062-3.442-12.82v-26.555h-7.576v-6.756h7.576v-8.996L826.698,536.736z"/>
            <path fill="#010101" d="M868.708,598.43c-13.119,0-23.418-9.695-23.418-25.142c0-16.354,10.801-25.938,24.225-25.938    c14.039,0,23.525,10.196,23.525,25.036c0,18.175-12.623,26.05-24.229,26.05h-0.103V598.43L868.708,598.43z M869.115,591.764    c8.481,0,14.846-7.975,14.846-19.089c0-8.267-4.146-18.686-14.643-18.686c-10.396,0-14.931,9.694-14.931,18.976    c0,10.717,6.052,18.783,14.638,18.783h0.09V591.764L869.115,591.764z"/>
            <path fill="#010101" d="M924.162,598.43c-13.119,0-23.406-9.695-23.406-25.142c0-16.354,10.801-25.938,24.213-25.938    c14.039,0,23.517,10.196,23.517,25.036c0,18.175-12.611,26.05-24.216,26.05h-0.106L924.162,598.43L924.162,598.43z     M924.574,591.764c8.487,0,14.834-7.975,14.834-19.089c0-8.267-4.129-18.686-14.638-18.686c-10.395,0-14.94,9.694-14.94,18.976    c0,10.717,6.063,18.783,14.643,18.783h0.103L924.574,591.764L924.574,591.764z"/>

            <!-- more SVG below this shape... -->

        </g>
    </g>
</svg>

Judging by the SVG above, it's easy to deduce that:

  • The canvas is approximately 1100 pixels wide and 700 pixels tall.
  • The letters should be drawn with paths (surface.createPath)
  • The fill color of each letter (per the logo) is #010101. This example will use a gradient fill, however.

Using the path information in the logo is simple:

// Arguments:  node, width, height
var surface = gfx.createSurface("surfaceElement",1100,700);

// Regular fill
var regularFill = { type: "linear", x1: 0, y1: 0, x2: 0, y2: 900, colors: [{ offset: 0, color: "#555" }, { offset: 1, color: "#000"}] };

// Create group too contain each letter of "toolkit"
var tkGroup = surface.createGroup();

// Tiny letter "t" in "toolkit"
var letterToolkitT = tkGroup.createPath("M826.698,536.736v11.722h12.71v6.758h-12.71v26.25c0,6.065,1.718,9.483,6.659,9.483    c2.427,0,3.834-0.203,5.147-0.61l0.406,6.759c-1.721,0.621-4.439,1.211-7.876,1.211c-4.145,0-7.466-1.404-9.575-3.729    c-2.429-2.729-3.442-7.062-3.442-12.82v-26.555h-7.576v-6.756h7.576v-8.996L826.698,536.736z").setFill(regularFill).setStroke("#000");

// "o"
var letterToolkitO1 = tkGroup.createPath("M868.708,598.43c-13.119,0-23.418-9.695-23.418-25.142c0-16.354,10.801-25.938,24.225-25.938    c14.039,0,23.525,10.196,23.525,25.036c0,18.175-12.623,26.05-24.229,26.05h-0.103V598.43L868.708,598.43z M869.115,591.764    c8.481,0,14.846-7.975,14.846-19.089c0-8.267-4.146-18.686-14.643-18.686c-10.396,0-14.931,9.694-14.931,18.976    c0,10.717,6.052,18.783,14.638,18.783h0.09V591.764L869.115,591.764z").setFill(regularFill).setStroke("#000");

// More "letter" shapes here...

When all of the paths are drawn to the surface, the following vector graphic will be created:

Dojo GFX Logo

GFX logo created from SVG paths

When the information describing the Dojo Toolkit logo is loaded, it may be converted to any of the supported renderers effortlessly by changing GFX's default rendering engine order. The graphic shapes and properties may also be animated or modified as desired. This demo uses many of the animation techniques described in this tutorial to modify and animate the Dojo Toolkit logo.

View Demo - Dojo Logo

View Demo - London Ajax Logo

Conclusion

Dojo's GFX library provides the ability to create simple vector graphics or more complex vector graphic groups. The Dojo Toolkit's advanced charting and drawing libraries are based on the power of GFX. No matter what your medium, the Dojo Toolkit provides an easy to use API for creating, animating, and managing your vector graphics!

GFX Resources

Looking for more detail about Dojo's GFX library? Check out these great resources: