# MuscleMap A native SwiftUI SDK for rendering interactive human body muscle maps with highlighting, heatmaps, multi-select, zoom, gesture-rich interaction, and UIKit support. Supports **male & female** body models with **front & back** views. Works with both **SwiftUI** and **UIKit**.

Male Front Male Back Female Front Female Back

## Features - SVG-based body rendering via SwiftUI `Canvas` - **36 muscle groups** (22 base + 14 sub-groups) with left/right side detection - **Muscle sub-groups** with parent/child inheritance and priority hit testing - **Always-visible sub-groups** (ankles, adductors, neck) — rendered by default, tap returns parent - Heatmap visualization with customizable color scales - Tap-to-select with hit testing - **Multi-select** (select multiple muscles at once) - **Long press gesture** (with configurable duration) - **Drag-to-select** (paint muscles by dragging) - **Pinch-to-zoom & pan** (with double-tap to reset) - **Tooltips** (custom content positioned above selected muscles) - **Undo/redo** (selection history tracking) - 4 preset styles (default, minimal, neon, medical) - **Gradient fills** (linear & radial gradients) - **Transition animations** (fade in/out on highlight changes) - **Pulse/glow animation** (for selected muscles) - **Shadow/drop shadow** support - **UIKit wrappers** (`MuscleMapView`, `HeatmapLegendUIView`) - **Accessibility** (VoiceOver support with localized muscle names) - **Localization** (11 languages: EN, TR, DE, ES, FR, JA, ZH, KO, AR, PT-BR, RU) - **DocC documentation** catalog - Zero external dependencies - iOS 17+ / macOS 14+ ## Installation ### Swift Package Manager Add to your `Package.swift`: ```swift dependencies: [ .package(url: "https://github.com/melihcolpan/MuscleMap.git", from: "1.9.0") ] ``` Or in Xcode: **File > Add Package Dependencies** and paste the repository URL. ### CocoaPods Add to your `Podfile`: ```ruby pod 'MuscleMap', '~> 1.9.0' ``` Then run `pod install`. ## Quick Start ```swift import SwiftUI import MuscleMap struct ContentView: View { var body: some View { BodyView(gender: .male, side: .front) .highlight(.chest, color: .red) .highlight(.biceps, color: .orange, opacity: 0.8) .frame(height: 400) } } ``` ## Usage ### Basic Highlighting ```swift BodyView(gender: .male, side: .front) .highlight(.chest, color: .red) .highlight(.abs, color: .yellow, opacity: 0.6) .highlight([.quadriceps, .calves], color: .orange) ``` ### Gradient Highlighting

Linear Gradient Radial Gradient Neon Gradient

```swift // Linear gradient (top to bottom) BodyView(gender: .male, side: .front) .highlight(.chest, linearGradient: [.red, .orange], startPoint: .top, endPoint: .bottom) // Radial gradient (center outward) .highlight(.biceps, radialGradient: [.white, .blue], center: .center, endRadius: 40) // Mix gradients and solid colors .highlight(.quadriceps, color: .purple) ``` ### Tap Detection ```swift BodyView(gender: .female, side: .front) .onMuscleSelected { muscle, side in print("\(muscle.displayName) (\(side))") } ``` ### Heatmap

Workout Heatmap Thermal Heatmap

```swift // Integer scale (0-4, like workout trackers) BodyView(gender: .male, side: .front) .intensities([ .chest: 3, .biceps: 2, .quadriceps: 4, .abs: 1 ]) // Custom intensity data (0.0 - 1.0) let data = [ MuscleIntensity(muscle: .chest, intensity: 0.8), MuscleIntensity(muscle: .biceps, intensity: 0.5, side: .left), MuscleIntensity(muscle: .abs, intensity: 0.3, color: .purple) ] BodyView(gender: .male, side: .front) .heatmap(data, colorScale: .thermal) ``` ### Color Scales | Scale | Colors | |-------|--------| | `.workout` | gray -> yellow -> orange -> red | | `.thermal` | blue -> green -> yellow -> red | | `.medical` | green -> yellow -> red | | `.monochrome` | light gray -> dark | | `.workoutStepped` | workout with 5 discrete steps | | `.thermalSmooth` | thermal with ease-in-out curve | Custom: ```swift let custom = HeatmapColorScale(colors: [.blue, .purple, .pink]) ``` ### Color Interpolation

Stepped Heatmap

Control how intensity values map to colors across the scale: ```swift // Ease-in-out for smoother transitions BodyView(gender: .male, side: .front) .heatmap(data, colorScale: .thermal) .heatmapInterpolation(.easeInOut) // Stepped (discrete levels) BodyView(gender: .male, side: .front) .heatmap(data, colorScale: .workoutStepped) // built-in 5-step preset // Custom curve .heatmapInterpolation(.custom { t in t * t * t }) ``` Available interpolations: `.linear`, `.easeIn`, `.easeOut`, `.easeInOut`, `.step(count:)`, `.custom()` A color scale's own interpolation is used by default, so `.workoutStepped` renders in steps and `.thermalSmooth` eases. An interpolation set with `.heatmapInterpolation(_:)` or a `HeatmapConfiguration` takes precedence when it is not `.linear`. ### Heatmap Threshold Hide muscles below a minimum intensity: ```swift BodyView(gender: .male, side: .front) .heatmap(data) .heatmapThreshold(0.2) // muscles with intensity < 0.2 are hidden ``` ### Gradient Heatmap Fill

Gradient Heatmap Stepped Heatmap

Apply intra-muscle gradients based on intensity (low-to-high color within each muscle): ```swift BodyView(gender: .male, side: .front) .heatmap(data, colorScale: .thermal) .heatmapGradient(direction: .topToBottom, lowFactor: 0.3) ``` Directions: `.topToBottom`, `.bottomToTop`, `.leftToRight`, `.rightToLeft` ### Heatmap Configuration Combine all heatmap settings in a single configuration: ```swift let config = HeatmapConfiguration( colorScale: .thermal, interpolation: .easeInOut, threshold: 0.2, isGradientFillEnabled: true, gradientDirection: .topToBottom, gradientLowIntensityFactor: 0.3 ) BodyView(gender: .male, side: .front) .heatmap(data, configuration: config) ``` ### Heatmap Legend Display a color bar legend alongside the body view: ```swift // Horizontal legend HeatmapLegendView(colorScale: .workout) .frame(width: 200) // Vertical legend with custom labels HeatmapLegendView( colorScale: .thermal, interpolation: .easeInOut, orientation: .vertical, barThickness: 20, labelMin: "Rest", labelMax: "Max" ) .frame(width: 60, height: 200) ``` ### Animated Heatmap Transitions When using `.animated()`, color transitions between heatmap states are now smoothly interpolated: ```swift BodyView(gender: .male, side: .front) .heatmap(currentData, colorScale: .thermal) .animated(duration: 0.5) ``` ### Styles

Neon Style Medical Style

```swift BodyView(gender: .male, side: .front) .bodyStyle(.neon) ``` | Style | Description | |-------|-------------| | `.default` | Gray fill, green selection | | `.minimal` | Subtle fill, thin strokes | | `.neon` | Dark background, cyan selection, glow shadow | | `.medical` | Clinical blue-gray tones | Custom: ```swift let style = BodyViewStyle( defaultFillColor: .gray, strokeColor: .white, strokeWidth: 1, selectionColor: .yellow, selectionStrokeColor: .yellow, selectionStrokeWidth: 3, headColor: .gray, hairColor: .black, shadowColor: .blue.opacity(0.5), shadowRadius: 6, shadowOffset: CGSize(width: 0, height: 2) ) ``` ### Animations #### Transition Animation Smooth fade-in/fade-out when highlights change: ```swift BodyView(gender: .male, side: .front) .highlight(.chest, color: .red) .animated(duration: 0.3) ``` #### Pulse Animation Pulsing glow effect on the selected muscle: ```swift @State private var selected: Muscle? BodyView(gender: .male, side: .front) .highlight(.chest, color: .red) .selected(selected) .pulseSelected(speed: 1.5, range: 0.6...1.0) .onMuscleSelected { muscle, _ in selected = muscle } ``` ### Selection State ```swift // Single selection (backward compatible) @State private var selected: Muscle? BodyView(gender: .male, side: .front) .selected(selected) .onMuscleSelected { muscle, _ in selected = muscle } ``` ### Multi-Select ```swift @State private var selectedMuscles: Set = [] BodyView(gender: .male, side: .front) .selected(selectedMuscles) .onMuscleSelected { muscle, _ in if selectedMuscles.contains(muscle) { selectedMuscles.remove(muscle) } else { selectedMuscles.insert(muscle) } } ``` ### Long Press ```swift BodyView(gender: .male, side: .front) .onMuscleLongPressed(duration: 0.5) { muscle, side in print("Long pressed: \(muscle.displayName)") } ``` ### Drag-to-Select ```swift BodyView(gender: .male, side: .front) .onMuscleDragged({ muscle, side in selectedMuscles.insert(muscle) }, onEnded: { print("Drag ended") }) ``` ### Pinch-to-Zoom ```swift BodyView(gender: .male, side: .front) .zoomable(minScale: 1.0, maxScale: 4.0) ``` ### Tooltips ```swift BodyView(gender: .male, side: .front) .selected(selectedMuscles) .tooltip { muscle, side in Text(muscle.displayName) .font(.caption) .padding(4) .background(.ultraThinMaterial) } ``` ### Undo/Redo ```swift @State private var history = SelectionHistory() BodyView(gender: .male, side: .front) .undoable(history) Button("Undo") { if let state = history.undo() { selectedMuscles = state } } .disabled(!history.canUndo) Button("Redo") { if let state = history.redo() { selectedMuscles = state } } .disabled(!history.canRedo) ``` ### Muscle Sub-Groups

Front Sub-Groups Back Sub-Groups

Sub-groups provide finer control over muscle regions. Each one is cut from its parent muscle's artwork: the upper and lower chest, the front deltoid, the upper and lower abs, the serratus slips on the ribs, the top of the quadriceps (hip flexors) and the inner and outer quadriceps heads. They inherit the parent muscle's highlight when no specific highlight is set, and take priority in hit testing. **Always-visible sub-groups** (ankles, adductors, neck) are rendered by default but return their parent muscle on tap — so tapping the ankle area returns `.feet`, tapping the neck returns `.head`, etc. ```swift // Highlight parent and sub-group with different intensities BodyView(gender: .male, side: .front) .highlight(.chest, color: .red, opacity: 0.4) // parent (dimmer) .highlight(.upperChest, color: .red, opacity: 0.9) // sub-group (brighter) .highlight(.quadriceps, color: .blue, opacity: 0.4) .highlight(.innerQuad, color: .blue, opacity: 0.9) ``` Query sub-group relationships: ```swift Muscle.chest.subGroups // [.upperChest, .lowerChest] Muscle.upperChest.parentGroup // .chest Muscle.upperChest.isSubGroup // true // Always-visible sub-groups Muscle.ankles.isAlwaysVisibleSubGroup // true Muscle.ankles.parentGroup // .feet ``` ### Gender & Side ```swift BodyView(gender: .male, side: .front) // Male front BodyView(gender: .male, side: .back) // Male back BodyView(gender: .female, side: .front) // Female front BodyView(gender: .female, side: .back) // Female back ``` ### Checking What Is Drawn

Back Regions

Every muscle is drawn in at least one view, but many only on the front or the back. Use `isDrawable` to check a muscle mapping for a specific view, for example in a unit test: ```swift Muscle.rhomboids.isDrawable // true (drawn somewhere) Muscle.rhomboids.isDrawable(gender: .male, side: .back) // true Muscle.rhomboids.isDrawable(gender: .male, side: .front) // false Muscle.head.isDrawable(gender: .female, side: .back) // false, the hair covers it ``` The back views have these extra regions (shown above with `.showSubGroups()`): - `.upperTrapezius` and `.lowerTrapezius`: the trapezius above and below the rhomboids. On the front views the visible trapezius is the upper part. - `.rhomboids`: inside the trapezius, painted only while highlighted or selected, so an unhighlighted body and a `.trapezius` highlight look exactly as before. - `.rearDeltoid`: the deltoid as seen from behind. - `.rotatorCuff`: the infraspinatus area above the shoulder blade. It shows the `.upperBack` highlight when it has none of its own. With sub-groups hidden (the default), tapping the rhomboids or the rotator cuff returns `.trapezius` / `.upperBack`. ### Faster First Render Creating a `BodyView` starts parsing the body artwork in the background. To have it ready before the first body appears, start it at launch: ```swift @main struct MyApp: App { init() { BodyView.preloadArtwork() } // ... } ``` ## Available Muscles ### Base Muscles (22) | Muscle | Key | |--------|-----| | Abs | `.abs` | | Biceps | `.biceps` | | Calves | `.calves` | | Chest | `.chest` | | Deltoids | `.deltoids` | | Feet | `.feet` | | Forearm | `.forearm` | | Gluteal | `.gluteal` | | Hamstring | `.hamstring` | | Hands | `.hands` | | Head | `.head` | | Knees | `.knees` | | Lower Back | `.lowerBack` | | Obliques | `.obliques` | | Quadriceps | `.quadriceps` | | Rhomboids | `.rhomboids` (back views) | | Rotator Cuff | `.rotatorCuff` (back views) | | Serratus | `.serratus` | | Tibialis | `.tibialis` | | Trapezius | `.trapezius` | | Triceps | `.triceps` | | Upper Back | `.upperBack` | ### Sub-Groups (14) | Sub-Group | Key | Parent | Always Visible | |-----------|-----|--------|:--------------:| | Upper Chest | `.upperChest` | `.chest` | | | Lower Chest | `.lowerChest` | `.chest` | | | Upper Abs | `.upperAbs` | `.abs` | | | Lower Abs | `.lowerAbs` | `.abs` | | | Inner Quad | `.innerQuad` | `.quadriceps` | | | Outer Quad | `.outerQuad` | `.quadriceps` | | | Hip Flexors | `.hipFlexors` | `.quadriceps` | | | Front Deltoid | `.frontDeltoid` | `.deltoids` | | | Rear Deltoid | `.rearDeltoid` | `.deltoids` | | | Upper Trapezius | `.upperTrapezius` | `.trapezius` | | | Lower Trapezius | `.lowerTrapezius` | `.trapezius` | | | Ankles | `.ankles` | `.feet` | Yes | | Adductors | `.adductors` | `.hamstring` | Yes | | Neck | `.neck` | `.head` | Yes | `.rearDeltoid`, `.upperTrapezius` and `.lowerTrapezius` are drawn on the back views, and `.upperTrapezius` on the front views too. ## UIKit Integration ### MuscleMapView Drop-in `UIView` wrapper for UIKit-based projects: ```swift import MuscleMap class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let muscleMap = MuscleMapView(gender: .male, side: .front) muscleMap.highlight(.chest, color: .systemRed) muscleMap.highlight(.biceps, color: .systemOrange, opacity: 0.8) muscleMap.onMuscleSelected = { muscle, side in print("\(muscle.displayName) tapped") } view.addSubview(muscleMap) muscleMap.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ muscleMap.centerXAnchor.constraint(equalTo: view.centerXAnchor), muscleMap.centerYAnchor.constraint(equalTo: view.centerYAnchor), muscleMap.widthAnchor.constraint(equalToConstant: 300), muscleMap.heightAnchor.constraint(equalToConstant: 500) ]) } } ``` ### HeatmapLegendUIView UIKit wrapper for the heatmap legend: ```swift let legend = HeatmapLegendUIView(colorScale: .thermal) legend.orientation = .vertical legend.labelMin = "Rest" legend.labelMax = "Max" view.addSubview(legend) ``` ## Accessibility MuscleMap includes full VoiceOver support. Each muscle region is exposed as an accessibility element with: - Localized muscle name as the accessibility label; muscles drawn on both sides get one element per side ("Biceps, Left", "Biceps, Right") - Selection state ("Selected" / "Not selected") - Tap and long press hints - Top-to-bottom traversal order (anatomical navigation) Cosmetic parts (e.g., head) are excluded from the accessibility tree. ```swift // Accessibility works automatically — no extra configuration needed BodyView(gender: .male, side: .front) .highlight(.chest, color: .red) .onMuscleSelected { muscle, side in // VoiceOver users can double-tap to select } ``` ## Localization All muscle names, side labels, and accessibility strings are localized in **11 languages**: | Language | Code | |----------|------| | English | `en` | | Turkish | `tr` | | German | `de` | | Spanish | `es` | | French | `fr` | | Japanese | `ja` | | Chinese (Simplified) | `zh-Hans` | | Korean | `ko` | | Arabic | `ar` | | Portuguese (Brazil) | `pt-BR` | | Russian | `ru` | Localized names are available via `displayName`: ```swift // Returns localized name based on user's device language Muscle.chest.displayName // "Chest" (EN), "Göğüs" (TR), "Brust" (DE) MuscleSide.left.displayName // "Left" (EN), "Sol" (TR), "Links" (DE) BodySide.front.displayName // "Front" (EN), "Ön" (TR), "Vorderseite" (DE) BodyGender.male.displayName // "Male" (EN), "Erkek" (TR), "Männlich" (DE) ``` ## Example App A demo app is included in the `Example/` directory. Open `Example/MuscleMapDemoApp.xcodeproj` in Xcode to explore all features interactively. ## Requirements - iOS 17.0+ - macOS 14.0+ - Swift 5.9+ (builds in the Swift 6 language mode on a Swift 6 toolchain) ## Development ```bash swift test # unit tests swift run --package-path Tools/ScreenshotGenerator ScreenshotGenerator Screenshots # regenerate the README screenshots ``` Every push and pull request runs the tests, an iOS build, the demo app build, the documentation build and `pod lib lint` on GitHub Actions. Dependabot keeps the workflow's actions up to date. The API documentation is on the [Swift Package Index](https://swiftpackageindex.com/melihcolpan/MuscleMap/documentation/musclemap). ## License MIT License. See [LICENSE](LICENSE) for details.