Redesign trip, progress, and achievement share cards with premium sports-media aesthetic. Remove unused milestone/context achievement card types (only used in debug exporter). Fix gold text unreadable in light mode. Fix sport selector to only show stroke on selected sport. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
214 lines
6.0 KiB
Swift
214 lines
6.0 KiB
Swift
//
|
|
// ShareCardSportBackground.swift
|
|
// SportsTime
|
|
//
|
|
// New visual language for share cards: atmospheric gradients, sport-specific linework,
|
|
// and strong edge shading for depth.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct ShareCardSportBackground: View {
|
|
let sports: Set<Sport>
|
|
let theme: ShareTheme
|
|
|
|
private var primarySport: Sport? {
|
|
sports.sorted { $0.rawValue < $1.rawValue }.first
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
baseLayer
|
|
glowLayer
|
|
patternLayer
|
|
edgeShadeLayer
|
|
}
|
|
}
|
|
|
|
private var baseLayer: some View {
|
|
LinearGradient(
|
|
colors: [
|
|
theme.gradientColors.first ?? .black,
|
|
theme.midGradientColor,
|
|
theme.gradientColors.last ?? .black
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
.overlay {
|
|
LinearGradient(
|
|
colors: [
|
|
.black.opacity(0.20),
|
|
.clear,
|
|
.black.opacity(0.30)
|
|
],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
}
|
|
}
|
|
|
|
private var glowLayer: some View {
|
|
GeometryReader { geo in
|
|
ZStack {
|
|
Circle()
|
|
.fill(theme.accentColor.opacity(0.24))
|
|
.frame(width: geo.size.width * 0.95)
|
|
.offset(x: -geo.size.width * 0.30, y: -geo.size.height * 0.35)
|
|
|
|
Circle()
|
|
.fill(theme.textColor.opacity(0.10))
|
|
.frame(width: geo.size.width * 0.72)
|
|
.offset(x: geo.size.width * 0.35, y: -geo.size.height * 0.10)
|
|
|
|
Ellipse()
|
|
.fill(theme.accentColor.opacity(0.16))
|
|
.frame(width: geo.size.width * 1.20, height: geo.size.height * 0.45)
|
|
.offset(y: geo.size.height * 0.42)
|
|
}
|
|
.blur(radius: 58)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var patternLayer: some View {
|
|
switch primarySport {
|
|
case .mlb?:
|
|
BaseballStitchPattern()
|
|
.stroke(theme.textColor.opacity(0.11), lineWidth: 1.8)
|
|
|
|
case .nba?, .wnba?:
|
|
CourtArcPattern()
|
|
.stroke(theme.textColor.opacity(0.10), lineWidth: 1.6)
|
|
|
|
case .nhl?:
|
|
IceShardPattern()
|
|
.stroke(theme.textColor.opacity(0.10), lineWidth: 1.4)
|
|
|
|
case .nfl?, .mls?, .nwsl?, nil:
|
|
PitchBandPattern()
|
|
.fill(theme.textColor.opacity(0.09))
|
|
}
|
|
}
|
|
|
|
private var edgeShadeLayer: some View {
|
|
ZStack {
|
|
RadialGradient(
|
|
colors: [.clear, .black.opacity(0.45)],
|
|
center: .center,
|
|
startRadius: 120,
|
|
endRadius: 980
|
|
)
|
|
|
|
LinearGradient(
|
|
colors: [.black.opacity(0.42), .clear, .black.opacity(0.42)],
|
|
startPoint: .top,
|
|
endPoint: .bottom
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct BaseballStitchPattern: Shape {
|
|
func path(in rect: CGRect) -> Path {
|
|
var path = Path()
|
|
let rowStep: CGFloat = 150
|
|
let colStep: CGFloat = 220
|
|
|
|
var y: CGFloat = -80
|
|
while y < rect.maxY + 120 {
|
|
var x: CGFloat = -60
|
|
while x < rect.maxX + 180 {
|
|
let start = CGPoint(x: x, y: y)
|
|
let mid = CGPoint(x: x + 85, y: y + 26)
|
|
let end = CGPoint(x: x + 170, y: y + 62)
|
|
|
|
path.move(to: start)
|
|
path.addQuadCurve(to: mid, control: CGPoint(x: x + 48, y: y - 22))
|
|
path.addQuadCurve(to: end, control: CGPoint(x: x + 122, y: y + 70))
|
|
|
|
x += colStep
|
|
}
|
|
y += rowStep
|
|
}
|
|
|
|
return path
|
|
}
|
|
}
|
|
|
|
private struct CourtArcPattern: Shape {
|
|
func path(in rect: CGRect) -> Path {
|
|
var path = Path()
|
|
let spacing: CGFloat = 170
|
|
|
|
var y: CGFloat = -80
|
|
while y < rect.maxY + 120 {
|
|
var x: CGFloat = -80
|
|
while x < rect.maxX + 120 {
|
|
let circleRect = CGRect(x: x, y: y, width: 132, height: 132)
|
|
path.addEllipse(in: circleRect)
|
|
path.move(to: CGPoint(x: x - 20, y: y + 66))
|
|
path.addLine(to: CGPoint(x: x + 152, y: y + 66))
|
|
x += spacing
|
|
}
|
|
y += spacing
|
|
}
|
|
|
|
return path
|
|
}
|
|
}
|
|
|
|
private struct IceShardPattern: Shape {
|
|
func path(in rect: CGRect) -> Path {
|
|
var path = Path()
|
|
let spacing: CGFloat = 92
|
|
|
|
var baseX: CGFloat = -120
|
|
while baseX < rect.maxX + 200 {
|
|
path.move(to: CGPoint(x: baseX, y: -80))
|
|
path.addLine(to: CGPoint(x: baseX + 44, y: rect.maxY * 0.26))
|
|
path.addLine(to: CGPoint(x: baseX - 26, y: rect.maxY * 0.52))
|
|
path.addLine(to: CGPoint(x: baseX + 30, y: rect.maxY + 100))
|
|
baseX += spacing
|
|
}
|
|
|
|
var baseY: CGFloat = -60
|
|
while baseY < rect.maxY + 160 {
|
|
path.move(to: CGPoint(x: -80, y: baseY))
|
|
path.addLine(to: CGPoint(x: rect.maxX + 80, y: baseY + 50))
|
|
baseY += spacing
|
|
}
|
|
|
|
return path
|
|
}
|
|
}
|
|
|
|
private struct PitchBandPattern: Shape {
|
|
func path(in rect: CGRect) -> Path {
|
|
var path = Path()
|
|
let stripeWidth: CGFloat = 54
|
|
let stripeGap: CGFloat = 34
|
|
let diagonalLength = rect.width + rect.height + 240
|
|
|
|
var x: CGFloat = -rect.height - 120
|
|
while x < rect.width + rect.height + 120 {
|
|
path.addRect(
|
|
CGRect(
|
|
x: x,
|
|
y: -140,
|
|
width: stripeWidth,
|
|
height: diagonalLength
|
|
)
|
|
)
|
|
x += stripeWidth + stripeGap
|
|
}
|
|
|
|
return path.applying(CGAffineTransform(rotationAngle: .pi / 3.8))
|
|
}
|
|
}
|
|
|
|
#Preview("Share Background") {
|
|
ShareCardSportBackground(sports: [.nfl], theme: .midnight)
|
|
.frame(width: 420, height: 720)
|
|
}
|