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
- Remove old points then create new points
- Create invisible points and change visibility
- setPoint (move a point using a translation)
- setPointDirectly (move a point by changing the cx- and cy-values)
- Apply a transform (translation, rotation, etc) to a point
- moveTo (move to a new location over a time period)
- visit (move to a new location then move back)
- moveAlong (move a point along an existing path)
- Apply a transform independent of the JSXGraph flow
- 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: - 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:
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:
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
- Create new points (slow, DOM heavy)
- Create invisible points (slow, DOM heavy)
- setPoint and setPointDirectly (slow
suspendUpdate()
) - Apply a transform (internal updates, high CPU)
- moveTo (one straight line animation OK)
- visit (straight OK, curves OK if once-only)
- moveAlong (straight OK, curves OK if once-only)
- Apply a transform independent of the JSXGraph flow (modCxCy)
- In general, transforms are the most efficient way to move SVG elements
The end
Thank you!
@bourne_2_learn