Smooth JSXGraph Animations

Murray Bourne
(formerly) intmath.com

JSXGraph International Conference Oct 2022

300 points, 4 rotation speeds, fps

Our smooth animation challenge

Our task

  • Animate a large number of points in JSXGraph (explode, then rotate)
  • Achieve 60 frames per second (screen refresh rate)
  • Interact with points after each animation
  • Ensure smooth animation even on low-end phones (30+ fps, no stutter)
  • Minimal DOM (number of HTML elements)
  • Minimal CPU.

JSXGraph animation possibilities

  1. Remove old points then create new points
  2. Create invisible points and change visibility
  3. setPoint (move a point using a translation)
  4. setPointDirectly (move a point by changing the cx- and cy-values)
  5. Apply a transform (translation, rotation, etc) to a point
  6. moveTo (move to a new location over a time period)
  7. visit (move to a new location then move back)
  8. moveAlong (move a point along an existing path)
  9. Apply a transform independent of the JSXGraph flow
  10. In general, transforms are the most efficient way to move SVG elements.

SVG basics

  • JSXGraph creates SVGs. A simple SVG with a "point":
    • <svg width="100" height="60">
    • <ellipse cx="10" cy="20" rx="3" ry="3" id="b_i2_j3"></ellipse>
    • </svg>
  • Move the point by changing cx and cy:
    • <ellipse cx="80" cy="40" ... ></ellipse>
  • Target the <ellipse> in JSXGraph with:
  • pt.rendNode
  • (equivalent to document.querySelector('#b_i2_j3'))

Animation basics

  • We use requestAnimationFrame since it fires 60 times per second (or screen refresh rate):
    • function doRotations() { // move points here raf = window.requestAnimationFrame(doRotations); }

Performance monitoring

  • Chrome developer tools has (choose from bottom 3 dots menu):
    • Rendering: "Frame rendering stats" (real time), plus many other useful tools
    • Performance monitor: CPU, DOM nodes, etc
  • Performance recorder (choose from top tab) where you can record page activity and analyse bottlenecks.

1. setPosition: code

    • board.suspendUpdate();
    • pt.setPosition(JXG.COORDS_BY_USER, [x,y]);
    • board.unsuspendUpdate();

1. setPosition: Demo

1. setPosition: conclusion

  • Can interact with points after rotation
  • Slow because of board.suspendUpdate()board.unsuspendUpdate()
  • Need to keep track of points in a separate matrix
  • Troublesome
  • For setPosition, the Performance Recorder shows:
  • performance stats setPosition
  • Similar (slow) result for setPositionDirectly.

2. Transform: code

  • board.suspendUpdate();
  • tr = board.create('transform', [-Math.PI/180], {type: 'rotate'});
  • tr.applyOnce(pt);
  • board.unsuspendUpdate();

2. Transform: Demo

2. Transform: conclusion

  • Can interact with points after rotation
  • Once again, slow because of board.suspendUpdate()board.unsuspendUpdate()
  • Troublesome.

3. moveTo: code

  • pt.moveTo([x,y], 500)
  • Internally, this uses setPosition but does it smoothly across the given duration (here, 0.5 second)
  • We also need to keep track of points that have been dragged, using our own matrix.

3. moveTo: Demo

3. moveTo: Conclusion

  • Can interact with points after rotation
  • Need to keep track of points in a separate matrix
  • Good for straight line movement (one step code); but
  • Not successful for circles and curves (needs new moveTo for each small increment).
  • For moveTo, the Performance Recorder shows:
  • performance stats moveTo

4. moveAlong (a given path): code

  • We need to set up the functions for the x- and y-values for the circles:
    • function f(x) { return -Math.sqrt(2)*Math.cos(x + 3*Math.PI/4); } function g(x) { return Math.sqrt(2)*Math.sin(x + 3*Math.PI/4); }
  • Then we need to set up the paths, with the first moveAlong:
    • var paths = []; thePoints.forEach( function(thePoint, j) { paths[j] = []; for (i = 0; 2*Math.PI > i; i+=0.1) { paths[j].push([(j-7)*f(i), (j-7)*g(i)]); } thePoint.moveAlong(paths[j], 5000, {interpolate:false}); });
  • Then we need to set up a timer for the repeats:
    • timer = setInterval(function() { thePoints.forEach( function(thePoint, j) { thePoint.moveAlong(paths[j], 5000, {interpolate:false}); }); }, 5000);

4. moveAlong: Demo

4. moveAlong: Conclusion

  • Achieves smooth 60 fps
  • Very troublesome to set up all points and circles and allow for stop/start and drag
  • No need to tell JSXGraph where the points are after translation and rotation, but:
  • Issues with dragging correct dot on phones.

5. modCxCy (non-JSXGraph): code (i)

  • This animation operates outside of the JSXGraph work flow
  • We change the cx- and cy-values of each point's <ellipse> directly:
    • var newCx = brdWidth/2 + cx*cos θ - cy*sin θ;
    • var newCy = brdHeight/2 + cx*sin θ + cy*cos θ;
    • pt.rendNode.setAttribute("cx", newCx);
    • pt.rendNode.setAttribute("cy", newCy);
  • After the animation (on "Stop"), we need to tell JSXGraph where the points are:
    • pt.coords.scrCoords[1] = 1 * pt.rendNode.getAttribute("cx");
    • pt.coords.scrCoords[2] = 1 * pt.rendNode.getAttribute("cy");

5. modCxCy: code (ii)

  • After each drag, we also need to tell JSXGraph where the points are.
  • I keep track of where everything is with my own separate cxcyMatrix matrix, as before.

5. modCxCy: Demo

5. modCxCy: Conclusion (i)

  • We directly change cx- and cy-values of each <ellipse>
  • (Could also directly use translate and rotate transformations)
  • Achieves smooth 60 fps (low CPU)
  • Performance recorder shows:
  • performance stats modCxCy

7. modCxCy: Conclusion (ii)

  • Can interact with points after rotation
  • Need to tell JSXGraph where the points are after each animation and drag
  • Need to keep track of points in a separate matrix.

Overall conclusions: smooth animations

  1. Create new points (slow, DOM heavy) red cross
  2. Create invisible points (slow, DOM heavy) red cross
  3. setPoint and setPointDirectly (slow suspendUpdate()) red cross
  4. Apply a transform (internal updates, high CPU) red cross
  5. moveTo (one straight line animation OK) green ticks
  6. visit (straight OK, curves OK if once-only) green ticks
  7. moveAlong (straight OK, curves OK if once-only) green ticks
  8. Apply a transform independent of the JSXGraph flow (modCxCy) green ticks
  9. In general, transforms are the most efficient way to move SVG elements green ticks

The end

Thank you!

Bourne2Learn logo

twitter logo @bourne_2_learn