update at 2026-03-15 15:58:14

This commit is contained in:
douboer@gmail.com
2026-03-15 15:58:14 +08:00
parent b38d932a05
commit 4b280073d4
24 changed files with 1509 additions and 596 deletions

View 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)
}