Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed Media/hero.jpg
Binary file not shown.
Binary file added Media/hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Aither

![Aither — Living particle orbs for SwiftUI](Media/hero.jpg)
![Aither — Living particle orbs for SwiftUI](Media/hero.png)

Aither is a dependency-free package for expressive, continuously looping
particle orbs. Its primary API is native SwiftUI, backed by an efficient
Expand Down
146 changes: 146 additions & 0 deletions Scripts/render-hero.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env swift

import AppKit

struct Particle {
var x: CGFloat
var y: CGFloat
var z: CGFloat
var size: CGFloat
var opacity: CGFloat
}

private let canvasWidth = 3_200
private let canvasHeight = 1_280
private let outputURL = URL(fileURLWithPath: CommandLine.arguments.dropFirst().first ?? "Media/hero.png")

guard let bitmap = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: canvasWidth,
pixelsHigh: canvasHeight,
bitsPerSample: 8,
samplesPerPixel: 2,
hasAlpha: true,
isPlanar: false,
colorSpaceName: .deviceWhite,
bytesPerRow: 0,
bitsPerPixel: 0
) else {
fatalError("Unable to allocate hero bitmap")
}

NSGraphicsContext.saveGraphicsState()
guard let graphicsContext = NSGraphicsContext(bitmapImageRep: bitmap) else {
fatalError("Unable to create graphics context")
}
NSGraphicsContext.current = graphicsContext

let context = graphicsContext.cgContext
let canvas = CGRect(x: 0, y: 0, width: canvasWidth, height: canvasHeight)
context.setAllowsAntialiasing(true)
context.setShouldAntialias(true)
context.setFillColor(gray: 0, alpha: 1)
context.fill(canvas)

// A high-density, fixed-phase interpretation of Aither's Zetesis particle shell.
// It uses the same latitude sampling, 3D rotation, depth sort and perspective
// projection as AitherView, scaled for a lossless 3200 × 1280 README asset.
let latitudeRows = 37
let phase: CGFloat = 0.17
let loop = phase * .pi * 2
let scan = loop * 2
var particles: [Particle] = []

for row in 0..<latitudeRows {
let normalizedRow = CGFloat(row) / CGFloat(latitudeRows - 1)
let latitude = (normalizedRow - 0.5) * .pi
let ringRadius = cos(latitude)
let count = max(18, Int(112 * abs(ringRadius)))

for index in 0..<count {
let longitude = CGFloat(index) / CGFloat(count) * .pi * 2
let delta = abs(atan2(sin(longitude - scan), cos(longitude - scan)))
let highlight = max(0, 1 - delta / 0.42)
particles.append(Particle(
x: cos(longitude) * ringRadius,
y: sin(latitude),
z: sin(longitude) * ringRadius,
size: 0.58 + highlight * 0.54,
opacity: 0.34 + highlight * 0.66
))
}
}

func rotated(_ point: Particle, x rx: CGFloat, y ry: CGFloat, z rz: CGFloat) -> Particle {
let x1 = point.x
let y1 = point.y * cos(rx) - point.z * sin(rx)
let z1 = point.y * sin(rx) + point.z * cos(rx)
let x2 = x1 * cos(ry) + z1 * sin(ry)
let y2 = y1
let z2 = -x1 * sin(ry) + z1 * cos(ry)
return Particle(
x: x2 * cos(rz) - y2 * sin(rz),
y: x2 * sin(rz) + y2 * cos(rz),
z: z2,
size: point.size,
opacity: point.opacity
)
}

let center = CGPoint(x: 2_680, y: 640)
let radius: CGFloat = 1_085
let baseDot: CGFloat = 5.2

let rendered = particles
.map { rotated($0, x: -0.16, y: loop * 0.12, z: 0.018) }
.sorted { $0.z < $1.z }

for particle in rendered {
let depth = max(0, min((particle.z + 1.15) / 2.3, 1))
let perspective = 1 / max(0.72, 1 - particle.z * 0.2)
let point = CGPoint(
x: center.x + particle.x * radius * perspective,
y: center.y + particle.y * radius * perspective
)
let dotRadius = baseDot * particle.size * (0.62 + depth * 0.58) * perspective
let alpha = max(0.08, min(particle.opacity * (0.12 + depth * 0.88), 1))

context.setFillColor(gray: 1, alpha: alpha)
context.fillEllipse(in: CGRect(
x: point.x - dotRadius,
y: point.y - dotRadius,
width: dotRadius * 2,
height: dotRadius * 2
))
}

let title = "AITHER"
let titleFont = NSFont.monospacedSystemFont(ofSize: 25, weight: .medium)
let titleColor = NSColor(white: 1, alpha: 0.82)
let tracking: CGFloat = 18
var titleX: CGFloat = 116

for character in title {
let glyph = String(character)
let attributes: [NSAttributedString.Key: Any] = [
.font: titleFont,
.foregroundColor: titleColor
]
let size = glyph.size(withAttributes: attributes)
glyph.draw(at: CGPoint(x: titleX, y: 105), withAttributes: attributes)
titleX += size.width + tracking
}

graphicsContext.flushGraphics()
NSGraphicsContext.restoreGraphicsState()

guard let png = bitmap.representation(using: .png, properties: [:]) else {
fatalError("Unable to encode hero PNG")
}

try FileManager.default.createDirectory(
at: outputURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try png.write(to: outputURL, options: .atomic)
print(outputURL.path)
Loading