Files
kindle-calendar/scripts/generate-kindle-clock-assets.swift
2026-03-15 15:58:14 +08:00

167 lines
4.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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)
}