add in icon pxd file
This commit is contained in:
287
SportsTime/Features/Settings/SportsIconImageGenerator.swift
Normal file
287
SportsTime/Features/Settings/SportsIconImageGenerator.swift
Normal file
@@ -0,0 +1,287 @@
|
||||
//
|
||||
// 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",
|
||||
"stadium.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 generatedImage != nil {
|
||||
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
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let image = SportsIconImageGenerator.generateImage()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user