update at 2026-03-15 15:58:14
This commit is contained in:
166
scripts/generate-kindle-clock-assets.swift
Normal file
166
scripts/generate-kindle-clock-assets.swift
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/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)
|
||||
}
|
||||
Reference in New Issue
Block a user