Animations

SVG supports animations based on SMIL event-driven models. Of particular interest in this case is the use of path-based animation where a given SVG group can be successively translated along a given path. As trains move along tracks, and in our design tracks are defined by sections from which SVG path definitions can be constructed easily, we should be able to simulate the movement of trains around our tracks. And so it proved.

The basic animation we used is effectively move this group g along this path p in a duration of dur seconds. For each section of the layout (i.e. a contiguous run of straight and curves, or the set and not set short sections of points), we calculate both a path description (the d property of svg:path) and the total length. For example the dashed blue line is the defined (single) path for the section2 track section, for which a total length of 4,256 has been calculated :

Figure 16. A track section path

A track section path

Assuming we wish our train to run at 100mm/s (a scale speed of ~ 7km/hr, i.e. a brisk walking pace), then the animation should take 42.5 seconds. This is achieved by forming up an svg:animateMotion definition element:

<animateMotion xmlns="http://www.w3.org/2000/svg"
   id="train.animation" xlink:href="#train"1
   begin="indefinite"  fill="freeze" repeatCount="1"2
   calcMode="linear" keyTimes="0;1" keyPoints="0;1"3
   rotate="auto"4 
   dur="42.5" onend="eventEnded('train;section2.trail')5>
   <mpath xlink:href="#section2.path"6/>
</animateMotion>

1

The graphics group that will be subject to the animation

2

Conditions for the start of the animation — in this case the animation waits until it is triggered explicitly. When the animation has finished freeze the graphics state, i.e leave the graphics translated to the end of the path and do not repeat.

3

keyTimes and keyPoints define a piecewise-linear mapping between proportions of the duration and proportions of the total length — this is used to support moving in reverse and altering speed.

4

auto adds a rotation transform to the animated graphics corresponding to the current path tangent direction, so the graphics object turns along the path.

5

When the animation completes the global function eventEnded() will be executed with an argument containing information about which train has completed a move and where — in this case arriving at the trail port of section2.

6

A reference to the path to be followed.

The animation is started by invoking the beginElement() function method of the animation element through a minimal global JavaScript function. Thus our train(in this case a cyan arrow) progresses along section2 as below:

Figure 17. Movement along a track section.

Movement along a track section.

When the animation finishes, the onend statement is invoked, which is fielded by the global JavaScript function eventEnded().

var ignoreEvent = false;
function eventEnded(e) { 
   if(!ignoreEvent) {1
      var event = new Event("change",{"bubbles":true});
      var store = this.document.getElementById("event");2
      store.value = e; 
      store.dispatchEvent(event);3
  }
  ignoreEvent = false;
}

1

There are cases (described below) when we need to ignore an end event temporarily.

2

A (hidden) checkbox element in the DOM tree that is used to hold the event information as its value property.

3

Propogating an event that the value of the event information store has changed.

After this function has executed, the checkbox id('event') receives a change event which is caught by an XSLT template:

<xsl:template match="*:input[@id eq 'event']" mode="ixsl:onchange"> 
   <xsl:variable name="layout" as="map(*)"
      select="$layouts(f:radioValue('layouts', .))"/> 1
   <xsl:variable name="parts" select="tokenize(@value, ';')"/> 2
   <xsl:choose>
      <xsl:when test="exists($parts[3])"> 3
         <!-- There is a new section to enter -->
         <xsl:call-template name="runTrain">
            <xsl:with-param name="engine" select="$parts[1]"/>
            <xsl:with-param name="trackComponentID" select="$parts[3]"/>
            <xsl:with-param name="tracks" select="$layout?tracks"/>
         </xsl:call-template>
      </xsl:when>
      <xsl:otherwise> 4
          <!-- There is a no new section to enter - end of the line -->
          <xsl:for-each select="id($parts[1])">
              <ixsl:set-attribute name="position" select="$parts[2]"/>
          </xsl:for-each>
          <xsl:variable name="engine" select="$parts[1]"/>
          <xsl:call-template name="stopEngine">
             <xsl:with-param name="engine" select="$engine"/>
          </xsl:call-template>
          <xsl:call-template name="reverseEngine">
              <xsl:with-param name="engine" select="$engine"/>
          </xsl:call-template>
      </xsl:otherwise>
   </xsl:choose>      
</xsl:template>

1

There are a number of possible layouts, held as a named map global variable. Which is the active one is determined by the value of the layouts radio button set.

2

This template expects the value of the event checkbox to be a string of the form train;current port[;next port].

3

If there is a next port, then the train is run on that new section from that port, on the current layout.

4

If not then the train is assumed to have reached the end of the line. It is stopped and the direction reversed, so that, as a convenience to the driver, opening the throttle again again will cause the train to move back along the section.

The trains are controlled by a simple interactive XHTML control group (obviously of class cab):

Figure 18. The Engine Cab

The Engine Cab
<div id="Arrow.cab" class="cab arrow">
  <div class="toggler">
    <input class="run" type="checkbox"
       value="Arrow" />
    <label class="text">Arrow</label>
  </div>
  <label class="title">Speed 
        <span class="value">0</span></label>
  <div name="direction" class="direction">
     <div class="toggler">
        <input class="direction" type="checkbox"
           value="reverse"/>
        <label class="text">reverse</label>
      </div>
  </div>
  <input type="range" min="0" max="1200"
       value="0" list="tickmarks" />
  <div class="radio speed">
     ...
     <div class="toggler">
        <input class="speed" type="radio"
          value="200" />
        <label class="text">slow</label>
     </div>
     ...
  </div>
</div>

Apart from selecting a locomotive to run, the only current action is to change its speed or direction of travel. A number of XSLT templates detect changes in the cab input controls such as:

<xsl:template match="input[contains-token(@class, 'speed')]" 
   mode="ixsl:onchange">
   <xsl:variable name="cab" 
      select="ancestor::div[contains-token(@class, 'cab')]"/>
   <xsl:variable name="run" select="$cab//input[@class eq 'run']"/>
   <xsl:variable name="value" select="@value"/>
   <ixsl:set-property object="$cab//input[@type eq 'range']"
       name="value" select="number($value)"/>
   <xsl:for-each select="$cab//span[contains-token(@class, 'value')]">
      <xsl:result-document href="?." method="ixsl:replace-content">
         <xsl:sequence select="string($value)"/>
      </xsl:result-document>
   </xsl:for-each>
   <xsl:if test="ixsl:get($run,'checked')">
      <xsl:variable name="engine" select="$run/@value"/>
      <xsl:for-each select="id($engine)">
         <ixsl:set-attribute name="speed" select="$value"/>
      </xsl:for-each>
      <xsl:call-template name="changeVelocity">
         <xsl:with-param name="engine" select="$engine"/>
      </xsl:call-template>
   </xsl:if>
</xsl:template>

which detects a change in the stop, slow, cruise, fast radio button set. The selected speed is the @value of the set, which is written into a span element within the cab div and used to set the slider to a suitable point. If the engine is running (the top left checkbox checked), then the demanded speed is written as an attribute onto the selected engine object and then the changeVelocity template is invoked.

The key idea here is to determine how far the current animation has progressed, from which the remaining distance to travel can be determined. This is computed by a global JavaScript function with the animation object a as argument:

function animProgress(a) {
  if(a.getAttribute("dur")==0 ||
     a.getAttribute("dur")=="indefinite") {
    return 0;
  }
  var startTime;
  try{
    startTime = a.getStartTime();
  } catch(e) {
    return 0;
  }
  var t_ratio=(a.getCurrentTime() - startTime)/a.getSimpleDuration();
  return t_ratio;
}  

which calculates the ratio of elapsed to total animation duration. In cases where the animation is not active (for which I can't find a simple test), the exception on finding start time is caught. Given the remaining distance and desired speed, a new duration can be determined and the animation restarted using the keyPoints property to start somewhere down the animation path, e.g. keyPoints="0.5;1" would be used for a speed change halfway along the track section[21].

The animation is restarted by invoking the beginElement() method — the ignoreEvent flag is used to prevent the implicit endElement() event, triggered before the restart, that would normally be used to signal completion of traversal of a section, propagating to the XSLT templates. In the case that the locomotive is running in reverse, the key points are reversed, e.g. keyPoints="0.66;0" would be used for a speed change one-third of the way backwards through a section.

In the absence of such speed changes a running locomotive involves animation movement along the current section until the end event is executed, fielded by the XSLT template shown earlier, which then starts animation along the next specified section. In the case of entering points, the state of the point is examined (from the status of the point control in the signal box!) and the correct path and next section determined for the animation[22]. When a locomotive enters a swap section, described above, its internal running in the wrong direction flag is inverted and it passes on to the following section.

A small number off other animation effects have been added. Firstly locomotives have wheels, which can be animated to rotate at a rate and direction suitable for their diameter and the locomotive's speed, using the animation element:

<animateTransform type="rotate" begin="indefinite" 
   attributeName="transform" from="0" to="360" 
   dur="…." attributeType="XML" repeatCount="indefinite"/>

Secondly, locomotives can be given running sound effects by invoking play() method on an audio element when they start movement, and canwhistle when they enter a (zero length) whistle pseudo-track section. The end point of this development was a case where multiple engines could be run on a layout, stopping, starting , reversing and changing their speed independently and altering points to move them to different sections of the layout:

Figure 19. Three engines running simultaneously

Three engines running simultaneously

But there is a problem with the isometric view trick and automatic path tangent rotation:

Figure 20. On the ceiling

On the ceiling

The animation rotation transformation is applied before the isometric projection and our 3D trick no longer works with significant rotations. How this may be overcome is discussed in the next section.



[21] The current animation may itself already involve a partial path, as a consequence of a previous change in speed — this is determined from the existing @keyPoints value on the animateMotion element to determine the distance to go.

[22] Changing a point while a locomotive is moving through it will not effect the locomotive's path.