使用 CAEmitterLayer 为 macOS 构建环境桌面粒子

发布: (2026年4月19日 GMT+8 23:52)
5 分钟阅读
原文: Dev.to

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)

单一的统一粒子看起来很人工。实现自然“氛围”的技巧是使用 多个粒子,它们在同一个发射器中具有不同的属性。

深度层

单元出生率尺寸寿命用途
aura214 s弥散的背景光辉
near5中等8 s前景斑点
mote1410 s中深度闪光
spark22极小12 s明亮点
dot35像素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.screensDidSleepNotificationsessionDidResignActiveNotification,然后将 emitter.birthRate = 0 设置为将 CPU 使用率降至约 0 %。
  • 尊重 NSWindow.SharingType.none – 只需一行代码即可让用户在屏幕录制和截图时排除粒子效果。
  • 零沙盒占用 – 无文件系统访问,无网络(除 Sparkle 的 appcast 用于检查更新外),仅使用默认权限。

性能

  • 在 M2 Pro 上约 1,200 粒子,CPU 空闲约 1 %。
  • 没有掉帧,也没有明显的发热。

来源与下载

  • 源代码 (MIT)
  • 下载应用(免费,macOS 14+)
0 浏览
Back to Blog

相关文章

阅读更多 »