[dojo-contributors] II: Unified 2D graphics subsystem for Dojo

Eugene Lazutkin eugene at lazutkin.com
Sun Jun 11 19:55:14 EDT 2006


Foreword
========

This is the major rewrite of the original proposal based mostly on 
feedback from Gavin Doughtie and Tom Trenka.

I skip Preamble. You can look it up in the original proposal:
http://article.gmane.org/gmane.comp.web.dojo.devel/1802/

After extensive consultations we strive to support two scenarios:

1) The drawing starts as a template written in an SVG subset (SVG 
Tiny?). In case of IE, SVG is translated to VML using XSLT preserving 
attach points. The widget author can modify elements using the provided API.

2) The drawing is created from scratch. It is created using the provided 
API.

Our main target is SVG. VML is supported using a translation layer. 
Canvas can be targeted later using the proposed API definition.

I tried hard not to over-constrained the implementation --- a lot of 
things are defined informally or skipped. I expect them to be defined 
during the implementation process --- I eat my dog food and follow my 
own advice (http://lazutkin.com/blog/2006/apr/2/what-programmers-do/, 
see #2, #3, and #4).

Proposed API
============

I will use dojo.gfx as a package name mainly because it is short. 
Possibly the API will be split across several packages.

Renderer-independent objects
----------------------------

Several objects were identified as renderer-independent. They describe
declaratively different aspects of  the graphics: Fill, Stroke, Matrix, 
and Font. We need a Color object with alpha channel (opacity), which is 
already provided by dojo.graphics.color. We need a Coordinate object, 
which I propose to represent with an array of two numeric values [x, y], 
or a dictionary {x: 1, y: 2}.

All renderer-independent methods define trivial self-inspection methods,
which were omitted for brevity.

dojo.math package deals with coordinates (point.js) and matrices 
(matrix.js). In my opinion it is too generic for our purposes, 
especially matrix operations. I think that array of arrays is an 
overkill for 6-value 2D matrix. Personally I like declarativeness of 
dictionaries and would prefer something like that:

c1 = {x: 1, y: 2};
c2 = [1, 2];
m1 = {xx: 1, yy: 1, dx: -1, dy: 2}; // missing values are assumed to be 0
m2 = [ 1, 0, 0, 1, -1, 2];

Fill object
~~~~~~~~~~~

Fill object describes different fill styles. Two specializations of Fill 
are proposed at the moment: GradientFill, which implements a linear 
gradient fill, and PatternFill, which fills with specified image. Radial 
gradient fill is possible but its implementation in VML is bad and not 
consistent with SVG. Solid fill is proposed to represent with a Color 
object for now.

dojo.gfx.createLinearGradient(c1, c2)

Returns a GradientFill object from c1 to c2 (coordinates), which can be 
used as a fill style. Colors are specified with color stops directly on
LinearGradient object.

dojo.gfx.createPattern(url, repetition, x, y, width, height)
dojo.gfx.createPattern(url, repetition, c, width, height)

Returns a (tiled) image PatternFill object, which can be used as a fill 
style. Optional repetition argument can be one of "repeat" (default), or 
  "no-repeat". Optional (x, y) pair or c coordinate provides an origin. 
Optional (width, height) pair specifies the image size in pixels.

GradientFill object members
+++++++++++++++++++++++++++

addColorStop(offset, color)

Adds a color point with specified offset (0.0-1.0). Returns itself.

PatternFill object members
++++++++++++++++++++++++++

No methods.

Stroke object
~~~~~~~~~~~~~

Stroke object describes different stroke styles. One specialization is 
proposed at he moment: LineStroke, which implements a solid line of 
specified width with optional caps, and joins. Solid color line with 
default width can be specified using a Color object.

dojo.gfx.createStroke(color, width, cap, join)

Returns a stroke object. Cap can be one of "butt", "round", or "square". 
Join can be one of "round", "bevel", or a numeric miter value. Cap and 
join are optional arguments.

LineStroke object members
+++++++++++++++++++++++++

No methods.

Matrix object
~~~~~~~~~~~~~

This object defines a transformation matrix.

dojo.gfx.translate(dx, dy)
dojo.gfx.translate(c)

Returns a Matrix object, which represents a 2D translation defined 
either by (dx, dy) pair, or a coordinate c.

dojo.gfx.rotate(angle)
dojo.gfx.rotateg(degree)

Returns a Matrix object, which represents a 2D rotation in radians 
specified by the angle argument, or in degrees specified by degree the 
argument.

dojo.gfx.scale(s)
dojo.gfx.scale(sx, sy)
dojo.gfx.scale(c)

Returns a Matrix object, which represents a 2D scaling specified by the 
s argument (for isotropic scaling), (sx, sy) pair or the coordinate c 
(for anisotropic scaling).

dojo.gfx.identity
dojo.gfx.flipX
dojo.gfx.flipY
dojo.gfx.flipXY

These are useful constant matrices, which can be used for combining 
transformations.

dojo.gfx.skewX(angle)
dojo.gfx.skewY(angle)
dojo.gfx.skewXg(angle)
dojo.gfx.skewYg(angle)

Returns a Matrix object, which represents a skew matrix specified in 
radians or degrees.

dojo.gfx.multiply(a, b, . . .)

Returns a Matrix object, which represents a consequtive application of 
matrices. This method is listed here for logical completeness.

dojo.gfx.multiply(m, x, y)
dojo.gfx.multiply(m, c)

Returns a Coordinate object, which represents an application of m matrix 
to a coordinate. This method is listed here for logical completeness.

dojo.gfx.invert(a)

Returns an inverted matrix. This method is listed here for logical 
completeness.

Matrix object members
+++++++++++++++++++++

Following member objects are available: xx, xy, yx, yy, dx, dy.

Renderer-dependent objects
--------------------------

These are renderer-dependent objects: Path, Shape, Group, Surface.

Path object
~~~~~~~~~~~

This object is used to build a path string procedurally.

dojo.gfx.createPath()

Returns an empty Path object.

Path object methods
+++++++++++++++++++

path.moveTo(x, y)
path.moveTo(c)

Moves the current point of the path starting a new subpath. Returns tself.

path.LineTo(x, y)
path.LineTo(c)

Adds the point to the path connecting the last point of the path with 
new point using a straight line. Returns itself.

path.arcTo(x1, y1, x2, y2, radius)
path.arcTo(c1, c2, radius)

Adds two points to the path. (x1, y1) is connected with the last point 
using a straight line, if they are different. (x2, y2) is connected with 
(x1, y1) with an arc. Returns itself.

path.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
path.bezierCurveTo(cp1, cp2, c)

Adds (x, y) connecting it to the last point with a cubic bezier curve. 
Returns itself.

path.closePath()

Closes the last subpath with a straight line. Returns itself.

Shape object
~~~~~~~~~~~~

Shape objects are used to represent geometric shapes. In fact they are 
proxy objects, which use underlying raw nodes.

dojo.gfx.attachShape(node)

Returns a shape object corresponding to specified raw node.

Shape object members
++++++++++++++++++++

shape.setFill(fill)

Returns itself. This method sets a fill style, which can be either a 
GradientFill or PatternFill object, or a color, which represent a solid 
color fill.

shape.setStroke(stroke)

Returns itself. This method sets a stroke style, which can be either a 
LineStroke object, or a color, which represent a solid color stroke with 
  default width.

shape.setPath(path)

Returns itself. This method sets a path, which can be a Path object or a 
path string in SVG notation. In order to be translatable to VML, it 
should use only pre-defined subset of path commands.

shape.setTranform(matrix)

Returns itself. This method sets a transformation matrix for the shape, 
which can be specified by a Matrix object, 6-item array, or a dictionary 
(xx, xy, yx, yy, dx, dy). It replaces the current transformation matrix.

shape.applyTransform(matrix)

Returns itself. This method apply a matrix to the shape. Logically it is
equivalent to: 
shape.setTransform(dojo.gfx.multiply(shape.getTransform(), matrix));

shape.applyLeftTransform(matrix)

Returns itself. This method left-apply a matrix to the shape. Logically 
it is equivalent to: shape.setTransform(dojo.gfx.multiply(matrix, 
shape.getTransform()));

shape.getNode()

Returns a raw node used for this shape.

Group object
~~~~~~~~~~~~

It is used to group several objects together. Essentially it is a shape 
object, wich supports setting a fill style, a stroke style, and a 
transformation. Additionally it defines one more method:

group.createShape()

Returns an empty shape object.

Surface object
~~~~~~~~~~~~~~

Surface objects are used to create shapes inside a predefined graphics area.

dojo.gfx.createSurface(parentNode, width, height, [renderer])

It creates a new surface for a picture. It corresponds to <svg> element 
of SVG, <v:g> element of VML, and <canvas> element of Canvas. Width and 
height are in pixels. Parent node specifies a parent for newly created 
surface (there are several ways to create an element, e.g., before or 
after given node --- we can address them later). Optional renderer is a 
text string, which identifies a renderer: "svg", "vml", or "canvas". It 
can be used for future extension. If it is unspecified, the 
preferred/default renderer is used. createSurface returns a surface 
object or null, if it cannot create a surface.

dojo.gfx.attachSurface(node)

It works just like createSurface but instead of creating new surface it
attaches to existing element.

surface.createShape()

Returns an empty shape object.

surface.createGroup()

Returns an empty group object.

Random notes
============

I didn't include SVG-to-VML conversion written in XSLT. I included 
SVG-path-to-VML-path conversion written in JavaScript (implicitly 
required by shape.setPath()).

I removed all Canvas-specific things. We can still create a Canvas-based
renderer, but it is not going to be a simple mapping. It is not cheduled 
to be a part of the first release.

I didn't tie in the existing facilities. We can move parts of these API 
in more appropriate packages.

I didn't include text yet. This is a complex part and I want to spend 
more time on that. Anyway it can be added independently.

Graphics attributes can be mapped to regular HTML. For example, 
LineStroke and a color stroke can be used for borders of HTML elements, 
PatternFill and a color fill can be used for backgrounds of HTML 
elements. Upcoming Font object can be used for HTML text as well. The 
inverse is true too. Do we need this kind of generalization?

Matrices and coordinates can be merged with existing Dojo facilities. 
The reason I kept them different is a convenience. Existing stuff is too 
generic, and most of it is not going to be used anyway.

It is possible to move Path in renderer-independent objects assuming 
that it always produce valid SVG subset, which is converted on the fly 
to VML, if needed. The other possibility is to keep them 
renderer-dependent, but allow reading a path back as an SVG path. I 
didn't like the latter idea because it assumes we have SVG-to-VML and 
VML-to-SVG path converters, which can lead to bigger code footprint.

I didn't include introspection methods to reduce a size of this 
document. They are trivial for renderer-independent objects. But they 
can be quite complex for renderer-dependent ones. For example, we need 
to decide, if we going to support shape.getFill(), shape.getStroke(), 
shape.getPath(), and so on. We can omit them for now, and deal with it 
later because I don't think they are essential.

I assume that a group is a shape as well. It doesn't accept setPath() 
method, but it does accept a fill style, a stroke style, and a 
transformation.

The API can be used for simple "raw node"-based manipulations, when all 
you need is to change attributes, and transformations. The rest 
(surfaces, and shape creation) can be packaged separately to reduce the 
code base for small cases.

I tried to simplify an object creation: attributes, matrices, and 
coordinates can be represented by simple JavaScript objects (arrays, 
dictionaries), so we can reduce the code base even more.

Simple convenience methods will be added as well, e.g. "zoom around a 
point", and "rect-to-rect mapping".

Many methods return the object itself to facilitate chaining. My thanks 
go to Tom for pointing this enhancement out.

Additional ideas
----------------

One possibility is to make graphical attributes (fill/stroke) a
renderer-dependent objects. It may be more efficient, and it will 
simplify get/set methods. On the other hand it will lead to some code 
duplications, and will make it harder to use dictionary-style objects 
instead of real objects.

Gavin proposed to have a template shape factory. It allows registering 
creators for commonly used custom shapes. The same approach can be used 
for graphical attributes. We can add this facility later.

If somebody wants to change graphical attributes on per-component basis 
and see incremental changes, it makes sense to provide an API like that:
shape.stroke.setWidth(2). It makes the attribute API friendlier and 
potentially faster, but it has to be implemented for all supported 
renderers, and it will make attributes renderer-dependent. We have to 
see, if we will have any performance problems first.

We may need a provision to go from a node to its associated object, if 
there is one. This problem is similar to widget-node and node-widget 
mapping.

For some applications we may need shape.containsPoint(c) method. I tried 
to avoid it because it is difficult to implement and the code size is 
relatively big. I suggest structuring an application, which needs this 
functionality, to use mouse events instead. In any case we can add it later.

Gavin proposed to implement the Matrix object according to SVG 
JavaScript mapping. I have 2 small reservations: it is quite big, and 
not a lot of people are familiar with it, so we cannot use it to ride a 
familiarity wave. I didn't put any methods in the Matrix object because 
I want to preserve the ability to specify matrices literally like that: 
{xx: 1, yy: 1}. We can add it later, if it is needed.



More information about the dojo-contributors mailing list