mirror of
https://github.com/EmergeTools/Pow.git
synced 2026-03-25 08:55:50 +00:00
373 lines
11 KiB
Swift
373 lines
11 KiB
Swift
import SwiftUI
|
|
import simd
|
|
#if os(iOS) && EMG_PREVIEWS
|
|
import SnapshotPreferences
|
|
#endif
|
|
|
|
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 {
|
|
Group {
|
|
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")
|
|
}
|
|
#if os(iOS) && EMG_PREVIEWS
|
|
.emergeSnapshotPrecision(0)
|
|
#endif
|
|
}
|
|
}
|
|
#endif
|