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
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Bird Dog</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 112.0 94.7 L 116.1 124.4 L 111.0 154.0" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 196.0 108.0 L 201.6 153.7 L 241.6 153.7" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 190.0 106.0 Q 147.0 99.2 106.0 92.7" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 106.0 92.7 L 110.1 122.4 L 105.0 152.0" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 190.0 106.0 L 195.6 151.7 L 235.6 151.7" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="86.2" cy="95.1" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="77.1" y1="99.4" x2="70.8" y2="102.3" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Bird Dog</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 112.0 94.7 L 116.1 124.4 L 111.0 154.0" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 196.0 108.0 L 240.4 96.1 L 279.1 85.7" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 190.0 106.0 Q 147.0 99.2 106.0 92.7" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 106.0 92.7 L 77.7 82.9 L 49.3 73.2" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 190.0 106.0 L 195.6 151.7 L 235.6 151.7" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="86.2" cy="95.1" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="76.5" y1="97.7" x2="69.8" y2="99.5" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+38
View File
@@ -0,0 +1,38 @@
# Bird Dog
Anti-rotation drill — extend opposite limbs while the hips and spine refuse to
twist.
- **Category:** Warm-up and main circuit
- **Type:** Anti-rotation
- **Targets:** Spinal erectors, glutes, deep core, shoulder stability
- **Prescription:** Warm-up 8 per side; main circuit 3 × 810 per side, slow
## Setup
On all fours: hands under shoulders, knees under hips, spine neutral, gaze at
the floor.
## Execution
1. Brace, then reach one arm forward and the *opposite* leg back until both are
in line with the torso.
2. Pause for a beat at full extension — hips stay square to the floor.
3. Return under control and switch sides.
## Cues
- Imagine a cup of water balanced on the lower back — nothing spills.
- Reach *long*, not high: heel pushes back, knuckles push forward.
- Slow, no momentum; the pause is where the work happens.
## Common Mistakes
- Hips rotating open as the leg lifts.
- Lifting the leg above hip height and arching the lower back.
- Racing through reps.
## Progression
Single limb only (arm *or* leg) → opposite arm + leg → paused 3-count hold →
elbow-to-knee touch under the body between reps → hands on an unstable surface
+23
View File
@@ -0,0 +1,23 @@
{
"name": "Bird Dog",
"primary": 2,
"working": ["arm_r", "leg_l"],
"frames": [
{
"hold": 0.5, "tween": 0.8,
"root": [190, 106],
"spine": [171, 171], "neck": 187, "gaze": 205,
"arm_r": [-90, -90], "arm_l": [-90, -90],
"leg_r": [-83, 0], "leg_l": [-83, 0],
"pins": {"hand_r": [105, 152], "hand_l": [111, 154]}
},
{
"hold": 1.4, "tween": 0.8,
"root": [190, 106],
"spine": [171, 171], "neck": 187, "gaze": 195,
"arm_r": [161, 161], "arm_l": [-90, -90],
"leg_r": [-83, 0], "leg_l": [15, 15],
"pins": {"hand_l": [111, 154]}
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Bird Dog</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 112.0 94.7 L 116.1 124.4 L 111.0 154.0" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 196.0 108.0 L 240.4 96.1 L 279.1 85.7" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 190.0 106.0 Q 147.0 99.2 106.0 92.7" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 106.0 92.7 L 77.7 82.9 L 49.3 73.2" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 190.0 106.0 L 195.6 151.7 L 235.6 151.7" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="86.2" cy="95.1" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="76.5" y1="97.7" x2="69.8" y2="99.5" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Cat-Cow</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 115.4 96.2 L 121.0 125.7 L 111.0 154.0" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 196.0 108.0 L 201.6 153.7 L 241.6 153.7" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 190.0 106.0 Q 145.2 123.9 109.4 94.2" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 109.4 94.2 L 115.0 123.7 L 105.0 152.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 190.0 106.0 L 195.6 151.7 L 235.6 151.7" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="89.4" cy="96.0" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="79.8" y1="93.2" x2="73.1" y2="91.3" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Cat-Cow</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 115.9 98.6 L 124.6 127.3 L 111.0 154.0" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 196.0 108.0 L 201.6 153.7 L 241.6 153.7" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 190.0 106.0 Q 152.1 74.4 109.9 96.6" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 109.9 96.6 L 118.6 125.3 L 105.0 152.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 190.0 106.0 L 195.6 151.7 L 235.6 151.7" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="93.6" cy="108.0" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="86.5" y1="115.1" x2="81.5" y2="120.1" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+37
View File
@@ -0,0 +1,37 @@
# Cat-Cow
Gentle spinal mobility drill that wakes up the whole trunk before core work.
- **Category:** Warm-up
- **Type:** Spinal mobility
- **Targets:** Thoracic and lumbar spine, deep core awareness
- **Prescription:** 10 slow reps (one rep = cat + cow)
## Setup
On all fours: hands under shoulders, knees under hips, spine neutral, gaze at
the floor.
## Execution
1. **Cat** — exhale, press the floor away, round the whole spine up toward the
ceiling and tuck chin and tailbone.
2. **Cow** — inhale, let the belly drop, lift the chest and tailbone, gaze
slightly forward.
3. Flow between the two, one breath per position.
## Cues
- Move segment by segment, not as one stiff plank.
- Let the breath set the tempo — no rushing.
- Reach the mid-back toward the ceiling in cat; most people only move the neck.
## Common Mistakes
- All the motion coming from the neck and hips, none from the mid-back.
- Collapsing into the shoulders instead of pressing the floor away.
## Progression
Smaller pain-free range → full range → slower tempo with a pause at each end →
segmental rolls (peel one vertebra at a time)
+23
View File
@@ -0,0 +1,23 @@
{
"name": "Cat-Cow",
"primary": 1,
"working": ["spine"],
"frames": [
{
"hold": 0.8, "tween": 0.9,
"root": [190, 106],
"spine": [188, 155], "neck": 185, "gaze": 164,
"arm_r": [-90, -90], "arm_l": [-90, -90],
"leg_r": [-83, 0], "leg_l": [-83, 0],
"pins": {"hand_r": [105, 152], "hand_l": [111, 154]}
},
{
"hold": 0.8, "tween": 0.9,
"root": [190, 106],
"spine": [155, 192], "neck": 215, "gaze": 225,
"arm_r": [-90, -90], "arm_l": [-90, -90],
"leg_r": [-83, 0], "leg_l": [-83, 0],
"pins": {"hand_r": [105, 152], "hand_l": [111, 154]}
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Cat-Cow</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 115.4 96.2 L 121.0 125.7 L 111.0 154.0" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 196.0 108.0 L 201.6 153.7 L 241.6 153.7" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 190.0 106.0 Q 145.2 123.9 109.4 94.2" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 109.4 94.2 L 115.0 123.7 L 105.0 152.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 190.0 106.0 L 195.6 151.7 L 235.6 151.7" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="89.4" cy="96.0" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="79.8" y1="93.2" x2="73.1" y2="91.3" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Dead Bug</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 106.0 150.0 L 106.0 120.0 L 106.0 90.0" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 191.0 150.0 L 211.2 108.7 L 250.6 115.6" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 185.0 148.0 Q 141.5 148.0 100.0 148.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 100.0 148.0 L 100.0 118.0 L 100.0 88.0" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 185.0 148.0 L 205.2 106.7 L 244.6 113.6" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="80.4" cy="143.8" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="80.4" y1="133.8" x2="80.4" y2="126.8" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Dead Bug</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 106.0 150.0 L 106.0 120.0 L 106.0 90.0" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 191.0 150.0 L 235.2 137.3 L 273.7 126.3" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 185.0 148.0 Q 141.5 148.0 100.0 148.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 100.0 148.0 L 78.8 126.8 L 57.6 105.6" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 185.0 148.0 L 205.2 106.7 L 244.6 113.6" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="80.4" cy="143.8" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="80.4" y1="133.8" x2="80.4" y2="126.8" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+37
View File
@@ -0,0 +1,37 @@
# Dead Bug
Anti-extension drill — the limbs move while the trunk stays perfectly still.
- **Category:** Warm-up
- **Type:** Anti-extension
- **Targets:** Deep core (transverse abdominis), hip flexor control
- **Prescription:** 8 per side, slow and controlled
## Setup
Lie on your back. Arms straight up over the shoulders, hips and knees bent to
90°, lower back pressed into the floor.
## Execution
1. Brace and exhale as you lower one arm overhead and the *opposite* leg toward
the floor.
2. Stop just before the lower back peels off the floor.
3. Return under control and switch sides.
## Cues
- Lower back glued to the floor the entire time — that contact *is* the exercise.
- Slow negatives; the reach out should take 23 seconds.
- Ribs down, no flaring as the arm goes overhead.
## Common Mistakes
- Arching the lower back as the leg lowers.
- Moving arm and leg on the same side.
- Rushing reps and turning it into a flutter kick.
## Progression
Heels sliding on floor → bent-knee reach → straight-leg reach → both legs
lowering together → light weight held overhead
+21
View File
@@ -0,0 +1,21 @@
{
"name": "Dead Bug",
"primary": 2,
"working": ["arm_r", "leg_l"],
"frames": [
{
"hold": 0.5, "tween": 0.8,
"root": [185, 148],
"spine": [180, 180], "neck": 168, "gaze": 90,
"arm_r": [90, 90], "arm_l": [90, 90],
"leg_r": [64, -10], "leg_l": [64, -10]
},
{
"hold": 0.9, "tween": 0.8,
"root": [185, 148],
"spine": [180, 180], "neck": 168, "gaze": 90,
"arm_r": [135, 135], "arm_l": [90, 90],
"leg_r": [64, -10], "leg_l": [16, 16]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Dead Bug</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 106.0 150.0 L 106.0 120.0 L 106.0 90.0" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 191.0 150.0 L 235.2 137.3 L 273.7 126.3" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 185.0 148.0 Q 141.5 148.0 100.0 148.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 100.0 148.0 L 78.8 126.8 L 57.6 105.6" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 185.0 148.0 L 205.2 106.7 L 244.6 113.6" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="80.4" cy="143.8" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="80.4" y1="133.8" x2="80.4" y2="126.8" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Hollow Body Hold</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="leg_l" d="M 191.0 150.0 L 237.0 150.0 L 277.0 150.0" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 185.0 148.0 Q 141.5 148.0 100.0 148.0" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 100.0 148.0 L 70.0 148.0 L 40.0 148.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 185.0 148.0 L 231.0 148.0 L 271.0 148.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="80.4" cy="143.8" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="80.4" y1="133.8" x2="80.4" y2="126.8" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Hollow Body Hold</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="leg_l" d="M 191.0 150.0 L 236.0 140.4 L 275.1 132.1" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 185.0 148.0 Q 140.6 148.4 102.8 129.1" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 102.8 129.1 L 75.6 116.5 L 48.4 103.8" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 185.0 148.0 L 230.0 138.4 L 269.1 130.1" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="87.4" cy="116.3" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="93.9" y1="108.6" x2="98.4" y2="103.3" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+37
View File
@@ -0,0 +1,37 @@
# Hollow Body Hold
The gold standard for deep core tension — a shallow, rigid banana shape.
- **Category:** Main circuit
- **Type:** Anti-extension (isometric hold)
- **Targets:** Rectus abdominis, deep core, hip flexors
- **Prescription:** 3 × 2040 s holds
## Setup
Lie on your back, arms extended overhead beside the ears, legs straight and
together.
## Execution
1. Press the lower back *into* the floor and hold it there — this never releases.
2. Lift shoulder blades and legs a few inches off the floor into a shallow
banana shape.
3. Reach long through fingers and toes and hold.
## Cues
- The lower back stays welded to the floor; if it lifts, shorten the shape.
- Chin gently tucked — look at your thighs, not the ceiling.
- Lower = harder. Height is not the goal, tension is.
## Common Mistakes
- Lower back arching off the floor (the hold has failed at that point).
- Folding at the hips into a V-sit.
- Neck cranked forward doing all the "work".
## Progression
Tuck hold (knees in) → one leg extended → full extension, arms overhead →
hollow rocks
@@ -0,0 +1,22 @@
{
"name": "Hollow Body Hold",
"primary": 2,
"working": ["spine"],
"hide": ["arm_l"],
"frames": [
{
"hold": 0.4, "tween": 0.8,
"root": [185, 148],
"spine": [180, 180], "neck": 168, "gaze": 90,
"arm_r": [180, 180],
"leg_r": [0, 0], "leg_l": [0, 0]
},
{
"hold": 1.8, "tween": 0.8,
"root": [185, 148],
"spine": [174, 160], "neck": 140, "gaze": 50,
"arm_r": [155, 155],
"leg_r": [12, 12], "leg_l": [12, 12]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Hollow Body Hold</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="leg_l" d="M 191.0 150.0 L 236.0 140.4 L 275.1 132.1" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 185.0 148.0 Q 140.6 148.4 102.8 129.1" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 102.8 129.1 L 75.6 116.5 L 48.4 103.8" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 185.0 148.0 L 230.0 138.4 L 269.1 130.1" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="87.4" cy="116.3" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="93.9" y1="108.6" x2="98.4" y2="103.3" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Leg Raises</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 106.0 150.0 L 136.0 151.0 L 166.0 152.1" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 191.0 150.0 L 214.0 110.2 L 234.0 75.5" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 185.0 148.0 Q 141.5 148.0 100.0 148.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 185.0 148.0 L 208.0 108.2 L 228.0 73.5" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="80.4" cy="143.8" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="80.4" y1="133.8" x2="80.4" y2="126.8" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Leg Raises</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 106.0 150.0 L 136.0 151.0 L 166.0 152.1" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 191.0 150.0 L 236.6 143.6 L 276.2 138.0" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 185.0 148.0 Q 141.5 148.0 100.0 148.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 185.0 148.0 L 230.6 141.6 L 270.2 136.0" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="80.4" cy="143.8" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="80.4" y1="133.8" x2="80.4" y2="126.8" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+37
View File
@@ -0,0 +1,37 @@
# Leg Raises
Lower-ab builder — the slow lowering is the exercise.
- **Category:** Main circuit
- **Type:** Anti-extension (dynamic)
- **Targets:** Lower rectus abdominis, hip flexors, deep core
- **Prescription:** 3 × 815 reps, slow negatives
## Setup
**Lying:** flat on your back, legs straight, hands at your sides (or under the
tailbone to start). **Hanging:** dead hang from a bar, shoulders packed.
## Execution
1. Press the lower back into the floor and brace.
2. Raise the legs to vertical without bending the knees more than slightly.
3. Lower *slowly* — 3 seconds or more — stopping before the lower back arches
off the floor.
## Cues
- Lower back flat against the floor on lying versions, always.
- The negative is the rep: lower slower than you lift.
- Exhale on the way down to keep the ribs from flaring.
## Common Mistakes
- Lower back arching off the floor near the bottom.
- Dropping the legs instead of lowering them.
- Using momentum (swinging) on hanging versions.
## Progression
Bent-knee lying raise → straight-leg lying raise → hanging knee raise →
hanging straight-leg raise → toes-to-bar
+22
View File
@@ -0,0 +1,22 @@
{
"name": "Leg Raises",
"primary": 1,
"working": ["leg_r", "leg_l"],
"hide": ["arm_r"],
"frames": [
{
"hold": 0.5, "tween": 1.4,
"root": [185, 148],
"spine": [180, 180], "neck": 168, "gaze": 90,
"arm_l": [-2, -2],
"leg_r": [60, 60], "leg_l": [60, 60]
},
{
"hold": 0.3, "tween": 0.6,
"root": [185, 148],
"spine": [180, 180], "neck": 168, "gaze": 90,
"arm_l": [-2, -2],
"leg_r": [8, 8], "leg_l": [8, 8]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

+16
View File
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Leg Raises</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 106.0 150.0 L 136.0 151.0 L 166.0 152.1" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 191.0 150.0 L 214.0 110.2 L 234.0 75.5" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 185.0 148.0 Q 141.5 148.0 100.0 148.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 185.0 148.0 L 208.0 108.2 L 228.0 73.5" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="80.4" cy="143.8" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="80.4" y1="133.8" x2="80.4" y2="126.8" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Plank</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 93.3 123.4 L 93.3 153.4 L 63.3 153.4" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 178.0 116.0 L 204.4 153.7 L 244.4 153.7" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 172.0 114.0 Q 128.7 117.8 87.3 121.4" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 87.3 121.4 L 87.3 151.4 L 57.3 151.4" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 172.0 114.0 L 198.4 151.7 L 238.4 151.7" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="67.4" cy="123.2" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="58.7" y1="128.2" x2="52.7" y2="131.7" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Plank</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 90.3 124.2 L 90.3 154.2 L 60.3 154.2" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 174.0 139.0 L 219.3 147.0 L 258.7 153.9" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 168.0 137.0 Q 125.2 129.4 84.3 122.2" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 84.3 122.2 L 84.3 152.2 L 54.3 152.2" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 168.0 137.0 L 213.3 145.0 L 252.7 151.9" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="64.6" cy="118.8" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="55.9" y1="123.8" x2="49.9" y2="127.3" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+36
View File
@@ -0,0 +1,36 @@
# Plank
The baseline anti-extension hold — a rigid line from head to heels.
- **Category:** Main circuit
- **Type:** Anti-extension (isometric hold)
- **Targets:** Rectus abdominis, deep core, glutes, shoulders
- **Prescription:** 3 × 3060 s holds
## Setup
Forearms on the floor, elbows under shoulders, feet together behind you. One
straight line from the back of the head to the heels.
## Execution
1. Squeeze glutes and quads hard, tuck ribs down toward the pelvis.
2. Press the floor away through the forearms — no sagging between the shoulders.
3. Breathe shallowly behind the brace and hold.
## Cues
- Glutes and quads *tight* — a plank fails at the hips first.
- Ribs down: think "short front, long back".
- If the hips sag or the lower back takes over, the set is done.
## Common Mistakes
- Hips sagging (lower back extension — the exact thing to resist).
- Hips piked high to make the hold easier.
- Holding the breath entirely.
## Progression
Knee plank → full plank → feet elevated → RKC plank (maximum-tension, shorter
holds) → weighted plank
+21
View File
@@ -0,0 +1,21 @@
{
"name": "Plank",
"primary": 2,
"working": ["spine"],
"frames": [
{
"hold": 0.4, "tween": 0.7,
"root": [172, 114],
"spine": [185, 185], "neck": 185, "gaze": 210,
"arm_r": [-90, 180], "arm_l": [-90, 180],
"leg_r": [-55, 0], "leg_l": [-55, 0]
},
{
"hold": 1.8, "tween": 0.7,
"root": [168, 137],
"spine": [170, 170], "neck": 170, "gaze": 210,
"arm_r": [-90, 180], "arm_l": [-90, 180],
"leg_r": [-10, -10], "leg_l": [-10, -10]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Plank</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 90.3 124.2 L 90.3 154.2 L 60.3 154.2" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 174.0 139.0 L 219.3 147.0 L 258.7 153.9" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 168.0 137.0 Q 125.2 129.4 84.3 122.2" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 84.3 122.2 L 84.3 152.2 L 54.3 152.2" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 168.0 137.0 L 213.3 145.0 L 252.7 151.9" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="64.6" cy="118.8" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="55.9" y1="123.8" x2="49.9" y2="127.3" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+29
View File
@@ -0,0 +1,29 @@
# Exercise Library
Reference material for exercises the app knows about — one folder per exercise,
named exactly as the exercise appears in the app.
Each entry contains:
- `info.md` — the details, always in this order: a one-line summary, quick facts
(**Category**, **Type**, **Targets**, **Prescription**), then **Setup**,
**Execution**, **Cues**, **Common Mistakes**, and **Progression**
(easier → harder — get strong by making the move harder, not by endless reps).
- `motion.json` — the exercise scripted as key frames of joint angles on the
shared rig (proportions in the library-level `body.json`), with IK pins for
planted hands/feet and tween timings. This is the **canonical source** for
all visuals and for the in-app animation planned for the lower half of the
exercise screen.
- Generated from it by `render.py` (never hand-edited):
`frames/frame-N.svg` (one per key frame), `preview.gif` (the tweened loop),
and `visual.svg` (the primary frame, for static contexts).
The rig and the visual language — right limbs dark / left limbs light with an
embedded R/L legend, nose tick for facing, teal on the working parts, timing
that encodes tempo, plus figure profiles, flipping, and theming — are defined
in `SYSTEM.md`, along with the `motion.json` schema and rendering
instructions.
The library lives at the repo root, deliberately outside the app targets'
source folders, and is not bundled into the app yet — the in-app presentation
format is still being worked out.
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Reverse Crunch</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 93.0 150.0 L 123.0 151.0 L 153.0 152.1" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 178.0 150.0 L 170.0 104.7 L 207.6 118.4" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 172.0 148.0 Q 128.5 148.0 87.0 148.0" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 172.0 148.0 L 164.0 102.7 L 201.6 116.4" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="67.4" cy="143.8" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="67.4" y1="133.8" x2="67.4" y2="126.8" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Reverse Crunch</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 88.6 145.5 L 118.6 146.5 L 148.6 147.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 172.0 138.0 L 152.6 96.3 L 192.0 103.3" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 166.0 136.0 Q 124.6 154.5 82.6 143.5" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 166.0 136.0 L 146.6 94.3 L 186.0 101.3" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="62.6" cy="144.2" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="62.6" y1="134.2" x2="62.6" y2="127.2" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+38
View File
@@ -0,0 +1,38 @@
# Reverse Crunch
Curl the pelvis toward the ribs — lower-ab flexion without neck strain.
- **Category:** Main circuit
- **Type:** Spinal flexion (lower)
- **Targets:** Lower rectus abdominis, deep core
- **Prescription:** 3 × 1215 reps, controlled negative
## Setup
Lie on your back, knees bent toward the chest, shins roughly parallel to the
floor, arms along your sides pressing lightly into the floor.
## Execution
1. Exhale and curl the pelvis up and toward the ribs, lifting the hips off the
floor — knees travel toward the chin.
2. Pause briefly at the top.
3. Lower the hips back down slowly, one vertebra at a time.
## Cues
- Curl, don't kick: the knees drifting up should come from the pelvis tilting,
not from leg swing.
- Controlled negative — resist gravity all the way down.
- Keep the head and shoulders relaxed on the floor.
## Common Mistakes
- Swinging the legs and lifting with momentum.
- Pushing hard through the arms instead of the abs.
- Letting the hips drop back down in one piece.
## Progression
Feet-elevated pelvic tilts → reverse crunch → slow 3-count negatives →
incline (head-up) reverse crunch → dragonflag negatives
@@ -0,0 +1,22 @@
{
"name": "Reverse Crunch",
"primary": 2,
"working": ["spine"],
"hide": ["arm_r"],
"frames": [
{
"hold": 0.5, "tween": 0.7,
"root": [172, 148],
"spine": [180, 180], "neck": 168, "gaze": 90,
"arm_l": [-2, -2],
"leg_r": [100, -20], "leg_l": [100, -20]
},
{
"hold": 0.8, "tween": 0.9,
"root": [166, 136],
"spine": [195, 175], "neck": 182, "gaze": 90,
"arm_l": [-2, -2],
"leg_r": [115, -10], "leg_l": [115, -10]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Reverse Crunch</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 88.6 145.5 L 118.6 146.5 L 148.6 147.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 172.0 138.0 L 152.6 96.3 L 192.0 103.3" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 166.0 136.0 Q 124.6 154.5 82.6 143.5" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 166.0 136.0 L 146.6 94.3 L 186.0 101.3" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="62.6" cy="144.2" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="62.6" y1="134.2" x2="62.6" y2="127.2" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+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.
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Side Plank</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 99.2 125.7 L 127.4 136.0 L 156.3 143.8" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 180.0 152.0 L 226.0 152.8 L 266.0 153.5" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 174.0 150.0 Q 132.6 136.6 93.2 123.7" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 93.2 123.7 L 90.0 153.6 L 60.0 152.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 174.0 150.0 L 220.0 150.8 L 260.0 151.5" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="74.1" cy="117.6" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="64.2" y1="116.7" x2="57.2" y2="116.1" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Side Plank</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 96.3 124.2 L 101.5 94.7 L 106.7 65.2" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 180.0 139.0 L 225.3 147.0 L 264.7 153.9" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 174.0 137.0 Q 131.2 129.4 90.3 122.2" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 90.3 122.2 L 90.0 152.2 L 60.0 152.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 174.0 137.0 L 219.3 145.0 L 258.7 151.9" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="70.6" cy="118.8" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="60.6" y1="118.8" x2="53.6" y2="118.8" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+36
View File
@@ -0,0 +1,36 @@
# Side Plank
Anti-lateral-flexion hold — resist the hips dropping toward the floor.
- **Category:** Main circuit
- **Type:** Anti-lateral flexion (isometric hold)
- **Targets:** Obliques, quadratus lumborum, glute medius, shoulder stability
- **Prescription:** 3 × 2045 s per side
## Setup
Lie on one side, elbow directly under the shoulder, forearm across the floor.
Feet stacked (or staggered), body in one straight line.
## Execution
1. Drive the hips up until shoulders, hips, and ankles form a straight line.
2. Stack the hips vertically — don't let the top hip roll forward or back.
3. Hold, then switch sides.
## Cues
- Hips *high*: most people sag an inch without noticing.
- Push the floor away through the forearm — no collapsing into the shoulder.
- Squeeze the bottom-side obliques and the glutes the whole hold.
## Common Mistakes
- Hips dropping toward the floor (the exact motion to resist).
- Rolling the chest toward the ceiling or the floor.
- Elbow drifting away from under the shoulder.
## Progression
Knees-down side plank → full side plank → top leg raised → reach-through
(thread the top arm under the body and back up)
+23
View File
@@ -0,0 +1,23 @@
{
"name": "Side Plank",
"primary": 2,
"working": ["spine"],
"frames": [
{
"hold": 0.4, "tween": 0.8,
"root": [174, 150],
"spine": [162, 162], "neck": 162, "gaze": 175,
"arm_r": [-90, 180], "arm_l": [-20, -15],
"leg_r": [-1, -1], "leg_l": [-1, -1],
"pins": {"hand_r": [60, 152]}
},
{
"hold": 1.8, "tween": 0.8,
"root": [174, 137],
"spine": [170, 170], "neck": 170, "gaze": 180,
"arm_r": [-90, 180], "arm_l": [80, 80],
"leg_r": [-10, -10], "leg_l": [-10, -10],
"pins": {"hand_r": [60, 152]}
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
<title>Side Plank</title>
<line x1="16" y1="156" x2="304" y2="156" stroke="#b9bec9" stroke-width="3" stroke-linecap="round"/>
<path id="arm_l" d="M 96.3 124.2 L 101.5 94.7 L 106.7 65.2" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_l" d="M 180.0 139.0 L 225.3 147.0 L 264.7 153.9" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path id="spine" d="M 174.0 137.0 Q 131.2 129.4 90.3 122.2" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="arm_r" d="M 90.3 122.2 L 90.0 152.2 L 60.0 152.0" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path id="leg_r" d="M 174.0 137.0 L 219.3 145.0 L 258.7 151.9" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<circle id="head" cx="70.6" cy="118.8" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
<line id="nose" x1="60.6" y1="118.8" x2="53.6" y2="118.8" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
<line x1="242" y1="16" x2="258" y2="16" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
<text x="264" y="20">R</text>
<line x1="282" y1="16" x2="298" y2="16" stroke="#a9afba" stroke-width="4" stroke-linecap="round"/>
<text x="304" y="20">L</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+24
View File
@@ -0,0 +1,24 @@
{
"neutral": {
"headR": 10,
"neck": 20,
"spine1": 43,
"spine2": 42,
"upperArm": 30,
"foreArm": 30,
"thigh": 46,
"shin": 40,
"leftOffset": [6, 2]
},
"female": {
"headR": 9,
"neck": 19,
"spine1": 40,
"spine2": 39,
"upperArm": 27,
"foreArm": 27,
"thigh": 47,
"shin": 41,
"leftOffset": [6, 2]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+442
View File
@@ -0,0 +1,442 @@
#!/usr/bin/env python3
"""Render Exercise Library visuals from an articulated 2D rig.
A body profile (body.json: bone lengths) plus a per-exercise motion script
(motion.json: key frames of absolute joint angles, root position, optional
IK pins, timing) resolve through forward kinematics into stick-figure frames.
See SYSTEM.md for the format and the visual language.
Outputs per exercise: frames/frame-N.svg, preview.gif (tweened, looping),
visual.svg (the primary frame). `--sheet` writes contact-sheet.png of every
key frame; `--demo` writes demo-sheet.png showing rig customizations (figure
profile, flip, theme). `--export` instead copies body.json and each entry's
motion.json (as `<Name>.motion.json`) into Workouts/Resources/ExerciseMotions
for the in-app SwiftUI renderer. SVGs need no dependencies; GIFs/sheets need
Pillow.
Angles are absolute world angles in degrees, y-up: 0 = toward +x (right),
90 = straight up, 180 = toward -x (left), -90 = straight down.
"""
import json
import math
import sys
from pathlib import Path
LIB = Path(__file__).parent
CANVAS = (320, 180)
GROUND_Y = 152
PALETTES = {
"default": {
"right": "#3a3f4b", "left": "#a9afba",
"right_working": "#0d9488", "left_working": "#86cfc5",
"ground": "#b9bec9", "legend_text": "#6b7180", "head_fill": "white",
},
"indigo": {
"right": "#3a3f4b", "left": "#a9afba",
"right_working": "#4f46e5", "left_working": "#a5b4fc",
"ground": "#b9bec9", "legend_text": "#6b7180", "head_fill": "white",
},
}
WIDTHS = {"right": 6, "left": 5, "spine": 6, "head": 6, "nose": 4}
# Draw order: left limbs behind, spine, right limbs, then the head on top —
# the head is filled opaque so limbs passing it (arms overhead beside the
# ears) are occluded instead of crossing through the face.
PART_ORDER = ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"]
LIMBS = { # limb -> (attach joint, [bone length keys], pin key)
"arm_r": ("shoulder_r", ["upperArm", "foreArm"], "hand_r"),
"arm_l": ("shoulder_l", ["upperArm", "foreArm"], "hand_l"),
"leg_r": ("hip_r", ["thigh", "shin"], "foot_r"),
"leg_l": ("hip_l", ["thigh", "shin"], "foot_l"),
}
ANGLE_KEYS = ["spine", "neck", "gaze", "arm_r", "arm_l", "leg_r", "leg_l"]
def bodies():
return json.loads((LIB / "body.json").read_text())
def dirv(deg):
"""Unit vector for a y-up angle, in y-down canvas coordinates."""
r = math.radians(deg)
return (math.cos(r), -math.sin(r))
def walk(start, angles, lengths):
"""Chain FK: [start, joint1, joint2, ...] for per-bone absolute angles."""
pts = [start]
x, y = start
for a, ln in zip(angles, lengths):
dx, dy = dirv(a)
x, y = x + dx * ln, y + dy * ln
pts.append((x, y))
return pts
def angle_of(a, b):
"""Y-up world angle of the segment a→b."""
return math.degrees(math.atan2(-(b[1] - a[1]), b[0] - a[0]))
def ik2(start, target, a, b, guess_angles):
"""Analytic 2-bone IK: angles [upper, lower] reaching from start toward
target, choosing the elbow solution nearest the authored guess."""
dx, dy_up = target[0] - start[0], -(target[1] - start[1])
d = max(abs(a - b) + 0.5, min(a + b - 0.01, math.hypot(dx, dy_up)))
base = math.degrees(math.atan2(dy_up, dx))
alpha = math.degrees(math.acos((a * a + d * d - b * b) / (2 * a * d)))
guess_elbow = walk(start, guess_angles, [a, b])[1]
best = None
for sign in (1, -1):
upper = base + sign * alpha
elbow = walk(start, [upper], [a])[1]
dist = math.dist(elbow, guess_elbow)
if best is None or dist < best[0]:
best = (dist, [upper, angle_of(elbow, target)])
return best[1]
# ------------------------------------------------------------------- solver
def flip_frame(kf, width):
"""Mirror a key frame horizontally: x -> width - x, angle -> 180 - angle."""
out = dict(kf)
out["root"] = [width - kf["root"][0], kf["root"][1]]
for key in ANGLE_KEYS:
if key not in kf:
continue
v = kf[key]
out[key] = [180 - a for a in v] if isinstance(v, list) else 180 - v
if "pins" in kf:
out["pins"] = {k: [width - x, y] for k, (x, y) in kf["pins"].items()}
return out
def normalize(kf, body, flip=False):
"""Resolve a key frame to pure angles: apply the mirror, then replace each
pinned limb's authored angles with its IK solution. Normalized frames
interpolate cleanly (angle space), and geometry() re-pins tween frames."""
if flip:
kf = flip_frame(kf, CANVAS[0])
out = {"root": list(kf["root"]), "pins": dict(kf.get("pins", {})),
"hold": kf.get("hold", 0.5), "tween": kf.get("tween", 0.6)}
for key in ANGLE_KEYS:
if key in kf:
out[key] = list(kf[key]) if isinstance(kf[key], list) else kf[key]
attach = attach_points(out, body, flip)
for limb, (joint, bone_keys, pin) in LIMBS.items():
if limb in out and pin in out["pins"]:
lengths = [body[k] for k in bone_keys]
out[limb] = ik2(attach[joint], out["pins"][pin], *lengths, out[limb])
return out
def attach_points(frame, body, flip=False):
"""FK for the trunk: pelvis, spine mid, neck joint, and limb attachments."""
pelvis = tuple(frame["root"])
mid = walk(pelvis, [frame["spine"][0]], [body["spine1"]])[1]
neck = walk(mid, [frame["spine"][1]], [body["spine2"]])[1]
ox, oy = body.get("leftOffset", [6, 2])
if flip:
ox = -ox
return {"pelvis": pelvis, "mid": mid, "neck": neck,
"shoulder_r": neck, "shoulder_l": (neck[0] + ox, neck[1] + oy),
"hip_r": pelvis, "hip_l": (pelvis[0] + ox, pelvis[1] + oy)}
def geometry(frame, body, hide=(), flip=False):
"""Normalized frame -> drawable points. Limbs with an active pin are
re-solved so planted hands/feet hold exactly through tweens."""
at = attach_points(frame, body, flip)
head = walk(at["neck"], [frame["neck"]], [body["neck"]])[1]
nose_tip = walk(head, [frame["gaze"]], [body["headR"] + 7])[1]
geo = {"head": head, "nose": nose_tip, "headR": body["headR"],
"spine": [at["pelvis"],
(2 * at["mid"][0] - (at["pelvis"][0] + at["neck"][0]) / 2,
2 * at["mid"][1] - (at["pelvis"][1] + at["neck"][1]) / 2),
at["neck"]]}
for limb, (joint, bone_keys, pin) in LIMBS.items():
if limb not in frame or limb in hide:
continue
lengths = [body[k] for k in bone_keys]
angles = frame[limb]
if pin in frame.get("pins", {}):
angles = ik2(at[joint], frame["pins"][pin], *lengths, angles)
geo[limb] = walk(at[joint], angles, lengths)
return geo
# ------------------------------------------------------------------ tweening
def ease(t):
return 3 * t * t - 2 * t * t * t
def lerp_angle(a, b, t):
return a + ((b - a + 180) % 360 - 180) * t
def lerp_norm(a, b, t):
out = {"root": [a["root"][0] + (b["root"][0] - a["root"][0]) * t,
a["root"][1] + (b["root"][1] - a["root"][1]) * t]}
for key in ANGLE_KEYS:
if key not in a or key not in b:
continue
va, vb = a[key], b[key]
out[key] = ([lerp_angle(x, y, t) for x, y in zip(va, vb)]
if isinstance(va, list) else lerp_angle(va, vb, t))
# A pin survives a tween only if planted in both neighboring key frames.
out["pins"] = {k: [a["pins"][k][0] + (b["pins"][k][0] - a["pins"][k][0]) * t,
a["pins"][k][1] + (b["pins"][k][1] - a["pins"][k][1]) * t]
for k in a["pins"] if k in b["pins"]}
return out
def timeline(norms, fps=20):
frames = []
for i, kf in enumerate(norms):
frames += [kf] * max(1, round(kf["hold"] * fps))
nxt = norms[(i + 1) % len(norms)]
steps = max(1, round(kf["tween"] * fps))
frames += [lerp_norm(kf, nxt, ease(s / steps)) for s in range(1, steps)]
return frames
# ------------------------------------------------------------------- drawing
def part_style(part, working, colors):
side = "left" if part.endswith("_l") else "right"
color = colors[f"{side}_working"] if part in working else colors[side]
width = WIDTHS["spine"] if part == "spine" else WIDTHS[side]
return color, width
def quad_points(p0, ctrl, p2, n=24):
pts = []
for i in range(n + 1):
t = i / n
pts.append(((1 - t) ** 2 * p0[0] + 2 * (1 - t) * t * ctrl[0] + t ** 2 * p2[0],
(1 - t) ** 2 * p0[1] + 2 * (1 - t) * t * ctrl[1] + t ** 2 * p2[1]))
return pts
def nose_segment(geo):
hx, hy = geo["head"]
nx, ny = geo["nose"]
d = math.hypot(nx - hx, ny - hy) or 1.0
ux, uy = (nx - hx) / d, (ny - hy) / d
r = geo["headR"]
return (hx + ux * r, hy + uy * r), (hx + ux * (r + 7), hy + uy * (r + 7))
def svg_for_frame(name, geo, working, colors):
w, h = CANVAS
parts = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" fill="none">',
f' <title>{name}</title>',
f' <line x1="16" y1="{GROUND_Y + 4}" x2="{w - 16}" y2="{GROUND_Y + 4}"'
f' stroke="{colors["ground"]}" stroke-width="3" stroke-linecap="round"/>']
for part in PART_ORDER:
if part == "head":
hx, hy = geo["head"]
(sx, sy), (ex, ey) = nose_segment(geo)
parts.append(f' <circle id="head" cx="{hx:.1f}" cy="{hy:.1f}" r="{geo["headR"]}"'
f' fill="{colors["head_fill"]}" stroke="{colors["right"]}" stroke-width="{WIDTHS["head"]}"/>')
parts.append(f' <line id="nose" x1="{sx:.1f}" y1="{sy:.1f}" x2="{ex:.1f}" y2="{ey:.1f}"'
f' stroke="{colors["right"]}" stroke-width="{WIDTHS["nose"]}" stroke-linecap="round"/>')
continue
if part not in geo:
continue
color, width = part_style(part, working, colors)
if part == "spine":
(ax, ay), (cx, cy), (bx, by) = geo["spine"]
d = f"M {ax:.1f} {ay:.1f} Q {cx:.1f} {cy:.1f} {bx:.1f} {by:.1f}"
else:
d = "M " + " L ".join(f"{x:.1f} {y:.1f}" for x, y in geo[part])
parts.append(f' <path id="{part}" d="{d}" stroke="{color}" stroke-width="{width}"'
f' stroke-linecap="round" stroke-linejoin="round"/>')
lx = w - 78
parts.append(f' <g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="{colors["legend_text"]}">')
parts.append(f' <line x1="{lx}" y1="16" x2="{lx + 16}" y2="16" stroke="{colors["right"]}" stroke-width="4" stroke-linecap="round"/>')
parts.append(f' <text x="{lx + 22}" y="20">R</text>')
parts.append(f' <line x1="{lx + 40}" y1="16" x2="{lx + 56}" y2="16" stroke="{colors["left"]}" stroke-width="4" stroke-linecap="round"/>')
parts.append(f' <text x="{lx + 62}" y="20">L</text>')
parts.append(' </g>')
parts.append('</svg>')
return "\n".join(parts) + "\n"
def draw_geo(geo, working, colors, scale=2, font=None):
from PIL import Image, ImageDraw
w, h = CANVAS[0] * scale, CANVAS[1] * scale
img = Image.new("RGB", (w, h), "white")
d = ImageDraw.Draw(img)
def line(pts, color, width):
pts = [(x * scale, y * scale) for x, y in pts]
d.line(pts, fill=color, width=width * scale, joint="curve")
r = width * scale / 2
for x, y in (pts[0], pts[-1]):
d.ellipse([x - r, y - r, x + r, y + r], fill=color)
line([(16, GROUND_Y + 4), (CANVAS[0] - 16, GROUND_Y + 4)], colors["ground"], 3)
for part in PART_ORDER:
if part == "head":
hx, hy = geo["head"]
r, sw = geo["headR"] * scale, WIDTHS["head"] * scale
d.ellipse([hx * scale - r, hy * scale - r, hx * scale + r, hy * scale + r],
fill=colors["head_fill"], outline=colors["right"], width=sw)
line(list(nose_segment(geo)), colors["right"], WIDTHS["nose"])
continue
if part not in geo:
continue
color, width = part_style(part, working, colors)
pts = quad_points(*geo["spine"]) if part == "spine" else geo[part]
line(pts, color, width)
lx = CANVAS[0] - 78
line([(lx, 16), (lx + 16, 16)], colors["right"], 4)
line([(lx + 40, 16), (lx + 56, 16)], colors["left"], 4)
if font:
d.text((lx * scale + 22 * scale, 16 * scale - 11 * scale / 2 - 2), "R",
fill=colors["legend_text"], font=font)
d.text((lx * scale + 62 * scale, 16 * scale - 11 * scale / 2 - 2), "L",
fill=colors["legend_text"], font=font)
return img.resize(CANVAS, Image.LANCZOS)
def legend_font(scale=2):
from PIL import ImageFont
try:
return ImageFont.load_default(size=11 * scale)
except TypeError:
return ImageFont.load_default()
# --------------------------------------------------------------------- main
def load_motion(folder):
return json.loads((folder / "motion.json").read_text())
def export_app_resources(folders):
"""Write the app's bundled copies: body.json plus one `<Name>.motion.json`
per library entry. Unique basenames — Xcode copies resources flat, so the
per-entry motion.json files can't ship under their own folder names."""
out = LIB.parent / "Workouts" / "Resources" / "ExerciseMotions"
out.mkdir(parents=True, exist_ok=True)
(out / "body.json").write_text((LIB / "body.json").read_text())
for folder in folders:
motion = load_motion(folder)
(out / f"{motion['name']}.motion.json").write_text(
(folder / "motion.json").read_text())
print(f" exported {motion['name']}.motion.json")
print(f" exported body.json -> {out}")
def render_exercise(folder, figure="neutral", flip=False):
motion = load_motion(folder)
body = bodies()[figure]
working = set(motion.get("working", []))
hide = set(motion.get("hide", []))
norms = [normalize(kf, body, flip) for kf in motion["frames"]]
geos = [geometry(n, body, hide, flip) for n in norms]
frames_dir = folder / "frames"
frames_dir.mkdir(exist_ok=True)
for old in frames_dir.glob("frame-*.svg"):
old.unlink()
colors = PALETTES["default"]
svgs = [svg_for_frame(motion["name"], g, working, colors) for g in geos]
for i, svg in enumerate(svgs, start=1):
(frames_dir / f"frame-{i}.svg").write_text(svg)
(folder / "visual.svg").write_text(svgs[motion.get("primary", 1) - 1])
try:
font = legend_font()
imgs = [draw_geo(geometry(n, body, hide, flip), working, colors, font=font)
for n in timeline(norms)]
imgs[0].save(folder / "preview.gif", save_all=True, append_images=imgs[1:],
duration=50, loop=0)
print(f" {motion['name']}: {len(svgs)} frames, preview.gif")
except ImportError:
print(f" {motion['name']}: Pillow missing — SVGs written, preview.gif skipped")
def contact_sheet(folders, figure="neutral"):
from PIL import Image, ImageDraw
font = legend_font()
body = bodies()[figure]
cells = []
for folder in folders:
motion = load_motion(folder)
working, hide = set(motion.get("working", [])), set(motion.get("hide", []))
for i, kf in enumerate(motion["frames"], start=1):
geo = geometry(normalize(kf, body), body, hide)
cells.append((f"{motion['name']} {i}/{len(motion['frames'])}",
draw_geo(geo, working, PALETTES["default"], font=font)))
save_sheet(cells, LIB / "contact-sheet.png")
def demo_sheet(folder):
"""One exercise's primary frame rendered four ways — the doors the rig
opens: neutral, female profile, flipped, alternate theme."""
motion = load_motion(folder)
working, hide = set(motion.get("working", [])), set(motion.get("hide", []))
kf = motion["frames"][motion.get("primary", 1) - 1]
font = legend_font()
variants = [("neutral", "neutral", False, "default"),
("female profile", "female", False, "default"),
("flipped", "neutral", True, "default"),
("themed (indigo)", "neutral", False, "indigo")]
cells = []
for label, figure, flip, palette in variants:
body = bodies()[figure]
geo = geometry(normalize(kf, body, flip), body, hide, flip)
cells.append((f"{motion['name']}{label}",
draw_geo(geo, working, PALETTES[palette], font=font)))
save_sheet(cells, LIB / "demo-sheet.png", cols=2)
def save_sheet(cells, path, cols=4):
from PIL import Image, ImageDraw
rows = (len(cells) + cols - 1) // cols
cw, ch, cap = CANVAS[0], CANVAS[1], 22
sheet = Image.new("RGB", (cols * (cw + 8) + 8, rows * (ch + cap + 8) + 8), "white")
d = ImageDraw.Draw(sheet)
for i, (label, img) in enumerate(cells):
x, y = 8 + (i % cols) * (cw + 8), 8 + (i // cols) * (ch + cap + 8)
sheet.paste(img, (x, y))
d.text((x + 6, y + ch + 3), label, fill=PALETTES["default"]["right"])
sheet.save(path)
print(f" {path.name} ({len(cells)} cells)")
def main():
flags = [a for a in sys.argv[1:] if a.startswith("--")]
names = [a for a in sys.argv[1:] if not a.startswith("--")]
figure = "neutral"
for f in flags:
if f.startswith("--figure="):
figure = f.split("=", 1)[1]
folders = ([LIB / n for n in names] if names
else sorted(p.parent for p in LIB.glob("*/motion.json")))
if "--export" in flags:
export_app_resources(folders)
return
for folder in folders:
render_exercise(folder, figure=figure, flip="--flip" in flags)
if "--sheet" in flags:
contact_sheet(folders, figure=figure)
if "--demo" in flags:
demo_sheet(folders[0] if names else LIB / "Bird Dog")
if __name__ == "__main__":
main()