Add Warm-Up and Stretching activity types
Adds WorkoutActivityType.warmUp (HealthKit .preparationAndRecovery) and .stretching (.flexibility), and retags the six starter splits that were all mislabeled as Functional Strength: - Warm-Up: Upper Body Warm-Up, Lower Body Warm-Up, Morning Wake-Up - Stretching: Morning Mobility, Full Body Stretch, Evening Stretch The split editor's activity picker surfaces them automatically (CaseIterable). Older app versions decode the new raw values as the default type — additive and not schema-gated, so no quarantine.
This commit is contained in:
@@ -12,10 +12,12 @@ import SwiftUI
|
||||
/// `Exercise Library/SYSTEM.md` for the visual language).
|
||||
///
|
||||
/// The solver hands back a depth-sorted draw order per frame (far parts first, the head
|
||||
/// always last so overhead arms pass behind the face) and a near/far tag per limb: the
|
||||
/// member nearer the camera draws in the dark ink at the heavier width, the far member
|
||||
/// in the light ink one step thinner and nudged by the readability offset. Working parts
|
||||
/// swap the same near/far inks for the accent teals; the spine is always dark.
|
||||
/// always last so overhead arms pass behind the face) and a per-vertex nearness for each
|
||||
/// limb: every joint is toned by its own camera depth, so each bone is stroked with a
|
||||
/// gradient between its joints' tones — the ink flows along a limb reaching in depth
|
||||
/// instead of snapping, and the far member sits lighter, thinner, and nudged by the
|
||||
/// readability offset. Working parts swap the near/far inks for the accent teals; the
|
||||
/// spine is always dark.
|
||||
|
||||
/// Everything the renderer needs for one exercise, resolved once from the bundle.
|
||||
struct FigureAnimation {
|
||||
@@ -28,9 +30,9 @@ struct FigureAnimation {
|
||||
/// full-size anatomy taller than the canvas and zoom out to fit (see SYSTEM.md).
|
||||
let zoom: Double
|
||||
|
||||
init?(exerciseName: String) {
|
||||
init?(exerciseName: String, profile: FigureProfile = .neutral) {
|
||||
guard
|
||||
let resources = ExerciseMotionLibrary.resources(for: exerciseName),
|
||||
let resources = ExerciseMotionLibrary.resources(for: exerciseName, profile: profile),
|
||||
let timeline = MotionTimeline(motion: resources.motion, profile: resources.profile)
|
||||
else { return nil }
|
||||
self.timeline = timeline
|
||||
@@ -56,6 +58,7 @@ struct FigureAnimation {
|
||||
struct ExerciseFigureSlot: View {
|
||||
let exerciseName: String
|
||||
|
||||
@AppStorage("figureProfile") private var figureProfile: FigureProfile = .neutral
|
||||
@State private var figure: FigureAnimation?
|
||||
|
||||
var body: some View {
|
||||
@@ -67,8 +70,9 @@ struct ExerciseFigureSlot: View {
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.task(id: exerciseName) {
|
||||
figure = FigureAnimation(exerciseName: exerciseName)
|
||||
// Rebuild when either the exercise or the chosen body type changes.
|
||||
.task(id: [exerciseName, figureProfile.rawValue]) {
|
||||
figure = FigureAnimation(exerciseName: exerciseName, profile: figureProfile)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,8 +140,8 @@ struct ExerciseFigureView: View {
|
||||
drawSpine(&ctx, geo)
|
||||
default:
|
||||
guard let limb = FigureLimb(rawValue: part), let points = geo.limbs[limb] else { continue }
|
||||
let shade = geo.shade[limb] ?? .near
|
||||
stroke(&ctx, points, color: ink(part, shade: shade), width: shade == .near ? 6 : 5)
|
||||
let tones = geo.nearness[limb] ?? Array(repeating: 1, count: points.count)
|
||||
strokeLimb(&ctx, points, tones: tones, part: part)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,7 +158,7 @@ struct ExerciseFigureView: View {
|
||||
}
|
||||
|
||||
private func drawSpine(_ ctx: inout GraphicsContext, _ geo: FigureGeometry) {
|
||||
let color = ink("spine", shade: .near)
|
||||
let color = ink("spine", nearness: 1)
|
||||
stroke(&ctx, geo.girdle, color: color, width: 5)
|
||||
stroke(&ctx, geo.pelvisBar, color: color, width: 5)
|
||||
var path = Path()
|
||||
@@ -201,13 +205,35 @@ struct ExerciseFigureView: View {
|
||||
ink == .prop ? .figureProp : .figureEquipment
|
||||
}
|
||||
|
||||
/// Near parts take the dark ink, far parts the light one; working parts swap for the
|
||||
/// accent teals. The spine (passed `.near`) is always dark unless it's the working part.
|
||||
private func ink(_ part: String, shade: Shade) -> Color {
|
||||
if figure.working.contains(part) {
|
||||
return shade == .near ? .figureNearWorking : .figureFarWorking
|
||||
/// A limb's ink at `nearness` (1 = near/dark side, 0 = far/light side): the near
|
||||
/// and far grays, or the accent teals for a working part. The spine (passed
|
||||
/// nearness 1) is the near ink unless it's working.
|
||||
private func ink(_ part: String, nearness t: Double) -> Color {
|
||||
.figureShaded(working: figure.working.contains(part), nearness: t)
|
||||
}
|
||||
|
||||
/// Strokes a limb chain with a per-vertex depth gradient: each bone segment is a
|
||||
/// round-capped line filled with a linear gradient between its two joints' depth
|
||||
/// tones, so the ink flows smoothly along a limb that reaches toward or away from
|
||||
/// the camera instead of the bone snapping to one flat tone. Adjacent segments share
|
||||
/// a joint tone, so their round caps meet seamlessly. Mirrors `render.py`.
|
||||
private func strokeLimb(_ ctx: inout GraphicsContext, _ points: [CGPoint],
|
||||
tones: [Double], part: String) {
|
||||
guard points.count >= 2, tones.count == points.count else { return }
|
||||
for i in 0..<(points.count - 1) {
|
||||
let a = points[i], b = points[i + 1]
|
||||
let t0 = tones[i], t1 = tones[i + 1]
|
||||
let style = StrokeStyle(lineWidth: 5 + (t0 + t1) / 2, lineCap: .round)
|
||||
var path = Path()
|
||||
path.move(to: a)
|
||||
path.addLine(to: b)
|
||||
if t0 == t1 || (abs(a.x - b.x) < 0.01 && abs(a.y - b.y) < 0.01) {
|
||||
ctx.stroke(path, with: .color(ink(part, nearness: t0)), style: style)
|
||||
} else {
|
||||
let gradient = Gradient(colors: [ink(part, nearness: t0), ink(part, nearness: t1)])
|
||||
ctx.stroke(path, with: .linearGradient(gradient, startPoint: a, endPoint: b), style: style)
|
||||
}
|
||||
}
|
||||
return shade == .near ? .figureNear : .figureFar
|
||||
}
|
||||
|
||||
private func stroke(_ ctx: inout GraphicsContext, _ points: [CGPoint], color: Color, width: CGFloat) {
|
||||
@@ -238,18 +264,28 @@ private extension Color {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Near-side limbs, spine, head, nose — the prominent stroke (`#3a3f4b`).
|
||||
static let figureNear = figure(light: (0.23, 0.25, 0.29), dark: (0.76, 0.79, 0.83))
|
||||
/// The gradient endpoints as `(lightMode, darkMode)` RGB pairs — the near (dark,
|
||||
/// prominent) and far (light, recessive) inks, plain and working. `figureShaded`
|
||||
/// blends between the far and near endpoint of a pair by a limb's nearness.
|
||||
private static let nearInk = (light: (0.23, 0.25, 0.29), dark: (0.76, 0.79, 0.83)) // #3a3f4b
|
||||
private static let farInk = (light: (0.66, 0.69, 0.73), dark: (0.36, 0.39, 0.43)) // #a9afba
|
||||
private static let nearWorkingInk = (light: (0.05, 0.58, 0.53), dark: (0.18, 0.83, 0.75)) // #0d9488
|
||||
private static let farWorkingInk = (light: (0.53, 0.81, 0.77), dark: (0.31, 0.56, 0.52)) // #86cfc5
|
||||
|
||||
/// Far-side limbs, drawn behind — the recessive stroke (`#a9afba`).
|
||||
static let figureFar = figure(light: (0.66, 0.69, 0.73), dark: (0.36, 0.39, 0.43))
|
||||
static func lerp3(_ a: (Double, Double, Double), _ b: (Double, Double, Double), _ t: Double) -> (Double, Double, Double) {
|
||||
(a.0 + (b.0 - a.0) * t, a.1 + (b.1 - a.1) * t, a.2 + (b.2 - a.2) * t)
|
||||
}
|
||||
|
||||
/// Working near-side parts — teal accent (`#0d9488`).
|
||||
static let figureNearWorking = figure(light: (0.05, 0.58, 0.53), dark: (0.18, 0.83, 0.75))
|
||||
/// A limb's ink blended by nearness (1 = near/dark side, 0 = far/light side),
|
||||
/// interpolating both the light- and dark-mode endpoints so it stays adaptive.
|
||||
static func figureShaded(working: Bool, nearness t: Double) -> Color {
|
||||
let far = working ? farWorkingInk : farInk
|
||||
let near = working ? nearWorkingInk : nearInk
|
||||
return figure(light: lerp3(far.light, near.light, t), dark: lerp3(far.dark, near.dark, t))
|
||||
}
|
||||
|
||||
/// Working far-side parts — light teal (`#86cfc5`), kept more muted than the near
|
||||
/// so the near/far hierarchy holds in both modes.
|
||||
static let figureFarWorking = figure(light: (0.53, 0.81, 0.77), dark: (0.31, 0.56, 0.52))
|
||||
/// Near-side ink for the head, nose, and non-gradient strokes (`#3a3f4b`).
|
||||
static let figureNear = figure(light: nearInk.light, dark: nearInk.dark)
|
||||
|
||||
/// Scene equipment — seats, benches, frames, cables (`#c5cad4`); sits behind
|
||||
/// the figure, one step lighter than the ground line.
|
||||
|
||||
Reference in New Issue
Block a user