krz/daybyday
clone: git clone https://gitbay.org/krz/daybyday.git
main: ShapeCard.swift · raw
1//
2// ShapeCard.swift
3// DayByDay
4//
5
6import SwiftUI
7
8/// A single large, colorful card representing one shape.
9/// Tapping the card plays a bounce animation and speaks the shape name aloud.
10struct ShapeCard: View {
11 let shape: LearnShape
12 @State private var isTapped = false
13
14 var body: some View {
15 ZStack {
16 RoundedRectangle(cornerRadius: 32, style: .continuous)
17 .fill(shape.color.gradient)
18 .shadow(color: shape.color.opacity(0.4), radius: 8, y: 4)
19
20 VStack(spacing: 16) {
21 shape.shapeView
22 .frame(width: 80, height: 80)
23 .foregroundStyle(.white)
24
25 Text(shape.name)
26 .font(.system(size: 32, weight: .bold, design: .rounded))
27 .foregroundStyle(.white)
28 }
29 }
30 .scaleEffect(isTapped ? 1.12 : 1.0)
31 .contentShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
32 .rotation3DEffect(.degrees(isTapped ? 6 : 0), axis: (x: 1, y: 0, z: 0))
33 .animation(.spring(response: 0.35, dampingFraction: 0.5), value: isTapped)
34 .onTapGesture {
35 isTapped = true
36 SpeechSynthesizer.shared.speak(shape.name)
37 DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
38 isTapped = false
39 }
40 }
41 .accessibilityLabel(shape.name)
42 .accessibilityAddTraits(.isButton)
43 }
44}
45
46// MARK: - Shape model
47
48enum LearnShape: Int, CaseIterable, Identifiable {
49 case circle, square, triangle, star, heart, oval, diamond, rectangle
50
51 var id: Int { rawValue }
52
53 var name: String {
54 switch self {
55 case .circle: "Circle"
56 case .square: "Square"
57 case .triangle: "Triangle"
58 case .star: "Star"
59 case .heart: "Heart"
60 case .oval: "Oval"
61 case .diamond: "Diamond"
62 case .rectangle: "Rectangle"
63 }
64 }
65
66 @ViewBuilder
67 var shapeView: some View {
68 switch self {
69 case .circle:
70 Circle().fill(.white)
71 case .square:
72 SwiftUI.Rectangle().fill(.white)
73 case .triangle:
74 TriangleShape().fill(.white)
75 case .star:
76 Image(systemName: "star.fill")
77 .font(.system(size: 72))
78 .foregroundStyle(.white)
79 case .heart:
80 Image(systemName: "heart.fill")
81 .font(.system(size: 72))
82 .foregroundStyle(.white)
83 case .oval:
84 Ellipse().fill(.white)
85 .frame(width: 90, height: 60)
86 case .diamond:
87 SwiftUI.Rectangle().fill(.white)
88 .frame(width: 60, height: 60)
89 .rotationEffect(.degrees(45))
90 case .rectangle:
91 SwiftUI.Rectangle().fill(.white)
92 .frame(width: 90, height: 55)
93 }
94 }
95
96 var color: Color {
97 switch self {
98 case .circle: Color(red: 0.9, green: 0.3, blue: 0.3) // red
99 case .square: Color(red: 0.3, green: 0.5, blue: 0.9) // blue
100 case .triangle: Color(red: 0.35, green: 0.75, blue: 0.45) // green
101 case .star: Color(red: 1.0, green: 0.75, blue: 0.15) // gold
102 case .heart: Color(red: 0.9, green: 0.35, blue: 0.5) // pink
103 case .oval: Color(red: 0.55, green: 0.4, blue: 0.85) // purple
104 case .diamond: Color(red: 0.2, green: 0.75, blue: 0.8) // teal
105 case .rectangle: Color(red: 1.0, green: 0.55, blue: 0.2) // orange
106 }
107 }
108}
109
110/// A simple triangle drawn with a Path.
111struct TriangleShape: Shape {
112 func path(in rect: CGRect) -> Path {
113 Path { path in
114 path.move(to: CGPoint(x: rect.midX, y: rect.minY))
115 path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
116 path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
117 path.closeSubpath()
118 }
119 }
120}
121
122#Preview {
123 HStack(spacing: 24) {
124 ShapeCard(shape: .triangle)
125 .frame(width: 200, height: 260)
126 ShapeCard(shape: .star)
127 .frame(width: 200, height: 260)
128 }
129 .padding()
130}