使用 CAEmitterLayer 为 macOS 构建环境桌面粒子
Source: Dev.to
介绍
我最近发布了一款名为 Wisp 的免费 macOS 应用——一个在菜单栏的工具,用于渲染环境桌面粒子。目标是在 4K 显示器上平滑漂浮约 1,000 个粒子,同时 CPU 占用约 1 % 。每帧更新 CALayer 的计时器驱动循环会太沉重,于是我转而使用 CAEmitterLayer,Apple 的 GPU 加速粒子系统。
最小化 CAEmitterLayer 设置
import QuartzCore
let emitter = CAEmitterLayer()
emitter.emitterShape = .rectangle
emitter.emitterMode = .surface
emitter.emitterPosition = CGPoint(x: screen.width / 2, y: screen.height / 2)
emitter.emitterSize = screen.size
emitter.renderMode = .additive // layered glow
let cell = CAEmitterCell()
cell.contents = particleTexture // a soft radial gradient CGImage
cell.birthRate = 14
cell.lifetime = 10
cell.lifetimeRange = 3
cell.velocity = 8
cell.velocityRange = 18
cell.emissionRange = .pi * 2 // spawn in any direction
cell.yAcceleration = -1 // drift upward
cell.scale = 0.05
cell.scaleRange = 0.02
cell.alphaSpeed = -0.058 // fade out
cell.color = UIColor(...) // main particle color
emitter.emitterCells = [cell]
hostLayer.addSublayer(emitter)
单一的统一粒子看起来很人工。实现自然“氛围”的技巧是使用 多个粒子,它们在同一个发射器中具有不同的属性。
深度层
| 单元 | 出生率 | 尺寸 | 寿命 | 用途 |
|---|---|---|---|---|
| aura | 2 | 大 | 14 s | 弥散的背景光辉 |
| near | 5 | 中等 | 8 s | 前景斑点 |
| mote | 14 | 小 | 10 s | 中深度闪光 |
| spark | 22 | 极小 | 12 s | 明亮点 |
| dot | 35 | 像素 | 16 s | 星空纹理 |
由于全部五个单元并行运行且使用 .additive 混合,重叠的粒子会产生更丰富的视觉效果。
以编程方式构建单元
func buildCells(for palette: [Color]) -> [CAEmitterCell] {
palette.flatMap { color in
[
makeCell(.aura, color: color, texture: softBlob),
makeCell(.near, color: color, texture: glow),
makeCell(.mote, color: color.lighter(0.3), texture: glow),
makeCell(.spark, color: color, texture: core),
makeCell(.dot, color: color.lighter(0.5), texture: hardDot),
]
}
}
粒子图像
每个单元格需要一个位图纹理。我在构建时生成五个单通道的柔和径向渐变:
enum ParticleImage {
static let glow: CGImage = radial(size: 64, falloff: 1.0)
static let core: CGImage = radial(size: 32, falloff: 0.4)
static let blob: CGImage = radial(size: 96, falloff: 1.0)
static let dot: CGImage = radial(size: 8, falloff: 0.25)
private static func radial(size: Int, falloff: CGFloat) -> CGImage {
let ctx = CGContext(
data: nil,
width: size,
height: size,
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
| CGBitmapInfo.byteOrder32Little.rawValue
)!
let center = CGPoint(x: CGFloat(size) / 2, y: CGFloat(size) / 2)
let gradient = CGGradient(
colorsSpace: nil,
colors: [
CGColor(red: 1, green: 1, blue: 1, alpha: 1),
CGColor(red: 1, green: 1, blue: 1, alpha: 0)
] as CFArray,
locations: [0, falloff]
)!
ctx.drawRadialGradient(
gradient,
startCenter: center, startRadius: 0,
endCenter: center, endRadius: CGFloat(size) / 2,
options: []
)
return ctx.makeImage()!
}
}
.additive 混合模式使用每个单元格的 color 属性为这些灰度纹理着色。
窗口配置
Wisp 需要在壁纸之上、桌面图标之下渲染,使图标能够干净地穿过。这通过在 desktopWindow + 1 级别创建一个无边框窗口实现:
window.level = NSWindow.Level(
rawValue: Int(CGWindowLevelForKey(.desktopWindow)) + 1
)
window.collectionBehavior = [
.canJoinAllSpaces,
.stationary,
.ignoresCycle
]
window.ignoresMouseEvents = true
window.backgroundColor = .clear
window.isOpaque = false
对于多显示器设置,会为每个 NSScreen 创建一个单独的窗口,并在 didChangeScreenParametersNotification 触发时重新创建它们。
节能与隐私小贴士
- 在电池供电 / 睡眠 / 锁屏时暂停 – 订阅
NSWorkspace.screensDidSleepNotification和sessionDidResignActiveNotification,然后将emitter.birthRate = 0设置为将 CPU 使用率降至约 0 %。 - 尊重
NSWindow.SharingType.none– 只需一行代码即可让用户在屏幕录制和截图时排除粒子效果。 - 零沙盒占用 – 无文件系统访问,无网络(除 Sparkle 的 appcast 用于检查更新外),仅使用默认权限。
性能
- 在 M2 Pro 上约 1,200 粒子,CPU 空闲约 1 %。
- 没有掉帧,也没有明显的发热。
来源与下载
- 源代码 (MIT) –
- 下载应用(免费,macOS 14+) –