Animating a sketch

sk.Animate(rows) plays a constraint sketch through rows of its dimensions as a matplotlib animation. Every frame is the sketch solved for one row and drawn. The frames are solved on a copy, so the sketch itself stays as it is, and all of them share one view. A notebook shows the animation as a player, and anim.save("linkage.gif") writes it to a file, a GIF with Pillow or an MP4 with ffmpeg. The rows come in three forms:

Rows

Frames

{"phi": [20, 25, 30, ...]}

One frame per value; several dimensions as arrays of equal length

[{"c": 28}, {"c": 36}]

One frame per dictionary; a row keeps the values the rows before it set

{"short": {"c": 28}, "long": {"c": 36}}

One frame per named row, titled with its name, as sk.ReadSpreadsheet returns them

steps= adds frames between two rows, fps= sets the frames per second, and label= writes a text into every frame, computed from the solved sketch.

[1]:
import math
import warnings

import numpy as np
import planex as px

A linkage through a full turn

The example is the four-bar linkage from Mechanisms: the sketch as a solver, with a crank of 10, a coupler of 35 and a rocker of 30 on pivots 40 apart. The crank angle phi runs through a full turn in steps of 5°, and the label shows the reference dimension span in every frame. The sketch keeps its own crank angle of 20°:

[2]:
sk = px.Sketch()
sk.Move((0, 0), px.Fix).Line(10, 3, id="crank").Line(32, 10, id="coupler")
sk.Line(-2, -13, id="rocker")
sk.Move("crank.start", px.Coincident).Line(40, 0, id="ground", construction=True)
sk.Constrain("ground.end", px.Fix)
sk.Constrain("rocker.end", "ground.end", px.Coincident)
sk.Constrain("crank", px.Distance, 10, id="a")
sk.Constrain("coupler", px.Distance, 35, id="b")
sk.Constrain("rocker", px.Distance, 30, id="c")
sk.Constrain("ground", "crank", px.Angle, 20, id="phi")
sk.Constrain("coupler.end", "crank.start", px.Distance, id="span", reference=True)

turn = sk.Animate({"phi": np.arange(20, 381, 5)}, fps=20, label=lambda s: "span %.2f" % s["span"])
print("phi after the animation", sk["phi"])
turn
phi after the animation 20.0
[2]:

Two designs and the steps between

Named rows compare designs. The example sets the rocker to 28 and then to 36, at the same crank angle. With steps=10, nine frames in between move the linkage from one design to the next, each one solved, while the design reached last stays drawn and the frames in between move over it, muted:

[3]:
sk.Animate({"rocker 28": {"c": 28}, "rocker 36": {"c": 36}}, steps=10, fps=10)
[3]:

Rows without a solution

A row without a solution keeps the last good geometry, drawn in gray with the reason in its title, and one warning lists all such rows. The example lets the coupler grow from 35 to 75 in steps of 10. At a crank angle of 20°, the joint can close only while the coupler is shorter than the rocker plus the distance from the crank’s end to the rocker’s pivot, so the last two rows have no solution:

[4]:
with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    grown = sk.Animate({"b": [35, 45, 55, 65, 75]}, fps=2)
print(caught[0].message)
reach = 30 + math.hypot(40 - 10 * math.cos(math.radians(20)), 10 * math.sin(math.radians(20)))
print("the longest coupler that closes", reach)
grown
Animate: 2 rows did not solve and show the last good geometry: b = 65, b = 75
the longest coupler that closes 60.79360166286615
[4]:

See Design tables in a spreadsheet for rows read from a file.