Files
Sportstime/SportsTime/Features/Settings/SportsIconImageGenerator.swift

287 lines
10 KiB
Swift

//
// SportsIconImageGenerator.swift
// SportsTime
//
// Generates a 1024x1024 PNG with randomly scattered sports icons.
//
import SwiftUI
import UIKit
struct SportsIconImageGenerator {
/// All sports icons used in the animated background
private static let sportsIcons: [String] = [
// Sports balls
"football.fill",
"basketball.fill",
"baseball.fill",
"hockey.puck.fill",
"soccerball",
"tennisball.fill",
"volleyball.fill",
// Sports figures
"figure.run",
"figure.baseball",
"figure.basketball",
"figure.hockey",
"figure.soccer",
// Venues/events
"sportscourt.fill",
"trophy.fill",
"ticket.fill",
// Travel/navigation
"mappin.circle.fill",
"car.fill",
"map.fill",
"flag.checkered"
]
/// Generates a 1024x1024 PNG image with randomly scattered sports icons
/// - Returns: A UIImage with transparent background containing the icons (exactly 1024x1024 pixels)
static func generateImage() -> UIImage {
let size = CGSize(width: 1024, height: 1024)
let maxIconSize: CGFloat = 150
let minIconSize: CGFloat = 40
let minSpacing: CGFloat = 80 // Minimum distance between icon centers
// Use scale 1.0 to ensure exact pixel dimensions (not retina scaled)
let format = UIGraphicsImageRendererFormat()
format.scale = 1.0
let renderer = UIGraphicsImageRenderer(size: size, format: format)
// Generate well-distributed positions using Poisson disk sampling
let positions = generateScatteredPositions(
in: size,
minSpacing: minSpacing,
targetCount: 45
)
let image = renderer.image { context in
// Clear background (transparent)
UIColor.clear.setFill()
context.fill(CGRect(origin: .zero, size: size))
// Shuffle icons to randomize which icon appears where
let shuffledIcons = sportsIcons.shuffled()
// Draw icons at scattered positions
for (index, position) in positions.enumerated() {
let iconName = shuffledIcons[index % shuffledIcons.count]
let targetSize = CGFloat.random(in: minIconSize...maxIconSize)
let rotation = CGFloat.random(in: -30...30) * .pi / 180
// Create SF Symbol image at a large point size for quality
let config = UIImage.SymbolConfiguration(pointSize: 100, weight: .regular)
if let symbolImage = UIImage(systemName: iconName, withConfiguration: config) {
// Tint with warm orange
let tintedImage = symbolImage.withTintColor(
UIColor(Theme.warmOrange),
renderingMode: .alwaysOriginal
)
// Calculate proportional size maintaining aspect ratio
let originalSize = tintedImage.size
let aspectRatio = originalSize.width / originalSize.height
let drawSize: CGSize
if aspectRatio > 1 {
// Wider than tall - constrain by width
drawSize = CGSize(width: targetSize, height: targetSize / aspectRatio)
} else {
// Taller than wide - constrain by height
drawSize = CGSize(width: targetSize * aspectRatio, height: targetSize)
}
// Save graphics state for rotation
context.cgContext.saveGState()
// Position at the scattered point
context.cgContext.translateBy(x: position.x, y: position.y)
context.cgContext.rotate(by: rotation)
// Draw the icon centered at origin with correct aspect ratio
let drawRect = CGRect(
x: -drawSize.width / 2,
y: -drawSize.height / 2,
width: drawSize.width,
height: drawSize.height
)
tintedImage.draw(in: drawRect)
context.cgContext.restoreGState()
}
}
}
return image
}
/// Generates well-distributed points using a grid-based approach with jitter
/// This ensures icons are scattered across the entire image, not clumped together
private static func generateScatteredPositions(
in size: CGSize,
minSpacing: CGFloat,
targetCount: Int
) -> [CGPoint] {
var positions: [CGPoint] = []
// Calculate grid dimensions to achieve roughly targetCount points
let gridSize = Int(ceil(sqrt(Double(targetCount))))
let cellWidth = size.width / CGFloat(gridSize)
let cellHeight = size.height / CGFloat(gridSize)
// Margin from edges
let margin: CGFloat = 40
for row in 0..<gridSize {
for col in 0..<gridSize {
// Base position at center of grid cell
let baseCenterX = (CGFloat(col) + 0.5) * cellWidth
let baseCenterY = (CGFloat(row) + 0.5) * cellHeight
// Add random jitter within the cell (up to 40% of cell size)
let jitterX = CGFloat.random(in: -cellWidth * 0.4...cellWidth * 0.4)
let jitterY = CGFloat.random(in: -cellHeight * 0.4...cellHeight * 0.4)
var x = baseCenterX + jitterX
var y = baseCenterY + jitterY
// Clamp to stay within margins
x = max(margin, min(size.width - margin, x))
y = max(margin, min(size.height - margin, y))
// Randomly skip some cells for more organic feel (keep ~80%)
if Double.random(in: 0...1) < 0.85 {
positions.append(CGPoint(x: x, y: y))
}
}
}
// Shuffle to randomize draw order (affects overlap)
return positions.shuffled()
}
/// Generates PNG data from the image
static func generatePNGData() -> Data? {
let image = generateImage()
return image.pngData()
}
}
// MARK: - SwiftUI View for generating and sharing
struct SportsIconImageGeneratorView: View {
@State private var generatedImage: UIImage?
@State private var isGenerating = false
@State private var showShareSheet = false
var body: some View {
VStack(spacing: 16) {
// Preview of generated image
if let image = generatedImage {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 300, maxHeight: 300)
.background(
// Checkerboard pattern to show transparency
CheckerboardPattern()
)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.secondary.opacity(0.3), lineWidth: 1)
)
}
// Generate button
Button {
generateNewImage()
} label: {
HStack {
if isGenerating {
ProgressView()
.tint(.white)
} else {
Image(systemName: "dice.fill")
}
Text(generatedImage == nil ? "Generate Image" : "Regenerate")
}
.frame(maxWidth: .infinity)
.padding()
.background(Theme.warmOrange)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.disabled(isGenerating)
// Share button (only visible when image exists)
if let generatedImage {
ShareLink(
item: Image(uiImage: generatedImage),
preview: SharePreview("Sports Icons", image: Image(uiImage: generatedImage))
) {
HStack {
Image(systemName: "square.and.arrow.up")
Text("Share via AirDrop")
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}
.padding()
}
private func generateNewImage() {
isGenerating = true
// Generate on background thread to avoid UI freeze
Task {
let image = await Task.detached(priority: .userInitiated) {
SportsIconImageGenerator.generateImage()
}.value
withAnimation {
generatedImage = image
isGenerating = false
}
}
}
}
// MARK: - Checkerboard Pattern for transparency preview
private struct CheckerboardPattern: View {
var body: some View {
Canvas { context, size in
let tileSize: CGFloat = 10
let rows = Int(ceil(size.height / tileSize))
let cols = Int(ceil(size.width / tileSize))
for row in 0..<rows {
for col in 0..<cols {
let isLight = (row + col) % 2 == 0
let rect = CGRect(
x: CGFloat(col) * tileSize,
y: CGFloat(row) * tileSize,
width: tileSize,
height: tileSize
)
context.fill(
Path(rect),
with: .color(isLight ? Color.gray.opacity(0.2) : Color.gray.opacity(0.3))
)
}
}
}
}
}
#Preview {
SportsIconImageGeneratorView()
}