167 lines
4.7 KiB
Swift
167 lines
4.7 KiB
Swift
#!/usr/bin/env swift
|
||
|
||
import AppKit
|
||
import Foundation
|
||
|
||
struct HandConfig {
|
||
let sourcePath: String
|
||
let outputDirectoryName: String
|
||
let frameCount: Int
|
||
let sourceWidth: CGFloat
|
||
let sourceHeight: CGFloat
|
||
let pivotX: CGFloat
|
||
let pivotY: CGFloat
|
||
let digits: Int
|
||
let angleStep: CGFloat
|
||
}
|
||
|
||
enum AssetError: Error, CustomStringConvertible {
|
||
case invalidImage(String)
|
||
case pngEncodingFailed(String)
|
||
|
||
var description: String {
|
||
switch self {
|
||
case let .invalidImage(path):
|
||
return "无法读取图片:\(path)"
|
||
case let .pngEncodingFailed(path):
|
||
return "无法编码 PNG:\(path)"
|
||
}
|
||
}
|
||
}
|
||
|
||
let fileManager = FileManager.default
|
||
let workingDirectory = URL(fileURLWithPath: fileManager.currentDirectoryPath, isDirectory: true)
|
||
let repoRoot = CommandLine.arguments.count > 1
|
||
? URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
|
||
: workingDirectory
|
||
let outputRoot = CommandLine.arguments.count > 2
|
||
? URL(fileURLWithPath: CommandLine.arguments[2], isDirectory: true)
|
||
: repoRoot.appendingPathComponent("assets/kindle-clock", isDirectory: true)
|
||
|
||
let faceSourceURL = repoRoot.appendingPathComponent("assets/clock-face.png")
|
||
let targetSize = NSSize(width: 220, height: 220)
|
||
let faceSourceSize = NSSize(width: 431, height: 431)
|
||
let scale = targetSize.width / faceSourceSize.width
|
||
|
||
let handConfigs = [
|
||
HandConfig(
|
||
sourcePath: "assets/hour-hand.png",
|
||
outputDirectoryName: "hour-hand",
|
||
frameCount: 720,
|
||
sourceWidth: 32,
|
||
sourceHeight: 205,
|
||
pivotX: 13,
|
||
pivotY: 138,
|
||
digits: 3,
|
||
angleStep: 0.5
|
||
),
|
||
HandConfig(
|
||
sourcePath: "assets/minite-hand.png",
|
||
outputDirectoryName: "minute-hand",
|
||
frameCount: 60,
|
||
sourceWidth: 32,
|
||
sourceHeight: 288,
|
||
pivotX: 15,
|
||
pivotY: 203,
|
||
digits: 2,
|
||
angleStep: 6
|
||
),
|
||
]
|
||
|
||
func loadImage(at url: URL) throws -> NSImage {
|
||
guard let image = NSImage(contentsOf: url) else {
|
||
throw AssetError.invalidImage(url.path)
|
||
}
|
||
|
||
return image
|
||
}
|
||
|
||
func savePNG(_ image: NSImage, to url: URL) throws {
|
||
guard
|
||
let tiffRepresentation = image.tiffRepresentation,
|
||
let bitmap = NSBitmapImageRep(data: tiffRepresentation),
|
||
let pngData = bitmap.representation(using: .png, properties: [:])
|
||
else {
|
||
throw AssetError.pngEncodingFailed(url.path)
|
||
}
|
||
|
||
try fileManager.createDirectory(
|
||
at: url.deletingLastPathComponent(),
|
||
withIntermediateDirectories: true
|
||
)
|
||
try pngData.write(to: url)
|
||
}
|
||
|
||
func renderImage(size: NSSize, draw: () -> Void) -> NSImage {
|
||
let image = NSImage(size: size)
|
||
image.lockFocusFlipped(true)
|
||
NSColor.clear.setFill()
|
||
NSBezierPath(rect: NSRect(origin: .zero, size: size)).fill()
|
||
draw()
|
||
image.unlockFocus()
|
||
return image
|
||
}
|
||
|
||
func renderFace() throws {
|
||
let faceImage = try loadImage(at: faceSourceURL)
|
||
let renderedFace = renderImage(size: targetSize) {
|
||
faceImage.draw(
|
||
in: NSRect(origin: .zero, size: targetSize),
|
||
from: NSRect(origin: .zero, size: faceImage.size),
|
||
operation: .sourceOver,
|
||
fraction: 1
|
||
)
|
||
}
|
||
|
||
try savePNG(renderedFace, to: outputRoot.appendingPathComponent("clock-face.png"))
|
||
}
|
||
|
||
func renderHandFrames(config: HandConfig) throws {
|
||
let sourceURL = repoRoot.appendingPathComponent(config.sourcePath)
|
||
let handImage = try loadImage(at: sourceURL)
|
||
let outputDirectory = outputRoot.appendingPathComponent(config.outputDirectoryName, isDirectory: true)
|
||
let scaledWidth = config.sourceWidth * scale
|
||
let scaledHeight = config.sourceHeight * scale
|
||
let scaledPivotX = config.pivotX * scale
|
||
let scaledPivotY = config.pivotY * scale
|
||
|
||
try fileManager.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
||
|
||
for frameIndex in 0..<config.frameCount {
|
||
let angle = CGFloat(frameIndex) * config.angleStep
|
||
let renderedFrame = renderImage(size: targetSize) {
|
||
guard let context = NSGraphicsContext.current?.cgContext else {
|
||
return
|
||
}
|
||
|
||
context.translateBy(x: targetSize.width / 2, y: targetSize.height / 2)
|
||
context.rotate(by: angle * .pi / 180)
|
||
context.translateBy(x: -scaledPivotX, y: -scaledPivotY)
|
||
|
||
handImage.draw(
|
||
in: NSRect(x: 0, y: 0, width: scaledWidth, height: scaledHeight),
|
||
from: NSRect(origin: .zero, size: handImage.size),
|
||
operation: .sourceOver,
|
||
fraction: 1
|
||
)
|
||
}
|
||
|
||
let filename = String(format: "%0\(config.digits)d.png", frameIndex)
|
||
try savePNG(renderedFrame, to: outputDirectory.appendingPathComponent(filename))
|
||
}
|
||
}
|
||
|
||
do {
|
||
try fileManager.createDirectory(at: outputRoot, withIntermediateDirectories: true)
|
||
try renderFace()
|
||
|
||
for config in handConfigs {
|
||
try renderHandFrames(config: config)
|
||
}
|
||
|
||
print("Generated Kindle clock assets at \(outputRoot.path)")
|
||
} catch {
|
||
fputs("\(error)\n", stderr)
|
||
exit(1)
|
||
}
|