krz/daybyday
clone: git clone https://gitbay.org/krz/daybyday.git
main: BodyPartCard.swift · raw
1//
2// BodyPartCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large, colorful card representing one body part.
9/// Tapping the card plays a bounce animation and speaks the body part name aloud.
10struct BodyPartCard: View {
11 let bodyPart: LearnBodyPart
12 @State private var isTapped = false
13
14 var body: some View {
15 ZStack {
16 RoundedRectangle(cornerRadius: 32, style: .continuous)
17 .fill(bodyPart.color.gradient)
18 .shadow(color: bodyPart.color.opacity(0.4), radius: 8, y: 4)
19
20 VStack(spacing: 16) {
21 Image(systemName: bodyPart.symbol)
22 .font(.system(size: 64))
23 .foregroundStyle(.white)
24 .symbolRenderingMode(.hierarchical)
25
26 Text(bodyPart.name)
27 .font(.system(size: 32, weight: .bold, design: .rounded))
28 .foregroundStyle(.white)
29 }
30 }
31 .scaleEffect(isTapped ? 1.12 : 1.0)
32 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
33 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
34 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
35 .onTapGesture {
36 isTapped = true
37 SpeechSynthesizer.shared.speak(bodyPart.name)
38 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
39 isTapped = false
40 }
41 }
42 .accessibilityLabel(bodyPart.name)
43 .accessibilityAddTraits(.isButton)
44 }
45}
46
47// MARK: - Body part model
48
49enum LearnBodyPart: Int, CaseIterable, Identifiable {
50 case head, eyes, ears, nose, mouth, hands, fingers, arms, legs, feet, heart, belly
51
52 var id: Int { rawValue }
53
54 var name: String {
55 switch self {
56 case .head: "Head"
57 case .eyes: "Eyes"
58 case .ears: "Ears"
59 case .nose: "Nose"
60 case .mouth: "Mouth"
61 case .hands: "Hands"
62 case .fingers: "Fingers"
63 case .arms: "Arms"
64 case .legs: "Legs"
65 case .feet: "Feet"
66 case .heart: "Heart"
67 case .belly: "Belly"
68 }
69 }
70
71 var symbol: String {
72 switch self {
73 case .head: "brain.head.profile"
74 case .eyes: "eye.fill"
75 case .ears: "ear.fill"
76 case .nose: "nose.fill"
77 case .mouth: "mouth.fill"
78 case .hands: "hand.raised.fill"
79 case .fingers: "hand.point.up.fill"
80 case .arms: "figure.arms.open"
81 case .legs: "figure.walk"
82 case .feet: "shoeprints.fill"
83 case .heart: "heart.fill"
84 case .belly: "circle.fill"
85 }
86 }
87
88 var color: Color {
89 switch self {
90 case .head: Color(red: 0.9, green: 0.5, blue: 0.3) // warm orange
91 case .eyes: Color(red: 0.3, green: 0.65, blue: 0.9) // blue
92 case .ears: Color(red: 0.6, green: 0.45, blue: 0.85) // purple
93 case .nose: Color(red: 0.9, green: 0.4, blue: 0.5) // rose
94 case .mouth: Color(red: 0.9, green: 0.3, blue: 0.35) // red
95 case .hands: Color(red: 0.95, green: 0.7, blue: 0.3) // golden
96 case .fingers: Color(red: 0.35, green: 0.75, blue: 0.5) // green
97 case .arms: Color(red: 0.25, green: 0.7, blue: 0.7) // teal
98 case .legs: Color(red: 0.5, green: 0.4, blue: 0.8) // indigo
99 case .feet: Color(red: 0.7, green: 0.5, blue: 0.3) // brown
100 case .heart: Color(red: 0.85, green: 0.3, blue: 0.45) // deep pink
101 case .belly: Color(red: 1.0, green: 0.6, blue: 0.4) // peach
102 }
103 }
104}
105
106#Preview {
107 HStack(spacing: 24) {
108 BodyPartCard(bodyPart: .head)
109 .frame(width: 200, height: 260)
110 BodyPartCard(bodyPart: .hands)
111 .frame(width: 200, height: 260)
112 }
113 .padding()
114}