Add the exercise reference library, animated exercise figures, and exercise categories

Exercise Library/ holds per-exercise reference docs (setup, cues,
mistakes, progressions) with SVG visuals and a Python-rendered motion
pipeline; Workouts/ExerciseFigure renders the bundled *.motion.json
rigs as animated stick figures on the exercise screen. Exercises gain
a warm-up/main-circuit category, timed exercises display hold time via
planSummary, and a completed exercise reopens to a check screen instead
of its timers.
This commit is contained in:
2026-07-06 01:15:52 -04:00
parent ddd5631ef2
commit 7274f155e9
79 changed files with 2521 additions and 11 deletions
+97
View File
@@ -0,0 +1,97 @@
# Exercise Visual System
Exercise visuals are produced by an **articulated 2D rig**: one shared stick
body posed per exercise by joint angles. Nothing is drawn by hand — a body
profile plus a motion script resolve through forward kinematics into every
frame, so figures are always in proportion, and the whole library can be
re-proportioned (male/female), flipped, rotated in-plane, or re-themed by
changing data, never artwork.
## The rig
- **`body.json`** — proportion profiles. Each profile is a table of bone
lengths: `headR`, `neck`, `spine1`/`spine2` (two chained segments so the
spine can curve), `upperArm`/`foreArm`, `thigh`/`shin`, plus `leftOffset`
(the small offset that separates left-limb attachments visually).
`neutral` is the default; add profiles to add figures.
- **`<Exercise>/motion.json`** — the exercise script: key frames of
**absolute joint angles** (degrees, y-up: `0`=forward/right, `90`=up,
`180`=back/left, `-90`=down), a `root` pelvis position, and timing.
```json
{
"name": "Bird Dog",
"primary": 2, // 1-based frame used for visual.svg
"working": ["arm_r", "leg_l"], // parts drawn in the accent color
"hide": [], // limbs fully occluded in this view
"frames": [
{
"hold": 0.5, // seconds held at this key frame
"tween": 0.8, // seconds animating to the NEXT frame
"root": [190, 106], // pelvis, canvas coords
"spine": [171, 171], // pelvis→mid, mid→neck angles
"neck": 187, "gaze": 205, // head direction; nose tick direction
"arm_r": [-90, -90], // upper-arm, forearm angles
"arm_l": [-90, -90],
"leg_r": [-83, 0], // thigh, shin angles
"leg_l": [-83, 0],
"pins": {"hand_r": [105, 152], "hand_l": [111, 154]}
}
]
}
```
- **Pins (IK)** — a planted hand/foot names a target point
(`hand_r`/`hand_l`/`foot_r`/`foot_l`); the renderer solves the two-bone
chain analytically so the extremity holds that point exactly, using the
authored angles only to pick the elbow/knee bend direction. A pin active in
two consecutive key frames stays planted *throughout the tween* (plank
forearms, side-plank support arm); a pin present in only one frame releases
naturally (bird-dog arm lifting off).
- **Tweening** happens in angle space (shortest path), so limbs swing in
natural arcs and bone lengths never distort. The last frame tweens back to
the first (looping). Asymmetric timing carries technique: leg raises lower
slowly (`tween` 1.4 s down, 0.6 s up).
- The same math is the plan for the app: a small SwiftUI renderer consumes
`body.json` + `motion.json` and tweens angles on the lower half of the
exercise screen (the paged timer flow occupies only the top half).
## The visual language
- **Right vs left limb** — the one rule that never bends: the figure's
**right**-side limbs are dark (`#3a3f4b`), its **left**-side limbs are light
(`#a9afba`) and drawn *behind* the body. Working limbs keep the split:
right = teal `#0d9488`, left = light teal `#86cfc5`. Every render embeds a
small `R —` / `L —` legend, so opposite-limb moves (bird dog, dead bug) are
visibly opposite: one dark-teal limb, one light-teal limb.
- **Facing / front-of-torso** — the head carries a **nose tick** (`gaze`
angle); the belly is on that side. Prone noses point at the floor, supine
at the ceiling. The head is drawn last, filled opaque, so overhead arms
pass behind the face.
- **Spine** — rendered as a smooth curve through pelvis → mid → neck; teal
when the trunk is the working part.
- Canvas 320×180, ground line at y = 152. Limbs listed in `hide` are fully
occluded in this view and not drawn.
## Rendering
```sh
cd "Exercise Library"
python3 render.py # all exercises: frames/*.svg, preview.gif, visual.svg
python3 render.py "Bird Dog" # one exercise
python3 render.py --sheet # + contact-sheet.png of every key frame
python3 render.py --demo # + demo-sheet.png: profile / flip / theme variants
python3 render.py --figure=female # render with another body profile
python3 render.py --flip # mirror the figure (faces the other way)
python3 render.py --export # copy body.json + <Name>.motion.json app resources
```
`render.py` needs only Pillow (for GIFs/sheets; the SVGs have no dependency).
The library lives at the repo root, outside every target's source folders —
same-named files per entry (`info.md`, `visual.svg`) would collide in Xcode's
flat resource copy, so the library itself never enters the app bundle. Only
the `--export` copies ship: `body.json` plus uniquely-named
`<Name>.motion.json` files in `Workouts/Resources/ExerciseMotions/`, consumed
by the in-app SwiftUI renderer (`Workouts/ExerciseFigure/`). Re-run
`python3 render.py --export` after editing any motion; the library stays the
source of truth.