--- jupyter: description: 'Animating a planex2d constraint sketch: sk.Animate solves the sketch for every row of its dimensions and plays the frames as a matplotlib animation. A four-bar linkage through a full turn, two designs with the steps between them, and rows without a solution.' --- # 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. ```python 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](mechanisms.md), 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°: ```python 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 ``` ## 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: ```python sk.Animate({"rocker 28": {"c": 28}, "rocker 36": {"c": 36}}, steps=10, fps=10) ``` ## 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: ```python 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 ``` See [Design tables in a spreadsheet](spreadsheets.md) for rows read from a file.