|
|
|
Better late than never.
Unified 2D graphics subsystem for Dojo
======================================
A realistic graphics subsystem should implement 6 major components:
* Graphics Context – a container for graphics, which implements
drawing API.
* Shapes – a set of basic geometric shapes. The most basic shape is a
path. One important concept is a group of shapes.
* Fill – a way to fill in a shape. The most basic fill is a solid
color fill. A pattern is a more generic fill. Linear and radial
gradients are useful too.
* Stroke – a way to stroke a shape. The most basic stroke should
accept a color and a line width. Fancier styles include a pattern-based
stroke, a line cap style, and line join style.
* Transformations – a way to transform shape’s geometry before actual
drawing.
Fill, stroke, and transformation are separated as concepts because they
can be applied to different shapes, or reused in some other way. This is
a common scenario for many drawings.
Advanced facilities include:
* Image operations – image-specific interpretation, composition,
transformations, filters, and so on.
* Text operations. For example, generation of paths with a text
string, selection of a font.
* Clipping – using shapes to clip drawings.
* Additional adornments like automatic shadows, and such.
Current state of affairs
========================
At this moment we have 3 vector-based APIs on the market: SVG, VML,
Canvas. SVG is supported natively by FF1.5 and Opera9, VML is supported
natively by IE, Canvas is supported natively by FF1.5, Opera9, and
Safari2.0.
SVG and VML define a markup language, while Canvas is provided as a
JavaScript API + a canvas tag. SVG and VML expose much more than a way
to draw pretty pictures: created objects can be modified on the fly,
events (like, onclick) can be attached to provide easy interactivity.
The idea is to define a generic API, which can be effectively
implemented in a renderer in terms of native APIs. This API can be used
by graphics widgets written in generic manner, which can be used in any
supported environment transparently. Clearly such API is going to be
simpler than underlying APIs. GCD functionality can be implemented
directly by all renderers. More advanced functionality can be
implemented using fallbacks.
Additional requirement is to come up with a markup, which can be used
inline independently of used renderer.
Given the current situation following functionality can be implemented
more or less uniformly across existing graphics APIs: graphics context
(surface), shapes (path, rectangle, polyline, ellipse, arcs, bezier),
fill (solid color, linear gradient, image pattern), stroke (color, line
width, line caps, line joins), transformation (translate, rotate,
scale), image primitive, state stack manipulations (save, restore).
State includes current values of transformation, fill style, and stroke
style.
Proposed API
============
Everything goes into a new package dojo.gfx (we can debate name later,
e.g., it can be placed in the existing dojo.graphics).
By necessity the API is heavily influenced by Canvas API --- the most
basic one.
Global package-level functions
------------------------------
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
is used to specify a renderer in an environment, where more than one
renderer is available. If it is unspecified, the preferred/default
renderer is used. createSurface returns a surface object or null, if
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 object
--------------
Note: this API follows (more or less) the Canvas API --- the most
restrictive one.
Path
++++
surface.beginPath()
Starts the path, returns nothing.
surface.moveTo(x, y)
Moves the current point of the path starting a new subpath. Returns nothing.
surface.LineTo(x, y)
Adds the point to the path connecting the last point of the path with
new point using a straight line. Returns nothing.
surface.arcTo(x1, y1, x2, y2, 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 nothing.
surface.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
Adds (x, y) connecting it to the last point with a cubic bezier curve.
Returns nothing.
surface.closePath()
Closes the last subpath with a straight line. Returns nothing.
surface.endPath()
Ends the path and sets it as the current path. Returns a shape object,
or null (for Canvas). A shape object can be used for later
modifications, or event processing.
Path (convenient)
+++++++++++++++++
surface.createRect(x, y, width, height)
This is a logical equivalent of (pseudo code):
function surface.createRect(x, y, width, height) {
this.moveTo(x, y);
this.lineTo(x + width, y);
this.lineTo(x + width, y + height);
this.lineTo(x, y + height);
this.lineTo(x, y);
}
surface.createEllipse(cx, cy, rx, [ry])
Just like createRect it creates an ellipse. If ry is not specified, it
creates a circle at (cx, cy) with rx radius. Returns nothing.
surface.createPoly(points)
Just like createRect it creates a polyline/polygon object. Returns nothing.
Image
+++++
surface.createImage(url, dx, dy [x, y, width, height])
Creates an image object from URL. Its top-left corner is placed at (dx,
dy). Optional x, y, width, and height are used to select a subimage.
Fills
+++++
surface.createLinearGradient(x1, y1, x2, y2)
Returns a gradient object from (x1, y1) to (x2, y2) , which can be used
as a fill style.
surface.createPattern(url, repetition)
Returns a (tiled) image pattern object, which can be used as a fill
style. Repetition can be one of "repeat", or "no-repeat".
surface.setFill(fill)
Sets the current fill style using a color or a fill object. Returns nothing.
surface.fill()
Fills the current shape with the current fill. Returns nothing.
Strokes
+++++++
surface.createStroke(width, cap, join, miter)
Return a stroke object. Cap can be one of “butt”, “round”, or “square”.
Join can be one of “round”, “bevel”, or a numeric miter value.
surface.setStroke(stroke)
Sets the current stroke style using a color or a stroke object. Returns
nothing.
surface.stroke()
Strokes the current path. Returns nothing.
Transformations
+++++++++++++++
surface.translate(dx, dy)
Applies a translation transformation to the current matrix. Effectively
shifts all coordinates by (dx, dy). Returns nothing.
surface.scale(sx, sy)
Applies a scaling transformation to the current matrix. Effectively
multiplies all x coordinates by sx, and all y coordinates by sy. Returns
nothing.
surface.rotate(angle)
Applies a rotation transformation to the current matrix. Effectively
rotates all coordinates around (0, 0) by “angle” radians.
State
+++++
surface.save()
Pushes the current state (transformation, fill style, and stroke style)
on the state stack. Returns nothing.
surface.restore()
Pops the current state (transformation, fill style, and stroke style)
from the state stack. Returns nothing.
Shape object
------------
See notes below.
Gradient object
---------------
addColorStop(offset, color)
Adds a color point with specified offset (0.0-1.0). Returns nothing.
Pattern object
--------------
No methods.
Stroke object
-------------
No methods.
Discussion
==========
Random notes
------------
The idea was to come up with an API to solve simple yet frequent
problems like charting, simple mapping, and simple art. If somebody
desires to use SVG-specific stuff (e.g., builtin animation), or
VML-specific stuff (e.g., formulas) please use the existing mechanism to
create different versions of your widget.
I decided to use the term "surface" for now. Mainly because "canvas" and
"context" are overused already. BTW, Cairo (FF's rendering engine) uses
this term.
Canvas restricted a lot of things --- that was the reason to use its API
as a model. For example it dictated save/restore state pattern.
VML has a pitiful support for radial gradient, which differs a lot from
SVG/Canvas --- I decided not to support it for now. VML doesn't support
more than 2 color stops. In most cases it is enough. Another VML
deficiency is its pattern support --- only two styles are allowed. VML
doesn't support path clipping --- it is not there either.
Alpha and z-position support should be addressed at some point.
It makes sense to implement some frequently used convenient
transformation methods: rect-to-rect transformation, scale around a
specified point, show an object, show a point, center. All of them can
be implemented in renderer-independent way using existing basic methods.
Text support is not there mainly because it is not implemented in
Canvas. In any case it is a difficult topic. I suggest to solve it on
HTML level in specific widgets. Anyway unless user wants to do a "word
art" it rarely makes sense to use distorted or decorated text. The only
exception I know of is a simple text rotated by pi/2 angle. This one is
truly useful for legends/labels.
I expect that beginPath()/endPath() will be implemented using <group> in
VML, and <g> in SVG.
It may be possible to implement a Flash-based renderer. I don't know for
sure because my knowledge of Flash is limited.
DOM support
-----------
Both VML and SVG support true objects, which can be used for event
processing and/or animation. For example, it is possible to process
onclick, change a position, or modify the fill style. In order to
support the event processing, we can return raw objects for that. If we
want to support an animation of some kind, we have to return some proxy
objects, which will normalize its API.
There are two ways to implement the Canvas renderer (surface):
1) Simple implementation: all shapes are stroked/filled immediately, no
objects are returned from endPath(), which will be an empty method. I
tried to make sure that the API supports this scenario.
2) More elaborate implementation: every time a proxy shape object is
created, it is saved in an internal list, and returned. Proxy shape
object will have all necessary geometry descriptions. If user modifies
it using standard API, the whole list is re-rendered reflecting the
change. Obviously either delayed regeneration is used, or a special
surface method should be called, when user finished modifying shapes. It
is theoretically possible to use the same list to analyze and
reverse-project mouse position to proper shapes thus simulating mouse
events.
Unified markup language
-----------------------
It can be used to define an art, e.g., logos, or decorative elements of
charts. I didn't include it here because it will depend on the API,
which is going to interpret it. There are two possibilities: we can use
some simplified subset of SVG (possibly augmented), or we can use our
own markup, which closely corresponds to the API and can be interpreted
consequently. Both approaches have obvious positive and negative sides.
I need your input.
That's all folks!
=================
Thoughts? Criticism? Improvements? Other ideas?
Thanks,
Eugene
PS: Yes, I implemented renderers like that before (in several commercial
systems) but not in js.
|
|