feat(home): custom SVG zone icons for body-zone cards (#15)
All checks were successful
Admin Web Docker / Admin Web Tests (push) Successful in 33s
CI / Detect Changes (push) Successful in 4s
Admin Web Docker / Docker Build Validation (push) Has been skipped
Admin Web Docker / Semantic Release (push) Successful in 10s
CI / YouTube Worker (push) Has been skipped
Admin Web Docker / Build & Push Docker Image (push) Successful in 53s
CI / Deploy (push) Has been skipped
All checks were successful
Admin Web Docker / Admin Web Tests (push) Successful in 33s
CI / Detect Changes (push) Successful in 4s
Admin Web Docker / Docker Build Validation (push) Has been skipped
Admin Web Docker / Semantic Release (push) Successful in 10s
CI / YouTube Worker (push) Has been skipped
Admin Web Docker / Build & Push Docker Image (push) Successful in 53s
CI / Deploy (push) Has been skipped
This commit was merged in pull request #15.
This commit is contained in:
@@ -1,131 +1,35 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BodySilhouetteShape: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let w = rect.width
|
||||
let h = rect.height
|
||||
let mx = w * 0.5
|
||||
var p = Path()
|
||||
|
||||
let headCY = h * 0.09
|
||||
let headR = w * 0.12
|
||||
p.addEllipse(in: CGRect(
|
||||
x: mx - headR, y: headCY - headR,
|
||||
width: headR * 2, height: headR * 2
|
||||
))
|
||||
|
||||
let neckW = w * 0.06
|
||||
let neckTop = headCY + headR
|
||||
let shoulderY = h * 0.20
|
||||
p.addRect(CGRect(
|
||||
x: mx - neckW, y: neckTop,
|
||||
width: neckW * 2, height: shoulderY - neckTop
|
||||
))
|
||||
|
||||
let shoulderHW = w * 0.42
|
||||
let armOuterBotY = h * 0.44
|
||||
let armInnerBotY = h * 0.44
|
||||
let armOuterX = w * 0.06
|
||||
let armInnerX = w * 0.12
|
||||
|
||||
p.move(to: CGPoint(x: mx - neckW, y: shoulderY))
|
||||
p.addLine(to: CGPoint(x: mx - shoulderHW, y: shoulderY))
|
||||
p.addLine(to: CGPoint(x: mx - armOuterX, y: armOuterBotY))
|
||||
p.addLine(to: CGPoint(x: mx - armInnerX, y: armInnerBotY))
|
||||
p.closeSubpath()
|
||||
|
||||
p.move(to: CGPoint(x: mx + neckW, y: shoulderY))
|
||||
p.addLine(to: CGPoint(x: mx + shoulderHW, y: shoulderY))
|
||||
p.addLine(to: CGPoint(x: mx + armOuterX, y: armOuterBotY))
|
||||
p.addLine(to: CGPoint(x: mx + armInnerX, y: armInnerBotY))
|
||||
p.closeSubpath()
|
||||
|
||||
let torsoHW = w * 0.18
|
||||
let waistY = h * 0.46
|
||||
let waistHW = w * 0.15
|
||||
p.move(to: CGPoint(x: mx - torsoHW, y: shoulderY))
|
||||
p.addLine(to: CGPoint(x: mx + torsoHW, y: shoulderY))
|
||||
p.addLine(to: CGPoint(x: mx + waistHW, y: waistY))
|
||||
p.addLine(to: CGPoint(x: mx - waistHW, y: waistY))
|
||||
p.closeSubpath()
|
||||
|
||||
let hipHW = w * 0.22
|
||||
let hipY = h * 0.52
|
||||
p.move(to: CGPoint(x: mx - waistHW, y: waistY))
|
||||
p.addLine(to: CGPoint(x: mx + waistHW, y: waistY))
|
||||
p.addLine(to: CGPoint(x: mx + hipHW, y: hipY))
|
||||
p.addLine(to: CGPoint(x: mx - hipHW, y: hipY))
|
||||
p.closeSubpath()
|
||||
|
||||
let gap = w * 0.02
|
||||
let legBotY = h * 0.92
|
||||
let footH = h * 0.05
|
||||
let footExtra = w * 0.04
|
||||
|
||||
p.move(to: CGPoint(x: mx - hipHW, y: hipY))
|
||||
p.addLine(to: CGPoint(x: mx - gap, y: hipY))
|
||||
p.addLine(to: CGPoint(x: mx - gap, y: legBotY))
|
||||
p.addLine(to: CGPoint(x: mx - hipHW - footExtra, y: legBotY + footH))
|
||||
p.addLine(to: CGPoint(x: mx - hipHW, y: legBotY))
|
||||
p.closeSubpath()
|
||||
|
||||
p.move(to: CGPoint(x: mx + gap, y: hipY))
|
||||
p.addLine(to: CGPoint(x: mx + hipHW, y: hipY))
|
||||
p.addLine(to: CGPoint(x: mx + hipHW, y: legBotY))
|
||||
p.addLine(to: CGPoint(x: mx + hipHW + footExtra, y: legBotY + footH))
|
||||
p.addLine(to: CGPoint(x: mx + gap, y: legBotY))
|
||||
p.closeSubpath()
|
||||
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
/// Body-zone highlight icon backed by vector SVG assets in `Assets.xcassets`.
|
||||
///
|
||||
/// Each zone renders its own artwork:
|
||||
/// - `upper-body` → `ZoneIconUpper`
|
||||
/// - `lower-body` → `ZoneIconLower`
|
||||
/// - `full-body` → `ZoneIconFull`
|
||||
///
|
||||
/// The SVGs ship with their own baked-in colors, so the icon is **not**
|
||||
/// tinted by `Theme.zoneGradient` — it shows the artwork as authored.
|
||||
/// To make icons auto-recolor per zone instead, switch the imagesets to
|
||||
/// single-color SVGs and set `"template-rendering-intent": "template"`
|
||||
/// in each `Contents.json`, then apply `.foregroundStyle(...)` here.
|
||||
///
|
||||
/// Drawn on a 56×80 frame with `scaledToFit()` so the SVG keeps its
|
||||
/// native aspect ratio inside the card.
|
||||
struct ZoneHighlightIcon: View {
|
||||
let zone: String
|
||||
|
||||
private var waistFraction: CGFloat { 0.50 }
|
||||
|
||||
var body: some View {
|
||||
let shape = BodySilhouetteShape()
|
||||
ZStack {
|
||||
shape
|
||||
.fill(.white.opacity(0.12))
|
||||
|
||||
shape
|
||||
.fill(zoneGradient)
|
||||
.mask(zoneMask)
|
||||
}
|
||||
.frame(width: 56, height: 80)
|
||||
Image(decorative: imageName, bundle: .main)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 56, height: 80)
|
||||
}
|
||||
|
||||
private var zoneGradient: LinearGradient {
|
||||
switch zone {
|
||||
case "upper-body":
|
||||
LinearGradient(colors: [.orange, .red.opacity(0.8)], startPoint: .top, endPoint: .bottom)
|
||||
case "lower-body":
|
||||
LinearGradient(colors: [.blue, .purple.opacity(0.8)], startPoint: .top, endPoint: .bottom)
|
||||
case "full-body":
|
||||
LinearGradient(colors: [Theme.brand, .purple], startPoint: .top, endPoint: .bottom)
|
||||
default:
|
||||
LinearGradient(colors: [.gray, .secondary], startPoint: .top, endPoint: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var zoneMask: some View {
|
||||
switch zone {
|
||||
case "upper-body":
|
||||
Rectangle()
|
||||
.frame(height: 80 * waistFraction)
|
||||
.frame(maxHeight: .infinity, alignment: .top)
|
||||
case "lower-body":
|
||||
Rectangle()
|
||||
.frame(height: 80 * waistFraction)
|
||||
.frame(maxHeight: .infinity, alignment: .bottom)
|
||||
case "full-body":
|
||||
Rectangle()
|
||||
default:
|
||||
Rectangle()
|
||||
private var imageName: String {
|
||||
switch zone.lowercased() {
|
||||
case "upper-body", "upper": return "ZoneIconUpper"
|
||||
case "lower-body", "lower": return "ZoneIconLower"
|
||||
default: return "ZoneIconFull"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user