SVGCanvas
Introduction
SVGCanvas is a JavaScript implimentation of most of the Canvas drawing API which creates SVG grapics elements instead of drawing to a pixel buffer. I wanted a way of scripting the creation of SVG graphics without always explicitly managing the DOM tree. Since Canvas is becomming a de facto web standard and the two interfaces are similar (e.g. linear and radial gradients of the same form) I took advantage of the API.
Canvas is a pixel-based representation like a paint program. You can draw shapes over each other and they will modify the pixels. SVG is more like a drawing program where shapes are always there even if they get covered up.
Canvas is a stack-based API where you set the color and line style, move to a spot, draw some lines, set another color, draw more lines, etc. Almost all graphical libraries impliment this becuase in many situations it's more convenient and efficient than passing the parameters with each call. SVG is a tree representation where you can group objects and apply fill color, for example, to the whole group that get applied for every member that doens't explicitly specify their own fill color. Some things are cleaner to do in one model or the other. See Canvas and SVG slides by Robert O'Callahan at mozilla.org for a good overview about why the web needs both.
It was easiest to get SVGPlot to efficiently generate clean SVG code using a combination of DOM tree manipulation and Canvas calls. Why would you want this?
- The Canvas API is a nice API to build up an SVG without explicit DOM manipulations.
- SVG (with JavaScript) is a W3C standard, but canvas is not.
- When your browser supports SVG not canvas.
- Code written for canvas can be usedto create an SVG.
- Save the result of canvas calls to an SVG file for later display, conversion to PDF or PNG, or printing at high-res.
- Can apply events to objects (shapes) rather thanhave to translate the (x,y) coordinates like you do in Canvas.
When wouldn't you use this?
- When speed matters. With this, you just stack SVG shapes on top of each other forever. Even you clear it, those others will still be around taking memory and rendering time. For things like games and photographic content, you want the "blast it to the screen and forget it" model.
- Sometimes the simplest representation of a graphic is a program to draw it (e.g. fractals, function plotting) rather than an XML-like description.
- When your browser supports canvas but not SVG
Live Demos
- live demo - Table of tests that you can modify and re-run client side. These can take up to a minute to render in Firefox.
- live demo 2 - More of the same.
- canvas extensions - Table of demos for extensions that added on top of the basic Canvas functionality.
Browse Code in SVN Repository
Related Work
- Batik impliments the Java AWT API to draw directly to SVG.
-
CanvaSVG
goes the other way, rendering SVGs onto a canvas. SVG has a richer
(more complicated) set of things like arcs, andstroke-dasharray that are not
built into the Canvas API making it hard on them and easy on me.
Other things like shadows and
globalCompositeOperation
are not straight forward to impliment without native SVG support. - Cairo can render to bitmaps, PDF, PS, SVG, OpenGL, and others. It's API is similar to Canvas.
- PostScript is really a programming language. You can write a PostScript program to print out arbitrarily many digits of pi, or have the printer calculate fractals. Canvas and SVG provide this level of programing using JavaScript.
Background/History
PostScript was a vector drawing state-machine-based language, PDF is a subset without the programming language pieces (conditionals and loops). Canvas is a drawing API inspired by PS and PDF. SVG is a scenegraph representation. It's like the difference between OpenGL and Open Inventor. All drawing of windows and widets in NeXT was done used PostScript, and in OS X using PDF. The advantage was that the same code that is drawing to the window can be used to draw to the printer without any translation.
Canvas, though simple, is quite powerful, supporting transparency, gradients, clipping, blending, bezeir curves, and others that Win32/GDI and QuickDraw2D, and Java's AWT do not. Except for transparency, these features come from PostScript.
State-Machine / Stack-Based / immediate-mode | Scenegraph / retained mode | |
2D | PostScript Canvas Quartz 2D / QuickDraw 2D on OS X Qt, GDK Java AWT java.awt.Graphics2D Windows Win32/GDI |
SVG |
3D | OpenGL RenderMan |
Open Inventor/ Performer
/ Coin3D OpenGL++ / Cosmo 3D / Fahrenheit Java3D VRML / X3D VTK |
Sometimes you want direct access to the pixels. In photo manipulation, 3D per-pixel lighting and shading, non-linear transformations, displaying image or video, a smoke or fire effect, filters, or interval graphing techniques. Dealing with a 2D array of pixels is more efficient than drawing a rectangle for each pixel using vector-graphics techniques. Some of these are addressed in the SVG filters -- you can have your drop shadow and the advantage of SVG. The other advantage is being able to both read and write the pixel array.
Overview of Canvas API
- WHATWG
<canvas>
API specification. - Apple
<canvas>
documentation - Mozilla
<canvas>
tutorial
Overview of my Canvas Implementation using SVG
There is a single object SVGCanvas
which acts like Canvas's 2D context
that you get from getContext("2d")
. Maybe I should have called it
SVGCanvas2dContext to keep my options open for 3D, but X3DCanvas is
probably a better route.
There were some decisions to be made regarding trading off speed to create the SVG, speed to render the SVG, generating clean factorized SVG. What happens when you draw, transform, draw, transform, draw, etc.
- Should SVGCanvas keep an explicit current transformation matrix and multiply each incomming coordinate by it and write out the transformed coordinates? This is what the Canvas and SVG renderers do internally? This produces ugly SVG with lots of decimal digits that have no relation to the often simple coordinates provided.
- Another option would be to construct a huge string of
transformations and include them in the SVG element:
transform="translate(75, 75) scale(0.4, 0.4) rotate(6)"
This is how you would write SVG "by hand." The problem with this is that the transformations can get longer and longer. Another problem is that you lineTo() rotate() lineTo() before you emit a path with fill() and there is no way specify a single transformation for the whole path.
fill()
,
stroke()
or clip()
or things like fillRect()
that call one of
these.Gradients, clipping paths, markers, patterns, and other objects that you define and use many times are created by standard Canvas methods or a Canvas inspired SVG-only one.
Overview of SVG Extensions
Since I want to be able to use this as an easy way to draw into an SVG rather than just a pure Canvas implimentation, there are several SVG specific features.
- You can pick the SVG group you're drawing into by setting
drawGroup
. It lives in the state. All elements go into this group. (They have transformations applied to them from higher-level goups.) - Actions like
stroke()
andfill()
return the DOM element of the path or group that was created. Usually you ignore it. - You can manipulate the state dictionary yourself with
getState
andsetState
. applyStyles
is part of the state. If you have an SVG group that already has styling properties you need, you can set this to just output the shape elements and let the styles be inherited.- Every SVG element can be created directly with the current
transformation and style applied by passing appropriate arguments to,
for example,
strokeCircle()
,fillCircle()
, ordrawCircle()
. -
text()
funtion that takes a string and a position. A big shortcoming of canvas is its lack of integrated text. I've also added font and textAnchor properties to the state. draw()
action: To draw a shape that has both afill()
andstroke()
, in Canvas you set up the path, then callfill()
thenstroke()
to stroke on top of the fill. You can do that here too, but it would generate two seperate SVG elemtns rather than a single one with both fill and stroke properties. If you edit the SVG and drag around the fill, the stroke won't come with it. Withinstroke()
I can look to see if I've just done afill()
, but this check is inneficient. Every time you do a stroke you have to compare the current state and the element you're trying to fill (path, rect, etc) with the previous thing. Maybe this is worth doing rather than defining a whole new non-canvas-API function, but it's far easier to add a draw function to the canvas context that callsfill()
thenstroke()
.