melihcolpan/musclemap
A native SwiftUI SDK for rendering interactive human body muscle maps with highlighting, heatmaps, multi-select, zoom, gesture-rich interaction, and UIKit support.
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:
dependencies: [
.package(url: "https://github.com/melihcolpan/MuscleMap.git", from: "1.6.4")
]Or in Xcode: File > Add Package Dependencies and paste the repository URL.
CocoaPods
Add to your Podfile:
pod 'MuscleMap', '~> 1.6.4'Then run pod install.
Quick Start
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
<p align="center">
<img src="https://raw.githubusercontent.com/melihcolpan/MuscleMap/main/Screenshots/gradient_linear.png" width="180" alt="Linear Gradient">
<img src="https://raw.githubusercontent.com/melihcolpan/MuscleMap/main/Screenshots/gradient_radial.png" width="180" alt="Radial Gradient">
<img src="https://raw.githubusercontent.com/melihcolpan/MuscleMap/main/Screenshots/gradient_neon.png" width="180" alt="Neon Gradient">
</p>
```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
<p align="center">
<img src="https://raw.githubusercontent.com/melihcolpan/MuscleMap/main/Screenshots/heatmap_workout.png" width="200" alt="Workout Heatmap">
<img src="https://raw.githubusercontent.com/melihcolpan/MuscleMap/main/Screenshots/heatmap_thermal.png" width="200" alt="Thermal Heatmap">
</p>
```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
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()`
### 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
<p align="center">
<img src="https://raw.githubusercontent.com/melihcolpan/MuscleMap/main/Screenshots/heatmap_v2_gradient.png" width="220" alt="Gradient Heatmap">
<img src="https://raw.githubusercontent.com/melihcolpan/MuscleMap/main/Screenshots/heatmap_v2_stepped.png" width="220" alt="Stepped Heatmap">
</p>
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
<p align="center">
<img src="https://raw.githubusercontent.com/melihcolpan/MuscleMap/main/Screenshots/style_neon.png" width="200" alt="Neon Style">
<img src="https://raw.githubusercontent.com/melihcolpan/MuscleMap/main/Screenshots/style_medical.png" width="200" alt="Medical Style">
</p>
```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<Muscle> = []
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
Sub-groups provide finer control over muscle regions. 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
```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 | | Rotator Cuff | .rotatorCuff | | 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 |
UIKit Integration
MuscleMapView
Drop-in UIView wrapper for UIKit-based projects:
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:
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
- 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.
// 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:
// 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+
License
MIT License. See LICENSE for details.
Package Metadata
Repository: melihcolpan/musclemap
Default branch: main
README: README.md