import SwiftUI import simd public extension AnyConditionalEffect { /// An effect that emits smoke from the view. static var smoke: AnyConditionalEffect { .smoke(layer: .local) } /// An effect that emits smoke from the view. /// /// - Parameter layer: The `ParticleLayer` on which to render the effect, default is `local`. static func smoke(layer: ParticleLayer) -> AnyConditionalEffect { .continuous( .modifier { isActive in SmokeEffect(isActive: isActive, layer: layer) } ) } } private struct SmokeEffect: ViewModifier, Continuous { var isActive: Bool let layer: ParticleLayer let particles = [ "anvil_smoke_gray", "anvil_smoke_gray_blur", "anvil_smoke_gray_alt", ] func body(content: Content) -> some View { content .background { smoke .mask(alignment: .trailing) { Rectangle() } .allowsHitTesting(false) } .particleLayerBackground(layer: layer) { smoke .overlay { Rectangle() .blendMode(.destinationOut) } .compositingGroup() .allowsHitTesting(false) } } private var smoke: some View { GeometryReader { proxy in ZStack { ForEach(Array(particles.enumerated()), id: \.element) { (offset, particle) in #if os(iOS) || os(visionOS) let image = UIImage(named: particle, in: .module, with: nil)!.cgImage! #elseif os(macOS) let image = Bundle.module.image(forResource: particle)!.cgImage(forProposedRect: nil, context: nil, hints: nil)! #endif SmokeLayerView(size: proxy.size, isActive: isActive, particle: image, seed: UInt32(offset)) } } } } } #if os(iOS) || os(visionOS) private class EmitterView: UIView { override class var layerClass : AnyClass { return CAEmitterLayer.self } var emitterLayer: CAEmitterLayer { layer as! CAEmitterLayer } } #endif #if os(macOS) private class EmitterView: NSView { override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.wantsLayer = true layer?.masksToBounds = false } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func makeBackingLayer() -> CALayer { CAEmitterLayer() } var emitterLayer: CAEmitterLayer { layer as! CAEmitterLayer } override var isFlipped: Bool { return true } } #endif private struct SmokeLayerView: ViewRepresentable { var size: CGSize var isActive: Bool var particle: CGImage var seed: UInt32 func makeView(context: Context) -> EmitterView { let view = EmitterView() let emitterLayer = view.emitterLayer emitterLayer.seed = seed let particleScale = size.width / 750.0 let particleWidth: CGFloat = 256 let inset: CGFloat = particleWidth * particleScale / 2.25 do { emitterLayer.emitterPosition = CGRect(origin: .zero, size: size) .divided(atDistance: 100, from: .minYEdge) .slice .center emitterLayer.emitterSize = CGRect(origin: .zero, size: size) .divided(atDistance: 100, from: .minYEdge) .slice .insetBy(dx: inset, dy: inset) .size emitterLayer.emitterShape = .rectangle let cell = CAEmitterCell() cell.birthRate = max(10.0, Float(size.width / 5.0)) cell.lifetime = 1.5 cell.velocity = min(175, size.width * 0.75) cell.velocityRange = 10 cell.spinRange = .pi cell.alphaRange = 1.0 cell.alphaSpeed = -1.0 cell.scale = size.width / 750.0 cell.scaleRange = size.width / 1000.0 cell.scaleSpeed = size.width / -2000.0 cell.emissionRange = .pi * 0.1 cell.emissionLongitude = .pi * -0.5 cell.contents = particle emitterLayer.emitterCells = [cell] emitterLayer.lifetime = isActive ? 1 : 0 } return view } func updateView(_ view: EmitterView, context: Context) { view.emitterLayer.lifetime = isActive ? 1 : 0 } } #if DEBUG struct ContinuousParticleEffect_Previews: PreviewProvider { private struct Preview: View { @State private var isEnabled: Bool = true var body: some View { GroupBox("Smoke") { VStack { Toggle("Enabled", isOn: $isEnabled) Button { } label: { Label("Burn", systemImage: "opticaldisc.fill") .foregroundColor(.orange) .font(.title3) } // .buttonBorderShape(.capsule) .buttonStyle(.borderedProminent) .controlSize(.large) .conditionalEffect(.smoke, condition: isEnabled) .tint(.init(white: 0.3)) } } .padding() } } private struct PreviewS: View { @State private var isEnabled: Bool = true var body: some View { GroupBox("Smoke") { VStack { Toggle("Enabled", isOn: $isEnabled) Button { } label: { Label("Burn", systemImage: "opticaldisc.fill") .foregroundColor(.orange) .font(.caption) } .buttonStyle(.borderedProminent) .controlSize(.small) .conditionalEffect(.smoke, condition: isEnabled) .tint(.init(white: 0.3)) } } .padding() } } private struct Preview2: View { @State private var isEnabled: Bool = true var body: some View { GroupBox("Smoke") { VStack { Toggle("Enabled", isOn: $isEnabled) Button { } label: { Label("Burn", systemImage: "opticaldisc.fill") .foregroundColor(.orange) .font(.largeTitle) .padding(.horizontal, 80) .padding(.vertical, 200) } .buttonStyle(.borderedProminent) // .buttonBorderShape(.roundedRectangle(radius: 70)) .controlSize(.large) .conditionalEffect(.smoke, condition: isEnabled) .tint(.init(white: 0.3)) } } .padding() } } private struct Preview3: View { @State private var isEnabled: Bool = true var body: some View { NavigationView { ScrollView { GroupBox("Smoke") { VStack { Button { } label: { Label("Burn", systemImage: "opticaldisc.fill") .foregroundColor(.orange) .font(.largeTitle) .padding() .padding() } .buttonStyle(.borderedProminent) .controlSize(.large) .conditionalEffect(.smoke(layer: .named("root")), condition: isEnabled) .tint(.init(white: 0.3)) Toggle("Enabled", isOn: $isEnabled) } } .clipped() .padding() } .navigationTitle("Smoke") } .particleLayer(name: "root") } } #if os(iOS) private struct PreviewLayer: View { @State private var isEnabled: Bool = true var body: some View { VStack { GeometryReader { proxy in SmokeLayerView(size: proxy.size, isActive: isEnabled, particle: UIImage(named: "anvil_smoke_gray", in: .module, with: nil)!.cgImage!, seed: 0) } .frame(width: 200, height: 100) .border(.red) Toggle("Enabled", isOn: $isEnabled) } .padding() } } #endif private struct PreviewAlt: View { @State private var isEnabled: Bool = true var body: some View { GroupBox("Smoke") { VStack { Toggle("Enabled", isOn: $isEnabled) Button { } label: { Label("Burn", systemImage: "opticaldisc.fill") .foregroundColor(.orange) .font(.title3) } // .buttonBorderShape(.capsule) .buttonStyle(.borderedProminent) .controlSize(.large) .conditionalEffect(.smoke, condition: isEnabled) .tint(.init(white: 0.3)) } } .padding() } } static var previews: some View { Preview() .preferredColorScheme(.dark) .previewDisplayName("Dark") Preview() .preferredColorScheme(.light) .previewDisplayName("Light") PreviewS() .preferredColorScheme(.dark) .previewDisplayName("Small") Preview2() .preferredColorScheme(.dark) .previewDisplayName("Large") Preview3() .preferredColorScheme(.dark) .previewDisplayName("Particle Layer") #if os(iOS) PreviewLayer() .previewDisplayName("Emitter Layer") #endif PreviewAlt() .preferredColorScheme(.dark) .previewDisplayName("Emitter Dark") } } #endif