Files
FlipClockSaver/FlipClockSaver/FlipDigit.swift
T
lc 83bda78b5b 重构:拆分单文件为 4 个模块 + 设置面板显示版本号/构建号
- 拆分 FlipClockSaver.swift(680 行)为:
  * FlipDigit.swift(单翻页数字渲染+动画)
  * FlipClockSaverView.swift(屏保主体、布局、绘制、设置面板入口)
  * FlipClockSettings.swift(设置存储 + .flipClockSettingsDidChange 通知)
  * FlipClockConfigController.swift(设置窗口 UI)
- 工程采用 PBXFileSystemSynchronizedRootGroup,新增/删除源文件由 Xcode 自动同步,无需改 pbxproj
- 设置窗口底部新增版本信息:“版本 1.0 · Build 1”
  (从 Bundle(for:) 读取 CFBundleShortVersionString / CFBundleVersion,比 Bundle.main 更可靠)
- Release 构建 BUILD SUCCEEDED,Developer ID 正式签名通过
2026-08-06 10:32:20 +08:00

142 lines
5.8 KiB
Swift

import AppKit
// MARK: - 单个翻页数字(Core Graphics 绘制,无 layer 拼接,彻底无黑缝)
@MainActor
final class FlipDigit {
var current: Int = -1
private var previous: Int = -1
private var animStart: TimeInterval = 0
private var animating = false
let duration: TimeInterval = 0.55
func set(_ newValue: Int, at time: TimeInterval, animated: Bool) {
guard newValue != current else { return }
previous = current < 0 ? newValue : current
current = newValue
if animated {
animStart = time
animating = true
} else {
animating = false
}
}
func advance(to time: TimeInterval) {
if animating && (time - animStart) >= duration {
animating = false
}
}
func draw(in ctx: CGContext,
rect: CGRect,
radius: CGFloat,
font: NSFont,
cardBg: NSColor,
textColor: NSColor,
now: TimeInterval) {
let t01 = animating ? min(max((now - animStart) / duration, 0), 1) : 1
let midY = rect.midY
let topRect = CGRect(x: rect.minX, y: midY, width: rect.width, height: rect.height / 2)
let botRect = CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: rect.height / 2)
let topDigit = current
let botDigit = animating ? previous : current
// 先铺整卡底色:中缝处底色即卡片色,彻底消除抗锯齿透出的黑缝
ctx.saveGState()
let basePath = CGMutablePath()
basePath.addRoundedRect(in: rect, cornerWidth: radius, cornerHeight: radius)
ctx.addPath(basePath); ctx.clip()
ctx.setFillColor(cardBg.cgColor)
ctx.fill(rect)
ctx.restoreGState()
drawCard(in: ctx, half: topRect, full: rect, digit: topDigit,
radius: radius, font: font, cardBg: cardBg, textColor: textColor)
drawCard(in: ctx, half: botRect, full: rect, digit: botDigit,
radius: radius, font: font, cardBg: cardBg, textColor: textColor)
if animating && t01 < 1 {
if t01 < 0.5 {
// 上半翻片:旧值从直立翻走(0 → -90°),用 cos 模拟 3D 压缩
let a = -t01 * 2 * (CGFloat.pi / 2)
let sy = max(cos(a), 0.0001)
ctx.saveGState()
ctx.beginPath(); ctx.addRect(topRect); ctx.clip()
ctx.translateBy(x: 0, y: midY)
ctx.scaleBy(x: 1, y: sy)
ctx.translateBy(x: 0, y: -midY)
drawCard(in: ctx, half: topRect, full: rect, digit: previous,
radius: radius, font: font, cardBg: cardBg, textColor: textColor)
ctx.setFillColor(CGColor(gray: 0, alpha: CGFloat(t01) * 0.40))
ctx.fill(topRect)
ctx.restoreGState()
} else {
// 下半翻片:新值从水平翻下(90° → 0°)
let t2 = (t01 - 0.5) * 2
let a = (1 - t2) * (CGFloat.pi / 2)
let sy = max(cos(a), 0.0001)
ctx.saveGState()
ctx.beginPath(); ctx.addRect(botRect); ctx.clip()
ctx.translateBy(x: 0, y: midY)
ctx.scaleBy(x: 1, y: sy)
ctx.translateBy(x: 0, y: -midY)
drawCard(in: ctx, half: botRect, full: rect, digit: current,
radius: radius, font: font, cardBg: cardBg, textColor: textColor)
ctx.setFillColor(CGColor(gray: 1, alpha: CGFloat(1 - t2) * 0.30))
ctx.fill(botRect)
ctx.restoreGState()
}
}
// 翻页钟铰链:一条细而自然的中缝分割线(位于翻片转轴处,始终可见)
drawHinge(in: ctx, rect: rect, midY: midY)
}
// 细而自然的中缝:主线为柔和阴影,下方一抹微弱高光模拟折边受光
private func drawHinge(in ctx: CGContext, rect: CGRect, midY: CGFloat) {
ctx.saveGState()
ctx.setFillColor(CGColor(gray: 0, alpha: 0.45))
ctx.fill(CGRect(x: rect.minX, y: midY - 0.75, width: rect.width, height: 1.5))
ctx.setFillColor(CGColor(gray: 1, alpha: 0.06))
ctx.fill(CGRect(x: rect.minX, y: midY + 0.75, width: rect.width, height: 1))
ctx.restoreGState()
}
// 整卡圆角 + 半区域双重裁剪:外缘圆角、接缝直边,且上下连续绘制 → 无黑缝
private func drawCard(in ctx: CGContext,
half: CGRect,
full: CGRect,
digit: Int,
radius: CGFloat,
font: NSFont,
cardBg: NSColor,
textColor: NSColor) {
ctx.saveGState()
let path = CGMutablePath()
path.addRoundedRect(in: full, cornerWidth: radius, cornerHeight: radius)
ctx.addPath(path); ctx.clip()
ctx.beginPath(); ctx.addRect(half); ctx.clip()
ctx.setFillColor(cardBg.cgColor)
ctx.fill(full)
// 数字在整卡内水平 + 垂直精确居中:先测量文字尺寸,再把绘制框对齐卡片中线
let str = "\(digit)" as NSString
let size = str.size(withAttributes: [.font: font])
let textRect = CGRect(x: full.minX,
y: full.midY - size.height / 2,
width: full.width,
height: size.height)
let ps = NSMutableParagraphStyle()
ps.alignment = .center
let attrs: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: textColor,
.paragraphStyle: ps
]
str.draw(in: textRect, withAttributes: attrs)
ctx.restoreGState()
}
}