1
votes

This is a follow-on to a previous question I asked, Make HTML Canvas generate PDF-ready Line art?. At @Peter O.'s suggestion, I reworked my software to use SVG instead of HTML Canvas. I am now abler to create a high-quality image:

enter image description here

There is an outer <svg> for the entire object. There is a translated <svg> for each view. Instead that the arrow is drawn as a path.

Here is the relevant code that produces the arrows:

    transformForPiece(p) {
        return 'translate(' + this.radius + ',' + this.radius + ') rotate('+p.angle+')'
    }
    idForPiece(p) {
        return "arrow-" + p.col + "," + p.row;
    }
    addPathForPiece(p) {
        let x = this.x(p.col);
        let y = this.y(p.row);
        let r = this.radius;
        var box = document.createElementNS('http://www.w3.org/2000/svg','svg');
        box.setAttribute('x', x-r);
        box.setAttribute('y', y-r);
        box.setAttribute('width', r*2);
        box.setAttribute('height', r*2);
        this.svg.append(box)

        /* linexy - draw a line to the X,Y position */
        function lxy(x,y){
            return "L " + x + " " + y + " ";
        }

        let arrow = document.createElementNS('http://www.w3.org/2000/svg','path');
        arrow.setAttribute('style', 'stroke:none;fill:black');
        arrow.setAttribute('transform', this.transformForPiece(p));
        arrow.setAttribute('d',
                          'M 0 0 '+lxy(0,-r) + lxy(-r, 0) + lxy(-r/2,0)
                          + lxy(-r/2, r) + lxy(+r/2,r) + lxy(+r/2,0) + lxy(r,0) + lxy(0,-r) + " Z ");
        arrow.setAttribute('id', this.idForPiece(p));
        p.path = arrow;         // modify the piece with its svg path
        box.append(arrow);
    }

I can animate the rotation of these arrows using a JavaScript timer:

/* Given a board drawer db, a piece p, a start and and end rotation, rotate it */
const INCREMENT_DEGREES = 10;
const INCREMENT_MS      = 10;
function rotatePieceTimer( db, p, start, end ) {
    if (start > end) {
        start = end;
    }
    p.angle = start;            // set the current angle
    db.drawPiece(p);            // draw the piece at the curren totation
    if (p.angle < end) {         // if we have more to go
        setTimeout( function () { rotatePieceTimer( db, p, start+INCREMENT_DEGREES, end); }, INCREMENT_MS);
    }
}

That is tied to an event:

    function roll (e) {
        /* Pick a random piece */
        let i = Math.floor(Math.random() * cells_wide);
        let j = Math.floor(Math.random() * cells_high);
        let p = board.piece(i,j); // get the piece
        console.log("roll p=",p);
        rotatePieceTimer(db, p, p.angle, Math.ceil(p.angle/180)*180+180) // rotate with timer
        //rotatePieceSVG(db, p, p.angle, Math.ceil(p.angle/180)*180+180) // rotate with timer
        //$('#time').text(boardHistory.length);
    }

    $('#roll').click(  roll );

As you can tell from the code that is commented out, I would like to do the animation with an SVG animationTransform. What I have works just fine, but it would be neat to know how to make animate Transforms work.

I have abstracted away the working code to here:

<!DOCTYPE html>
<html lang="en" class="stylish" type="text/css>"
      <body>
        <svg width="400" height="400">
          <svg x="203" y="203" width="60.66666666666667" height="60.66666666666667">
            <path style="stroke:none;fill:black" transform="translate(30.333333333333336,30.333333333333336) rotate(45)"
                  d="M 0 0 L 0 -30.333333333333336 L -30.333333333333336 0 L -15.166666666666668 0 L -15.166666666666668 30.333333333333336 L 15.166666666666668
                     30.333333333333336 L 15.166666666666668 0 L 30.333333333333336 0 L 0 -30.333333333333336  Z "
                  id="arrow-3,3">
              <animateTransform attributeName="transform" attributeType="XML" type="rotate" from="180 30.333333333333336 30.333333333333336"
                                to="360 30.333333333333336 30.333333333333336" dur="1s" repeatCount="100">
              </animateTransform>
            </path>
    </svg>
    </svg>
      </body>
</html>

The problem with this use of animateTransform is that the arrow doesn't rotate nicely, the way it does when I simply update the rotate(45) CSS transform in the path.

Another problem I have is that I want to make this happen when the user clicks a button, sort of with this code (which is broken):

function rotatePieceSVG( db, p, start, end){
    // this just changes the attribute:

    var noAnimation = false;
    if (noAnimation) {
        p.angle = end;
        p.path.setAttribute('transform',db.transformForPiece(p));
        return;
    }

    // This is the animation I can't get to work...
    var t = document.createElementNS('http://www.w3.org/2000/svg', 'animateTransform');
    t.setAttribute('attributeName','transform');
    t.setAttribute('attributeType','XML');
    t.setAttribute('type','rotate');
    t.setAttribute('from',p.angle+' '+db.radius+' '+db.radius);
    p.angle = end;
    t.setAttribute('to',p.angle+' '+db.radius+' '+db.radius);
    t.setAttribute('dur','1s');
    p.path.append(t);
    console.log("after rotate: p=",p);
}

I've verified that this code adds the animateTransform to the proper location of the DOM. However, it seems that adding the animateTransform to an existing path object doesn't result in the path being animated. I've read the spec, but it's big and I may have missed something. Do I need to delete the old path object and add a new one, rather than just adding the animate transform? Probably. I haven't tried that yet.

So my question:

  1. How can I get the animateTransform to properly rotate my arrow? I've tried fiddling with the values in the from= and the to= and I can never get the arrow rotating in the center.

  2. Is there a way to add the animateTransform to an existing path, or do I need to delete the current path and add a new one?

The full code is here: https://jsfiddle.net/simsong/9udcga6r/1/

1

1 Answers

1
votes

What sticks out in your code is the lack of a value for begin in your <animateTransform>. begin values are pretty versatile.

  • You can set them to "0s", which is the default, and coincides with the time the document timeline begins (basically the window load event). It is not the time when you later interactively add the tag. At that time, 0s will have passed.

  • You can set them to "indefinite", so the animation does not start. This is usually used in conjunction with a script-based trigger, for example SVGAnimationElement.beginElement().

  • You can set them to an arbitrary event. "myButton.click" will start the animation when the element with id="myButton" receives a click.

The syntax also provides for start time lists and delays. It is well worth studying the syntax in the spec to find out what you can do. (The only part of the time value spec not implemented by browsers is the wallclock() function.)

For the rotation center, you need to look closely at what you transform, and what you rotate. As your code stands, the <animateTransform> replaces the transform attribute on your element in its entirety. What you probably want is a supplemental transformation that is added to the end of the existing transform list. This is steered by the attribute additive="sum" (default is "replace").

        <path style="stroke:none;fill:black" transform="translate(30.333,30.333) rotate(45)"
              d="M 0 0 L 0 -30.333 L -30.333 0 L -15.166 0 L -15.166 30.333 L 15.166
                 30.333 L 15.166 0 L 30.333 0 L 0 -30.333 Z "
              id="arrow-3,3">
          <animateTransform attributeName="transform" type="rotate"
                            from="0"
                            to="180"
                            begin="myButton.click" dur="1s" repeatCount="100"
                            additive="sum">
          </animateTransform>
        </path>

This will rotate the element around the (0, 0) point in local userspace coordinates, 100 times in a row, from 0 to 180 degrees. Three things to look carefully into:

  1. The center of rotation must be set in local userspace coordinates, after the application of all previous transforms, as it is added to the end of the transform list. (Transformations of coordinate systems, not of objects drawn into them.) Wht this amounts to is: it is the same coordinate system as the one used by the path data. The center of that path is at the coordinate system origin.

  2. The transformation adds to the underlying element transform. This means, it does not replace the initial 45deg rotation, but adds to it. Therefore it is generally a good idea to start additive transforms with an identity transform.

  3. As you defined, the rotation goes from 0 to 180 degrees, and then immediately restarts. This means it will jump back to its initial position without delay. If you want a smooth movement, either rotate from 0 to 360 degrees, or set the attribute accumulate="sum", which will start the next iteration at the end of the previous one.