├── Sources └── Confetti3D │ ├── Resources │ ├── confetti_01.png │ ├── confetti_02.png │ ├── confetti_03.png │ ├── confetti_04.png │ ├── confetti_05.png │ ├── confetti_06.png │ ├── confetti_07.png │ └── confetti_08.png │ ├── Model │ ├── Confetti │ │ ├── C3DConfetti.swift │ │ └── C3DConfettiOptions.swift │ ├── C3DConfettiType.swift │ └── Glitter │ │ └── C3DGlitterOptions.swift │ └── View │ └── C3DView.swift ├── Package.swift ├── LICENSE └── README.md /Sources/Confetti3D/Resources/confetti_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime-day/Confetti3D/HEAD/Sources/Confetti3D/Resources/confetti_01.png -------------------------------------------------------------------------------- /Sources/Confetti3D/Resources/confetti_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime-day/Confetti3D/HEAD/Sources/Confetti3D/Resources/confetti_02.png -------------------------------------------------------------------------------- /Sources/Confetti3D/Resources/confetti_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime-day/Confetti3D/HEAD/Sources/Confetti3D/Resources/confetti_03.png -------------------------------------------------------------------------------- /Sources/Confetti3D/Resources/confetti_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime-day/Confetti3D/HEAD/Sources/Confetti3D/Resources/confetti_04.png -------------------------------------------------------------------------------- /Sources/Confetti3D/Resources/confetti_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime-day/Confetti3D/HEAD/Sources/Confetti3D/Resources/confetti_05.png -------------------------------------------------------------------------------- /Sources/Confetti3D/Resources/confetti_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime-day/Confetti3D/HEAD/Sources/Confetti3D/Resources/confetti_06.png -------------------------------------------------------------------------------- /Sources/Confetti3D/Resources/confetti_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime-day/Confetti3D/HEAD/Sources/Confetti3D/Resources/confetti_07.png -------------------------------------------------------------------------------- /Sources/Confetti3D/Resources/confetti_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxime-day/Confetti3D/HEAD/Sources/Confetti3D/Resources/confetti_08.png -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Confetti3D", 6 | platforms: [ 7 | .iOS(.v15) 8 | ], 9 | products: [ 10 | .library( 11 | name: "Confetti3D", 12 | targets: ["Confetti3D"]), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "Confetti3D", 17 | resources: [ 18 | .process("Resources") 19 | ] 20 | ), 21 | .testTarget( 22 | name: "Confetti3DTests", 23 | dependencies: ["Confetti3D"] 24 | ), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Sources/Confetti3D/Model/Confetti/C3DConfetti.swift: -------------------------------------------------------------------------------- 1 | // 2 | // C3DConfetti.swift 3 | // Confetti3D 4 | // 5 | // Created by Maxime Daymard on 26/10/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | /// A single confetti element, consisting of an image and an optional color overlay. 11 | public struct C3DConfetti: Sendable { 12 | 13 | /// The image used to render the confetti. 14 | let image: UIImage 15 | 16 | /// An optional tint color applied to the image. 17 | let color: UIColor? 18 | 19 | /// Initializes a new confetti element. 20 | /// 21 | /// - Parameters: 22 | /// - image: The image to be used for the confetti shape. 23 | /// - color: An optional color overlay applied to the image. 24 | public init(image: UIImage, color: UIColor?) { 25 | self.image = image 26 | self.color = color 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Confetti3D/Model/C3DConfettiType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // C3DConfettiType.swift 3 | // Confetti3D 4 | // 5 | // Created by Maxime Daymard on 26/10/2024. 6 | // 7 | 8 | /// Defines the type of confetti effect to be rendered. 9 | /// 10 | /// - `confetti`: A classic confetti effect using a variety of shapes and colors. 11 | /// - `glitter`: A glitter effect with warm, metallic-inspired tones for a more elegant or festive style. 12 | public enum C3DConfettiType: Sendable { 13 | /// A confetti effect using the provided `C3DConfettiOptions`. 14 | case confetti(C3DConfettiOptions) 15 | 16 | /// A glitter effect using the provided `C3DGlitterOptions`. 17 | case glitter(C3DGlitterOptions) 18 | } 19 | 20 | extension C3DConfettiType { 21 | /// The default confetti type used by the system. 22 | /// 23 | /// Defaults to `.confetti` with `C3DConfettiOptions.defaultOptions`. 24 | /// This provides a colorful and playful visual effect. 25 | public static let `default`: C3DConfettiType = .confetti(.defaultOptions) 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Maxime Daymard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎉 Confetti3D 2 | 3 | **Confetti3D** is a lightweight and customizable Swift package that allows you to easily add confetti effects to your iOS applications using **SwiftUI** or **UIKit**. Under the hood, it leverages **SceneKit** to provide smooth and efficient 3D confetti animations. 4 | 5 | ## ✨ Features 6 | 7 | - 🎨 Fully customizable confetti shapes, colors, and behaviors 8 | - 🏎️ Optimized for performance using SceneKit 9 | - 📱 Works seamlessly with both SwiftUI and UIKit 10 | - 🛠️ Simple and easy-to-use API 11 | - 📡 **Interactive effects**: Confetti adapts to the device's **accelerometer**, reacting to movements for a more immersive experience 12 | 13 | | Confetti | Glitter | 14 | |----------|----------| 15 | | ![Confetti GIF](https://github.com/maxime-day/confetti3Dgifs/blob/main/confetti.gif) | ![Glitter GIF](https://github.com/maxime-day/confetti3Dgifs/blob/main/glitter.gif) | 16 | (looks a bit laggy here but it's smooth on devices) 17 | 18 | ## 📦 Installation 19 | 20 | Confetti3D is available via **Swift Package Manager (SPM)**. To install it, add the following dependency to your `Package.swift`: 21 | 22 | ```swift 23 | .package(url: "https://github.com/maxime-day/Confetti3D.git", from: "1.0.0") 24 | ``` 25 | 26 | ## Usage 27 | 28 | ``` swift 29 | struct Confetti3DTestView: View { 30 | 31 | private let confettiView = C3DView() 32 | 33 | var body: some View { 34 | ZStack { 35 | confettiView 36 | } 37 | .onAppear { 38 | confettiView.throwConfetti() 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | Two confetti types exist for now : `confetti` or `glitter` . `default` is confetti. 45 | 46 | You can customize the images for the confetti type : 47 | 48 | ```swift 49 | let customConfetti = [ 50 | C3DConfetti(image: UIImage(named: "confetti_01")!, 51 | color: .blue), 52 | C3DConfetti(image: UIImage(named: "confetti_02")!, 53 | color: .green), 54 | C3DConfetti(image: UIImage(named: "confetti_03")!, 55 | color: .yellow), 56 | ] 57 | ``` 58 | 59 | And also change some parameters (birth rate, emission duration, life span, and accelerometer) : 60 | 61 | ```swift 62 | confettiView.throwConfetti(type: .confetti(C3DConfettiOptions(confetti: customConfetti, 63 | isAffectedByGravity: true, 64 | birthRate: 20, 65 | emissionDuration: 3, 66 | lifeSpan: 15, 67 | size: 1.0, 68 | position: SCNVector3(0, 0.75, 0), 69 | emittingDirection: SCNVector3(0, -1, 0)))) 70 | ``` 71 | 72 | The `glitter` type also has the same parameters for now. 73 | -------------------------------------------------------------------------------- /Sources/Confetti3D/Model/Confetti/C3DConfettiOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // C3DConfettiOptions.swift 3 | // Confetti3D 4 | // 5 | // Created by Maxime Daymard on 26/10/2024. 6 | // 7 | 8 | import UIKit 9 | import SceneKit 10 | 11 | /// Configuration options for the confetti particle system. 12 | public struct C3DConfettiOptions: Sendable { 13 | /// The list of confetti elements to emit. 14 | let confetti: [C3DConfetti] 15 | 16 | /// Indicates whether the confetti is affected by gravity. 17 | let isAffectedByGravity: Bool 18 | 19 | /// The number of particles emitted per second. 20 | let birthRate: Int 21 | 22 | /// The duration (in seconds) during which particles are emitted. 23 | let emissionDuration: TimeInterval 24 | 25 | /// The total lifespan (in seconds) of each particle. 26 | let lifeSpan: TimeInterval 27 | 28 | /// The base size multiplier applied to each confetti image. 29 | let size: CGFloat 30 | 31 | /// The position of the emitter 32 | let position: SCNVector3 33 | 34 | /// The direction of the emitter 35 | let emittingDirection: SCNVector3 36 | 37 | /// Initializes a new set of options for the confetti emitter. 38 | /// 39 | /// - Parameters: 40 | /// - confetti: An array of `C3DConfetti` elements to be used in the emitter. 41 | /// - isAffectedByGravity: Whether particles should be influenced by gravity. Defaults to `true`. 42 | /// - birthRate: The number of particles emitted per second. Defaults to `40`. 43 | /// - emissionDuration: The duration during which the emitter releases particles. Defaults to `3` seconds. 44 | /// - lifeSpan: The lifespan of each confetti particle. Defaults to `10` seconds. 45 | /// - size: A scale factor applied to confetti images. Defaults to `1`. 46 | public init(confetti: [C3DConfetti], 47 | isAffectedByGravity: Bool = true, 48 | birthRate: Int = 70, 49 | emissionDuration: TimeInterval = 1, 50 | lifeSpan: TimeInterval = 10, 51 | size: CGFloat = 1, 52 | position: SCNVector3 = SCNVector3(0, 0, 0), 53 | emittingDirection: SCNVector3 = SCNVector3(0, 1, 0)) { 54 | self.confetti = confetti 55 | self.isAffectedByGravity = isAffectedByGravity 56 | self.birthRate = birthRate 57 | self.emissionDuration = emissionDuration 58 | self.lifeSpan = lifeSpan 59 | self.size = size 60 | self.position = position 61 | self.emittingDirection = emittingDirection 62 | } 63 | } 64 | 65 | extension C3DConfettiOptions { 66 | /// A predefined set of default confetti options using the default confetti assets. 67 | /// 68 | /// Includes 8 colorful confetti images with associated tint colors, 69 | /// default emission values, and gravity enabled. 70 | public static let defaultOptions = C3DConfettiOptions(confetti: Constants.defaultConfetti) 71 | } 72 | 73 | fileprivate enum Constants { 74 | /// The default confetti elements used in `C3DConfettiOptions.defaultOptions`. 75 | /// 76 | /// Each confetti is represented by a unique image located in the module bundle, 77 | /// and is paired with a specific color overlay to enhance visual diversity. 78 | static let defaultConfetti = [ 79 | C3DConfetti(image: UIImage(named: "confetti_01", in: .module, with: nil)!, 80 | color: .blue), 81 | C3DConfetti(image: UIImage(named: "confetti_02", in: .module, with: nil)!, 82 | color: .green), 83 | C3DConfetti(image: UIImage(named: "confetti_03", in: .module, with: nil)!, 84 | color: .yellow), 85 | C3DConfetti(image: UIImage(named: "confetti_04", in: .module, with: nil)!, 86 | color: .purple), 87 | C3DConfetti(image: UIImage(named: "confetti_05", in: .module, with: nil)!, 88 | color: .red), 89 | C3DConfetti(image: UIImage(named: "confetti_06", in: .module, with: nil)!, 90 | color: .cyan), 91 | C3DConfetti(image: UIImage(named: "confetti_07", in: .module, with: nil)!, 92 | color: UIColor(red: 1, green: 0.1, blue: 0, alpha: 1)), // Orange 93 | C3DConfetti(image: UIImage(named: "confetti_08", in: .module, with: nil)!, 94 | color: UIColor(red: 1, green: 0.2, blue: 0, alpha: 1)), // Gold 95 | ] 96 | } 97 | 98 | -------------------------------------------------------------------------------- /Sources/Confetti3D/Model/Glitter/C3DGlitterOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // C3DGlitterOptions.swift 3 | // Confetti3D 4 | // 5 | // Created by Maxime Daymard on 26/10/2024. 6 | // 7 | 8 | import UIKit 9 | import SceneKit 10 | 11 | /// Configuration options for the glitter particle system. 12 | public struct C3DGlitterOptions: Sendable { 13 | /// The list of confetti elements to emit. 14 | let confetti: [C3DConfetti] 15 | 16 | /// Indicates whether the confetti is affected by gravity. 17 | let isAffectedByGravity: Bool 18 | 19 | /// The number of particles emitted per second. 20 | let birthRate: Int 21 | 22 | /// The duration (in seconds) during which particles are emitted. 23 | let emissionDuration: TimeInterval 24 | 25 | /// The total lifespan (in seconds) of each particle. 26 | let lifeSpan: TimeInterval 27 | 28 | /// The base size multiplier applied to each confetti image. 29 | let size: CGFloat 30 | 31 | /// The position of the emitter 32 | let position: SCNVector3 33 | 34 | /// The direction of the emitter 35 | let emittingDirection: SCNVector3 36 | 37 | /// Initializes a new set of options for the confetti emitter. 38 | /// 39 | /// - Parameters: 40 | /// - confetti: An array of `C3DConfetti` elements to be used in the emitter. 41 | /// - isAffectedByGravity: Whether particles should be influenced by gravity. Defaults to `true`. 42 | /// - birthRate: The number of particles emitted per second. Defaults to `40`. 43 | /// - emissionDuration: The duration during which the emitter releases particles. Defaults to `3` seconds. 44 | /// - lifeSpan: The lifespan of each confetti particle. Defaults to `10` seconds. 45 | /// - size: A scale factor applied to confetti images. Defaults to `1`. 46 | public init(confetti: [C3DConfetti], 47 | isAffectedByGravity: Bool = true, 48 | birthRate: Int = 700, 49 | emissionDuration: TimeInterval = 0.5, 50 | lifeSpan: TimeInterval = 12, 51 | size: CGFloat = 1, 52 | position: SCNVector3 = SCNVector3(0, 0, 0), 53 | emittingDirection: SCNVector3 = SCNVector3(0, 1, 0)) { 54 | self.confetti = confetti 55 | self.isAffectedByGravity = isAffectedByGravity 56 | self.birthRate = birthRate 57 | self.emissionDuration = emissionDuration 58 | self.lifeSpan = lifeSpan 59 | self.size = size 60 | self.position = position 61 | self.emittingDirection = emittingDirection 62 | } 63 | } 64 | 65 | extension C3DGlitterOptions { 66 | /// A predefined set of glitter options using a single glitter image 67 | /// with multiple warm and metallic-inspired tint colors. 68 | /// 69 | /// This configuration is ideal for celebratory or elegant effects, 70 | /// using the same image tinted with variations like gold, copper, and rose. 71 | public static let defaultOptions = C3DGlitterOptions(confetti: Constants.defaultGlitter) 72 | } 73 | 74 | fileprivate enum Constants { 75 | /// The base glitter image used for all glitter particles. 76 | /// 77 | /// Loaded from the module bundle. This image is reused with different color overlays. 78 | static let defaultGlitterImage = UIImage(named: "confetti_08", in: .module, with: nil)! 79 | 80 | /// The default glitter configuration with a warm metallic color palette. 81 | /// 82 | /// Includes multiple `C3DConfetti` instances with the same base image but 83 | /// different tint colors such as orange, champagne pink, copper, bronze, and gold. 84 | static let defaultGlitter = [ 85 | C3DConfetti(image: defaultGlitterImage, 86 | color: .orange), 87 | C3DConfetti(image: defaultGlitterImage, 88 | color: .white), 89 | C3DConfetti(image: defaultGlitterImage, 90 | color: .brown), 91 | C3DConfetti(image: defaultGlitterImage, 92 | color: UIColor(red: 1.0, green: 0.84, blue: 0.72, alpha: 1.0)), // Champagne pink 93 | C3DConfetti(image: defaultGlitterImage, 94 | color: UIColor(red: 0.95, green: 0.6, blue: 0.3, alpha: 1.0)), // Light copper 95 | C3DConfetti(image: defaultGlitterImage, 96 | color: UIColor(red: 0.6, green: 0.4, blue: 0.2, alpha: 1.0)), // Warm bronze 97 | C3DConfetti(image: defaultGlitterImage, 98 | color: UIColor(red: 0.85, green: 0.5, blue: 0.4, alpha: 1.0)), // Deep rose gold 99 | C3DConfetti(image: defaultGlitterImage, 100 | color: UIColor(red: 1.0, green: 0.78, blue: 0.2, alpha: 1.0)), // Classic warm gold 101 | ] 102 | } 103 | -------------------------------------------------------------------------------- /Sources/Confetti3D/View/C3DView.swift: -------------------------------------------------------------------------------- 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | 4 | import SceneKit 5 | import CoreMotion 6 | import SwiftUI 7 | import Combine 8 | 9 | /// A protocol that defines the ability to trigger a confetti effect. 10 | public protocol ConfettiThrower { 11 | /// Triggers the confetti animation using the specified type. 12 | /// 13 | /// - Parameter type: The type of confetti effect to throw (e.g., `.confetti`, `.glitter`). 14 | func throwConfetti(type: C3DConfettiType) 15 | } 16 | 17 | // MARK: - SwiftUI 18 | 19 | /// A SwiftUI-compatible wrapper for displaying and triggering 3D confetti effects. 20 | /// 21 | /// Internally wraps a `UIC3DView` and conforms to `ConfettiThrower` 22 | /// for triggering effects programmatically. 23 | public struct C3DView: UIViewRepresentable, ConfettiThrower { 24 | public typealias UIViewType = UIC3DView 25 | public typealias Context = UIViewRepresentableContext 26 | 27 | private let confettiView = UIC3DView() 28 | 29 | public init() {} 30 | 31 | public func updateUIView(_ uiView: UIViewType, context: Context) { 32 | } 33 | 34 | public func makeUIView(context: Context) -> UIViewType { 35 | return confettiView 36 | } 37 | 38 | public func throwConfetti(type: C3DConfettiType = .default) { 39 | confettiView.throwConfetti(type: type) 40 | } 41 | } 42 | 43 | // MARK: - UIKit 44 | 45 | /// A UIKit-compatible 3D SceneKit view for rendering and triggering confetti effects. 46 | /// 47 | /// Subclass of `SCNView` that conforms to `ConfettiThrower`, 48 | /// allowing programmatic triggering of confetti animations. 49 | public final class UIC3DView: SCNView, ConfettiThrower { 50 | private lazy var motionManager = CMMotionManager() 51 | private lazy var mainParticlesNode = SCNNode() 52 | 53 | public init() { 54 | super.init(frame: .zero) 55 | setup() 56 | } 57 | 58 | public override init(frame: CGRect, options: [String: Any]? = nil) { 59 | super.init(frame: frame, options: options) 60 | setup() 61 | } 62 | 63 | required init?(coder: NSCoder) { 64 | super.init(coder: coder) 65 | setup() 66 | } 67 | 68 | private func setup() { 69 | let scene = SCNScene() 70 | scene.background.contents = UIColor.clear 71 | scene.physicsWorld.gravity = SCNVector3(x: 0, y: -1, z: 0) 72 | scene.physicsWorld.speed = 6 73 | self.scene = scene 74 | 75 | mainParticlesNode.position = SCNVector3(0, 0, 0) 76 | scene.rootNode.addChildNode(mainParticlesNode) 77 | 78 | let cameraNode = SCNNode() 79 | scene.rootNode.addChildNode(cameraNode) 80 | cameraNode.camera = SCNCamera() 81 | cameraNode.position = SCNVector3(0, 0.1, 3.5) 82 | 83 | let turbulenceNode = SCNNode() 84 | let turbulenceField = SCNPhysicsField.turbulenceField(smoothness: 0, animationSpeed: 0) 85 | scene.rootNode.addChildNode(turbulenceNode) 86 | turbulenceNode.position = SCNVector3(0, 0.5, -1) 87 | turbulenceNode.physicsField = turbulenceField 88 | turbulenceField.strength = 4 89 | turbulenceField.falloffExponent = 0 90 | turbulenceField.minimumDistance = 0 91 | 92 | let turbulenceNode2 = SCNNode() 93 | let turbulenceField2 = SCNPhysicsField.turbulenceField(smoothness: 0, animationSpeed: 0) 94 | scene.rootNode.addChildNode(turbulenceNode2) 95 | turbulenceNode2.position = SCNVector3(0, 0.2, -0.2) 96 | turbulenceNode2.physicsField = turbulenceField2 97 | turbulenceField2.strength = 4 98 | turbulenceField2.falloffExponent = 0 99 | turbulenceField2.minimumDistance = 0 100 | 101 | allowsCameraControl = false 102 | isUserInteractionEnabled = false 103 | backgroundColor = .clear 104 | } 105 | 106 | public func throwConfetti(type: C3DConfettiType = .default) { 107 | print("Throwing confetti") 108 | 109 | switch type { 110 | case .confetti(let options): 111 | setupConfetti(with: options) 112 | case .glitter(let options): 113 | setupGlitter(with: options) 114 | } 115 | } 116 | 117 | private func setupConfetti(with options: C3DConfettiOptions) { 118 | let confetti = options.confetti 119 | 120 | let nbVariations = 2 121 | 122 | for _ in 1...nbVariations { 123 | for confetto in confetti { 124 | let particleSystem = SCNParticleSystem() 125 | // Image & color 126 | particleSystem.particleImage = confetto.image 127 | particleSystem.particleColor = confetto.color ?? .white 128 | particleSystem.particleColorVariation = SCNVector4(0, 0, 0, 0) 129 | 130 | // Emitter 131 | particleSystem.birthRate = CGFloat(options.birthRate / nbVariations) 132 | particleSystem.emissionDuration = options.emissionDuration 133 | particleSystem.emittingDirection = options.emittingDirection 134 | particleSystem.spreadingAngle = 40 135 | particleSystem.particleAngleVariation = 360 136 | 137 | // Simulation 138 | particleSystem.particleLifeSpan = options.lifeSpan 139 | particleSystem.particleVelocity = 3 + CGFloat.random(in: 0...0.01) 140 | particleSystem.particleAngularVelocity = CGFloat(Int.random(in: 100...400)) 141 | particleSystem.particleAngularVelocityVariation = 100 142 | 143 | // Image 144 | particleSystem.particleSize = 0.05 * options.size 145 | particleSystem.particleSizeVariation = 0.05 146 | 147 | // Rendering 148 | particleSystem.isBlackPassEnabled = true 149 | particleSystem.writesToDepthBuffer = true 150 | particleSystem.orientationMode = .free 151 | 152 | // Physics 153 | particleSystem.isAffectedByGravity = true 154 | particleSystem.isAffectedByPhysicsFields = true 155 | particleSystem.particleDiesOnCollision = false 156 | particleSystem.particleFriction = 100 157 | 158 | particleSystem.dampingFactor = 1 159 | particleSystem.acceleration = SCNVector3(0, -0.5, 0) 160 | 161 | particleSystem.particleMass = 1.5 162 | particleSystem.particleMassVariation = 0.005 163 | 164 | particleSystem.loops = false 165 | 166 | let particleSystemNode = SCNNode() 167 | particleSystemNode.position = options.position 168 | particleSystemNode.addParticleSystem(particleSystem) 169 | mainParticlesNode.addChildNode(particleSystemNode) 170 | 171 | particleSystemNode.runAction(.fadeOut(duration: options.lifeSpan)) 172 | } 173 | } 174 | 175 | if options.isAffectedByGravity { 176 | startDeviceMotionIfNeeded() 177 | } 178 | } 179 | 180 | private func setupGlitter(with options: C3DGlitterOptions) { 181 | let glitter = options.confetti 182 | let nbVariations = 2 183 | 184 | for _ in 1...nbVariations { 185 | for confetto in glitter { 186 | 187 | let particleSystem = SCNParticleSystem() 188 | particleSystem.particleImage = confetto.image 189 | particleSystem.particleColor = confetto.color ?? .white 190 | particleSystem.particleColorVariation = SCNVector4(0.01, 0.01, 0, 0) 191 | 192 | // Emitter 193 | particleSystem.birthRate = CGFloat(options.birthRate / nbVariations) 194 | particleSystem.emissionDuration = options.emissionDuration 195 | particleSystem.emittingDirection = options.emittingDirection 196 | particleSystem.spreadingAngle = 60 197 | particleSystem.particleAngleVariation = 360 198 | 199 | // Simulation 200 | particleSystem.particleLifeSpan = options.lifeSpan 201 | particleSystem.particleVelocity = 4 202 | particleSystem.particleVelocityVariation = 2 203 | 204 | particleSystem.particleAngularVelocity = CGFloat(Int.random(in: 500...1000)) 205 | particleSystem.particleAngularVelocityVariation = 10 206 | 207 | // Image 208 | particleSystem.particleSize = 0.04 * options.size 209 | particleSystem.particleSizeVariation = 0.005 210 | 211 | // Rendering 212 | particleSystem.isBlackPassEnabled = true 213 | particleSystem.writesToDepthBuffer = true 214 | particleSystem.orientationMode = .free 215 | 216 | // Physics 217 | particleSystem.isAffectedByGravity = true 218 | particleSystem.isAffectedByPhysicsFields = true 219 | particleSystem.particleDiesOnCollision = false 220 | particleSystem.particleFriction = 200 221 | particleSystem.dampingFactor = 1.8 222 | particleSystem.acceleration = SCNVector3(0, 0.3, 0) 223 | 224 | particleSystem.loops = false 225 | 226 | let particleSystemNode = SCNNode() 227 | particleSystemNode.position = options.position 228 | particleSystemNode.addParticleSystem(particleSystem) 229 | mainParticlesNode.addChildNode(particleSystemNode) 230 | 231 | particleSystemNode.runAction(.fadeOut(duration: options.lifeSpan)) 232 | } 233 | } 234 | 235 | if options.isAffectedByGravity { 236 | startDeviceMotionIfNeeded() 237 | } 238 | } 239 | 240 | private func startDeviceMotionIfNeeded() { 241 | guard !motionManager.isDeviceMotionActive, 242 | motionManager.isDeviceMotionAvailable else { 243 | return 244 | } 245 | motionManager.deviceMotionUpdateInterval = 1.0 / 30.0 246 | motionManager.startDeviceMotionUpdates(to: .main) { [weak self] (data, error) in 247 | data.map { 248 | self?.updateGravity(with: $0) 249 | } 250 | error.map { 251 | print("Error getting device motion: \($0.localizedDescription)") 252 | } 253 | } 254 | } 255 | 256 | private func updateGravity(with data: CMDeviceMotion) { 257 | let gravity = data.gravity 258 | scene?.physicsWorld.gravity = SCNVector3(gravity.x, gravity.y, gravity.z) 259 | } 260 | } 261 | --------------------------------------------------------------------------------