diff --git a/Exercise Library/Abdominal/frames/frame-1.svg b/Exercise Library/Abdominal/frames/frame-1.svg new file mode 100644 index 0000000..0fac729 --- /dev/null +++ b/Exercise Library/Abdominal/frames/frame-1.svg @@ -0,0 +1,23 @@ + + Abdominal + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Abdominal/frames/frame-2.svg b/Exercise Library/Abdominal/frames/frame-2.svg new file mode 100644 index 0000000..e0f75fc --- /dev/null +++ b/Exercise Library/Abdominal/frames/frame-2.svg @@ -0,0 +1,23 @@ + + Abdominal + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Abdominal/info.md b/Exercise Library/Abdominal/info.md new file mode 100644 index 0000000..b272ba1 --- /dev/null +++ b/Exercise Library/Abdominal/info.md @@ -0,0 +1,40 @@ +# Abdominal + +The seated crunch machine — loaded spinal flexion with the lower back +supported. + +- **Category:** Main circuit +- **Type:** Machine-based trunk flexion +- **Targets:** Rectus abdominis, deep core +- **Prescription:** 4 × 10 + +## Setup + +Sit with the pads resting against your chest and your feet flat on the +platform. Grip the handles lightly and set the seat so your hips stay tucked +into the base of the back pad. + +## Execution + +1. Contract your abs to curl your ribs down toward your pelvis, driving the + chest pad forward. +2. Pause at the bottom of the crunch, feeling the abs fully shortened. +3. Return under control until the torso is tall again — don't let the weight + yank you back up. + +## Cues + +- Curl the spine, don't just hinge at the hips — think "shorten the front". +- Keep your lower back in contact with the pad throughout. +- Let the abs move the weight; the arms only steady the handles. + +## Common Mistakes + +- Pulling with the arms or shoulders instead of the abs. +- Hinging from the hips with a flat, rigid spine. +- Rushing the return and bouncing out of the top. + +## Progression + +Add weight once all four sets of ten are a controlled curl; slow the lowering +phase to three seconds for extra time under tension. diff --git a/Exercise Library/Abdominal/motion.json b/Exercise Library/Abdominal/motion.json new file mode 100644 index 0000000..36f15cb --- /dev/null +++ b/Exercise Library/Abdominal/motion.json @@ -0,0 +1,34 @@ +{ + "name": "Abdominal", + "primary": 2, + "working": ["spine"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[120, 128], [114, 62]], "w": 8}, + {"kind": "line", "pts": [[106, 130], [150, 130]], "w": 8}, + {"kind": "line", "pts": [[122, 132], [122, 150]], "w": 4}, + {"kind": "line", "pts": [[102, 150], [150, 150]], "w": 4}, + {"kind": "line", "pts": [[156, 149], [190, 149]], "w": 5} + ]}, + {"type": "pad", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 14, "w": 6} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [128, 126], + "spine": [94, 90], "neck": 82, "gaze": -8, + "arm_r": [-12, -52], "arm_l": [-12, -52], + "leg_r": [-20, -90], "leg_l": [-20, -90], + "pins": {"hand_r": [155, 62], "hand_l": [161, 64], "foot_r": [168, 150], "foot_l": [172, 150]} + }, + { + "hold": 0.4, "tween": 1.0, + "root": [128, 126], + "spine": [80, 40], "neck": 22, "gaze": -42, + "arm_r": [-12, -52], "arm_l": [-12, -52], + "leg_r": [-20, -90], "leg_l": [-20, -90], + "pins": {"hand_r": [190, 76], "hand_l": [196, 78], "foot_r": [168, 150], "foot_l": [172, 150]} + } + ] +} diff --git a/Exercise Library/Abdominal/preview.gif b/Exercise Library/Abdominal/preview.gif new file mode 100644 index 0000000..934972f Binary files /dev/null and b/Exercise Library/Abdominal/preview.gif differ diff --git a/Exercise Library/Abdominal/visual.svg b/Exercise Library/Abdominal/visual.svg new file mode 100644 index 0000000..e0f75fc --- /dev/null +++ b/Exercise Library/Abdominal/visual.svg @@ -0,0 +1,23 @@ + + Abdominal + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Abductor/frames/frame-1.svg b/Exercise Library/Abductor/frames/frame-1.svg new file mode 100644 index 0000000..5670ff6 --- /dev/null +++ b/Exercise Library/Abductor/frames/frame-1.svg @@ -0,0 +1,22 @@ + + Abductor + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Abductor/frames/frame-2.svg b/Exercise Library/Abductor/frames/frame-2.svg new file mode 100644 index 0000000..c611420 --- /dev/null +++ b/Exercise Library/Abductor/frames/frame-2.svg @@ -0,0 +1,22 @@ + + Abductor + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Abductor/info.md b/Exercise Library/Abductor/info.md new file mode 100644 index 0000000..acedd3c --- /dev/null +++ b/Exercise Library/Abductor/info.md @@ -0,0 +1,38 @@ +# Abductor + +The seated hip-abduction machine — presses the knees apart to load the outer +hips and glutes. + +- **Category:** Main circuit +- **Type:** Machine-based hip abduction +- **Targets:** Gluteus medius, gluteus minimus, outer thighs +- **Prescription:** 4 × 10 + +## Setup + +Sit with your legs inside the pads, back flat against the seat, and knees +starting close together. Rest your hands on the seat sides and keep your torso +upright. + +## Execution + +1. Push your knees outward against the pads to spread the legs. +2. Pause at the widest point, squeezing the outer hips and glutes. +3. Bring the legs back together under control — resist the pads on the way in. + +## Cues + +- Stay upright; leaning forward hands the work to the lower back. +- Drive the knees out through the outer hips, not by rocking the pelvis. +- Control the return so the pads don't slam your knees closed. + +## Common Mistakes + +- Leaning forward to force the legs wider. +- Letting the pads snap the knees back together. +- Using a bouncy, momentum-driven range instead of steady tension. + +## Progression + +Add weight in small steps once all four sets of ten are smooth; pause two +seconds at full spread to build end-range strength. diff --git a/Exercise Library/Abductor/motion.json b/Exercise Library/Abductor/motion.json new file mode 100644 index 0000000..e986689 --- /dev/null +++ b/Exercise Library/Abductor/motion.json @@ -0,0 +1,34 @@ +{ + "name": "Abductor", + "primary": 2, + "working": ["leg_r", "leg_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "rect", "x": 152, "y": 56, "w": 16, "h": 72, "r": 5}, + {"kind": "rect", "x": 130, "y": 126, "w": 60, "h": 10, "r": 3}, + {"kind": "line", "pts": [[136, 78], [136, 108]], "w": 4, "color": "prop"}, + {"kind": "line", "pts": [[188, 78], [188, 108]], "w": 4, "color": "prop"}, + {"kind": "line", "pts": [[213, 110], [213, 132]], "w": 9, "color": "prop"}, + {"kind": "line", "pts": [[107, 110], [107, 132]], "w": 9, "color": "prop"} + ]} + ], + "frames": [ + { + "hold": 0.5, "tween": 1.1, + "root": [160, 116], + "spine": [90, 90], "neck": 90, + "arm_r": [-48, -82], "arm_l": [-132, -98], + "leg_r": [-22, 0], "leg_l": [-158, 0], + "pins": {"foot_r": [180, 156], "foot_l": [140, 156]} + }, + { + "hold": 0.5, "tween": 1.1, + "root": [160, 116], + "spine": [90, 90], "neck": 90, + "arm_r": [-48, -82], "arm_l": [-132, -98], + "leg_r": [-22, 0], "leg_l": [-158, 0], + "pins": {"foot_r": [216, 156], "foot_l": [104, 156]} + } + ] +} diff --git a/Exercise Library/Abductor/preview.gif b/Exercise Library/Abductor/preview.gif new file mode 100644 index 0000000..88b6a83 Binary files /dev/null and b/Exercise Library/Abductor/preview.gif differ diff --git a/Exercise Library/Abductor/visual.svg b/Exercise Library/Abductor/visual.svg new file mode 100644 index 0000000..c611420 --- /dev/null +++ b/Exercise Library/Abductor/visual.svg @@ -0,0 +1,22 @@ + + Abductor + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Adductor/frames/frame-1.svg b/Exercise Library/Adductor/frames/frame-1.svg new file mode 100644 index 0000000..26f2537 --- /dev/null +++ b/Exercise Library/Adductor/frames/frame-1.svg @@ -0,0 +1,22 @@ + + Adductor + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Adductor/frames/frame-2.svg b/Exercise Library/Adductor/frames/frame-2.svg new file mode 100644 index 0000000..1432983 --- /dev/null +++ b/Exercise Library/Adductor/frames/frame-2.svg @@ -0,0 +1,22 @@ + + Adductor + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Adductor/info.md b/Exercise Library/Adductor/info.md new file mode 100644 index 0000000..e1e3010 --- /dev/null +++ b/Exercise Library/Adductor/info.md @@ -0,0 +1,39 @@ +# Adductor + +The seated hip-adduction machine — squeezes the knees together to load the +inner thighs. + +- **Category:** Main circuit +- **Type:** Machine-based hip adduction +- **Targets:** Adductors, inner thighs +- **Prescription:** 4 × 10 + +## Setup + +Sit with your legs placed outside the pads, back flat against the seat, and +knees starting spread apart. Rest your hands on the seat sides and keep your +torso upright. + +## Execution + +1. Squeeze your knees together against the pads using your inner thighs. +2. Pause when the knees nearly touch, holding the contraction. +3. Let the legs spread back open under control — don't let the pads fling them + apart. + +## Cues + +- Drive the movement from the inner thighs, not by rocking the hips. +- Keep the torso upright and the back against the seat. +- Control both directions; the opening phase trains the muscle too. + +## Common Mistakes + +- Letting the pads throw the legs open at the start of each rep. +- Setting too wide a start range before the muscle is warm. +- Fast, swinging reps that rely on momentum instead of the adductors. + +## Progression + +Add weight once all four sets of ten are controlled in both directions; pause +two seconds with the knees together to strengthen the fully-shortened position. diff --git a/Exercise Library/Adductor/motion.json b/Exercise Library/Adductor/motion.json new file mode 100644 index 0000000..c012ce9 --- /dev/null +++ b/Exercise Library/Adductor/motion.json @@ -0,0 +1,34 @@ +{ + "name": "Adductor", + "primary": 2, + "working": ["leg_r", "leg_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "rect", "x": 152, "y": 56, "w": 16, "h": 72, "r": 5}, + {"kind": "rect", "x": 130, "y": 126, "w": 60, "h": 10, "r": 3}, + {"kind": "line", "pts": [[136, 78], [136, 108]], "w": 4, "color": "prop"}, + {"kind": "line", "pts": [[188, 78], [188, 108]], "w": 4, "color": "prop"}, + {"kind": "line", "pts": [[197, 110], [197, 132]], "w": 9, "color": "prop"}, + {"kind": "line", "pts": [[123, 110], [123, 132]], "w": 9, "color": "prop"} + ]} + ], + "frames": [ + { + "hold": 0.5, "tween": 1.1, + "root": [160, 116], + "spine": [90, 90], "neck": 90, + "arm_r": [-48, -82], "arm_l": [-132, -98], + "leg_r": [-22, 0], "leg_l": [-158, 0], + "pins": {"foot_r": [216, 156], "foot_l": [104, 156]} + }, + { + "hold": 0.5, "tween": 1.1, + "root": [160, 116], + "spine": [90, 90], "neck": 90, + "arm_r": [-48, -82], "arm_l": [-132, -98], + "leg_r": [-22, 0], "leg_l": [-158, 0], + "pins": {"foot_r": [180, 156], "foot_l": [140, 156]} + } + ] +} diff --git a/Exercise Library/Adductor/preview.gif b/Exercise Library/Adductor/preview.gif new file mode 100644 index 0000000..763a32d Binary files /dev/null and b/Exercise Library/Adductor/preview.gif differ diff --git a/Exercise Library/Adductor/visual.svg b/Exercise Library/Adductor/visual.svg new file mode 100644 index 0000000..1432983 --- /dev/null +++ b/Exercise Library/Adductor/visual.svg @@ -0,0 +1,22 @@ + + Adductor + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Arm Curl/frames/frame-1.svg b/Exercise Library/Arm Curl/frames/frame-1.svg new file mode 100644 index 0000000..369d1c0 --- /dev/null +++ b/Exercise Library/Arm Curl/frames/frame-1.svg @@ -0,0 +1,21 @@ + + Arm Curl + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Arm Curl/frames/frame-2.svg b/Exercise Library/Arm Curl/frames/frame-2.svg new file mode 100644 index 0000000..093d0c8 --- /dev/null +++ b/Exercise Library/Arm Curl/frames/frame-2.svg @@ -0,0 +1,21 @@ + + Arm Curl + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Arm Curl/info.md b/Exercise Library/Arm Curl/info.md new file mode 100644 index 0000000..9989727 --- /dev/null +++ b/Exercise Library/Arm Curl/info.md @@ -0,0 +1,37 @@ +# Arm Curl + +The seated machine curl — biceps isolation with the upper arms braced flat on +the pad so only the forearms move. + +- **Category:** Main circuit +- **Type:** Machine-based elbow flexion (isolation) +- **Targets:** Biceps brachii, brachialis +- **Prescription:** 4 × 10 + +## Setup + +Sit with your upper arms flat along the pad and grip the handles. Start with +your forearms extended down along the pad, elbows just short of locked. + +## Execution + +1. Curl the handles up toward your shoulders, squeezing the biceps at the top. +2. Keep your upper arms pinned to the pad — only the forearms move. +3. Lower under control until the forearms are fully extended again. + +## Cues + +- Upper arms stay glued to the pad the entire rep. +- Squeeze hard at the top, then resist the weight all the way down. +- Wrists stay neutral — don't curl with the hands. + +## Common Mistakes + +- Rocking the torso or lifting the elbows to swing the weight up. +- Cutting the lowering phase short and dropping the weight. +- Not fully extending at the bottom, shortening the range. + +## Progression + +Add weight once all four sets of ten are strict; slow the lowering phase to +three seconds before adding load again. diff --git a/Exercise Library/Arm Curl/motion.json b/Exercise Library/Arm Curl/motion.json new file mode 100644 index 0000000..cf63762 --- /dev/null +++ b/Exercise Library/Arm Curl/motion.json @@ -0,0 +1,32 @@ +{ + "name": "Arm Curl", + "primary": 2, + "working": ["arm_r", "arm_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[116, 134], [150, 134]], "w": 8}, + {"kind": "line", "pts": [[146, 42], [176, 68]], "w": 8}, + {"kind": "line", "pts": [[176, 68], [176, 128]], "w": 5} + ]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 6} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [130, 124], + "spine": [88, 86], "neck": 82, "gaze": -18, + "arm_r": [-35, -52], "arm_l": [-35, -52], + "leg_r": [-8, -72], "leg_l": [-8, -72], + "pins": {"foot_r": [176, 150], "foot_l": [180, 151]} + }, + { + "hold": 0.4, "tween": 1.2, + "root": [130, 124], + "spine": [88, 86], "neck": 82, "gaze": -18, + "arm_r": [-35, 138], "arm_l": [-35, 138], + "leg_r": [-8, -72], "leg_l": [-8, -72], + "pins": {"foot_r": [176, 150], "foot_l": [180, 151]} + } + ] +} diff --git a/Exercise Library/Arm Curl/preview.gif b/Exercise Library/Arm Curl/preview.gif new file mode 100644 index 0000000..217d0d3 Binary files /dev/null and b/Exercise Library/Arm Curl/preview.gif differ diff --git a/Exercise Library/Arm Curl/visual.svg b/Exercise Library/Arm Curl/visual.svg new file mode 100644 index 0000000..093d0c8 --- /dev/null +++ b/Exercise Library/Arm Curl/visual.svg @@ -0,0 +1,21 @@ + + Arm Curl + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Calfs/frames/frame-1.svg b/Exercise Library/Calfs/frames/frame-1.svg new file mode 100644 index 0000000..c09ea7c --- /dev/null +++ b/Exercise Library/Calfs/frames/frame-1.svg @@ -0,0 +1,25 @@ + + Calfs + + + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Calfs/frames/frame-2.svg b/Exercise Library/Calfs/frames/frame-2.svg new file mode 100644 index 0000000..c3ffa53 --- /dev/null +++ b/Exercise Library/Calfs/frames/frame-2.svg @@ -0,0 +1,25 @@ + + Calfs + + + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Calfs/info.md b/Exercise Library/Calfs/info.md new file mode 100644 index 0000000..98918ef --- /dev/null +++ b/Exercise Library/Calfs/info.md @@ -0,0 +1,40 @@ +# Calfs + +Seated heel raises through a full range — a deep stretch below the platform and +a hard squeeze at the top. + +- **Category:** Main circuit +- **Type:** Machine-based calf raise +- **Targets:** Soleus, gastrocnemius +- **Prescription:** 4 × 10 + +## Setup + +Sit with the balls of your feet on the platform, heels hanging off the back +edge, and the pad snug on your lower thighs just above the knees. Hold the pad +handles lightly. + +## Execution + +1. Raise your heels by contracting your calves, pressing up onto the balls of + your feet against the pad. +2. Pause at the top with the calves fully shortened. +3. Slowly lower your heels below the platform level for a full stretch. + +## Cues + +- Chase the full range — a hard squeeze up top, a deep stretch at the bottom. +- Drive through the balls of the feet, not the toes. +- Control the tempo; the stretch under the platform is where the work is. + +## Common Mistakes + +- Bouncing through short, choppy reps instead of the full range. +- Rocking the weight up with the hips instead of the calves. +- Rushing the lowering and skipping the bottom stretch. + +## Progression + +Add weight in small increments once all four sets of ten are controlled; +single-leg raises and paused reps at the stretched bottom build the hardest end +of the range. diff --git a/Exercise Library/Calfs/motion.json b/Exercise Library/Calfs/motion.json new file mode 100644 index 0000000..2d07937 --- /dev/null +++ b/Exercise Library/Calfs/motion.json @@ -0,0 +1,36 @@ +{ + "name": "Calfs", + "primary": 2, + "working": ["leg_r", "leg_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[124, 126], [158, 124]], "w": 9}, + {"kind": "line", "pts": [[138, 128], [138, 150]], "w": 5}, + {"kind": "line", "pts": [[120, 150], [156, 150]], "w": 5}, + {"kind": "rect", "x": 198, "y": 147, "w": 34, "h": 6, "r": 1}, + {"kind": "line", "pts": [[183, 62], [183, 124]], "w": 4}, + {"kind": "rect", "x": 170, "y": 121, "w": 27, "h": 7, "r": 2}, + {"kind": "line", "pts": [[164, 76], [183, 76]], "w": 3, "color": "prop"} + ]}, + {"type": "pad", "at": ["foot_r", "foot_l"], "angle": 15, "halfLen": 7, "w": 5} + ], + "frames": [ + { + "hold": 0.4, "tween": 0.9, + "root": [140, 118], + "spine": [100, 96], "neck": 80, "gaze": -10, + "arm_r": [-40, -15], "arm_l": [-40, -15], + "leg_r": [-18, -8], "leg_l": [-18, -8], + "pins": {"hand_r": [166, 74], "hand_l": [170, 76], "foot_r": [212, 148], "foot_l": [216, 150]} + }, + { + "hold": 0.4, "tween": 1.2, + "root": [140, 118], + "spine": [100, 96], "neck": 80, "gaze": -10, + "arm_r": [-40, -15], "arm_l": [-40, -15], + "leg_r": [-14, 4], "leg_l": [-14, 4], + "pins": {"hand_r": [166, 74], "hand_l": [170, 76], "foot_r": [212, 139], "foot_l": [216, 141]} + } + ] +} diff --git a/Exercise Library/Calfs/preview.gif b/Exercise Library/Calfs/preview.gif new file mode 100644 index 0000000..237e744 Binary files /dev/null and b/Exercise Library/Calfs/preview.gif differ diff --git a/Exercise Library/Calfs/visual.svg b/Exercise Library/Calfs/visual.svg new file mode 100644 index 0000000..c3ffa53 --- /dev/null +++ b/Exercise Library/Calfs/visual.svg @@ -0,0 +1,25 @@ + + Calfs + + + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Chest Press/frames/frame-1.svg b/Exercise Library/Chest Press/frames/frame-1.svg new file mode 100644 index 0000000..1e6a755 --- /dev/null +++ b/Exercise Library/Chest Press/frames/frame-1.svg @@ -0,0 +1,22 @@ + + Chest Press + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Chest Press/frames/frame-2.svg b/Exercise Library/Chest Press/frames/frame-2.svg new file mode 100644 index 0000000..fc88385 --- /dev/null +++ b/Exercise Library/Chest Press/frames/frame-2.svg @@ -0,0 +1,22 @@ + + Chest Press + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Chest Press/info.md b/Exercise Library/Chest Press/info.md new file mode 100644 index 0000000..585165a --- /dev/null +++ b/Exercise Library/Chest Press/info.md @@ -0,0 +1,38 @@ +# Chest Press + +The seated horizontal press — the machine bench press, pushing the handles +forward from the chest. + +- **Category:** Main circuit +- **Type:** Machine-based horizontal press +- **Targets:** Pectorals, anterior deltoids, triceps + +## Setup + +Adjust the seat so the handles sit at mid-chest height. Grip the handles, set +your back against the pad, and keep your wrists straight and stacked over the +forearms. + +## Execution + +1. Push the handles forward until your arms are nearly extended — stop short of + locking the elbows. +2. Pause briefly, then return slowly until the handles are back at your chest. +3. Keep your shoulder blades gently pinned to the pad throughout. + +## Cues + +- Drive through the palms; wrists stay straight, not bent back. +- Don't let the elbows drop far below the handles at the bottom. +- Return under control — resist the weight rather than letting it pull you back. + +## Common Mistakes + +- Letting the elbows sink too low, straining the shoulders. +- Bouncing the handles at the chest to start the next rep. +- Locking the elbows hard at full extension. + +## Progression + +Add weight in small plates once all four sets of ten are controlled; alternate +to a single-arm press to expose and correct side-to-side differences. diff --git a/Exercise Library/Chest Press/motion.json b/Exercise Library/Chest Press/motion.json new file mode 100644 index 0000000..8b8cf53 --- /dev/null +++ b/Exercise Library/Chest Press/motion.json @@ -0,0 +1,33 @@ +{ + "name": "Chest Press", + "primary": 2, + "working": ["arm_r", "arm_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[114, 132], [86, 40]], "w": 8}, + {"kind": "line", "pts": [[106, 133], [146, 133]], "w": 8}, + {"kind": "line", "pts": [[122, 135], [122, 151]], "w": 5}, + {"kind": "line", "pts": [[108, 151], [138, 151]], "w": 4} + ]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 8} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.1, + "root": [116, 122], + "spine": [104, 100], "neck": 84, "gaze": 5, + "arm_r": [-100, 40], "arm_l": [-100, 40], + "leg_r": [-22, -72], "leg_l": [-22, -72], + "pins": {"foot_r": [158, 151], "foot_l": [162, 152]} + }, + { + "hold": 0.4, "tween": 1.1, + "root": [116, 122], + "spine": [104, 100], "neck": 84, "gaze": 5, + "arm_r": [-6, -14], "arm_l": [-6, -14], + "leg_r": [-22, -72], "leg_l": [-22, -72], + "pins": {"foot_r": [158, 151], "foot_l": [162, 152]} + } + ] +} diff --git a/Exercise Library/Chest Press/preview.gif b/Exercise Library/Chest Press/preview.gif new file mode 100644 index 0000000..7a5fe2e Binary files /dev/null and b/Exercise Library/Chest Press/preview.gif differ diff --git a/Exercise Library/Chest Press/visual.svg b/Exercise Library/Chest Press/visual.svg new file mode 100644 index 0000000..fc88385 --- /dev/null +++ b/Exercise Library/Chest Press/visual.svg @@ -0,0 +1,22 @@ + + Chest Press + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Lat Pull Down/frames/frame-1.svg b/Exercise Library/Lat Pull Down/frames/frame-1.svg new file mode 100644 index 0000000..d235eb9 --- /dev/null +++ b/Exercise Library/Lat Pull Down/frames/frame-1.svg @@ -0,0 +1,23 @@ + + Lat Pull Down + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Lat Pull Down/frames/frame-2.svg b/Exercise Library/Lat Pull Down/frames/frame-2.svg new file mode 100644 index 0000000..427b7cd --- /dev/null +++ b/Exercise Library/Lat Pull Down/frames/frame-2.svg @@ -0,0 +1,23 @@ + + Lat Pull Down + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Lat Pull Down/info.md b/Exercise Library/Lat Pull Down/info.md new file mode 100644 index 0000000..8db8d56 --- /dev/null +++ b/Exercise Library/Lat Pull Down/info.md @@ -0,0 +1,37 @@ +# Lat Pull Down + +The seated vertical pull — building width through the lats with a bar driven +down from overhead. + +- **Category:** Main circuit +- **Type:** Machine-based vertical pull +- **Targets:** Latissimus dorsi, mid-back, rear delts, biceps +- **Prescription:** 4 × 10 + +## Setup + +Sit upright with your knees secured under the pad and feet flat. Grip the bar +wider than shoulder-width, arms fully extended overhead, torso just off vertical. + +## Execution + +1. Pull the bar down to your upper chest, driving your elbows down and back. +2. Squeeze your shoulder blades together at the bottom and pause briefly. +3. Return the bar overhead under control until your arms are fully extended. + +## Cues + +- Lead with the elbows, not the hands — think "elbows to the ribs". +- Squeeze the shoulder blades down and together at the bottom. +- Keep the torso tall; only a slight lean back, never a swing. + +## Common Mistakes + +- Leaning back excessively and turning it into a row. +- Using momentum to yank the bar down. +- Stopping short — not pulling the bar all the way to the chest. + +## Progression + +Add weight once all four sets of ten are clean; progress to a wider or neutral +grip, then to strict pull-ups as pulling strength builds. diff --git a/Exercise Library/Lat Pull Down/motion.json b/Exercise Library/Lat Pull Down/motion.json new file mode 100644 index 0000000..e4d0031 --- /dev/null +++ b/Exercise Library/Lat Pull Down/motion.json @@ -0,0 +1,36 @@ +{ + "name": "Lat Pull Down", + "primary": 2, + "working": ["arm_r", "arm_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[126, 130], [162, 130]], "w": 8}, + {"kind": "line", "pts": [[124, 130], [124, 56]], "w": 5}, + {"kind": "line", "pts": [[124, 56], [198, 12]], "w": 5}, + {"kind": "rect", "x": 170, "y": 112, "w": 40, "h": 7, "r": 3} + ]}, + {"type": "cable", "from": [198, 8], "to": ["hand_r", "hand_l"]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "halfLen": 28} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [150, 122], + "spine": [93, 90], "neck": 85, "gaze": 55, + "arm_r": [20, 10], "arm_l": [20, 10], + "leg_r": [-12, -80], "leg_l": [-12, -80], + "pins": {"hand_r": [198, 24], "hand_l": [202, 26], + "foot_r": [193, 150], "foot_l": [197, 151]} + }, + { + "hold": 0.4, "tween": 1.2, + "root": [150, 122], + "spine": [93, 90], "neck": 85, "gaze": 30, + "arm_r": [-115, -20], "arm_l": [-115, -20], + "leg_r": [-12, -80], "leg_l": [-12, -80], + "pins": {"hand_r": [165, 66], "hand_l": [169, 68], + "foot_r": [193, 150], "foot_l": [197, 151]} + } + ] +} diff --git a/Exercise Library/Lat Pull Down/preview.gif b/Exercise Library/Lat Pull Down/preview.gif new file mode 100644 index 0000000..daab8d5 Binary files /dev/null and b/Exercise Library/Lat Pull Down/preview.gif differ diff --git a/Exercise Library/Lat Pull Down/visual.svg b/Exercise Library/Lat Pull Down/visual.svg new file mode 100644 index 0000000..427b7cd --- /dev/null +++ b/Exercise Library/Lat Pull Down/visual.svg @@ -0,0 +1,23 @@ + + Lat Pull Down + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Leg Curl/frames/frame-1.svg b/Exercise Library/Leg Curl/frames/frame-1.svg new file mode 100644 index 0000000..db6e71d --- /dev/null +++ b/Exercise Library/Leg Curl/frames/frame-1.svg @@ -0,0 +1,25 @@ + + Leg Curl + + + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Leg Curl/frames/frame-2.svg b/Exercise Library/Leg Curl/frames/frame-2.svg new file mode 100644 index 0000000..d7317a1 --- /dev/null +++ b/Exercise Library/Leg Curl/frames/frame-2.svg @@ -0,0 +1,25 @@ + + Leg Curl + + + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Leg Curl/info.md b/Exercise Library/Leg Curl/info.md new file mode 100644 index 0000000..135a9e1 --- /dev/null +++ b/Exercise Library/Leg Curl/info.md @@ -0,0 +1,37 @@ +# Leg Curl + +Isolated hamstring work — the heels curl toward the glutes while a pad locks +the thighs in place. + +- **Category:** Main circuit +- **Type:** Machine-based isolation (knee flexion) +- **Targets:** Hamstrings (biceps femoris, semitendinosus, semimembranosus) +- **Prescription:** 4 × 10 + +## Setup + +Sit (or lie face down, depending on the version) with the thigh pad locked over +your legs and the roller behind your ankles. Start with the legs extended. + +## Execution + +1. Curl your legs toward your glutes, driving through the hamstrings. +2. Squeeze hard at the fully bent position. +3. Return the roller under control until the legs are extended again. + +## Cues + +- Focus on hamstring engagement — feel the pull behind the knee, not the back. +- Keep your hips and back still against the pad; no arching to cheat. +- Curl through the full range, all the way to the glutes. + +## Common Mistakes + +- Arching the lower back to yank the weight up. +- Using momentum instead of a controlled curl. +- Cutting the range short and never reaching full flexion. + +## Progression + +Add load once all four sets of ten are controlled; single-leg curls for +asymmetry, or a slow eccentric to overload the lengthening hamstring. diff --git a/Exercise Library/Leg Curl/motion.json b/Exercise Library/Leg Curl/motion.json new file mode 100644 index 0000000..0151be6 --- /dev/null +++ b/Exercise Library/Leg Curl/motion.json @@ -0,0 +1,34 @@ +{ + "name": "Leg Curl", + "primary": 2, + "working": ["leg_r", "leg_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[110, 122], [86, 48]], "w": 9}, + {"kind": "line", "pts": [[104, 124], [166, 120]], "w": 9}, + {"kind": "line", "pts": [[130, 105], [158, 106]], "w": 7}, + {"kind": "line", "pts": [[108, 126], [108, 150]], "w": 5}, + {"kind": "line", "pts": [[160, 124], [160, 150]], "w": 5}, + {"kind": "line", "pts": [[104, 150], [166, 150]], "w": 5}, + {"kind": "circle", "c": [114, 90], "r": 3.5, "fill": true, "color": "prop"} + ]}, + {"type": "pad", "at": ["foot_r", "foot_l"], "halfLen": 9} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [118, 116], + "spine": [110, 106], "neck": 74, "gaze": -20, + "arm_r": [-75, -60], "arm_l": [-75, -60], + "leg_r": [10, 4], "leg_l": [10, 4] + }, + { + "hold": 0.4, "tween": 1.2, + "root": [118, 116], + "spine": [110, 106], "neck": 74, "gaze": -20, + "arm_r": [-75, -60], "arm_l": [-75, -60], + "leg_r": [10, -95], "leg_l": [10, -95] + } + ] +} diff --git a/Exercise Library/Leg Curl/preview.gif b/Exercise Library/Leg Curl/preview.gif new file mode 100644 index 0000000..ecc0c95 Binary files /dev/null and b/Exercise Library/Leg Curl/preview.gif differ diff --git a/Exercise Library/Leg Curl/visual.svg b/Exercise Library/Leg Curl/visual.svg new file mode 100644 index 0000000..d7317a1 --- /dev/null +++ b/Exercise Library/Leg Curl/visual.svg @@ -0,0 +1,25 @@ + + Leg Curl + + + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Leg Extension/frames/frame-1.svg b/Exercise Library/Leg Extension/frames/frame-1.svg new file mode 100644 index 0000000..6482955 --- /dev/null +++ b/Exercise Library/Leg Extension/frames/frame-1.svg @@ -0,0 +1,24 @@ + + Leg Extension + + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Leg Extension/frames/frame-2.svg b/Exercise Library/Leg Extension/frames/frame-2.svg new file mode 100644 index 0000000..65601f1 --- /dev/null +++ b/Exercise Library/Leg Extension/frames/frame-2.svg @@ -0,0 +1,24 @@ + + Leg Extension + + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Leg Extension/info.md b/Exercise Library/Leg Extension/info.md new file mode 100644 index 0000000..1490e87 --- /dev/null +++ b/Exercise Library/Leg Extension/info.md @@ -0,0 +1,40 @@ +# Leg Extension + +Isolated quadriceps work — the knee opens against the roller while the hips +stay pinned to the seat. + +- **Category:** Main circuit +- **Type:** Machine-based isolation (knee extension) +- **Targets:** Quadriceps (rectus femoris, vastus lateralis/medialis) +- **Prescription:** 4 × 10 + +## Setup + +Sit upright with your back flat against the pad and align your knees with the +machine's pivot point. Hook the roller pad against the front of your ankles, +hands resting on the side grips. + +## Execution + +1. Extend your legs to a straightened position — stop just short of snapping + the knees into lockout. +2. Pause briefly at the top with the quads squeezed hard. +3. Lower the roller back down with control to the start. + +## Cues + +- Knees stack over the pivot — realign the seat if they sit forward or back. +- Hips glued to the seat; if they lift, the weight is too heavy. +- Own the lowering — no free-falling back to the stack. + +## Common Mistakes + +- Jerky, momentum-driven reps instead of a smooth extension. +- Lifting the hips off the seat to swing the weight up. +- Slamming into a locked knee at the top. + +## Progression + +Add load in small increments once all four sets of ten are smooth; single-leg +extensions for asymmetry, or a paused top / slow eccentric for more time under +tension. diff --git a/Exercise Library/Leg Extension/motion.json b/Exercise Library/Leg Extension/motion.json new file mode 100644 index 0000000..367717c --- /dev/null +++ b/Exercise Library/Leg Extension/motion.json @@ -0,0 +1,33 @@ +{ + "name": "Leg Extension", + "primary": 2, + "working": ["leg_r", "leg_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[110, 122], [86, 48]], "w": 9}, + {"kind": "line", "pts": [[104, 124], [166, 120]], "w": 9}, + {"kind": "line", "pts": [[108, 126], [108, 150]], "w": 5}, + {"kind": "line", "pts": [[160, 124], [160, 150]], "w": 5}, + {"kind": "line", "pts": [[104, 150], [166, 150]], "w": 5}, + {"kind": "circle", "c": [114, 90], "r": 3.5, "fill": true, "color": "prop"} + ]}, + {"type": "pad", "at": ["foot_r", "foot_l"], "halfLen": 9} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [118, 116], + "spine": [110, 106], "neck": 74, "gaze": -20, + "arm_r": [-75, -60], "arm_l": [-75, -60], + "leg_r": [10, -83], "leg_l": [10, -83] + }, + { + "hold": 0.4, "tween": 1.2, + "root": [118, 116], + "spine": [110, 106], "neck": 74, "gaze": -20, + "arm_r": [-75, -60], "arm_l": [-75, -60], + "leg_r": [10, 4], "leg_l": [10, 4] + } + ] +} diff --git a/Exercise Library/Leg Extension/preview.gif b/Exercise Library/Leg Extension/preview.gif new file mode 100644 index 0000000..e14bb27 Binary files /dev/null and b/Exercise Library/Leg Extension/preview.gif differ diff --git a/Exercise Library/Leg Extension/visual.svg b/Exercise Library/Leg Extension/visual.svg new file mode 100644 index 0000000..65601f1 --- /dev/null +++ b/Exercise Library/Leg Extension/visual.svg @@ -0,0 +1,24 @@ + + Leg Extension + + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Leg Press/frames/frame-1.svg b/Exercise Library/Leg Press/frames/frame-1.svg new file mode 100644 index 0000000..8763d4b --- /dev/null +++ b/Exercise Library/Leg Press/frames/frame-1.svg @@ -0,0 +1,24 @@ + + Leg Press + + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Leg Press/frames/frame-2.svg b/Exercise Library/Leg Press/frames/frame-2.svg new file mode 100644 index 0000000..ec26558 --- /dev/null +++ b/Exercise Library/Leg Press/frames/frame-2.svg @@ -0,0 +1,24 @@ + + Leg Press + + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Leg Press/info.md b/Exercise Library/Leg Press/info.md new file mode 100644 index 0000000..f051bda --- /dev/null +++ b/Exercise Library/Leg Press/info.md @@ -0,0 +1,39 @@ +# Leg Press + +The seated machine squat — maximum lower-body load with the back fully +supported. + +- **Category:** Main circuit +- **Type:** Machine-based compound push +- **Targets:** Quadriceps, glutes, hamstrings +- **Prescription:** 4 × 10 + +## Setup + +Sit with your back and hips flat against the pad. Place your feet +shoulder-width on the platform, heels never floating. Adjust the seat so your +knees start near 90°. + +## Execution + +1. Press through your heels until your legs are nearly straight — stop short + of locking the knees. +2. Pause briefly, then bend the knees to return under control. +3. Keep your back and hips glued to the pad the whole rep. + +## Cues + +- Drive through the heels, not the toes. +- Knees track over the feet — don't let them cave inward. +- Lower back stays on the pad; if the hips curl up, you've gone too deep. + +## Common Mistakes + +- Locking the knees hard at the top. +- Bouncing out of the bottom. +- Letting the hips roll off the pad at the bottom of the rep. + +## Progression + +Add weight in small plates once all four sets of ten are controlled; single-leg +presses for asymmetry work. diff --git a/Exercise Library/Leg Press/motion.json b/Exercise Library/Leg Press/motion.json new file mode 100644 index 0000000..c28c4eb --- /dev/null +++ b/Exercise Library/Leg Press/motion.json @@ -0,0 +1,35 @@ +{ + "name": "Leg Press", + "primary": 2, + "working": ["leg_r", "leg_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[134, 123], [96, 36]], "w": 9}, + {"kind": "line", "pts": [[126, 127], [156, 122]], "w": 9}, + {"kind": "line", "pts": [[138, 130], [138, 150]], "w": 5}, + {"kind": "line", "pts": [[118, 150], [158, 150]], "w": 5}, + {"kind": "line", "pts": [[142, 90], [142, 78]], "w": 3}, + {"kind": "circle", "c": [142, 77], "r": 3.5, "fill": true, "color": "prop"} + ]}, + {"type": "pad", "at": ["foot_r", "foot_l"], "angle": 88, "halfLen": 20, "w": 6} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [140, 116], + "spine": [116, 112], "neck": 75, "gaze": -15, + "arm_r": [-75, -20], "arm_l": [-75, -20], + "leg_r": [40, -58], "leg_l": [40, -58], + "pins": {"foot_r": [185, 102], "foot_l": [189, 104]} + }, + { + "hold": 0.4, "tween": 1.2, + "root": [140, 116], + "spine": [116, 112], "neck": 75, "gaze": -15, + "arm_r": [-75, -20], "arm_l": [-75, -20], + "leg_r": [20, -5], "leg_l": [20, -5], + "pins": {"foot_r": [220, 101], "foot_l": [224, 103]} + } + ] +} diff --git a/Exercise Library/Leg Press/preview.gif b/Exercise Library/Leg Press/preview.gif new file mode 100644 index 0000000..819d134 Binary files /dev/null and b/Exercise Library/Leg Press/preview.gif differ diff --git a/Exercise Library/Leg Press/visual.svg b/Exercise Library/Leg Press/visual.svg new file mode 100644 index 0000000..ec26558 --- /dev/null +++ b/Exercise Library/Leg Press/visual.svg @@ -0,0 +1,24 @@ + + Leg Press + + + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Rotary/frames/frame-1.svg b/Exercise Library/Rotary/frames/frame-1.svg new file mode 100644 index 0000000..68bcaa4 --- /dev/null +++ b/Exercise Library/Rotary/frames/frame-1.svg @@ -0,0 +1,20 @@ + + Rotary + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Rotary/frames/frame-2.svg b/Exercise Library/Rotary/frames/frame-2.svg new file mode 100644 index 0000000..3039099 --- /dev/null +++ b/Exercise Library/Rotary/frames/frame-2.svg @@ -0,0 +1,20 @@ + + Rotary + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Rotary/info.md b/Exercise Library/Rotary/info.md new file mode 100644 index 0000000..72fe969 --- /dev/null +++ b/Exercise Library/Rotary/info.md @@ -0,0 +1,39 @@ +# Rotary + +The rotary torso machine — controlled trunk rotation that isolates the +obliques. + +- **Category:** Main circuit +- **Type:** Machine-based trunk rotation +- **Targets:** Obliques, deep core +- **Prescription:** 4 × 10 + +## Setup + +Sit tall with your hips squared and locked against the pads. Rest your +forearms on the pads and grip the handle in front of your chest so the trunk, +not the arms, drives the machine. + +## Execution + +1. Rotate your torso to one side in a smooth, controlled arc, keeping your hips + still. +2. Pause at the end of the range without letting the weight pull you further. +3. Return through center and rotate to the other side with the same control. + +## Cues + +- Twist from the waist — the obliques generate the movement, not the arms. +- Keep the hips and pelvis pinned; only the trunk turns. +- Move slowly; momentum steals the work from the obliques. + +## Common Mistakes + +- Throwing the weight with momentum instead of controlling the twist. +- Pushing with the arms rather than rotating the trunk. +- Letting the hips swing along with the torso. + +## Progression + +Add weight once both sides move with equal control; pause two seconds at full +rotation to kill any momentum. diff --git a/Exercise Library/Rotary/motion.json b/Exercise Library/Rotary/motion.json new file mode 100644 index 0000000..1fbab42 --- /dev/null +++ b/Exercise Library/Rotary/motion.json @@ -0,0 +1,32 @@ +{ + "name": "Rotary", + "primary": 1, + "working": ["spine"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[138, 130], [182, 130]], "w": 8}, + {"kind": "line", "pts": [[160, 132], [160, 150]], "w": 5}, + {"kind": "line", "pts": [[144, 150], [176, 150]], "w": 5} + ]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 14} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [160, 122], + "spine": [104, 109], "neck": 109, + "arm_r": [-50, -85], "arm_l": [-55, -90], + "leg_r": [-62, -82], "leg_l": [-118, -98], + "pins": {"hand_r": [112, 72], "hand_l": [118, 74], "foot_r": [190, 150], "foot_l": [130, 150]} + }, + { + "hold": 0.4, "tween": 1.0, + "root": [160, 122], + "spine": [76, 71], "neck": 71, + "arm_r": [-125, -95], "arm_l": [-130, -90], + "leg_r": [-62, -82], "leg_l": [-118, -98], + "pins": {"hand_r": [208, 72], "hand_l": [202, 74], "foot_r": [190, 150], "foot_l": [130, 150]} + } + ] +} diff --git a/Exercise Library/Rotary/preview.gif b/Exercise Library/Rotary/preview.gif new file mode 100644 index 0000000..0482f89 Binary files /dev/null and b/Exercise Library/Rotary/preview.gif differ diff --git a/Exercise Library/Rotary/visual.svg b/Exercise Library/Rotary/visual.svg new file mode 100644 index 0000000..68bcaa4 --- /dev/null +++ b/Exercise Library/Rotary/visual.svg @@ -0,0 +1,20 @@ + + Rotary + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/SYSTEM.md b/Exercise Library/SYSTEM.md index 3adc340..e3123ef 100644 --- a/Exercise Library/SYSTEM.md +++ b/Exercise Library/SYSTEM.md @@ -52,6 +52,48 @@ changing data, never artwork. 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). +- **Face-on figures** — `gaze` is optional: a frame without it faces the + viewer and draws no nose tick. Used by exercises whose motion is lateral + (abductor/adductor), where a side view would hide the movement. + +## The props layer + +Machines and free weights are data too: an optional top-level `"props"` array +adds an equipment layer around the figure. `scene` shapes and `cable`s draw +*behind* the figure in a recessive gray; joint-attached items (`bar`, +`dumbbell`, `pad`) draw *over* the limbs in a darker gray and follow the +resolved hand/foot positions every frame — a pinned foot pressing a `pad` +carries the platform with it through the tween for free. The figure stays the +hero: props are schematic silhouettes (a seat, a backrest, one handle), never +scale drawings of the machine. + +```json +"props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[134, 123], [96, 36]], "w": 9}, + {"kind": "rect", "x": 54, "y": 104, "w": 40, "h": 8, "r": 3}, + {"kind": "circle", "c": [142, 77], "r": 3.5, "fill": true, "color": "prop"} + ]}, + {"type": "cable", "from": [190, 8], "to": ["hand_r", "hand_l"]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "halfLen": 26, "plateR": 0}, + {"type": "dumbbell", "at": "hand_r"}, + {"type": "pad", "at": ["foot_r", "foot_l"], "angle": 88, "halfLen": 20, "w": 6} +] +``` + +- **`scene`** — static shapes in canvas coordinates: `line` (polyline, stroke + width `w`), `circle` (`fill: false` for an outline), `rect` (filled, corner + radius `r`). A shape may set `"color": "prop"` to use the darker + attached-item gray (e.g. a fixed handle the hands rest on). +- **`cable`** — a thin line from a fixed anchor `from` to a moving joint `to`; + the machine's pulley line. +- **`bar` / `dumbbell` / `pad`** — a segment centered on the joint(s) in + `at` (a single extremity, or the midpoint of `["hand_r", "hand_l"]`). + `bar` lies at a fixed world `angle` (default 0 = horizontal — in side view a + two-handed bar is drawn horizontal by convention); `dumbbell` and `pad` + default to perpendicular to the lower bone (forearm/shin), or take an + explicit `angle`. `plateR` puts filled discs on both ends (dumbbells default + to 4.5). A prop whose limb is hidden that frame simply isn't drawn. - 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). @@ -61,9 +103,12 @@ changing data, never artwork. - **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. + right = teal `#0d9488`, left = light teal `#86cfc5`. Opposite-limb moves + (bird dog, dead bug) read as visibly opposite: one dark-teal limb, one + light-teal limb. `render.py` reference renders embed a small `R —` / `L —` + legend for the rig author; the in-app renderer deliberately omits it — the + labels are only anatomically true for right-facing figures, and the + contrast alone carries the meaning. - **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 @@ -83,7 +128,7 @@ 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 + .motion.json app resources +python3 render.py --export # copy body.json + .motion.json + .info.md app resources ``` `render.py` needs only Pillow (for GIFs/sheets; the SVGs have no dependency). @@ -91,7 +136,9 @@ 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 -`.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. +`.motion.json` and `.info.md` files in +`Workouts/Resources/ExerciseMotions/`, consumed by the in-app SwiftUI renderer +(`Workouts/ExerciseFigure/`) and the exercise-library reference screen +(`ExerciseInfo.swift` parses the info pages). Re-run +`python3 render.py --export` after editing any motion or info page; the +library stays the source of truth. diff --git a/Exercise Library/Seated Row/frames/frame-1.svg b/Exercise Library/Seated Row/frames/frame-1.svg new file mode 100644 index 0000000..c5388f1 --- /dev/null +++ b/Exercise Library/Seated Row/frames/frame-1.svg @@ -0,0 +1,22 @@ + + Seated Row + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Seated Row/frames/frame-2.svg b/Exercise Library/Seated Row/frames/frame-2.svg new file mode 100644 index 0000000..b017cad --- /dev/null +++ b/Exercise Library/Seated Row/frames/frame-2.svg @@ -0,0 +1,22 @@ + + Seated Row + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Seated Row/info.md b/Exercise Library/Seated Row/info.md new file mode 100644 index 0000000..9dfb106 --- /dev/null +++ b/Exercise Library/Seated Row/info.md @@ -0,0 +1,37 @@ +# Seated Row + +The seated horizontal pull — thickness for the mid-back by driving the elbows +straight back against the chest pad. + +- **Category:** Main circuit +- **Type:** Machine-based horizontal pull +- **Targets:** Mid-back, latissimus dorsi, rear delts, biceps +- **Prescription:** 4 × 10 + +## Setup + +Sit with your chest firmly against the pad and feet on the footrest. Grip the +handles with your arms extended forward toward the anchor. + +## Execution + +1. Pull straight back, keeping your elbows close to your body. +2. Drive the handles to your ribs and retract your shoulder blades hard. +3. Return to full extension under control without rounding your back. + +## Cues + +- Retract the shoulder blades first, then the arms follow. +- Elbows stay tucked, tracing a line along your sides. +- Chest stays glued to the pad the whole rep. + +## Common Mistakes + +- Rounding the back to reach forward at the extension. +- Flaring the elbows out wide instead of driving them back. +- Heaving with the whole torso instead of pulling with the back. + +## Progression + +Add weight once all four sets of ten are controlled; vary the grip width to +shift emphasis between the lats and the mid-back. diff --git a/Exercise Library/Seated Row/motion.json b/Exercise Library/Seated Row/motion.json new file mode 100644 index 0000000..5e582b0 --- /dev/null +++ b/Exercise Library/Seated Row/motion.json @@ -0,0 +1,35 @@ +{ + "name": "Seated Row", + "primary": 2, + "working": ["arm_r", "arm_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[122, 132], [156, 132]], "w": 8}, + {"kind": "line", "pts": [[159, 46], [157, 96]], "w": 8}, + {"kind": "line", "pts": [[176, 150], [206, 150]], "w": 5} + ]}, + {"type": "cable", "from": [244, 120], "to": ["hand_r", "hand_l"]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 7} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [140, 122], + "spine": [85, 82], "neck": 80, "gaze": 5, + "arm_r": [-10, -25], "arm_l": [-10, -25], + "leg_r": [-8, -75], "leg_l": [-8, -75], + "pins": {"hand_r": [200, 62], "hand_l": [204, 64], + "foot_r": [188, 148], "foot_l": [192, 149]} + }, + { + "hold": 0.4, "tween": 1.2, + "root": [140, 122], + "spine": [85, 82], "neck": 80, "gaze": 5, + "arm_r": [-120, -35], "arm_l": [-120, -35], + "leg_r": [-8, -75], "leg_l": [-8, -75], + "pins": {"hand_r": [158, 72], "hand_l": [162, 74], + "foot_r": [188, 148], "foot_l": [192, 149]} + } + ] +} diff --git a/Exercise Library/Seated Row/preview.gif b/Exercise Library/Seated Row/preview.gif new file mode 100644 index 0000000..7adf014 Binary files /dev/null and b/Exercise Library/Seated Row/preview.gif differ diff --git a/Exercise Library/Seated Row/visual.svg b/Exercise Library/Seated Row/visual.svg new file mode 100644 index 0000000..b017cad --- /dev/null +++ b/Exercise Library/Seated Row/visual.svg @@ -0,0 +1,22 @@ + + Seated Row + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Shoulder Press/frames/frame-1.svg b/Exercise Library/Shoulder Press/frames/frame-1.svg new file mode 100644 index 0000000..7afd240 --- /dev/null +++ b/Exercise Library/Shoulder Press/frames/frame-1.svg @@ -0,0 +1,22 @@ + + Shoulder Press + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Shoulder Press/frames/frame-2.svg b/Exercise Library/Shoulder Press/frames/frame-2.svg new file mode 100644 index 0000000..5db76f8 --- /dev/null +++ b/Exercise Library/Shoulder Press/frames/frame-2.svg @@ -0,0 +1,22 @@ + + Shoulder Press + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Shoulder Press/info.md b/Exercise Library/Shoulder Press/info.md new file mode 100644 index 0000000..c7ea8bd --- /dev/null +++ b/Exercise Library/Shoulder Press/info.md @@ -0,0 +1,39 @@ +# Shoulder Press + +The seated overhead press — vertical pushing strength with the trunk braced +against the pad. + +- **Category:** Main circuit +- **Type:** Machine-based vertical press +- **Targets:** Deltoids, triceps, upper trapezius + +## Setup + +Sit with your back against the pad and grip the handles just outside +shoulder-width, elbows bent so the handles start at shoulder height. Plant your +feet flat and set your ribs down over your hips. + +## Execution + +1. Press the handles straight up until your arms are nearly extended — stop + short of locking out the elbows. +2. Pause at the top without shrugging, then lower under control back to shoulder + height. +3. Keep your neck relaxed and your back flat on the pad the whole rep. + +## Cues + +- Press up, not forward — the handles travel over the shoulders. +- Keep the ribs down; don't arch the lower back to help the press. +- Shoulders stay set — avoid shrugging the handles up with the traps. + +## Common Mistakes + +- Locking the elbows hard at the top. +- Shrugging the shoulders to finish the press. +- Arching the lower back off the pad as the weight gets heavy. + +## Progression + +Add weight in small increments once all four sets of ten are smooth; move to a +single-arm press for even loading and extra core demand. diff --git a/Exercise Library/Shoulder Press/motion.json b/Exercise Library/Shoulder Press/motion.json new file mode 100644 index 0000000..2502f62 --- /dev/null +++ b/Exercise Library/Shoulder Press/motion.json @@ -0,0 +1,33 @@ +{ + "name": "Shoulder Press", + "primary": 2, + "working": ["arm_r", "arm_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[116, 140], [88, 50]], "w": 8}, + {"kind": "line", "pts": [[110, 141], [148, 141]], "w": 8}, + {"kind": "line", "pts": [[126, 143], [126, 151]], "w": 5}, + {"kind": "line", "pts": [[110, 151], [142, 151]], "w": 4} + ]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "halfLen": 10} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.1, + "root": [120, 130], + "spine": [110, 104], "neck": 80, "gaze": 8, + "arm_r": [-60, 90], "arm_l": [-60, 90], + "leg_r": [-25, -72], "leg_l": [-25, -72], + "pins": {"foot_r": [164, 151], "foot_l": [168, 152]} + }, + { + "hold": 0.4, "tween": 1.1, + "root": [120, 130], + "spine": [110, 104], "neck": 80, "gaze": 8, + "arm_r": [54, 36], "arm_l": [54, 36], + "leg_r": [-25, -72], "leg_l": [-25, -72], + "pins": {"foot_r": [164, 151], "foot_l": [168, 152]} + } + ] +} diff --git a/Exercise Library/Shoulder Press/preview.gif b/Exercise Library/Shoulder Press/preview.gif new file mode 100644 index 0000000..a4dada7 Binary files /dev/null and b/Exercise Library/Shoulder Press/preview.gif differ diff --git a/Exercise Library/Shoulder Press/visual.svg b/Exercise Library/Shoulder Press/visual.svg new file mode 100644 index 0000000..5db76f8 --- /dev/null +++ b/Exercise Library/Shoulder Press/visual.svg @@ -0,0 +1,22 @@ + + Shoulder Press + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Tricep Press/frames/frame-1.svg b/Exercise Library/Tricep Press/frames/frame-1.svg new file mode 100644 index 0000000..9bc4e3b --- /dev/null +++ b/Exercise Library/Tricep Press/frames/frame-1.svg @@ -0,0 +1,22 @@ + + Tricep Press + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Tricep Press/frames/frame-2.svg b/Exercise Library/Tricep Press/frames/frame-2.svg new file mode 100644 index 0000000..a77e7ee --- /dev/null +++ b/Exercise Library/Tricep Press/frames/frame-2.svg @@ -0,0 +1,22 @@ + + Tricep Press + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/Tricep Press/info.md b/Exercise Library/Tricep Press/info.md new file mode 100644 index 0000000..e60a145 --- /dev/null +++ b/Exercise Library/Tricep Press/info.md @@ -0,0 +1,39 @@ +# Tricep Press + +The seated press-down — elbows pinned to the sides, driving the handles from the +chest to full extension. + +- **Category:** Main circuit +- **Type:** Machine-based elbow extension +- **Targets:** Triceps brachii + +## Setup + +Sit tall with your back on the pad and your upper arms tucked close to your +sides. Take the handles at chest height with your elbows bent to about a right +angle. + +## Execution + +1. Press the handles down by straightening your elbows, keeping your upper arms + still against your sides. +2. Extend fully without snapping the elbows, pausing briefly at the bottom. +3. Let the handles rise back to chest height under control, elbows staying + tucked. + +## Cues + +- Only the forearms move — the upper arms stay glued to the torso. +- Keep the motion controlled; don't fling the handles down. +- Squeeze the triceps at the bottom rather than leaning your weight in. + +## Common Mistakes + +- Flaring the elbows out away from the sides. +- Using the shoulders or bodyweight to drive the handles down. +- Cutting the extension short and never reaching full lockout. + +## Progression + +Add weight in small increments once all four sets of ten are clean; slow the +lowering phase to three seconds to add time under tension for the triceps. diff --git a/Exercise Library/Tricep Press/motion.json b/Exercise Library/Tricep Press/motion.json new file mode 100644 index 0000000..fe227fa --- /dev/null +++ b/Exercise Library/Tricep Press/motion.json @@ -0,0 +1,33 @@ +{ + "name": "Tricep Press", + "primary": 2, + "working": ["arm_r", "arm_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[116, 134], [92, 42]], "w": 8}, + {"kind": "line", "pts": [[108, 135], [146, 135]], "w": 8}, + {"kind": "line", "pts": [[124, 137], [124, 151]], "w": 5}, + {"kind": "line", "pts": [[110, 151], [140, 151]], "w": 4} + ]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 6} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.1, + "root": [118, 124], + "spine": [100, 98], "neck": 86, "gaze": 6, + "arm_r": [-78, 30], "arm_l": [-78, 30], + "leg_r": [-24, -74], "leg_l": [-24, -74], + "pins": {"foot_r": [156, 151], "foot_l": [160, 152]} + }, + { + "hold": 0.4, "tween": 1.1, + "root": [118, 124], + "spine": [100, 98], "neck": 86, "gaze": 6, + "arm_r": [-78, -88], "arm_l": [-78, -88], + "leg_r": [-24, -74], "leg_l": [-24, -74], + "pins": {"foot_r": [156, 151], "foot_l": [160, 152]} + } + ] +} diff --git a/Exercise Library/Tricep Press/preview.gif b/Exercise Library/Tricep Press/preview.gif new file mode 100644 index 0000000..512378d Binary files /dev/null and b/Exercise Library/Tricep Press/preview.gif differ diff --git a/Exercise Library/Tricep Press/visual.svg b/Exercise Library/Tricep Press/visual.svg new file mode 100644 index 0000000..a77e7ee --- /dev/null +++ b/Exercise Library/Tricep Press/visual.svg @@ -0,0 +1,22 @@ + + Tricep Press + + + + + + + + + + + + + + + + R + + L + + diff --git a/Exercise Library/contact-sheet.png b/Exercise Library/contact-sheet.png index 362701f..4ccc472 100644 Binary files a/Exercise Library/contact-sheet.png and b/Exercise Library/contact-sheet.png differ diff --git a/Exercise Library/render.py b/Exercise Library/render.py index 5ad2f8c..33b1b68 100644 --- a/Exercise Library/render.py +++ b/Exercise Library/render.py @@ -32,11 +32,13 @@ PALETTES = { "right": "#3a3f4b", "left": "#a9afba", "right_working": "#0d9488", "left_working": "#86cfc5", "ground": "#b9bec9", "legend_text": "#6b7180", "head_fill": "white", + "equipment": "#c5cad4", "prop": "#6b7180", }, "indigo": { "right": "#3a3f4b", "left": "#a9afba", "right_working": "#4f46e5", "left_working": "#a5b4fc", "ground": "#b9bec9", "legend_text": "#6b7180", "head_fill": "white", + "equipment": "#c5cad4", "prop": "#6b7180", }, } WIDTHS = {"right": 6, "left": 5, "spine": 6, "head": 6, "nose": 4} @@ -55,6 +57,8 @@ LIMBS = { # limb -> (attach joint, [bone length keys], pin key) ANGLE_KEYS = ["spine", "neck", "gaze", "arm_r", "arm_l", "leg_r", "leg_l"] +JOINT_LIMB = {"hand_r": "arm_r", "hand_l": "arm_l", "foot_r": "leg_r", "foot_l": "leg_l"} + def bodies(): return json.loads((LIB / "body.json").read_text()) @@ -153,12 +157,14 @@ def geometry(frame, body, hide=(), flip=False): 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"], + geo = {"head": head, "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"]]} + # `gaze` is optional: a frame without it faces the viewer, so no nose tick. + if "gaze" in frame: + geo["nose"] = walk(head, [frame["gaze"]], [body["headR"] + 7])[1] for limb, (joint, bone_keys, pin) in LIMBS.items(): if limb not in frame or limb in hide: continue @@ -206,6 +212,134 @@ def timeline(norms, fps=20): return frames +# -------------------------------------------------------------------- props +# Equipment layer (see SYSTEM.md): `scene` shapes and `cable`s draw behind the +# figure in the recessive equipment gray; joint-attached items (`bar`, +# `dumbbell`, `pad`) draw over the limbs in the darker prop gray, following +# the resolved hand/foot positions frame by frame. + +def flip_props(props, width): + """Mirror the props horizontally, matching flip_frame. Joint-attached props + follow the mirrored limbs automatically; only fixed coordinates and world + angles need mirroring.""" + def fx(p): + return [width - p[0], p[1]] + out = [] + for prop in props: + p = json.loads(json.dumps(prop)) + if p["type"] == "scene": + for s in p["shapes"]: + if s["kind"] == "line": + s["pts"] = [fx(pt) for pt in s["pts"]] + elif s["kind"] == "circle": + s["c"] = fx(s["c"]) + elif s["kind"] == "rect": + s["x"] = width - s["x"] - s["w"] + elif p["type"] == "cable": + p["from"] = fx(p["from"]) + elif p["type"] in ("bar", "pad") and "angle" in p: + p["angle"] = 180 - p["angle"] + out.append(p) + return out + + +def joint_points(geo, ref): + """Resolve a joint ref — `"hand_r"` or a `["hand_r", "hand_l"]` midpoint — + to (point, lower-bone unit direction). None when the limb isn't drawn.""" + names = ref if isinstance(ref, list) else [ref] + pts, direction = [], None + for name in names: + limb = geo.get(JOINT_LIMB[name]) + if not limb: + return None, None + pts.append(limb[-1]) + if direction is None: + a, b = limb[-2], limb[-1] + d = math.hypot(b[0] - a[0], b[1] - a[1]) or 1.0 + direction = ((b[0] - a[0]) / d, (b[1] - a[1]) / d) + return ((sum(p[0] for p in pts) / len(pts), + sum(p[1] for p in pts) / len(pts)), direction) + + +def resolve_props(props, geo): + """Props -> drawable primitives for one frame: (background, foreground).""" + bg, fg = [], [] + for p in props or []: + t = p["type"] + if t == "scene": + for s in p["shapes"]: + bg.append(dict(s, color=s.get("color", "equipment"))) + elif t == "cable": + end, _ = joint_points(geo, p["to"]) + if end: + bg.append({"kind": "line", "pts": [list(p["from"]), list(end)], + "w": p.get("w", 2), "color": "equipment"}) + elif t in ("bar", "dumbbell", "pad"): + c, d = joint_points(geo, p["at"]) + if not c: + continue + if t == "bar" or "angle" in p: + ux, uy = dirv(p.get("angle", 0)) # fixed world angle + else: + ux, uy = -d[1], d[0] # perpendicular to the lower bone + h = p.get("halfLen", {"bar": 24, "dumbbell": 7, "pad": 8}[t]) + a = (c[0] - ux * h, c[1] - uy * h) + b = (c[0] + ux * h, c[1] + uy * h) + fg.append({"kind": "line", "pts": [a, b], + "w": p.get("w", {"bar": 4, "dumbbell": 3, "pad": 7}[t]), + "color": "prop"}) + plate = p.get("plateR", 4.5 if t == "dumbbell" else 0) + if plate: + for e in (a, b): + fg.append({"kind": "circle", "c": list(e), "r": plate, + "fill": True, "color": "prop"}) + return bg, fg + + +def svg_prims(prims, colors): + lines = [] + for p in prims: + color = colors[p["color"]] + if p["kind"] == "line": + d = "M " + " L ".join(f"{x:.1f} {y:.1f}" for x, y in p["pts"]) + lines.append(f' ') + elif p["kind"] == "circle": + cx, cy = p["c"] + if p.get("fill"): + lines.append(f' ') + else: + lines.append(f' ') + elif p["kind"] == "rect": + lines.append(f' ') + return lines + + +def draw_prims(d, prims, colors, scale): + for p in prims: + color = colors[p["color"]] + if p["kind"] == "line": + pts = [(x * scale, y * scale) for x, y in p["pts"]] + w = p.get("w", 4) * scale + d.line(pts, fill=color, width=w, joint="curve") + for x, y in (pts[0], pts[-1]): + d.ellipse([x - w / 2, y - w / 2, x + w / 2, y + w / 2], fill=color) + elif p["kind"] == "circle": + cx, cy = p["c"][0] * scale, p["c"][1] * scale + r = p["r"] * scale + if p.get("fill"): + d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=color) + else: + d.ellipse([cx - r, cy - r, cx + r, cy + r], outline=color, + width=p.get("w", 3) * scale) + elif p["kind"] == "rect": + x, y = p["x"] * scale, p["y"] * scale + d.rounded_rectangle([x, y, x + p["w"] * scale, y + p["h"] * scale], + radius=p.get("r", 2) * scale, fill=color) + + # ------------------------------------------------------------------- drawing def part_style(part, working, colors): @@ -233,20 +367,24 @@ def nose_segment(geo): return (hx + ux * r, hy + uy * r), (hx + ux * (r + 7), hy + uy * (r + 7)) -def svg_for_frame(name, geo, working, colors): +def svg_for_frame(name, geo, working, colors, props=None): + bg, fg = resolve_props(props, geo) w, h = CANVAS parts = [f'', f' {name}', f' '] + parts += svg_prims(bg, colors) for part in PART_ORDER: if part == "head": + parts += svg_prims(fg, colors) hx, hy = geo["head"] - (sx, sy), (ex, ey) = nose_segment(geo) parts.append(f' ') - parts.append(f' ') + if "nose" in geo: + (sx, sy), (ex, ey) = nose_segment(geo) + parts.append(f' ') continue if part not in geo: continue @@ -269,9 +407,10 @@ def svg_for_frame(name, geo, working, colors): return "\n".join(parts) + "\n" -def draw_geo(geo, working, colors, scale=2, font=None): +def draw_geo(geo, working, colors, scale=2, font=None, props=None): from PIL import Image, ImageDraw + bg, fg = resolve_props(props, geo) w, h = CANVAS[0] * scale, CANVAS[1] * scale img = Image.new("RGB", (w, h), "white") d = ImageDraw.Draw(img) @@ -284,13 +423,16 @@ def draw_geo(geo, working, colors, scale=2, font=None): 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) + draw_prims(d, bg, colors, scale) for part in PART_ORDER: if part == "head": + draw_prims(d, fg, colors, scale) 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"]) + if "nose" in geo: + line(list(nose_segment(geo)), colors["right"], WIDTHS["nose"]) continue if part not in geo: continue @@ -325,8 +467,9 @@ def load_motion(folder): def export_app_resources(folders): """Write the app's bundled copies: body.json plus one `.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.""" + and one `.info.md` per library entry. Unique basenames — Xcode copies + resources flat, so the per-entry motion.json/info.md 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()) @@ -335,6 +478,10 @@ def export_app_resources(folders): (out / f"{motion['name']}.motion.json").write_text( (folder / "motion.json").read_text()) print(f" exported {motion['name']}.motion.json") + info = folder / "info.md" + if info.exists(): + (out / f"{motion['name']}.info.md").write_text(info.read_text()) + print(f" exported {motion['name']}.info.md") print(f" exported body.json -> {out}") @@ -343,6 +490,9 @@ def render_exercise(folder, figure="neutral", flip=False): body = bodies()[figure] working = set(motion.get("working", [])) hide = set(motion.get("hide", [])) + props = motion.get("props", []) + if flip: + props = flip_props(props, CANVAS[0]) norms = [normalize(kf, body, flip) for kf in motion["frames"]] geos = [geometry(n, body, hide, flip) for n in norms] @@ -351,14 +501,14 @@ def render_exercise(folder, figure="neutral", flip=False): 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] + svgs = [svg_for_frame(motion["name"], g, working, colors, props) 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) + imgs = [draw_geo(geometry(n, body, hide, flip), working, colors, font=font, props=props) for n in timeline(norms)] imgs[0].save(folder / "preview.gif", save_all=True, append_images=imgs[1:], duration=50, loop=0) @@ -367,7 +517,7 @@ def render_exercise(folder, figure="neutral", flip=False): print(f" {motion['name']}: Pillow missing — SVGs written, preview.gif skipped") -def contact_sheet(folders, figure="neutral"): +def contact_sheet(folders, figure="neutral", out=None): from PIL import Image, ImageDraw font = legend_font() @@ -376,11 +526,12 @@ def contact_sheet(folders, figure="neutral"): for folder in folders: motion = load_motion(folder) working, hide = set(motion.get("working", [])), set(motion.get("hide", [])) + props = motion.get("props", []) 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") + draw_geo(geo, working, PALETTES["default"], font=font, props=props))) + save_sheet(cells, Path(out) if out else LIB / "contact-sheet.png") def demo_sheet(folder): @@ -397,9 +548,10 @@ def demo_sheet(folder): cells = [] for label, figure, flip, palette in variants: body = bodies()[figure] + props = flip_props(motion.get("props", []), CANVAS[0]) if flip else motion.get("props", []) geo = geometry(normalize(kf, body, flip), body, hide, flip) cells.append((f"{motion['name']} — {label}", - draw_geo(geo, working, PALETTES[palette], font=font))) + draw_geo(geo, working, PALETTES[palette], font=font, props=props))) save_sheet(cells, LIB / "demo-sheet.png", cols=2) @@ -422,9 +574,12 @@ 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" + sheet = None # False = off, None+flag = default path, str = custom path for f in flags: if f.startswith("--figure="): figure = f.split("=", 1)[1] + elif f.startswith("--sheet"): + sheet = f.split("=", 1)[1] if "=" in f else True folders = ([LIB / n for n in names] if names else sorted(p.parent for p in LIB.glob("*/motion.json"))) if "--export" in flags: @@ -432,8 +587,8 @@ def main(): return for folder in folders: render_exercise(folder, figure=figure, flip="--flip" in flags) - if "--sheet" in flags: - contact_sheet(folders, figure=figure) + if sheet: + contact_sheet(folders, figure=figure, out=None if sheet is True else sheet) if "--demo" in flags: demo_sheet(folders[0] if names else LIB / "Bird Dog") diff --git a/Workouts/ExerciseFigure/ExerciseFigureView.swift b/Workouts/ExerciseFigure/ExerciseFigureView.swift index 117533b..91d5602 100644 --- a/Workouts/ExerciseFigure/ExerciseFigureView.swift +++ b/Workouts/ExerciseFigure/ExerciseFigureView.swift @@ -13,7 +13,7 @@ import SwiftUI /// /// Draw order matters: left limbs behind, spine, right limbs, then the head filled /// opaque with the background color so overhead arms pass behind the face — plus the -/// nose tick (gaze) and the small R/L legend that makes opposite-limb moves readable. +/// nose tick (gaze). /// Everything the renderer needs for one exercise, resolved once from the bundle. struct FigureAnimation { @@ -22,6 +22,8 @@ struct FigureAnimation { let working: Set /// Limbs fully occluded in this view — never drawn. let hide: Set + /// Equipment layer (see SYSTEM.md "The props layer"). + let props: [MotionProp] let bodyProfile: ExerciseBodyProfile init?(exerciseName: String) { @@ -32,6 +34,7 @@ struct FigureAnimation { self.timeline = timeline self.working = Set(resources.motion.working ?? []) self.hide = Set(resources.motion.hide ?? []) + self.props = resources.motion.props ?? [] self.bodyProfile = resources.body } @@ -96,6 +99,9 @@ struct ExerciseFigureView: View { CGPoint(x: Self.designSize.width - 16, y: Self.groundY + 4)], color: .figureGround, width: 3) + // Equipment behind the figure: scene shapes and cables. + drawBackgroundProps(&ctx, geo) + // Left limbs (behind), spine, right limbs. drawLimb(&ctx, geo, .armL) drawLimb(&ctx, geo, .legL) @@ -103,6 +109,9 @@ struct ExerciseFigureView: View { drawLimb(&ctx, geo, .armR) drawLimb(&ctx, geo, .legR) + // Joint-attached equipment over the limbs (bars, dumbbells, pads). + drawAttachedProps(&ctx, geo) + // Head last, filled with the background color so limbs pass behind the face. let r = geo.headRadius let headRect = CGRect(x: geo.headCenter.x - r, y: geo.headCenter.y - r, @@ -110,9 +119,9 @@ struct ExerciseFigureView: View { let headPath = Path(ellipseIn: headRect) ctx.fill(headPath, with: .color(.figureHeadFill)) ctx.stroke(headPath, with: .color(.figureRight), lineWidth: 6) - stroke(&ctx, [geo.noseStart, geo.noseEnd], color: .figureRight, width: 4) - - drawLegend(&ctx) + if let noseStart = geo.noseStart, let noseEnd = geo.noseEnd { + stroke(&ctx, [noseStart, noseEnd], color: .figureRight, width: 4) + } } private func drawLimb(_ ctx: inout GraphicsContext, _ geo: FigureGeometry, _ limb: FigureLimb) { @@ -129,19 +138,103 @@ struct ExerciseFigureView: View { style: StrokeStyle(lineWidth: 6, lineCap: .round, lineJoin: .round)) } - /// Small `R — / L —` legend in the top-right corner, so opposite-limb moves are - /// visibly opposite. - private func drawLegend(_ ctx: inout GraphicsContext) { - let lx = Self.designSize.width - 78 - stroke(&ctx, [CGPoint(x: lx, y: 16), CGPoint(x: lx + 16, y: 16)], - color: .figureRight, width: 4) - stroke(&ctx, [CGPoint(x: lx + 40, y: 16), CGPoint(x: lx + 56, y: 16)], - color: .figureLeft, width: 4) - let legendFont = Font.system(size: 11) - ctx.draw(Text("R").font(legendFont).foregroundStyle(.secondary), - at: CGPoint(x: lx + 22, y: 16), anchor: .leading) - ctx.draw(Text("L").font(legendFont).foregroundStyle(.secondary), - at: CGPoint(x: lx + 62, y: 16), anchor: .leading) + // MARK: Props + + /// The extremity point a prop follows (plus the lower-bone unit direction for + /// perpendicular items). Nil when any referenced limb isn't drawn this frame. + private func jointAnchor(_ geo: FigureGeometry, _ ref: PropJointRef) -> (point: CGPoint, direction: CGVector)? { + var points: [CGPoint] = [] + var direction: CGVector? + for name in ref.names { + guard let limb = FigureLimb.allCases.first(where: { $0.pinKey == name }), + let chain = geo.limbs[limb], chain.count >= 2 else { return nil } + let a = chain[chain.count - 2] + let b = chain[chain.count - 1] + points.append(b) + if direction == nil { + let d = max(hypot(b.x - a.x, b.y - a.y), 1) + direction = CGVector(dx: (b.x - a.x) / d, dy: (b.y - a.y) / d) + } + } + guard let direction, !points.isEmpty else { return nil } + let center = CGPoint(x: points.reduce(0) { $0 + $1.x } / CGFloat(points.count), + y: points.reduce(0) { $0 + $1.y } / CGFloat(points.count)) + return (center, direction) + } + + private func drawBackgroundProps(_ ctx: inout GraphicsContext, _ geo: FigureGeometry) { + for prop in figure.props { + switch prop.type { + case "scene": + for shape in prop.shapes ?? [] { drawSceneShape(&ctx, shape) } + case "cable": + guard let from = prop.from, from.count == 2, let to = prop.to, + let anchor = jointAnchor(geo, to) else { continue } + stroke(&ctx, [CGPoint(x: from[0], y: from[1]), anchor.point], + color: .figureEquipment, width: prop.w ?? 2) + default: + break + } + } + } + + /// Bars sit at a fixed world angle (default horizontal); dumbbells and pads + /// default to perpendicular to the lower bone. Kept 1:1 with the reference + /// renderer's `resolve_props` — change them in lockstep. + private func drawAttachedProps(_ ctx: inout GraphicsContext, _ geo: FigureGeometry) { + for prop in figure.props { + let defaults: (halfLen: Double, width: Double, plateR: Double) + switch prop.type { + case "bar": defaults = (24, 4, 0) + case "dumbbell": defaults = (7, 3, 4.5) + case "pad": defaults = (8, 7, 0) + default: continue + } + guard let at = prop.at, let anchor = jointAnchor(geo, at) else { continue } + let axis: CGVector + if prop.type == "bar" || prop.angle != nil { + axis = MotionSolver.direction(prop.angle ?? 0) + } else { + axis = CGVector(dx: -anchor.direction.dy, dy: anchor.direction.dx) + } + let h = prop.halfLen ?? defaults.halfLen + let a = CGPoint(x: anchor.point.x - axis.dx * h, y: anchor.point.y - axis.dy * h) + let b = CGPoint(x: anchor.point.x + axis.dx * h, y: anchor.point.y + axis.dy * h) + stroke(&ctx, [a, b], color: .figureProp, width: prop.w ?? defaults.width) + let plateR = prop.plateR ?? defaults.plateR + if plateR > 0 { + for end in [a, b] { + let rect = CGRect(x: end.x - plateR, y: end.y - plateR, + width: 2 * plateR, height: 2 * plateR) + ctx.fill(Path(ellipseIn: rect), with: .color(.figureProp)) + } + } + } + } + + private func drawSceneShape(_ ctx: inout GraphicsContext, _ shape: PropSceneShape) { + let color: Color = shape.color == "prop" ? .figureProp : .figureEquipment + switch shape.kind { + case "line": + guard let pts = shape.pts else { return } + stroke(&ctx, pts.compactMap { $0.count == 2 ? CGPoint(x: $0[0], y: $0[1]) : nil }, + color: color, width: shape.w ?? 4) + case "circle": + guard let c = shape.c, c.count == 2, let r = shape.r else { return } + let rect = CGRect(x: c[0] - r, y: c[1] - r, width: 2 * r, height: 2 * r) + if shape.fill ?? true { + ctx.fill(Path(ellipseIn: rect), with: .color(color)) + } else { + ctx.stroke(Path(ellipseIn: rect), with: .color(color), lineWidth: shape.w ?? 3) + } + case "rect": + guard let x = shape.x, let y = shape.y, let w = shape.w, let h = shape.h else { return } + let path = Path(roundedRect: CGRect(x: x, y: y, width: w, height: h), + cornerRadius: shape.r ?? 2) + ctx.fill(path, with: .color(color)) + default: + break + } } private func color(for part: String, isLeft: Bool) -> Color { @@ -163,44 +256,50 @@ struct ExerciseFigureView: View { /// The reference palette (`render.py`), made dark-mode adaptive: the prominent right /// side stays strong (near-black → light gray), the recessive left side stays muted, -/// and the working teals brighten/desaturate so they read on black. +/// and the working teals brighten/desaturate so they read on black. watchOS has no +/// dynamic-provider `UIColor` and renders on black, so it takes the dark variant +/// verbatim. private extension Color { + static func figure(light: (Double, Double, Double), dark: (Double, Double, Double)) -> Color { + #if os(watchOS) + Color(red: dark.0, green: dark.1, blue: dark.2) + #else + Color(UIColor { traits in + traits.userInterfaceStyle == .dark + ? UIColor(red: dark.0, green: dark.1, blue: dark.2, alpha: 1) + : UIColor(red: light.0, green: light.1, blue: light.2, alpha: 1) + }) + #endif + } + /// Right-side limbs, head, nose — the prominent stroke (`#3a3f4b`). - static let figureRight = Color(UIColor { traits in - traits.userInterfaceStyle == .dark - ? UIColor(red: 0.76, green: 0.79, blue: 0.83, alpha: 1) - : UIColor(red: 0.23, green: 0.25, blue: 0.29, alpha: 1) - }) + static let figureRight = figure(light: (0.23, 0.25, 0.29), dark: (0.76, 0.79, 0.83)) /// Left-side limbs, drawn behind — the recessive stroke (`#a9afba`). - static let figureLeft = Color(UIColor { traits in - traits.userInterfaceStyle == .dark - ? UIColor(red: 0.36, green: 0.39, blue: 0.43, alpha: 1) - : UIColor(red: 0.66, green: 0.69, blue: 0.73, alpha: 1) - }) + static let figureLeft = figure(light: (0.66, 0.69, 0.73), dark: (0.36, 0.39, 0.43)) /// Working right-side parts — teal accent (`#0d9488`). - static let figureRightWorking = Color(UIColor { traits in - traits.userInterfaceStyle == .dark - ? UIColor(red: 0.18, green: 0.83, blue: 0.75, alpha: 1) - : UIColor(red: 0.05, green: 0.58, blue: 0.53, alpha: 1) - }) + static let figureRightWorking = figure(light: (0.05, 0.58, 0.53), dark: (0.18, 0.83, 0.75)) /// Working left-side parts — light teal (`#86cfc5`), kept more muted than the /// right so the R/L hierarchy holds in both modes. - static let figureLeftWorking = Color(UIColor { traits in - traits.userInterfaceStyle == .dark - ? UIColor(red: 0.31, green: 0.56, blue: 0.52, alpha: 1) - : UIColor(red: 0.53, green: 0.81, blue: 0.77, alpha: 1) - }) + static let figureLeftWorking = figure(light: (0.53, 0.81, 0.77), dark: (0.31, 0.56, 0.52)) + + /// Scene equipment — seats, benches, frames, cables (`#c5cad4`); sits behind + /// the figure, one step lighter than the ground line. + static let figureEquipment = figure(light: (0.77, 0.79, 0.83), dark: (0.25, 0.27, 0.31)) + + /// Joint-attached items — bars, dumbbells, pads (`#6b7180`); darker than the + /// scene so the thing being moved reads over the limbs. + static let figureProp = figure(light: (0.42, 0.44, 0.50), dark: (0.55, 0.58, 0.63)) /// Ground line (`#b9bec9`). - static let figureGround = Color(UIColor { traits in - traits.userInterfaceStyle == .dark - ? UIColor(red: 0.29, green: 0.31, blue: 0.36, alpha: 1) - : UIColor(red: 0.73, green: 0.75, blue: 0.79, alpha: 1) - }) + static let figureGround = figure(light: (0.73, 0.75, 0.79), dark: (0.29, 0.31, 0.36)) /// Opaque head fill — the screen background, so limbs pass behind the face. + #if os(watchOS) + static let figureHeadFill = Color.black + #else static let figureHeadFill = Color(.systemBackground) + #endif } diff --git a/Workouts/ExerciseFigure/ExerciseInfo.swift b/Workouts/ExerciseFigure/ExerciseInfo.swift new file mode 100644 index 0000000..5e8843c --- /dev/null +++ b/Workouts/ExerciseFigure/ExerciseInfo.swift @@ -0,0 +1,148 @@ +// +// ExerciseInfo.swift +// Workouts +// +// Copyright 2026 Rouslan Zenetl. All Rights Reserved. +// + +import Foundation + +/// Parsed form of an exercise's authored `info.md` (see `Exercise Library/SYSTEM.md`): +/// a one-paragraph summary, the Category/Type/Targets metadata bullets, then the +/// instructional sections (Setup, Execution, Cues, Common Mistakes, Progression…). +/// The library's markdown is uniform, so this parses the known shape rather than +/// general Markdown — SwiftUI's inline-Markdown `Text` can't render headers/lists, +/// and the app wants the structure (target chips, numbered steps) anyway. +struct ExerciseInfo { + /// The intro paragraph(s) between the `# Title` and the metadata bullets. + var summary: String + var category: String? + var type: String? + /// The comma-separated `**Targets:**` values, split for chip rendering. + var targets: [String] + var sections: [Section] + + struct Section { + var title: String + var items: [Item] + } + + /// One block within a section, preserving the authored list flavor: numbered + /// steps render with their ordinal, bullets with a dot, paragraphs plain. + enum Item { + case step(String) + case bullet(String) + case paragraph(String) + + var text: String { + switch self { + case .step(let s), .bullet(let s), .paragraph(let s): s + } + } + } +} + +extension ExerciseInfo { + /// Parse the library's `info.md` shape. Hard-wrapped lines are rejoined: a line + /// that doesn't start a new block (heading, list marker, blank) continues the + /// previous one, matching how Markdown renders soft breaks. + static func parse(markdown: String) -> ExerciseInfo { + var summaryLines: [String] = [] + var category: String? + var type: String? + var targets: [String] = [] + var sections: [Section] = [] + + var currentSection: Section? + var currentItem: Item? + + func metadataValue(_ line: String, key: String) -> String? { + let prefix = "- **\(key):**" + guard line.hasPrefix(prefix) else { return nil } + return String(line.dropFirst(prefix.count)).trimmingCharacters(in: .whitespaces) + } + + func flushItem() { + guard let item = currentItem else { return } + currentItem = nil + if currentSection != nil { + currentSection?.items.append(item) + } else if case .paragraph(let text) = item { + summaryLines.append(text) + } + } + + func flushSection() { + flushItem() + if let section = currentSection { sections.append(section) } + currentSection = nil + } + + func append(to item: Item?, _ text: String) -> Item? { + switch item { + case .step(let s): .step(s + " " + text) + case .bullet(let s): .bullet(s + " " + text) + case .paragraph(let s): .paragraph(s + " " + text) + case nil: .paragraph(text) + } + } + + for rawLine in markdown.components(separatedBy: .newlines) { + let line = rawLine.trimmingCharacters(in: .whitespaces) + + if line.isEmpty { + flushItem() + } else if line.hasPrefix("## ") { + flushSection() + currentSection = Section(title: String(line.dropFirst(3)), items: []) + } else if line.hasPrefix("# ") { + continue // the document title duplicates the exercise name + } else if currentSection == nil, + let value = metadataValue(line, key: "Category") { + flushItem() + category = value + } else if currentSection == nil, + let value = metadataValue(line, key: "Type") { + flushItem() + type = value + } else if currentSection == nil, + let value = metadataValue(line, key: "Targets") { + flushItem() + targets = value.components(separatedBy: ",") + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + } else if let range = line.range(of: #"^\d+\.\s+"#, options: .regularExpression) { + flushItem() + currentItem = .step(String(line[range.upperBound...])) + } else if line.hasPrefix("- ") { + flushItem() + currentItem = .bullet(String(line.dropFirst(2))) + } else { + // Continuation of a hard-wrapped block (or the start of a paragraph). + currentItem = append(to: currentItem, line) + } + } + flushSection() + + return ExerciseInfo( + summary: summaryLines.joined(separator: "\n\n"), + category: category, + type: type, + targets: targets, + sections: sections + ) + } +} + +/// Finds and parses the bundled `.info.md` exported next to the +/// motion rigs by `render.py --export`. +enum ExerciseInfoLibrary { + /// The parsed info for `exerciseName`, or `nil` when none is bundled. + static func info(for exerciseName: String) -> ExerciseInfo? { + guard + let url = Bundle.main.url(forResource: exerciseName, withExtension: "info.md"), + let markdown = try? String(contentsOf: url, encoding: .utf8) + else { return nil } + return ExerciseInfo.parse(markdown: markdown) + } +} diff --git a/Workouts/ExerciseFigure/ExerciseMotion.swift b/Workouts/ExerciseFigure/ExerciseMotion.swift index 0bd6bc2..0b62dc2 100644 --- a/Workouts/ExerciseFigure/ExerciseMotion.swift +++ b/Workouts/ExerciseFigure/ExerciseMotion.swift @@ -39,9 +39,81 @@ struct ExerciseMotion: Codable { let working: [String]? /// Limbs fully occluded in this view — never drawn. let hide: [String]? + /// Equipment layer: scene shapes and cables behind the figure, joint-attached + /// items (bar/dumbbell/pad) over the limbs. See SYSTEM.md "The props layer". + let props: [MotionProp]? let frames: [MotionKeyFrame] } +/// A prop's joint reference: one extremity (`"hand_r"`) or the midpoint of two +/// (`["foot_r", "foot_l"]`). Extremity keys match the pin keys. +enum PropJointRef: Codable { + case single(String) + case midpoint([String]) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let name = try? container.decode(String.self) { + self = .single(name) + } else { + self = .midpoint(try container.decode([String].self)) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .single(let name): try container.encode(name) + case .midpoint(let names): try container.encode(names) + } + } + + var names: [String] { + switch self { + case .single(let name): [name] + case .midpoint(let names): names + } + } +} + +/// One static shape of a `scene` prop, in canvas coordinates. +struct PropSceneShape: Codable { + let kind: String // "line" | "circle" | "rect" + /// line: polyline points; `w` is the stroke width. + let pts: [[Double]]? + let w: Double? + /// circle: center + radius; `fill` false draws an outline. + let c: [Double]? + let r: Double? + let fill: Bool? + /// rect: origin + size (`w`/`h`), corner radius `r`; always filled. + let x: Double? + let y: Double? + let h: Double? + /// Optional palette override: `"prop"` for the darker attached-item gray. + let color: String? +} + +/// One equipment prop. `type` selects the flavor; the other fields apply per type +/// (mirroring the reference renderer's `resolve_props`). +struct MotionProp: Codable { + let type: String // "scene" | "cable" | "bar" | "dumbbell" | "pad" + /// scene: the static shapes. + let shapes: [PropSceneShape]? + /// cable: fixed anchor `[x, y]` → moving joint `to`. + let from: [Double]? + let to: PropJointRef? + /// bar/dumbbell/pad: the joint(s) the item is centered on. + let at: PropJointRef? + /// Fixed world angle (degrees, y-up). Default: bars are horizontal; + /// dumbbells/pads sit perpendicular to the lower bone. + let angle: Double? + let halfLen: Double? + let w: Double? + /// End-disc radius (dumbbell plates default 4.5; bars none). + let plateR: Double? +} + /// A key frame of absolute joint angles. Limbs listed in the motion's `hide` may be /// absent entirely; a two-element array is `[upper, lower]` bone angles. struct MotionKeyFrame: Codable { @@ -56,8 +128,9 @@ struct MotionKeyFrame: Codable { let spine: [Double] /// Head direction from the neck joint. let neck: Double - /// Nose-tick direction (the belly is on that side). - let gaze: Double + /// Nose-tick direction (the belly is on that side). Absent when the figure + /// faces the viewer — no nose is drawn. + let gaze: Double? let armR: [Double]? let armL: [Double]? let legR: [Double]? diff --git a/Workouts/ExerciseFigure/MotionSolver.swift b/Workouts/ExerciseFigure/MotionSolver.swift index 7cebd1e..8aac885 100644 --- a/Workouts/ExerciseFigure/MotionSolver.swift +++ b/Workouts/ExerciseFigure/MotionSolver.swift @@ -60,7 +60,8 @@ struct FigurePose { var root: CGPoint var spine: [Double] var neck: Double - var gaze: Double + /// Absent for figures facing the viewer — no nose tick. + var gaze: Double? var limbs: [FigureLimb: [Double]] var pins: [String: CGPoint] var hold: Double @@ -89,8 +90,9 @@ struct FigureAttachments { struct FigureGeometry { var headCenter: CGPoint var headRadius: Double - var noseStart: CGPoint - var noseEnd: CGPoint + /// Nil when the pose has no gaze (face-on view). + var noseStart: CGPoint? + var noseEnd: CGPoint? /// Quadratic Bézier through pelvis → mid → neck (control = 2·mid − (pelvis+neck)/2). var spineStart: CGPoint var spineControl: CGPoint @@ -213,7 +215,7 @@ enum MotionSolver { y: a.root.y + (b.root.y - a.root.y) * t), spine: zip(a.spine, b.spine).map { lerpAngle($0, $1, t) }, neck: lerpAngle(a.neck, b.neck, t), - gaze: lerpAngle(a.gaze, b.gaze, t), + gaze: (a.gaze != nil && b.gaze != nil) ? lerpAngle(a.gaze!, b.gaze!, t) : nil, limbs: limbs, pins: pins, hold: a.hold, tween: a.tween ) } @@ -223,12 +225,16 @@ enum MotionSolver { static func geometry(of pose: FigurePose, body: ExerciseBodyProfile, hide: Set) -> FigureGeometry { let at = attachments(root: pose.root, spine: pose.spine, body: body) let head = walk(from: at.neck, angles: [pose.neck], lengths: [body.neck])[1] - let noseTip = walk(from: head, angles: [pose.gaze], lengths: [body.headR + 7])[1] - // Nose tick: 7pt outward from the head rim along the gaze direction. - let d = max(hypot(noseTip.x - head.x, noseTip.y - head.y), 1) - let ux = (noseTip.x - head.x) / d - let uy = (noseTip.y - head.y) / d let r = body.headR + // Nose tick: 7pt outward from the head rim along the gaze direction + // (omitted for face-on poses without a gaze). + var noseStart: CGPoint? + var noseEnd: CGPoint? + if let gaze = pose.gaze { + let d = direction(gaze) + noseStart = CGPoint(x: head.x + d.dx * r, y: head.y + d.dy * r) + noseEnd = CGPoint(x: head.x + d.dx * (r + 7), y: head.y + d.dy * (r + 7)) + } var limbs: [FigureLimb: [CGPoint]] = [:] for (limb, authored) in pose.limbs where !hide.contains(limb.rawValue) { @@ -244,8 +250,8 @@ enum MotionSolver { return FigureGeometry( headCenter: head, headRadius: r, - noseStart: CGPoint(x: head.x + ux * r, y: head.y + uy * r), - noseEnd: CGPoint(x: head.x + ux * (r + 7), y: head.y + uy * (r + 7)), + noseStart: noseStart, + noseEnd: noseEnd, spineStart: at.pelvis, spineControl: CGPoint(x: 2 * at.mid.x - (at.pelvis.x + at.neck.x) / 2, y: 2 * at.mid.y - (at.pelvis.y + at.neck.y) / 2), diff --git a/Workouts/Resources/ExerciseMotions/Abdominal.info.md b/Workouts/Resources/ExerciseMotions/Abdominal.info.md new file mode 100644 index 0000000..b272ba1 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Abdominal.info.md @@ -0,0 +1,40 @@ +# Abdominal + +The seated crunch machine — loaded spinal flexion with the lower back +supported. + +- **Category:** Main circuit +- **Type:** Machine-based trunk flexion +- **Targets:** Rectus abdominis, deep core +- **Prescription:** 4 × 10 + +## Setup + +Sit with the pads resting against your chest and your feet flat on the +platform. Grip the handles lightly and set the seat so your hips stay tucked +into the base of the back pad. + +## Execution + +1. Contract your abs to curl your ribs down toward your pelvis, driving the + chest pad forward. +2. Pause at the bottom of the crunch, feeling the abs fully shortened. +3. Return under control until the torso is tall again — don't let the weight + yank you back up. + +## Cues + +- Curl the spine, don't just hinge at the hips — think "shorten the front". +- Keep your lower back in contact with the pad throughout. +- Let the abs move the weight; the arms only steady the handles. + +## Common Mistakes + +- Pulling with the arms or shoulders instead of the abs. +- Hinging from the hips with a flat, rigid spine. +- Rushing the return and bouncing out of the top. + +## Progression + +Add weight once all four sets of ten are a controlled curl; slow the lowering +phase to three seconds for extra time under tension. diff --git a/Workouts/Resources/ExerciseMotions/Abdominal.motion.json b/Workouts/Resources/ExerciseMotions/Abdominal.motion.json new file mode 100644 index 0000000..36f15cb --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Abdominal.motion.json @@ -0,0 +1,34 @@ +{ + "name": "Abdominal", + "primary": 2, + "working": ["spine"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[120, 128], [114, 62]], "w": 8}, + {"kind": "line", "pts": [[106, 130], [150, 130]], "w": 8}, + {"kind": "line", "pts": [[122, 132], [122, 150]], "w": 4}, + {"kind": "line", "pts": [[102, 150], [150, 150]], "w": 4}, + {"kind": "line", "pts": [[156, 149], [190, 149]], "w": 5} + ]}, + {"type": "pad", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 14, "w": 6} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [128, 126], + "spine": [94, 90], "neck": 82, "gaze": -8, + "arm_r": [-12, -52], "arm_l": [-12, -52], + "leg_r": [-20, -90], "leg_l": [-20, -90], + "pins": {"hand_r": [155, 62], "hand_l": [161, 64], "foot_r": [168, 150], "foot_l": [172, 150]} + }, + { + "hold": 0.4, "tween": 1.0, + "root": [128, 126], + "spine": [80, 40], "neck": 22, "gaze": -42, + "arm_r": [-12, -52], "arm_l": [-12, -52], + "leg_r": [-20, -90], "leg_l": [-20, -90], + "pins": {"hand_r": [190, 76], "hand_l": [196, 78], "foot_r": [168, 150], "foot_l": [172, 150]} + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Abductor.info.md b/Workouts/Resources/ExerciseMotions/Abductor.info.md new file mode 100644 index 0000000..acedd3c --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Abductor.info.md @@ -0,0 +1,38 @@ +# Abductor + +The seated hip-abduction machine — presses the knees apart to load the outer +hips and glutes. + +- **Category:** Main circuit +- **Type:** Machine-based hip abduction +- **Targets:** Gluteus medius, gluteus minimus, outer thighs +- **Prescription:** 4 × 10 + +## Setup + +Sit with your legs inside the pads, back flat against the seat, and knees +starting close together. Rest your hands on the seat sides and keep your torso +upright. + +## Execution + +1. Push your knees outward against the pads to spread the legs. +2. Pause at the widest point, squeezing the outer hips and glutes. +3. Bring the legs back together under control — resist the pads on the way in. + +## Cues + +- Stay upright; leaning forward hands the work to the lower back. +- Drive the knees out through the outer hips, not by rocking the pelvis. +- Control the return so the pads don't slam your knees closed. + +## Common Mistakes + +- Leaning forward to force the legs wider. +- Letting the pads snap the knees back together. +- Using a bouncy, momentum-driven range instead of steady tension. + +## Progression + +Add weight in small steps once all four sets of ten are smooth; pause two +seconds at full spread to build end-range strength. diff --git a/Workouts/Resources/ExerciseMotions/Abductor.motion.json b/Workouts/Resources/ExerciseMotions/Abductor.motion.json new file mode 100644 index 0000000..e986689 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Abductor.motion.json @@ -0,0 +1,34 @@ +{ + "name": "Abductor", + "primary": 2, + "working": ["leg_r", "leg_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "rect", "x": 152, "y": 56, "w": 16, "h": 72, "r": 5}, + {"kind": "rect", "x": 130, "y": 126, "w": 60, "h": 10, "r": 3}, + {"kind": "line", "pts": [[136, 78], [136, 108]], "w": 4, "color": "prop"}, + {"kind": "line", "pts": [[188, 78], [188, 108]], "w": 4, "color": "prop"}, + {"kind": "line", "pts": [[213, 110], [213, 132]], "w": 9, "color": "prop"}, + {"kind": "line", "pts": [[107, 110], [107, 132]], "w": 9, "color": "prop"} + ]} + ], + "frames": [ + { + "hold": 0.5, "tween": 1.1, + "root": [160, 116], + "spine": [90, 90], "neck": 90, + "arm_r": [-48, -82], "arm_l": [-132, -98], + "leg_r": [-22, 0], "leg_l": [-158, 0], + "pins": {"foot_r": [180, 156], "foot_l": [140, 156]} + }, + { + "hold": 0.5, "tween": 1.1, + "root": [160, 116], + "spine": [90, 90], "neck": 90, + "arm_r": [-48, -82], "arm_l": [-132, -98], + "leg_r": [-22, 0], "leg_l": [-158, 0], + "pins": {"foot_r": [216, 156], "foot_l": [104, 156]} + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Adductor.info.md b/Workouts/Resources/ExerciseMotions/Adductor.info.md new file mode 100644 index 0000000..e1e3010 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Adductor.info.md @@ -0,0 +1,39 @@ +# Adductor + +The seated hip-adduction machine — squeezes the knees together to load the +inner thighs. + +- **Category:** Main circuit +- **Type:** Machine-based hip adduction +- **Targets:** Adductors, inner thighs +- **Prescription:** 4 × 10 + +## Setup + +Sit with your legs placed outside the pads, back flat against the seat, and +knees starting spread apart. Rest your hands on the seat sides and keep your +torso upright. + +## Execution + +1. Squeeze your knees together against the pads using your inner thighs. +2. Pause when the knees nearly touch, holding the contraction. +3. Let the legs spread back open under control — don't let the pads fling them + apart. + +## Cues + +- Drive the movement from the inner thighs, not by rocking the hips. +- Keep the torso upright and the back against the seat. +- Control both directions; the opening phase trains the muscle too. + +## Common Mistakes + +- Letting the pads throw the legs open at the start of each rep. +- Setting too wide a start range before the muscle is warm. +- Fast, swinging reps that rely on momentum instead of the adductors. + +## Progression + +Add weight once all four sets of ten are controlled in both directions; pause +two seconds with the knees together to strengthen the fully-shortened position. diff --git a/Workouts/Resources/ExerciseMotions/Adductor.motion.json b/Workouts/Resources/ExerciseMotions/Adductor.motion.json new file mode 100644 index 0000000..c012ce9 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Adductor.motion.json @@ -0,0 +1,34 @@ +{ + "name": "Adductor", + "primary": 2, + "working": ["leg_r", "leg_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "rect", "x": 152, "y": 56, "w": 16, "h": 72, "r": 5}, + {"kind": "rect", "x": 130, "y": 126, "w": 60, "h": 10, "r": 3}, + {"kind": "line", "pts": [[136, 78], [136, 108]], "w": 4, "color": "prop"}, + {"kind": "line", "pts": [[188, 78], [188, 108]], "w": 4, "color": "prop"}, + {"kind": "line", "pts": [[197, 110], [197, 132]], "w": 9, "color": "prop"}, + {"kind": "line", "pts": [[123, 110], [123, 132]], "w": 9, "color": "prop"} + ]} + ], + "frames": [ + { + "hold": 0.5, "tween": 1.1, + "root": [160, 116], + "spine": [90, 90], "neck": 90, + "arm_r": [-48, -82], "arm_l": [-132, -98], + "leg_r": [-22, 0], "leg_l": [-158, 0], + "pins": {"foot_r": [216, 156], "foot_l": [104, 156]} + }, + { + "hold": 0.5, "tween": 1.1, + "root": [160, 116], + "spine": [90, 90], "neck": 90, + "arm_r": [-48, -82], "arm_l": [-132, -98], + "leg_r": [-22, 0], "leg_l": [-158, 0], + "pins": {"foot_r": [180, 156], "foot_l": [140, 156]} + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Arm Curl.info.md b/Workouts/Resources/ExerciseMotions/Arm Curl.info.md new file mode 100644 index 0000000..9989727 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Arm Curl.info.md @@ -0,0 +1,37 @@ +# Arm Curl + +The seated machine curl — biceps isolation with the upper arms braced flat on +the pad so only the forearms move. + +- **Category:** Main circuit +- **Type:** Machine-based elbow flexion (isolation) +- **Targets:** Biceps brachii, brachialis +- **Prescription:** 4 × 10 + +## Setup + +Sit with your upper arms flat along the pad and grip the handles. Start with +your forearms extended down along the pad, elbows just short of locked. + +## Execution + +1. Curl the handles up toward your shoulders, squeezing the biceps at the top. +2. Keep your upper arms pinned to the pad — only the forearms move. +3. Lower under control until the forearms are fully extended again. + +## Cues + +- Upper arms stay glued to the pad the entire rep. +- Squeeze hard at the top, then resist the weight all the way down. +- Wrists stay neutral — don't curl with the hands. + +## Common Mistakes + +- Rocking the torso or lifting the elbows to swing the weight up. +- Cutting the lowering phase short and dropping the weight. +- Not fully extending at the bottom, shortening the range. + +## Progression + +Add weight once all four sets of ten are strict; slow the lowering phase to +three seconds before adding load again. diff --git a/Workouts/Resources/ExerciseMotions/Arm Curl.motion.json b/Workouts/Resources/ExerciseMotions/Arm Curl.motion.json new file mode 100644 index 0000000..cf63762 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Arm Curl.motion.json @@ -0,0 +1,32 @@ +{ + "name": "Arm Curl", + "primary": 2, + "working": ["arm_r", "arm_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[116, 134], [150, 134]], "w": 8}, + {"kind": "line", "pts": [[146, 42], [176, 68]], "w": 8}, + {"kind": "line", "pts": [[176, 68], [176, 128]], "w": 5} + ]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 6} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [130, 124], + "spine": [88, 86], "neck": 82, "gaze": -18, + "arm_r": [-35, -52], "arm_l": [-35, -52], + "leg_r": [-8, -72], "leg_l": [-8, -72], + "pins": {"foot_r": [176, 150], "foot_l": [180, 151]} + }, + { + "hold": 0.4, "tween": 1.2, + "root": [130, 124], + "spine": [88, 86], "neck": 82, "gaze": -18, + "arm_r": [-35, 138], "arm_l": [-35, 138], + "leg_r": [-8, -72], "leg_l": [-8, -72], + "pins": {"foot_r": [176, 150], "foot_l": [180, 151]} + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Bird Dog.info.md b/Workouts/Resources/ExerciseMotions/Bird Dog.info.md new file mode 100644 index 0000000..a8b8514 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Bird Dog.info.md @@ -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 × 8–10 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 diff --git a/Workouts/Resources/ExerciseMotions/Calfs.info.md b/Workouts/Resources/ExerciseMotions/Calfs.info.md new file mode 100644 index 0000000..98918ef --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Calfs.info.md @@ -0,0 +1,40 @@ +# Calfs + +Seated heel raises through a full range — a deep stretch below the platform and +a hard squeeze at the top. + +- **Category:** Main circuit +- **Type:** Machine-based calf raise +- **Targets:** Soleus, gastrocnemius +- **Prescription:** 4 × 10 + +## Setup + +Sit with the balls of your feet on the platform, heels hanging off the back +edge, and the pad snug on your lower thighs just above the knees. Hold the pad +handles lightly. + +## Execution + +1. Raise your heels by contracting your calves, pressing up onto the balls of + your feet against the pad. +2. Pause at the top with the calves fully shortened. +3. Slowly lower your heels below the platform level for a full stretch. + +## Cues + +- Chase the full range — a hard squeeze up top, a deep stretch at the bottom. +- Drive through the balls of the feet, not the toes. +- Control the tempo; the stretch under the platform is where the work is. + +## Common Mistakes + +- Bouncing through short, choppy reps instead of the full range. +- Rocking the weight up with the hips instead of the calves. +- Rushing the lowering and skipping the bottom stretch. + +## Progression + +Add weight in small increments once all four sets of ten are controlled; +single-leg raises and paused reps at the stretched bottom build the hardest end +of the range. diff --git a/Workouts/Resources/ExerciseMotions/Calfs.motion.json b/Workouts/Resources/ExerciseMotions/Calfs.motion.json new file mode 100644 index 0000000..2d07937 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Calfs.motion.json @@ -0,0 +1,36 @@ +{ + "name": "Calfs", + "primary": 2, + "working": ["leg_r", "leg_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[124, 126], [158, 124]], "w": 9}, + {"kind": "line", "pts": [[138, 128], [138, 150]], "w": 5}, + {"kind": "line", "pts": [[120, 150], [156, 150]], "w": 5}, + {"kind": "rect", "x": 198, "y": 147, "w": 34, "h": 6, "r": 1}, + {"kind": "line", "pts": [[183, 62], [183, 124]], "w": 4}, + {"kind": "rect", "x": 170, "y": 121, "w": 27, "h": 7, "r": 2}, + {"kind": "line", "pts": [[164, 76], [183, 76]], "w": 3, "color": "prop"} + ]}, + {"type": "pad", "at": ["foot_r", "foot_l"], "angle": 15, "halfLen": 7, "w": 5} + ], + "frames": [ + { + "hold": 0.4, "tween": 0.9, + "root": [140, 118], + "spine": [100, 96], "neck": 80, "gaze": -10, + "arm_r": [-40, -15], "arm_l": [-40, -15], + "leg_r": [-18, -8], "leg_l": [-18, -8], + "pins": {"hand_r": [166, 74], "hand_l": [170, 76], "foot_r": [212, 148], "foot_l": [216, 150]} + }, + { + "hold": 0.4, "tween": 1.2, + "root": [140, 118], + "spine": [100, 96], "neck": 80, "gaze": -10, + "arm_r": [-40, -15], "arm_l": [-40, -15], + "leg_r": [-14, 4], "leg_l": [-14, 4], + "pins": {"hand_r": [166, 74], "hand_l": [170, 76], "foot_r": [212, 139], "foot_l": [216, 141]} + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Cat-Cow.info.md b/Workouts/Resources/ExerciseMotions/Cat-Cow.info.md new file mode 100644 index 0000000..86b29ef --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Cat-Cow.info.md @@ -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) diff --git a/Workouts/Resources/ExerciseMotions/Chest Press.info.md b/Workouts/Resources/ExerciseMotions/Chest Press.info.md new file mode 100644 index 0000000..585165a --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Chest Press.info.md @@ -0,0 +1,38 @@ +# Chest Press + +The seated horizontal press — the machine bench press, pushing the handles +forward from the chest. + +- **Category:** Main circuit +- **Type:** Machine-based horizontal press +- **Targets:** Pectorals, anterior deltoids, triceps + +## Setup + +Adjust the seat so the handles sit at mid-chest height. Grip the handles, set +your back against the pad, and keep your wrists straight and stacked over the +forearms. + +## Execution + +1. Push the handles forward until your arms are nearly extended — stop short of + locking the elbows. +2. Pause briefly, then return slowly until the handles are back at your chest. +3. Keep your shoulder blades gently pinned to the pad throughout. + +## Cues + +- Drive through the palms; wrists stay straight, not bent back. +- Don't let the elbows drop far below the handles at the bottom. +- Return under control — resist the weight rather than letting it pull you back. + +## Common Mistakes + +- Letting the elbows sink too low, straining the shoulders. +- Bouncing the handles at the chest to start the next rep. +- Locking the elbows hard at full extension. + +## Progression + +Add weight in small plates once all four sets of ten are controlled; alternate +to a single-arm press to expose and correct side-to-side differences. diff --git a/Workouts/Resources/ExerciseMotions/Chest Press.motion.json b/Workouts/Resources/ExerciseMotions/Chest Press.motion.json new file mode 100644 index 0000000..8b8cf53 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Chest Press.motion.json @@ -0,0 +1,33 @@ +{ + "name": "Chest Press", + "primary": 2, + "working": ["arm_r", "arm_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[114, 132], [86, 40]], "w": 8}, + {"kind": "line", "pts": [[106, 133], [146, 133]], "w": 8}, + {"kind": "line", "pts": [[122, 135], [122, 151]], "w": 5}, + {"kind": "line", "pts": [[108, 151], [138, 151]], "w": 4} + ]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 8} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.1, + "root": [116, 122], + "spine": [104, 100], "neck": 84, "gaze": 5, + "arm_r": [-100, 40], "arm_l": [-100, 40], + "leg_r": [-22, -72], "leg_l": [-22, -72], + "pins": {"foot_r": [158, 151], "foot_l": [162, 152]} + }, + { + "hold": 0.4, "tween": 1.1, + "root": [116, 122], + "spine": [104, 100], "neck": 84, "gaze": 5, + "arm_r": [-6, -14], "arm_l": [-6, -14], + "leg_r": [-22, -72], "leg_l": [-22, -72], + "pins": {"foot_r": [158, 151], "foot_l": [162, 152]} + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Dead Bug.info.md b/Workouts/Resources/ExerciseMotions/Dead Bug.info.md new file mode 100644 index 0000000..d581080 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Dead Bug.info.md @@ -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 2–3 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 diff --git a/Workouts/Resources/ExerciseMotions/Hollow Body Hold.info.md b/Workouts/Resources/ExerciseMotions/Hollow Body Hold.info.md new file mode 100644 index 0000000..e098364 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Hollow Body Hold.info.md @@ -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 × 20–40 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 diff --git a/Workouts/Resources/ExerciseMotions/Lat Pull Down.info.md b/Workouts/Resources/ExerciseMotions/Lat Pull Down.info.md new file mode 100644 index 0000000..8db8d56 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Lat Pull Down.info.md @@ -0,0 +1,37 @@ +# Lat Pull Down + +The seated vertical pull — building width through the lats with a bar driven +down from overhead. + +- **Category:** Main circuit +- **Type:** Machine-based vertical pull +- **Targets:** Latissimus dorsi, mid-back, rear delts, biceps +- **Prescription:** 4 × 10 + +## Setup + +Sit upright with your knees secured under the pad and feet flat. Grip the bar +wider than shoulder-width, arms fully extended overhead, torso just off vertical. + +## Execution + +1. Pull the bar down to your upper chest, driving your elbows down and back. +2. Squeeze your shoulder blades together at the bottom and pause briefly. +3. Return the bar overhead under control until your arms are fully extended. + +## Cues + +- Lead with the elbows, not the hands — think "elbows to the ribs". +- Squeeze the shoulder blades down and together at the bottom. +- Keep the torso tall; only a slight lean back, never a swing. + +## Common Mistakes + +- Leaning back excessively and turning it into a row. +- Using momentum to yank the bar down. +- Stopping short — not pulling the bar all the way to the chest. + +## Progression + +Add weight once all four sets of ten are clean; progress to a wider or neutral +grip, then to strict pull-ups as pulling strength builds. diff --git a/Workouts/Resources/ExerciseMotions/Lat Pull Down.motion.json b/Workouts/Resources/ExerciseMotions/Lat Pull Down.motion.json new file mode 100644 index 0000000..e4d0031 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Lat Pull Down.motion.json @@ -0,0 +1,36 @@ +{ + "name": "Lat Pull Down", + "primary": 2, + "working": ["arm_r", "arm_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[126, 130], [162, 130]], "w": 8}, + {"kind": "line", "pts": [[124, 130], [124, 56]], "w": 5}, + {"kind": "line", "pts": [[124, 56], [198, 12]], "w": 5}, + {"kind": "rect", "x": 170, "y": 112, "w": 40, "h": 7, "r": 3} + ]}, + {"type": "cable", "from": [198, 8], "to": ["hand_r", "hand_l"]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "halfLen": 28} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [150, 122], + "spine": [93, 90], "neck": 85, "gaze": 55, + "arm_r": [20, 10], "arm_l": [20, 10], + "leg_r": [-12, -80], "leg_l": [-12, -80], + "pins": {"hand_r": [198, 24], "hand_l": [202, 26], + "foot_r": [193, 150], "foot_l": [197, 151]} + }, + { + "hold": 0.4, "tween": 1.2, + "root": [150, 122], + "spine": [93, 90], "neck": 85, "gaze": 30, + "arm_r": [-115, -20], "arm_l": [-115, -20], + "leg_r": [-12, -80], "leg_l": [-12, -80], + "pins": {"hand_r": [165, 66], "hand_l": [169, 68], + "foot_r": [193, 150], "foot_l": [197, 151]} + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Leg Curl.info.md b/Workouts/Resources/ExerciseMotions/Leg Curl.info.md new file mode 100644 index 0000000..135a9e1 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Leg Curl.info.md @@ -0,0 +1,37 @@ +# Leg Curl + +Isolated hamstring work — the heels curl toward the glutes while a pad locks +the thighs in place. + +- **Category:** Main circuit +- **Type:** Machine-based isolation (knee flexion) +- **Targets:** Hamstrings (biceps femoris, semitendinosus, semimembranosus) +- **Prescription:** 4 × 10 + +## Setup + +Sit (or lie face down, depending on the version) with the thigh pad locked over +your legs and the roller behind your ankles. Start with the legs extended. + +## Execution + +1. Curl your legs toward your glutes, driving through the hamstrings. +2. Squeeze hard at the fully bent position. +3. Return the roller under control until the legs are extended again. + +## Cues + +- Focus on hamstring engagement — feel the pull behind the knee, not the back. +- Keep your hips and back still against the pad; no arching to cheat. +- Curl through the full range, all the way to the glutes. + +## Common Mistakes + +- Arching the lower back to yank the weight up. +- Using momentum instead of a controlled curl. +- Cutting the range short and never reaching full flexion. + +## Progression + +Add load once all four sets of ten are controlled; single-leg curls for +asymmetry, or a slow eccentric to overload the lengthening hamstring. diff --git a/Workouts/Resources/ExerciseMotions/Leg Curl.motion.json b/Workouts/Resources/ExerciseMotions/Leg Curl.motion.json new file mode 100644 index 0000000..0151be6 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Leg Curl.motion.json @@ -0,0 +1,34 @@ +{ + "name": "Leg Curl", + "primary": 2, + "working": ["leg_r", "leg_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[110, 122], [86, 48]], "w": 9}, + {"kind": "line", "pts": [[104, 124], [166, 120]], "w": 9}, + {"kind": "line", "pts": [[130, 105], [158, 106]], "w": 7}, + {"kind": "line", "pts": [[108, 126], [108, 150]], "w": 5}, + {"kind": "line", "pts": [[160, 124], [160, 150]], "w": 5}, + {"kind": "line", "pts": [[104, 150], [166, 150]], "w": 5}, + {"kind": "circle", "c": [114, 90], "r": 3.5, "fill": true, "color": "prop"} + ]}, + {"type": "pad", "at": ["foot_r", "foot_l"], "halfLen": 9} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [118, 116], + "spine": [110, 106], "neck": 74, "gaze": -20, + "arm_r": [-75, -60], "arm_l": [-75, -60], + "leg_r": [10, 4], "leg_l": [10, 4] + }, + { + "hold": 0.4, "tween": 1.2, + "root": [118, 116], + "spine": [110, 106], "neck": 74, "gaze": -20, + "arm_r": [-75, -60], "arm_l": [-75, -60], + "leg_r": [10, -95], "leg_l": [10, -95] + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Leg Extension.info.md b/Workouts/Resources/ExerciseMotions/Leg Extension.info.md new file mode 100644 index 0000000..1490e87 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Leg Extension.info.md @@ -0,0 +1,40 @@ +# Leg Extension + +Isolated quadriceps work — the knee opens against the roller while the hips +stay pinned to the seat. + +- **Category:** Main circuit +- **Type:** Machine-based isolation (knee extension) +- **Targets:** Quadriceps (rectus femoris, vastus lateralis/medialis) +- **Prescription:** 4 × 10 + +## Setup + +Sit upright with your back flat against the pad and align your knees with the +machine's pivot point. Hook the roller pad against the front of your ankles, +hands resting on the side grips. + +## Execution + +1. Extend your legs to a straightened position — stop just short of snapping + the knees into lockout. +2. Pause briefly at the top with the quads squeezed hard. +3. Lower the roller back down with control to the start. + +## Cues + +- Knees stack over the pivot — realign the seat if they sit forward or back. +- Hips glued to the seat; if they lift, the weight is too heavy. +- Own the lowering — no free-falling back to the stack. + +## Common Mistakes + +- Jerky, momentum-driven reps instead of a smooth extension. +- Lifting the hips off the seat to swing the weight up. +- Slamming into a locked knee at the top. + +## Progression + +Add load in small increments once all four sets of ten are smooth; single-leg +extensions for asymmetry, or a paused top / slow eccentric for more time under +tension. diff --git a/Workouts/Resources/ExerciseMotions/Leg Extension.motion.json b/Workouts/Resources/ExerciseMotions/Leg Extension.motion.json new file mode 100644 index 0000000..367717c --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Leg Extension.motion.json @@ -0,0 +1,33 @@ +{ + "name": "Leg Extension", + "primary": 2, + "working": ["leg_r", "leg_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[110, 122], [86, 48]], "w": 9}, + {"kind": "line", "pts": [[104, 124], [166, 120]], "w": 9}, + {"kind": "line", "pts": [[108, 126], [108, 150]], "w": 5}, + {"kind": "line", "pts": [[160, 124], [160, 150]], "w": 5}, + {"kind": "line", "pts": [[104, 150], [166, 150]], "w": 5}, + {"kind": "circle", "c": [114, 90], "r": 3.5, "fill": true, "color": "prop"} + ]}, + {"type": "pad", "at": ["foot_r", "foot_l"], "halfLen": 9} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [118, 116], + "spine": [110, 106], "neck": 74, "gaze": -20, + "arm_r": [-75, -60], "arm_l": [-75, -60], + "leg_r": [10, -83], "leg_l": [10, -83] + }, + { + "hold": 0.4, "tween": 1.2, + "root": [118, 116], + "spine": [110, 106], "neck": 74, "gaze": -20, + "arm_r": [-75, -60], "arm_l": [-75, -60], + "leg_r": [10, 4], "leg_l": [10, 4] + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Leg Press.info.md b/Workouts/Resources/ExerciseMotions/Leg Press.info.md new file mode 100644 index 0000000..f051bda --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Leg Press.info.md @@ -0,0 +1,39 @@ +# Leg Press + +The seated machine squat — maximum lower-body load with the back fully +supported. + +- **Category:** Main circuit +- **Type:** Machine-based compound push +- **Targets:** Quadriceps, glutes, hamstrings +- **Prescription:** 4 × 10 + +## Setup + +Sit with your back and hips flat against the pad. Place your feet +shoulder-width on the platform, heels never floating. Adjust the seat so your +knees start near 90°. + +## Execution + +1. Press through your heels until your legs are nearly straight — stop short + of locking the knees. +2. Pause briefly, then bend the knees to return under control. +3. Keep your back and hips glued to the pad the whole rep. + +## Cues + +- Drive through the heels, not the toes. +- Knees track over the feet — don't let them cave inward. +- Lower back stays on the pad; if the hips curl up, you've gone too deep. + +## Common Mistakes + +- Locking the knees hard at the top. +- Bouncing out of the bottom. +- Letting the hips roll off the pad at the bottom of the rep. + +## Progression + +Add weight in small plates once all four sets of ten are controlled; single-leg +presses for asymmetry work. diff --git a/Workouts/Resources/ExerciseMotions/Leg Press.motion.json b/Workouts/Resources/ExerciseMotions/Leg Press.motion.json new file mode 100644 index 0000000..c28c4eb --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Leg Press.motion.json @@ -0,0 +1,35 @@ +{ + "name": "Leg Press", + "primary": 2, + "working": ["leg_r", "leg_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[134, 123], [96, 36]], "w": 9}, + {"kind": "line", "pts": [[126, 127], [156, 122]], "w": 9}, + {"kind": "line", "pts": [[138, 130], [138, 150]], "w": 5}, + {"kind": "line", "pts": [[118, 150], [158, 150]], "w": 5}, + {"kind": "line", "pts": [[142, 90], [142, 78]], "w": 3}, + {"kind": "circle", "c": [142, 77], "r": 3.5, "fill": true, "color": "prop"} + ]}, + {"type": "pad", "at": ["foot_r", "foot_l"], "angle": 88, "halfLen": 20, "w": 6} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [140, 116], + "spine": [116, 112], "neck": 75, "gaze": -15, + "arm_r": [-75, -20], "arm_l": [-75, -20], + "leg_r": [40, -58], "leg_l": [40, -58], + "pins": {"foot_r": [185, 102], "foot_l": [189, 104]} + }, + { + "hold": 0.4, "tween": 1.2, + "root": [140, 116], + "spine": [116, 112], "neck": 75, "gaze": -15, + "arm_r": [-75, -20], "arm_l": [-75, -20], + "leg_r": [20, -5], "leg_l": [20, -5], + "pins": {"foot_r": [220, 101], "foot_l": [224, 103]} + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Leg Raises.info.md b/Workouts/Resources/ExerciseMotions/Leg Raises.info.md new file mode 100644 index 0000000..af527a2 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Leg Raises.info.md @@ -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 × 8–15 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 diff --git a/Workouts/Resources/ExerciseMotions/Plank.info.md b/Workouts/Resources/ExerciseMotions/Plank.info.md new file mode 100644 index 0000000..6e738d6 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Plank.info.md @@ -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 × 30–60 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 diff --git a/Workouts/Resources/ExerciseMotions/Reverse Crunch.info.md b/Workouts/Resources/ExerciseMotions/Reverse Crunch.info.md new file mode 100644 index 0000000..a0ba037 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Reverse Crunch.info.md @@ -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 × 12–15 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 diff --git a/Workouts/Resources/ExerciseMotions/Rotary.info.md b/Workouts/Resources/ExerciseMotions/Rotary.info.md new file mode 100644 index 0000000..72fe969 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Rotary.info.md @@ -0,0 +1,39 @@ +# Rotary + +The rotary torso machine — controlled trunk rotation that isolates the +obliques. + +- **Category:** Main circuit +- **Type:** Machine-based trunk rotation +- **Targets:** Obliques, deep core +- **Prescription:** 4 × 10 + +## Setup + +Sit tall with your hips squared and locked against the pads. Rest your +forearms on the pads and grip the handle in front of your chest so the trunk, +not the arms, drives the machine. + +## Execution + +1. Rotate your torso to one side in a smooth, controlled arc, keeping your hips + still. +2. Pause at the end of the range without letting the weight pull you further. +3. Return through center and rotate to the other side with the same control. + +## Cues + +- Twist from the waist — the obliques generate the movement, not the arms. +- Keep the hips and pelvis pinned; only the trunk turns. +- Move slowly; momentum steals the work from the obliques. + +## Common Mistakes + +- Throwing the weight with momentum instead of controlling the twist. +- Pushing with the arms rather than rotating the trunk. +- Letting the hips swing along with the torso. + +## Progression + +Add weight once both sides move with equal control; pause two seconds at full +rotation to kill any momentum. diff --git a/Workouts/Resources/ExerciseMotions/Rotary.motion.json b/Workouts/Resources/ExerciseMotions/Rotary.motion.json new file mode 100644 index 0000000..1fbab42 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Rotary.motion.json @@ -0,0 +1,32 @@ +{ + "name": "Rotary", + "primary": 1, + "working": ["spine"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[138, 130], [182, 130]], "w": 8}, + {"kind": "line", "pts": [[160, 132], [160, 150]], "w": 5}, + {"kind": "line", "pts": [[144, 150], [176, 150]], "w": 5} + ]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 14} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [160, 122], + "spine": [104, 109], "neck": 109, + "arm_r": [-50, -85], "arm_l": [-55, -90], + "leg_r": [-62, -82], "leg_l": [-118, -98], + "pins": {"hand_r": [112, 72], "hand_l": [118, 74], "foot_r": [190, 150], "foot_l": [130, 150]} + }, + { + "hold": 0.4, "tween": 1.0, + "root": [160, 122], + "spine": [76, 71], "neck": 71, + "arm_r": [-125, -95], "arm_l": [-130, -90], + "leg_r": [-62, -82], "leg_l": [-118, -98], + "pins": {"hand_r": [208, 72], "hand_l": [202, 74], "foot_r": [190, 150], "foot_l": [130, 150]} + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Seated Row.info.md b/Workouts/Resources/ExerciseMotions/Seated Row.info.md new file mode 100644 index 0000000..9dfb106 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Seated Row.info.md @@ -0,0 +1,37 @@ +# Seated Row + +The seated horizontal pull — thickness for the mid-back by driving the elbows +straight back against the chest pad. + +- **Category:** Main circuit +- **Type:** Machine-based horizontal pull +- **Targets:** Mid-back, latissimus dorsi, rear delts, biceps +- **Prescription:** 4 × 10 + +## Setup + +Sit with your chest firmly against the pad and feet on the footrest. Grip the +handles with your arms extended forward toward the anchor. + +## Execution + +1. Pull straight back, keeping your elbows close to your body. +2. Drive the handles to your ribs and retract your shoulder blades hard. +3. Return to full extension under control without rounding your back. + +## Cues + +- Retract the shoulder blades first, then the arms follow. +- Elbows stay tucked, tracing a line along your sides. +- Chest stays glued to the pad the whole rep. + +## Common Mistakes + +- Rounding the back to reach forward at the extension. +- Flaring the elbows out wide instead of driving them back. +- Heaving with the whole torso instead of pulling with the back. + +## Progression + +Add weight once all four sets of ten are controlled; vary the grip width to +shift emphasis between the lats and the mid-back. diff --git a/Workouts/Resources/ExerciseMotions/Seated Row.motion.json b/Workouts/Resources/ExerciseMotions/Seated Row.motion.json new file mode 100644 index 0000000..5e582b0 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Seated Row.motion.json @@ -0,0 +1,35 @@ +{ + "name": "Seated Row", + "primary": 2, + "working": ["arm_r", "arm_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[122, 132], [156, 132]], "w": 8}, + {"kind": "line", "pts": [[159, 46], [157, 96]], "w": 8}, + {"kind": "line", "pts": [[176, 150], [206, 150]], "w": 5} + ]}, + {"type": "cable", "from": [244, 120], "to": ["hand_r", "hand_l"]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 7} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.0, + "root": [140, 122], + "spine": [85, 82], "neck": 80, "gaze": 5, + "arm_r": [-10, -25], "arm_l": [-10, -25], + "leg_r": [-8, -75], "leg_l": [-8, -75], + "pins": {"hand_r": [200, 62], "hand_l": [204, 64], + "foot_r": [188, 148], "foot_l": [192, 149]} + }, + { + "hold": 0.4, "tween": 1.2, + "root": [140, 122], + "spine": [85, 82], "neck": 80, "gaze": 5, + "arm_r": [-120, -35], "arm_l": [-120, -35], + "leg_r": [-8, -75], "leg_l": [-8, -75], + "pins": {"hand_r": [158, 72], "hand_l": [162, 74], + "foot_r": [188, 148], "foot_l": [192, 149]} + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Shoulder Press.info.md b/Workouts/Resources/ExerciseMotions/Shoulder Press.info.md new file mode 100644 index 0000000..c7ea8bd --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Shoulder Press.info.md @@ -0,0 +1,39 @@ +# Shoulder Press + +The seated overhead press — vertical pushing strength with the trunk braced +against the pad. + +- **Category:** Main circuit +- **Type:** Machine-based vertical press +- **Targets:** Deltoids, triceps, upper trapezius + +## Setup + +Sit with your back against the pad and grip the handles just outside +shoulder-width, elbows bent so the handles start at shoulder height. Plant your +feet flat and set your ribs down over your hips. + +## Execution + +1. Press the handles straight up until your arms are nearly extended — stop + short of locking out the elbows. +2. Pause at the top without shrugging, then lower under control back to shoulder + height. +3. Keep your neck relaxed and your back flat on the pad the whole rep. + +## Cues + +- Press up, not forward — the handles travel over the shoulders. +- Keep the ribs down; don't arch the lower back to help the press. +- Shoulders stay set — avoid shrugging the handles up with the traps. + +## Common Mistakes + +- Locking the elbows hard at the top. +- Shrugging the shoulders to finish the press. +- Arching the lower back off the pad as the weight gets heavy. + +## Progression + +Add weight in small increments once all four sets of ten are smooth; move to a +single-arm press for even loading and extra core demand. diff --git a/Workouts/Resources/ExerciseMotions/Shoulder Press.motion.json b/Workouts/Resources/ExerciseMotions/Shoulder Press.motion.json new file mode 100644 index 0000000..2502f62 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Shoulder Press.motion.json @@ -0,0 +1,33 @@ +{ + "name": "Shoulder Press", + "primary": 2, + "working": ["arm_r", "arm_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[116, 140], [88, 50]], "w": 8}, + {"kind": "line", "pts": [[110, 141], [148, 141]], "w": 8}, + {"kind": "line", "pts": [[126, 143], [126, 151]], "w": 5}, + {"kind": "line", "pts": [[110, 151], [142, 151]], "w": 4} + ]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "halfLen": 10} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.1, + "root": [120, 130], + "spine": [110, 104], "neck": 80, "gaze": 8, + "arm_r": [-60, 90], "arm_l": [-60, 90], + "leg_r": [-25, -72], "leg_l": [-25, -72], + "pins": {"foot_r": [164, 151], "foot_l": [168, 152]} + }, + { + "hold": 0.4, "tween": 1.1, + "root": [120, 130], + "spine": [110, 104], "neck": 80, "gaze": 8, + "arm_r": [54, 36], "arm_l": [54, 36], + "leg_r": [-25, -72], "leg_l": [-25, -72], + "pins": {"foot_r": [164, 151], "foot_l": [168, 152]} + } + ] +} diff --git a/Workouts/Resources/ExerciseMotions/Side Plank.info.md b/Workouts/Resources/ExerciseMotions/Side Plank.info.md new file mode 100644 index 0000000..c1126cf --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Side Plank.info.md @@ -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 × 20–45 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) diff --git a/Workouts/Resources/ExerciseMotions/Tricep Press.info.md b/Workouts/Resources/ExerciseMotions/Tricep Press.info.md new file mode 100644 index 0000000..e60a145 --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Tricep Press.info.md @@ -0,0 +1,39 @@ +# Tricep Press + +The seated press-down — elbows pinned to the sides, driving the handles from the +chest to full extension. + +- **Category:** Main circuit +- **Type:** Machine-based elbow extension +- **Targets:** Triceps brachii + +## Setup + +Sit tall with your back on the pad and your upper arms tucked close to your +sides. Take the handles at chest height with your elbows bent to about a right +angle. + +## Execution + +1. Press the handles down by straightening your elbows, keeping your upper arms + still against your sides. +2. Extend fully without snapping the elbows, pausing briefly at the bottom. +3. Let the handles rise back to chest height under control, elbows staying + tucked. + +## Cues + +- Only the forearms move — the upper arms stay glued to the torso. +- Keep the motion controlled; don't fling the handles down. +- Squeeze the triceps at the bottom rather than leaning your weight in. + +## Common Mistakes + +- Flaring the elbows out away from the sides. +- Using the shoulders or bodyweight to drive the handles down. +- Cutting the extension short and never reaching full lockout. + +## Progression + +Add weight in small increments once all four sets of ten are clean; slow the +lowering phase to three seconds to add time under tension for the triceps. diff --git a/Workouts/Resources/ExerciseMotions/Tricep Press.motion.json b/Workouts/Resources/ExerciseMotions/Tricep Press.motion.json new file mode 100644 index 0000000..fe227fa --- /dev/null +++ b/Workouts/Resources/ExerciseMotions/Tricep Press.motion.json @@ -0,0 +1,33 @@ +{ + "name": "Tricep Press", + "primary": 2, + "working": ["arm_r", "arm_l"], + "hide": [], + "props": [ + {"type": "scene", "shapes": [ + {"kind": "line", "pts": [[116, 134], [92, 42]], "w": 8}, + {"kind": "line", "pts": [[108, 135], [146, 135]], "w": 8}, + {"kind": "line", "pts": [[124, 137], [124, 151]], "w": 5}, + {"kind": "line", "pts": [[110, 151], [140, 151]], "w": 4} + ]}, + {"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 6} + ], + "frames": [ + { + "hold": 0.4, "tween": 1.1, + "root": [118, 124], + "spine": [100, 98], "neck": 86, "gaze": 6, + "arm_r": [-78, 30], "arm_l": [-78, 30], + "leg_r": [-24, -74], "leg_l": [-24, -74], + "pins": {"foot_r": [156, 151], "foot_l": [160, 152]} + }, + { + "hold": 0.4, "tween": 1.1, + "root": [118, 124], + "spine": [100, 98], "neck": 86, "gaze": 6, + "arm_r": [-78, -88], "arm_l": [-78, -88], + "leg_r": [-24, -74], "leg_l": [-24, -74], + "pins": {"foot_r": [156, 151], "foot_l": [160, 152]} + } + ] +} diff --git a/Workouts/Views/Exercises/ExerciseLibraryView.swift b/Workouts/Views/Exercises/ExerciseLibraryView.swift new file mode 100644 index 0000000..e48d156 --- /dev/null +++ b/Workouts/Views/Exercises/ExerciseLibraryView.swift @@ -0,0 +1,183 @@ +// +// ExerciseLibraryView.swift +// Workouts +// +// Copyright 2026 Rouslan Zenetl. All Rights Reserved. +// + +import SwiftUI + +/// The Exercises library screen, pushed from Settings → Library: every exercise with +/// a bundled motion rig (the same catalog the picker offers), each opening its +/// reference page. +struct ExerciseLibraryView: View { + var body: some View { + List(ExerciseMotionLibrary.exerciseNames, id: \.self) { name in + NavigationLink(name) { + ExerciseLibraryDetailView(exerciseName: name) + } + } + .navigationTitle("Exercises") + } +} + +/// One library exercise, in the standard half-and-half exercise layout: the authored +/// reference content (summary, targets, setup/execution/cues…) scrolls in the top +/// half, the looping form-guide figure holds the bottom. Falls back to a full-screen +/// figure when no `info.md` is bundled. +struct ExerciseLibraryDetailView: View { + let exerciseName: String + + private var info: ExerciseInfo? { + ExerciseInfoLibrary.info(for: exerciseName) + } + + var body: some View { + VStack(spacing: 0) { + if let info { + ExerciseInfoContent(info: info) + } + ExerciseFigureSlot(exerciseName: exerciseName) + } + .navigationTitle(exerciseName) + .navigationBarTitleDisplayMode(.inline) + } +} + +/// Renders a parsed `ExerciseInfo`: summary, target chips, then each authored +/// section with its numbered steps or bullets. +private struct ExerciseInfoContent: View { + let info: ExerciseInfo + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + if !info.summary.isEmpty { + Text(info.summary) + .font(.callout) + .foregroundStyle(.secondary) + } + + if !info.targets.isEmpty { + TargetChips(targets: info.targets) + } + + ForEach(info.sections, id: \.title) { section in + VStack(alignment: .leading, spacing: 6) { + Text(section.title) + .font(.headline) + ForEach(Array(section.items.enumerated()), id: \.offset) { index, item in + itemRow(item, ordinal: ordinal(for: item, at: index, in: section)) + } + } + } + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + /// Steps number themselves by their position among the section's steps, so an + /// interleaved paragraph doesn't break the count. + private func ordinal(for item: ExerciseInfo.Item, at index: Int, in section: ExerciseInfo.Section) -> Int? { + guard case .step = item else { return nil } + return section.items[...index].reduce(0) { count, it in + if case .step = it { count + 1 } else { count } + } + } + + @ViewBuilder + private func itemRow(_ item: ExerciseInfo.Item, ordinal: Int?) -> some View { + switch item { + case .step(let text): + HStack(alignment: .firstTextBaseline, spacing: 6) { + Text("\(ordinal ?? 0).") + .foregroundStyle(.secondary) + .monospacedDigit() + Text(text) + } + .font(.callout) + case .bullet(let text): + HStack(alignment: .firstTextBaseline, spacing: 6) { + Text("•") + .foregroundStyle(.secondary) + Text(text) + } + .font(.callout) + case .paragraph(let text): + Text(text) + .font(.callout) + } + } +} + +/// The `Targets:` muscles as wrapping capsule chips. +private struct TargetChips: View { + let targets: [String] + + var body: some View { + FlowLayout(spacing: 6) { + ForEach(targets, id: \.self) { target in + Text(target) + .font(.caption) + .padding(.horizontal, 10) + .padding(.vertical, 4) + .background(Color.accentColor.opacity(0.12), in: Capsule()) + .foregroundStyle(Color.accentColor) + } + } + } +} + +/// Minimal leading-aligned wrapping layout for the target chips. +private struct FlowLayout: Layout { + var spacing: CGFloat = 6 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let rows = layoutRows(maxWidth: proposal.width ?? .infinity, subviews: subviews) + let height = rows.map(\.height).reduce(0, +) + spacing * CGFloat(max(0, rows.count - 1)) + return CGSize(width: proposal.width ?? rows.map(\.width).max() ?? 0, height: height) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + var y = bounds.minY + var index = 0 + for row in layoutRows(maxWidth: bounds.width, subviews: subviews) { + var x = bounds.minX + for size in row.sizes { + subviews[index].place( + at: CGPoint(x: x, y: y), + proposal: ProposedViewSize(size) + ) + x += size.width + spacing + index += 1 + } + y += row.height + spacing + } + } + + private struct Row { + var sizes: [CGSize] = [] + var width: CGFloat = 0 + var height: CGFloat = 0 + } + + private func layoutRows(maxWidth: CGFloat, subviews: Subviews) -> [Row] { + var rows: [Row] = [] + var current = Row() + for subview in subviews { + let size = subview.sizeThatFits(.unspecified) + let next = current.sizes.isEmpty ? size.width : current.width + spacing + size.width + if next > maxWidth, !current.sizes.isEmpty { + rows.append(current) + current = Row() + } + current.sizes.append(size) + current.width = current.sizes.isEmpty ? 0 : current.width + (current.sizes.count == 1 ? size.width : spacing + size.width) + current.height = max(current.height, size.height) + } + if !current.sizes.isEmpty { rows.append(current) } + return rows + } +} diff --git a/WorkoutsTests/ExerciseInfoTests.swift b/WorkoutsTests/ExerciseInfoTests.swift new file mode 100644 index 0000000..46c73b3 --- /dev/null +++ b/WorkoutsTests/ExerciseInfoTests.swift @@ -0,0 +1,45 @@ +import Foundation +import Testing +@testable import Workouts + +/// Locks the `info.md` parsing contract: the bundled reference pages must parse into +/// the summary / metadata / sections shape the library detail screen renders, and +/// every shipped motion rig must come with one (the authoring convention). +struct ExerciseInfoTests { + + @Test func bundledChestPressInfoParses() throws { + let info = try #require(ExerciseInfoLibrary.info(for: "Chest Press")) + #expect(info.summary.contains("machine bench press")) + #expect(info.category == "Main circuit") + #expect(info.type == "Machine-based horizontal press") + #expect(info.targets == ["Pectorals", "anterior deltoids", "triceps"]) + #expect(info.sections.map(\.title) == ["Setup", "Execution", "Cues", "Common Mistakes", "Progression"]) + + let execution = try #require(info.sections.first { $0.title == "Execution" }) + #expect(execution.items.count == 3) + guard case .step(let firstStep) = execution.items[0] else { + Issue.record("expected Execution to start with a numbered step") + return + } + // Hard-wrapped source lines must rejoin into one step. + #expect(firstStep.contains("stop short of locking the elbows")) + + let cues = try #require(info.sections.first { $0.title == "Cues" }) + guard case .bullet = cues.items[0] else { + Issue.record("expected Cues to be bullets") + return + } + } + + /// Every bundled motion rig ships a parseable info page — the exporter copies + /// `info.md` per entry, and the detail screen counts on the shape. + @Test func everyBundledExerciseHasParseableInfo() throws { + #expect(!ExerciseMotionLibrary.exerciseNames.isEmpty) + for name in ExerciseMotionLibrary.exerciseNames { + let info = try #require(ExerciseInfoLibrary.info(for: name), "\(name) has no bundled info.md") + #expect(!info.summary.isEmpty, "\(name): empty summary") + #expect(!info.targets.isEmpty, "\(name): no targets") + #expect(!info.sections.isEmpty, "\(name): no sections") + } + } +} diff --git a/project.yml b/project.yml index a9856e0..6a4f691 100644 --- a/project.yml +++ b/project.yml @@ -96,6 +96,11 @@ targets: excludes: - "Resources/Info-*.plist" - "Resources/*.entitlements" + # The form-guide figure renderer and its bundled motion rigs are shared + # with the iOS app (which includes them via the whole `Workouts` folder). + - path: Workouts/ExerciseFigure + - path: Workouts/Resources/ExerciseMotions + buildPhase: resources dependencies: - package: IndieSync - target: Workouts Watch Widget