Files
Sportstime/SportsTime/Export/Sharing/ShareCardSportBackground.swift
Trey t 244ea5e107 feat: redesign all share cards, remove unused achievement types, fix sport selector
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>
2026-02-09 14:55:53 -06:00

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)
}