Path_ology


1. Introduction

This is the seventh time that I have implemented a library of useful functions. The motivation this time was to get a more robust and simpler solution based on:

Most operations on SVG paths are straightforward. The one exception is reversing a path. The original SVG path description was poorly defined for this operation. It was not always clear when a new subpath started, nor was it clear that reversing a path twice would have the same subpath structure as the original path. The aim in the new implementation is therefore to have a subpath structure that makes reversing a path just the operation of reversing the order of the subpaths and reversing the drawing commands of each subpath.

Animating a path is still poorly described in SVG 1.2. To make life easier the internal form should require:

The proposed internal form is therefore:

Reversing a subpath consists of:

  1. The last command defines the type of curve
  2. Output a moveto to the final position
  3. Reverse each command starting from the last drawing command
  4. Change the initial moveto command to the last command (open or close)

2. A Simple Example

Suppose we have the following path:

<path d="
M xa,ya C xb,yb xc,yc xd,yd Z M xh,yh L xi,yi M xj,yj L xk,yk
"/>

We need an internal representation that allows us to change relative to absolute and vice versa and to reverse the path quickly and easily. This requires the individual path elements to be self-sufficient. To change to relative produces:

<path d="
M xa,ya 
c 
xb-xa,yb-ya 
xc-xa,yc-ya 
xd-xa,yd-ya 
     Lxa-xd,ya-yd  (inserted possibly)
Z 
m xh-xa,yh-ya or Mxh,yh (the second alternative is necessary as Chrome 
                         currently has a bug withrespect to Zm)
l xi-xh,yi-yh 
m xj-xi,yj-yi or M xj,yj 
l xk-xj,yk-yj
"/>

Reversing the path produces:

<path d="
Mxk,yk
Lxj,yj
Mxi,yi
Lxh,yh
Mxa,ya
Lxd,yd
Cxc,yc xb,yb xa,ya
Z
"/>

Internal forms for the line drawing commands are reasonably obvious:

<c t="C" o="C" cx="xa" cy="ya" x1="xb" y1="yb" x2="xc" y2="yc" 
                               x3="xd" y3="yd"/>
<c t="L" o="L" cx="xi" cy="xj" x3="yi" y3="yj"/>

Just adding the initial position makes both reversing and changing to relative straightforward. The attribute t defines the type and o defines the original type. All abbreviations are changed to one or other of these on input (l,v,h,V,H all become L) (S,Q,T,c,s,q,t all become C).

The SVG document states that each path consists of a set of subpaths which are either open or closed. The Z command effectively completes one subpath and starts a new one. The meaning of the two being different in terms of linecap etc. This suggests that an internal form should consist of a set of subpaths (M to Z) or (M to C/L). As the Z is effectively adding an additional L command, the two would be closer in form if M to Z added the closing line and M to C/L added an end curve command. Also the Z should add an M command after it if the next command is a line drawing command.Curves would either be M...LZ or M...E. On reversing, Z and E effectively become M and M becomes either Z or E. That would imply that either the internal form should differentiate between the two type of M or the subpaths should be named differently. I prefer the first approach as it keeps the individual elements with all the information required. If an M is turned to relative it must retain the previous origin so each M element must contain two coordinates, the previous current position and the new one. Similarly, as the E becomes a C, it must retain the next current position. The same is true for Z. The forms for the four commands should be:

<c t="M" o="M" cx="xbf" cy="ybf" x3="xa" y3="ya"/>
<c t="N" o="M" cx="xbf" cy="ybf" x3="xa" y3="ya"/>
<c t="Z" o="Z" cx="xk" cy="yk" x1="xi" y1="yi"/>
<c t="E" o=""  cx="xk" cy="yk" x1="xi" y1="yi"/>

Suppose we have the following SVG document:

<svg width="1000" height="1000">
<style type="text/css">
path {stroke:blue;stroke-width:2;fill:none}
</style>
<path style="stroke:blue" id="example" d="M100,250
L125,275 
H150V250
M150,250      
l25,25h25v-25C215,275 235,225 250,250S285,225 300,250Q325,275 350,250
T400,250A20,20,1,0,0,450,250a30,30,1,0,0,50,0
c15,25 35-25 50,0s35-25 50,0s35,25 50,0q25,25 50,0t50,0t50,0
v100h-650z
l100,-100h300z
m500,-100v40h50
m25,0 
m25,0 v50h40z
h50
">
<set attributeName="id" to="'example'" begin="1s" />
</path>

</svg>

The internal form for the path command would be:

<path style="stroke:blue" id="example">
<s>
<c t="N" o="M" cx="0" cy="0" x3="100" y3="250"/>
<c t="L" o="L" cx="100" cy="250" x3="125" y3="275"/>
<c t="L" o="H" cx="125" cy="275" x3="150" y3="275"/>
<c t="L" o="V" cx="150" cy="275" x3="150" y3="250"/>
<c t="E" o="" cx="150" cy="250" x1="150" y1="250"/>
</s>
<s>
<c t="M" o="M" cx="150" cy="250" x3="150" y3="250"/>
<c t="L" o="l" cx="150" cy="250" x3="175" y3="275"/>
<c t="L" o="h" cx="175" cy="275" x3="200" y3="275"/>
<c t="L" o="v" cx="200" cy="275" x3="200" y3="250"/>
<c t="C" o="C" cx="200" cy="250" x1="215" y1="275" x2="235" y2="225" 
                                 x3="250" y3="250"/>
<c t="C" o="S" cx="250" cy="250" x1="265" y1="275" x2="285" y2="225" 
                                 x3="300" y3="250"/>
<c t="C" o="Q" cx="300" cy="250" x1="316.667" y1="266.667" 
               x2="333.333" y2="266.667" x3="350" y3="250"/>
<c t="C" o="T" cx="350" cy="250" x1="366.667" y1="233.333" 
               x2="383.333" y2="233.333" x3="400" y3="250"/>
<c t="A" o="A" cx="400" cy="250" a="20" b="20" d="1" e="0" f="0" 
                                    x3="450" y3="250"/>
<c t="A" o="a" cx="450" cy="250" a="30" b="30" d="1" e="0" f="0" 
                                    x3="500" y3="250"/>
<c t="C" o="c" cx="500" cy="250" x1="515" y1="275" 
               x2="535" y2="225" x3="550" y3="250"/>
<c t="C" o="s" cx="550" cy="250" x1="565" y1="275" 
               x2="585" y2="225" x3="600" y3="250"/>
<c t="C" o="s" cx="600" cy="250" x1="615" y1="275" 
               x2="635" y2="275" x3="650" y3="250"/>
<c t="C" o="q" cx="650" cy="250" x1="666.667" y1="266.667" 
               x2="683.333" y2="266.667" x3="700" y3="250"/>
<c t="C" o="t" cx="700" cy="250" x1="716.667" y1="233.333" 
               x2="733.333" y2="233.333" x3="750" y3="250"/>
<c t="C" o="t" cx="750" cy="250" x1="766.667" y1="266.667" 
               x2="783.333" y2="266.667" x3="800" y3="250"/>
<c t="L" o="v" cx="800" cy="250" x3="800" y3="350"/>
<c t="L" o="h" cx="800" cy="350" x3="150" y3="350"/>
<c t="L" o="" cx="150" cy="350" x3="150" y3="250"/>
<c t="Z" o="Z" cx="150" cy="250" x1="150" y1="250"/>
</s>
<s>
<c t="M" o="" cx="150" cy="250" x3="150" y3="250"/>
<c t="L" o="l" cx="150" cy="250" x3="250" y3="150"/>
<c t="L" o="h" cx="250" cy="150" x3="550" y3="150"/>
<c t="L" o="" cx="550" cy="150" x3="150" y3="250"/>
<c t="Z" o="Z" cx="150" cy="250" x1="650" y1="150" />
</s>
<s>
<c t="N" o="m" cx="150" cy="250" x3="650" y3="150"/>
<c t="L" o="v" cx="650" cy="150" x3="650" y3="190"/>
<c t="L" o="h" cx="650" cy="190" x3="700" y3="190"/>
<c t="E" o="" cx="700" cy="190" x1="725" y1="190" />
</s>
<s>
<c t="N" o="m" cx="700" cy="190" x3="725" y3="190"/>
<c t="E" o="" cx="725" cy="190" x1="750" y1="190"/>
</s>
<s>
<c t="M" o="m" cx="725" cy="190" x3="750" y3="190"/>
<c t="L" o="v" cx="750" cy="190" x3="750" y3="240"/>
<c t="L" o="h" cx="750" cy="240" x3="790" y3="240"/>
<c t="L" o="" cx="790" cy="240" x3="750" y3="190"/>
<c t="Z" o="Z" cx="750" cy="190" x1="750" y1="190"/>
</s>
<s>
<c t="N" o="" cx="750" cy="190" x3="750" y3="190"/>
<c t="L" o="h" cx="750" cy="190" x3="800" y3="190"/>
<c t="E" o="" cx="800" cy="190" x1="0" y1="0"/>
</s>
</path>

2. Conversion to XML

The basic architecture for the 2D functions is shown below:

No SVG plugin

Path_ology Basic Architecture

The conversion to SVG takes place in four steps:

break_path

This transformation takes the full path description and turns it into XML, one element per command. A typical output might be:

 <path style="stroke:blue" id="example">
  <M o="M" x="100" y="250" /> 
  <L o="L" x="125" y="275" /> 
  <H o="H" x="150" /> 
  <V o="V" y="250" /> 
  <M o="M" x="150" y="250" /> 
  <l o="l" x="25" y="25" /> 
  <h o="h" x="25" /> 
  <v o="v" y="-25" /> 
  <C o="C" r="215" u="275" v="235" w="225" x="250" y="250" /> 
  <S o="S" v="285" w="225" x="300" y="250" /> 
  <Q o="Q" v="325" w="275" x="350" y="250" /> 
  <T o="T" x="400" y="250" /> 
  <A o="A" g="20" b="20" d="1" e="0" f="0" x="450" y="250" /> 
  <a o="a" g="30" b="30" d="1" e="0" f="0" x="50" y="0" /> 
   <content>
  <set attributeName="id" to="'example'" begin="1s" /> 
  </content>
</path>

A major problem is handling what can be a very long d attribute (this can be over 1 Mbyte). To avoid very large internal variables, the string being worked on is always a small part of the total string. The next command's maximum length can be estimated given that the number of arguments to the command are known. Currently numbers with exponents are not catered for but it would be relatively easy to change if the need was there. The command recogniser is a finite state table that is optimised for numbers having several digits either before or after the decomal point. It runs significantly faster than a version based on regular expressions.

make_abs

Quite a straightforward recursive template that keeps a record of the last current position and possible Q and C control points. It uses this information to transform the next command into either an absolute cubic or a line. The appearance of a Z command is always preceded by an L command back to the initial point of the closed path. Each unclosed path is terminated by an E element and each Z followed by a drawing command has an M element inserted. The internal form of the commands is established here. Two attributes t and o give the internal type and the original type of the command. Each command includes attributes cx,cythat give the current position. The attributes x3,y3 always contain the end point that will be the next current position. The cubic command has additional attributes x1,y1,x2,y2. The arc command has additional attributes a,b,d,e,f. The attribute names are either one or two characters long to keep the file size down and each arithmetic computation rounds the number to 3 decimal places (this can be changed by the user). A typical output file might be:

<path style="stroke:blue" id="example">
  <c t="M" o="M" cx="0" cy="0" x3="100" y3="250" /> 
  <c t="L" o="L" cx="100" cy="250" x3="125" y3="275" /> 
  <c t="L" o="H" cx="125" cy="275" x3="150" y3="275" /> 
  <c t="L" o="V" cx="150" cy="275" x3="150" y3="250" /> 
  <c t="E" o="" cx="150" cy="250" /> 
  <c t="M" o="M" cx="150" cy="250" x3="150" y3="250" /> 
  <c t="L" o="l" cx="150" cy="250" x3="175" y3="275" /> 
  <c t="L" o="h" cx="175" cy="275" x3="200" y3="275" /> 
  <c t="L" o="v" cx="200" cy="275" x3="200" y3="250" /> 
  <c t="C" o="C" cx="200" cy="250" x1="215" y1="275" 
                 x2="235" y2="225" x3="250" y3="250" /> 
  <c t="C" o="S" cx="250" cy="250" x1="265" y1="275" 
                 x2="285" y2="225" x3="300" y3="250" /> 
  <c t="C" o="Q" cx="300" cy="250" x1="316.667" y1="266.667" 
                 x2="333.333" y2="266.667" x3="350" y3="250" /> 
  <c t="C" o="T" cx="350" cy="250" x1="366.667" y1="233.333" 
                 x2="383.333" y2="233.333" x3="400" y3="250" /> 
  <c t="A" o="A" cx="400" cy="250" a="20" b="20" d="1" e="0" 
                                      f="0" x3="450" y3="250" /> 
  <c t="A" o="a" cx="450" cy="250" a="30" b="30" d="1" e="0" 
                                      f="0" x3="500" y3="250" /> 
....
</path>
add_subpath

A simple transformation that inserts a subpath element s around each group starting with a command of type M. This is an XSLT2 transformation using the for-each-group function.

Typical output would be:

<path style="stroke:blue" id="example">
  <s>
  <c t="M" o="M" cx="0" cy="0" x3="100" y3="250" /> 
  <c t="L" o="L" cx="100" cy="250" x3="125" y3="275" /> 
  <c t="L" o="H" cx="125" cy="275" x3="150" y3="275" /> 
  <c t="L" o="V" cx="150" cy="275" x3="150" y3="250" /> 
  <c t="E" o="" cx="150" cy="250" /> 
</s>
<s>
  <c t="M" o="" cx="150" cy="250" x3="150" y3="250" /> 
  <c t="L" o="l" cx="150" cy="250" x3="250" y3="150" /> 
  <c t="L" o="h" cx="250" cy="150" x3="550" y3="150" /> 
  <c t="L" o="" cx="550" cy="150" x3="150" y3="250" /> 
  <c t="Z" o="Z" cx="150" cy="250" /> 
</s>
</path>
make_xml

Finally, this transformation makes the individual subpaths self-sufficient by changing M commands to N commands if the command starts an open subpath. The closing E command adds the current position of the next curve as x1,y1 attributes. This facilitates a straightforward function to do path reversal. The final form is something like:

  <path style="stroke:blue" id="example">
  <s>
  <c t="N" o="M" cx="0" cy="0" x3="101" y3="151" /> 
  <c t="L" o="h" cx="101" cy="151" x3="251" y3="151" /> 
  <c t="L" o="v" cx="251" cy="151" x3="251" y3="301" /> 
  <c t="L" o="h" cx="251" cy="301" x3="101" y3="301" /> 
  <c t="E" o="" cx="101" cy="301" x1="302" y1="152" /> 
  </s>
  <s>
  <c t="N" o="M" cx="101" cy="301" x3="302" y3="152" /> 
  <c t="L" o="h" cx="302" cy="152" x3="452" y3="152" /> 
  <c t="L" o="v" cx="452" cy="152" x3="452" y3="302" /> 
  <c t="L" o="h" cx="452" cy="302" x3="302" y3="302" /> 
  <c t="E" o="" cx="302" cy="302" x1="103" y1="353" /> 
  </s>
  <s>
  <c t="M" o="M" cx="302" cy="302" x3="103" y3="353" /> 
  <c t="L" o="h" cx="103" cy="353" x3="253" y3="353" /> 
  <c t="L" o="v" cx="253" cy="353" x3="253" y3="503" /> 
  <c t="L" o="h" cx="253" cy="503" x3="103" y3="503" /> 
  <c t="L" o="" cx="103" cy="503" x3="103" y3="353" /> 
  <c t="Z" o="Z" cx="103" cy="353" x1="304" y1="354" /> 
  </s>
  <s>
  <c t="M" o="M" cx="103" cy="353" x3="304" y3="354" /> 
  <c t="L" o="h" cx="304" cy="354" x3="454" y3="354" /> 
  <c t="L" o="v" cx="454" cy="354" x3="454" y3="504" /> 
  <c t="L" o="h" cx="454" cy="504" x3="304" y3="504" /> 
  <c t="L" o="" cx="304" cy="504" x3="304" y3="354" /> 
  <c t="Z" o="Z" cx="304" cy="354" x1="505" y1="155" /> 
  </s>
  <s>
  <c t="N" o="M" cx="304" cy="354" x3="505" y3="155" /> 
  <c t="L" o="h" cx="505" cy="155" x3="655" y3="155" /> 
  <c t="L" o="v" cx="655" cy="155" x3="655" y3="305" /> 
  <c t="L" o="h" cx="655" cy="305" x3="505" y3="305" /> 
  <c t="E" o="" cx="505" cy="305" x1="0" y1="0" /> 
  </s>
</path>

There are two transformations that change the SVG internal form back to the d attribute:

back_to_svg
This generates a d attribute consisting of M, Z, A, C and L commands only. The commands can either be absolute or relative. This is achieved by a mode parameter added to the bat file (mode=abs or mode=rel). A bug in the current Chrome implementation (May 2009) means that an m command following a Z command is not animated correctly. In consequence, a third mode is provided (relMabs) that always outputs absolute M commands.
simplify_and_back_to_svg

This is similar to back_to_svg except that it tries to simplify both L and C commands. If possible C commands are turned to S, Q or T commands while L commands are turned to H and V. A parameter toline if set to ctol will also attempt to convert C to L, H or V if appropriate. A typical bat file might be:

java -jar saxon.jar  -o inner.xml  
              simpilify_and_back_to_svg.xsl mode=relMabs toline=ctol

3. Transformations

The library contains a number of transformations that take the internal form of a set of paths and converts them to a transformed version of the internal form. these are:

reverse_path

This takes each subpath and reverses it. An example is shown below:

No SVG plugin

Reversing a path

change_accuracy
Reduces the accuracy of each of the path parameters. The parameter decplaces defines the number of decimal places that the coordinate values are rounded to. The parameter can take values between 3 and -3. It would be a trivial change to allow other rounding values.
apply_transform
This is an XSLT 2.0 transformation that calculates the matrix for a set of transformations that make up the transform attribute. It then removes the transform attribute and applies the transform matrix to all the coordinates in the XSLT transformation. The transformation is useful when you are animating between transformed and untransformed values.
smooth_short_lines

For an object made up of a series of short lines, this transformation takes any short line, which has lines before and after it, and converts it to a cubic Bezier with the tangent at the start of the curve equal to the direction of the first line and similarly the end tangent the same as the direction of the third line. The parameter short defines the maximum length of the line which is converted.

The heuristic is due to Maxim Shemanarev http://www.antigrain.com/agg_research/bezier_interpolation.html.

No SVG plugin

Smooth Short Lines

break_path
remove_line
Transforms all line commands to the equivalent cubic
remove_arc

Transforms each arc command to between 1 and 4 cubic commands depending on the length of the arc. Conversion from arc to C is complicated by the SVG format for arcs where the arc radii effectively define the aspect ratio of the ellipse and, if the radii are too small to define a curve from the start position to the end, it is scaled until a solution is available. For arcs greater than 90 degrees, it is sensible to break the arc into sections. A maximum of 4 cubics is needed. The papers by Maisonobe (Drawing an elliptical arc using polylines, quadratic or cubic Bezier curves. July, 2003 (http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf)) and Hoffman (Bezier Curves, (http://www.fho-emden.de/~hoffmann/bezier18122002.pdf)) give ways in which this can be tackled. Essentially the tangent at the two end points is known and it is a matter of positioning the two control points at places that yield the minimum error between the cubic and the arc. There is not a unique solution.

No SVG plugin

Arc Conversion

merge
Merges two short cubics into a single cubic where the variable smallc is the maximum length of the two curves
break_cubic
Breaks each cubic at the midpoint into two cubics.
path_length/curve_length
The transformation curve_length calculates the length of each line or cubic adding this as an attribute l to each command together with an attribute er which indicates the estimated maximum error in the length value. The transformation path_length sums the individual lengths and errors, adding the length and error to the path as a whole and the individual subpaths. These two transformations are normally used together to calculate the length of a path.
shorten
Takes two short drawing commands (either cubics or lines) and generates a single line between start and endpoints. Normally used in association with smooth_short_lines to reduce the size of paths made up of too many segments.
thicken_cubic
Although animating stroke-dashoffset gives a reasonable approximation to line drawing, it seemed worthwhile having a transformation that took a cubic curve and expanded it to an area using offset curves and then animating the filling of the area. This is more a template to be modified to suit a particular application when the need arises.
random
Given the last random number in a sequence, the transformation calculates the next random number. A useful utility function.
remove_basic
Changes the basic line drawing elements (rect, circle, ellipse, polyline and polygon) to path elements.
caligraphy
Given a path made up of short cubics (splitting cubes first if necessary), the path is drawn as though by a caligraphic pen with the angle of the pen and the thickness specified
paint_cubic
Similar to caligraphy but thickens the path and then draws it by a set of line paths close together to give the appearance of being painted.

4. 3D SVG:

A transformation, def_perspective_scenes.xsl, has been implemented that takes some existing SVG paths, adds a Z=0 to each path coordinate and then allows transformations to be applied to the 3D objects before performing a perspective transformation back to a 2D path. The file to be transformed should be a regular SVG file with the following format:

<svg ...>
Any SVG commands
  <metadata>
    <scenes>
      <timing>6s</timing>
      <objbg transform="translate(0,0,-300)">
        <obj class="w18mt01" href="w18mt01"/>
        .....
      </objbg>
      <objbg transform="rotx(90)">
        <obj class="w18fg01" href="w18fg01"/>
        ....
      </objbg>
      <scene id="w18st1">
        <perspective>
          <over>-20</over>
          <up>-11</up>
          <rad>1600</rad>
          <pp>1800</pp>
          <newx>0</newx>
          <newy>0</newy>
          <scale>1</scale>
          <spline>constant</spline>
        </perspective>
        <objfg transform="translate(-145,-64,80) roty(45)"> 
          <obj class="w18wdm1" href="w18wdm1"/>
          <obj class="w18wdm1l" href="w18wdm1l" />
          <obj class="w18wdm1r" href="w18wdm1r" />
          <obj class="w18wdm2" href="w18wdm2" />
          <obj class="w18wdm3" href="w18wdm3" transform="rotz(60)" />
          <obj class="w18wdm4" href="w18wdm4" transform="rotz(60)" />
        </objfg>
      </scene>
      <scene id="w18st2">
        ......
      </scene>
      ......
    </scenes>
  </metadata>
  <defs>
    <path id="w18mt01"  d="M-20,10...."/>
    <path id="w18fg01"  d="M-40,30...."/>
    <path id="w18wdm1" d="M-25,60C-8.5,65,8.3,65,25,60..."/>
    <path id="w18wdm1r" d="M25,60C25,60,25,20,25,20..."/>
    <path id="w18wdm3" d="M1.3,1.3L2.5,0L32.5,30..."/>
    <path id="w18wdm4" d="M5,5L10,0..."/>
  </defs>
  More SVG
</svg>

The pseudo SVG is transformed by a set of command as follows:

java -jar saxon.jar  -o t1.xml file.svg   break_path.xsl
java -jar saxon.jar  -o t2.xml  t1.xml    make_abs.xsl
java -jar saxon9.jar        t2.xml        add_subpath.xsl >t3.xml
java -jar saxon.jar  -o t4.xml  t3.xml    make_xml.xsl
java -jar saxon9.jar        t4.xml        def_perspective_scenes.xsl >/t5.xml
java -jar saxon.jar  -o t6.xml  t5.xml    change_accuracy.xsl decplaces=1
java -jar saxon.jar  -o out.svg t6.xml    back_to_svg.xsl mode=relMabs
java -jar saxon.jar  -o anim.svg out.svg  animate_scenes.xsl 

The first four commands convert the individual path descriptions in the defs section to the internal XML form.

The individual scene definitions use these path descriptions to construct the individual key frames of an animation. Constant background material that applies to all scenes is given by a set of objbg elements. In the example there are two separate backgrounds. The first is placed at the back of the scene with a far-away Z value. The second is an X-Y scene that is changed to an X-Z scene that forms the ground layout for the animation.

Each scene consists of a set of foreground objects defined as objfg elements. Note that both the objbg and objfg elements can have a 3D transformation applied to the individual obj elements and to the background or foreground element itself. In the foreground object example, a rotation about the Y-axis followed by a translation is applied to the whole object and two of the individual paths making up the object have a transformation about the Z-axis as well.

The perspective transformation is defined by the perspective element and this also defines any post processing to be applied after the conversion back to 2D. The 3D axes have the Y-axis downwards as shown below.

No SVG plugin

SVG 3D Axes

The perspective transformation is defined by the parameters in the following diagram:

No SVG plugin

Perspective Transformation

A viewing point of this scene is defined in spherical coordinates by the elements over, up and rad. Both over and up are given in degrees and rad is the distance from the origin. A plane can be imagined perpendicular to the line from the viewing point to the origin. This plane is called the Picture Plane. The element pp defines the distance from the viewing point to the picture plane. The scene to be viewed normally lies on the opposite side of this plane. Visual rays from the objects to the viewing point pierce the picture plane, tracing out a perspective drawing as it scans the scene. It is this projected view that is converted back to 2D. If the picture plane is moved towards the viewer, the image becomes proportionately smaller, and vice versa. The coordinates of the viewing point can be located in any of the eight octants, so that the figure can be observed from any possible position.

Once the perspective view has been calculated, the size of the view and the origin can be changed by the parameters newx,newy and size.

Definitions of the individual matrices used in the transformations are given in Appendix B.

5. Animation

The output from the transformation def_perspective_scenes.xsl consists of a set of scene definitions overlayed on top of each other, which is not particularly useful unless only a single scene is defined. However it is the basis for generating an animation using the individual scenes as the key frames. Usually it is worth reducing the accuracy before converting the XML back to SVG.

The output will have the form:

<svg ...>
Original SVG commands in file
<g id="scenes">
  <metadata>
    <timing>6s</timing>
  </metadata>
  <g id="scene1" title="constant">
    <path id="w18strt1p1" class="w18mt01" d="M..."/>
    .....
    <path id="w18strt1p6" class="w18fg01" d="M..."/>
    .....
    <path id="w18strt1p24" class="w18wdm1" d="M..."/>
    <path id="w18strt1p25" class="w18wdm1r" d="M..."/>
    <path id="w18strt1p26" class="w18wdm3" d="M..."/>
    <path id="w18strt1p27" class="w18wdm4" d="M..."/>
  </g>
  <g id="scene2" title="constant">
    <path id="w18strt1p1" class="w18mt01" d="M..."/>
    .....
    <path id="w18strt1p6" class="w18fg01" d="M..."/>
    .....
    <path id="w18strt1p24" class="w18wdm1" d="M..."/>
    <path id="w18strt1p25" class="w18wdm1r" d="M..."/>
    <path id="w18strt1p26" class="w18wdm3" d="M..."/>
    <path id="w18strt1p27" class="w18wdm4" d="M..."/>
  </g>

</g>
Any SVG commands in the original
</svg>

The original SVG context from the original is retained. The individual scenes have a title attribute that is used to define the spline effect required in the animation between this frame and the next. Each path has a unique id added that is used by the animation transformation. The metadata associated with the g element with id attribute set to scenes defines the duration of the animation. The animate_scenes transformation is straightforward. It takes each path defined in the first scene and animates the d attribute. For example:

<path id="w18strt1p1" class="w18mt01" 
d="M-3211.2,-661.3l251.2,775.9l4215.1,-244.8l48.2,-360.7l-4514.5,-170.4Z"/>
<animate attributeName="d" calcMode="spline" 
values="
M-3211.2,-661.3l251.2,775.9l4215.1,-244.8l48.2,-360.7l-4514.5,-170.4Z;
M-3217.3,-658.4l258.3,776l4214.6,-251.4l49.6,-361.1l-4522.5,-163.5Z;
...
" fill="freeze" begin="w18strt0.end" dur="6s" 
xlink:href="#w18strt1p1" 
keySplines="0 0 1 1;0 0 1 1;0 0 1 1;0 0 1 1;0 0 1 1"/>

6. Shadows

Any 3D scene can be enhanced by adding shadows of objects on the Y=0 plane. The transformation add_drop_shadow.xsl is performed before def_perspective_scenes.xsl. The scene definition is enhanced by an additional shadow element:

<shadow><over>50</over><up>-40</up></shadow>
<scene id="..">

The shadow element defines the position of the sun. Unless otherwise specified, every object defined in the scene has an additional object added which is the shadow. The class of this object is the same as the original object but with drop added before. The user needs to add another style rule indicating how the shadow is to be rendered. For example:

path.dropxxx {fill:#333333;stroke:none;fill-opacity:0.5}

Making the shadow semi-transparent allows any objects underneath it to be visible.

For complex objects where only some contribute to the shadow, it is possible to improve the efficiency by not rendering the shadow of those objects. An additional attribute is added to the object:

<obj class="xxx" drop="no" href="yyy" />

7. 3D Shorthands

Two methods of defining 3D paths have been added for convenience.

<path3d id="abc" d="M25,60,0L47,38,5"/>

<p_path3d  id="w18wdm1"> 
 <M p="pb"/>
 <C c1="ph" c2="pm" p="pe" />
 <L p="pb"/>
 <Z />
</p_path3d>

<point id="pa" x="0" y="0" z="0"/>
<point id="pb" x="-25" y="60" z="0"/>
<point id="pc" x="-25" y="20" z="0"/>

The element path3d is quite similar to path but has a z parameter for each point. Only M, Z, C, L, m, z, c, l commands are allowed.

The element p_path3d uses a set of point elements that define positions and constructs 3d paths from them. Only M, L, C and Z are defined. Mostly useful for defining structures where the number of points is small but many faces exist using those points. A box would be a good example.

Appendix A: Break Path

The first stage of converting the d attribute to XML is the most demanding because:

  • The attribute can be over a Megabyte in size
  • Often the format is quite regular if it was generated automatically
  • The intermediate commands are often omitted if they are the same. A run of 60,000 numbers after a single C command is often found.

The d attribute initially has spaces changed to commas and a translate function checks whether the next character is a command letter or a number. The approach is then to output the command element for each command letter and call a template to capture the set of numbers required. The number recogniser is a finite-state machine which is rather verbose but by eliminating tests reduces the time for a large file from 85 secs to 25 secs. Each time a non-number character appears, the previous number is output. The initial string has a ficiticious B command added at the end as a terminator which saves the need to test for end of string and ensures final number is terminated.

The make_commands template removes any separators before the first command and passes the string to a recursive make_command template.

The only real complication is that commands do not have to have the command element in front of them. A succession of cubic beziers can just have an initial C followed by a multiple of 6 numbers following. Similarly for the other elements. In the case of the M element , subsequent pairs of numbers after the first pair represent lines.

make_command makes an estimate of the size of string needed to obtain the set of numbers that make up a single command and passes this to a template make_numbers. Some long sequences of parameters with no command letters do exist (several megabytes) and this avoids a recursion with large strings that will blow up the existing XSLT transformers.

Appendix B: Matrix Definitions

1. Introduction

Most of the transformation in the 3D system can be defined as 4 by 4 matrices. The aim here is to define the set of matrices in use. As with the 2D system we will assume that the matrices are applied right to left. The basic 2D transform is as follows:

xmacexold
ym=bdf×yold
10011

The individual transformations are:

translate(DX,DY)
xm10DXxold
ym=01DY×yold
10011
scale(SX,SY)
xmSX00xold
ym=0SY0×yold
10011
rotate(TH)
xmcos(TH)-sin(TH)0xold
ym=sin(TH)cos(TH)0×yold
10011
rotate(TH,X,Y)
xmcos(TH)-sin(TH)-cos(TH)X +sin(TH)Y+Xxold
ym=sin(TH)cos(TH))-sin(TH)X -cos(TH)Y+Y×yold
10011
skewX
xm1tan(TH)0xold
ym=010×yold
10011
skewY
xm100xold
ym=tan(TH)10×yold
10011
matrix
xmacexold
ym=bdf×yold
10011

2. Equivalent 3D Matrices

No SVG plugin

SVG 3D Axes

Matrix
xhaeimxold
yh=bfjn×yold
zhcgkozold
whdhlp1
translate(DX,DY,DZ)
xm100DXxold
ym=010DY×yold
zm001DZzold
100011
scale(SX,SY,SZ)
xmSX000xold
ym=0SY00×yold
zm00SZ0zold
100011

No SVG plugin

SVG 3D Axes

rotateZ(TH)
xmcos(TH)-sin(TH)00xold
ym=sin(TH)cos(TH)00×yold
zm0010zold
100011
rotateZ(TH,X,Y,Z)
xmcos(TH)-sin(TH)0-cos(TH)X +sin(TH)Y+Xxold
ym=sin(TH)cos(TH)0-sin(TH)X -cos(TH)Y+Y×yold
zm0010zold
100011
rotateY(TH,X,Y,Z)
xmcos(TH)0sin(TH)-cos(TH)X-sin(TH)Z+Xxold
ym0100yold
zm=-sin(TH)0cos(TH)sin(TH)X -cos(TH)Z+Z×zold
100011
rotateX(TH,X,Y,Z)
xm1000xold
ym0cos(TH)-sin(TH)-cos(TH)Y +sin(TH)Z+Yyold
zm=0sin(TH)cos(TH)-sin(TH)Y -cos(TH)Z+Z×zold
100011

3. Perspective

No SVG plugin

Perspective Transformation

perspective(PP,OV,UP,RD)
xhPP cos(OV)0-PP sin(OV)0xold
yh-PP sin(UP) sin(OV)PP cos(UP)-PP sin(UP) cos(OV)0yold
zh=0010×zold
wh-sin(OV) cos(UP)-sin(UP)-cos(OV) cos(UP)RD1

xm = xh/wh and ym = yh/wh

New Origin and Scaling after Perspective transformation
xmSC0NEWXxold
ym=0SCNEWY×yold
10011