diff --git a/README.md b/README.md index f522b41..336b355 100644 --- a/README.md +++ b/README.md @@ -100,8 +100,9 @@ struct ContentView: View { } } - func createSnow() -> VortexSystem { - let system = VortexSystem(tags: ["circle"]) + func createSnow() -> VortexSettings { + let system = VortexSettings() + system.tags = ["circle"] system.position = [0.5, 0] system.speed = 0.5 system.speedVariation = 0.25 @@ -170,7 +171,7 @@ The initializer for `VortexSystem` takes a wide range of configuration options t The `VortexSystem` initializer parameters are: - `tags` (`[String]`, *required*) should be the names of one or more views you're passing into a `VortexView` to render this particle system. This string array might only be *some* of the views you're passing in – you might have a secondary system that uses different tags, for example. -- `secondarySystems` (`[VortexSystem]`, defaults to an empty array) should contain all the secondary particle systems that should be attached to this primary emitter. +- `secondarySettings` (`[VortexSettings]`, defaults to an empty array) should contain all the secondary particle settings that should be attached to this primary emitter. - `spawnOccasion` (`SpawnOccasion`, defaults to `.onBirth`) determines when this secondary system should be created. Ignored if this is your primary particle system. - `position` (`SIMD2`, defaults to `[0.5, 0.5]`) determines the center position of this particle system. - `shape` (`Shape`, defaults to `.point`) determines the bounds of where particles are emitted. diff --git a/Sandbox/Sandbox/PreviewViews/ConfettiView.swift b/Sandbox/Sandbox/PreviewViews/ConfettiView.swift index 71f0fa2..0e5b963 100644 --- a/Sandbox/Sandbox/PreviewViews/ConfettiView.swift +++ b/Sandbox/Sandbox/PreviewViews/ConfettiView.swift @@ -15,7 +15,7 @@ struct ConfettiView: View { ZStack { Text("Tap anywhere to create confetti.") - VortexView(.confetti.makeUniqueCopy()) { + VortexView(.confetti) { Rectangle() .fill(.white) .frame(width: 16, height: 16) diff --git a/Sandbox/Sandbox/PreviewViews/FireView.swift b/Sandbox/Sandbox/PreviewViews/FireView.swift index 3d2a636..54c6710 100644 --- a/Sandbox/Sandbox/PreviewViews/FireView.swift +++ b/Sandbox/Sandbox/PreviewViews/FireView.swift @@ -23,7 +23,7 @@ struct FireView: View { .offset(y: 50) } - VortexView(.fire.makeUniqueCopy()) { + VortexView(.fire) { Circle() .fill(.white) .frame(width: 32) diff --git a/Sandbox/Sandbox/PreviewViews/FirefliesView.swift b/Sandbox/Sandbox/PreviewViews/FirefliesView.swift index 4de2926..8e9bb16 100644 --- a/Sandbox/Sandbox/PreviewViews/FirefliesView.swift +++ b/Sandbox/Sandbox/PreviewViews/FirefliesView.swift @@ -23,7 +23,7 @@ struct FirefliesView: View { .padding(.bottom, 20) } - VortexView(.fireflies.makeUniqueCopy()) { + VortexView(.fireflies) { Circle() .fill(.white) .frame(width: 32) diff --git a/Sandbox/Sandbox/PreviewViews/FireworksView.swift b/Sandbox/Sandbox/PreviewViews/FireworksView.swift index f792c1d..45c02e3 100644 --- a/Sandbox/Sandbox/PreviewViews/FireworksView.swift +++ b/Sandbox/Sandbox/PreviewViews/FireworksView.swift @@ -11,7 +11,7 @@ import Vortex /// A sample view demonstrating the built-in fireworks preset. struct FireworksView: View { var body: some View { - VortexView(.fireworks.makeUniqueCopy()) { + VortexView(.fireworks) { Circle() .fill(.white) .frame(width: 32) diff --git a/Sandbox/Sandbox/PreviewViews/MagicView.swift b/Sandbox/Sandbox/PreviewViews/MagicView.swift index 1fa2dc4..f5b05b3 100644 --- a/Sandbox/Sandbox/PreviewViews/MagicView.swift +++ b/Sandbox/Sandbox/PreviewViews/MagicView.swift @@ -11,7 +11,7 @@ import Vortex /// A sample view demonstrating the built-in magic preset. struct MagicView: View { var body: some View { - VortexView(.magic.makeUniqueCopy()) { + VortexView(.magic) { Image(.sparkle) .blendMode(.plusLighter) .tag("sparkle") diff --git a/Sandbox/Sandbox/PreviewViews/RainView.swift b/Sandbox/Sandbox/PreviewViews/RainView.swift index 07c6d22..544bf3a 100644 --- a/Sandbox/Sandbox/PreviewViews/RainView.swift +++ b/Sandbox/Sandbox/PreviewViews/RainView.swift @@ -11,7 +11,7 @@ import Vortex /// A sample view demonstrating the built-in rain preset. struct RainView: View { var body: some View { - VortexView(.rain.makeUniqueCopy()) { + VortexView(.rain) { Circle() .fill(.white) .frame(width: 32) diff --git a/Sandbox/Sandbox/PreviewViews/SmokeView.swift b/Sandbox/Sandbox/PreviewViews/SmokeView.swift index 4d51f7c..8a91814 100644 --- a/Sandbox/Sandbox/PreviewViews/SmokeView.swift +++ b/Sandbox/Sandbox/PreviewViews/SmokeView.swift @@ -11,7 +11,7 @@ import Vortex /// A sample view demonstrating the built-in smoke preset. struct SmokeView: View { var body: some View { - VortexView(.smoke.makeUniqueCopy()) { + VortexView(.smoke) { Circle() .fill(.white) .frame(width: 64) diff --git a/Sandbox/Sandbox/PreviewViews/SnowView.swift b/Sandbox/Sandbox/PreviewViews/SnowView.swift index 0758084..291ff62 100644 --- a/Sandbox/Sandbox/PreviewViews/SnowView.swift +++ b/Sandbox/Sandbox/PreviewViews/SnowView.swift @@ -11,7 +11,7 @@ import Vortex /// A sample view demonstrating the built-in snow preset. struct SnowView: View { var body: some View { - VortexView(.snow.makeUniqueCopy()) { + VortexView(.snow) { Circle() .fill(.white) .frame(width: 24) diff --git a/Sandbox/Sandbox/PreviewViews/SparkView.swift b/Sandbox/Sandbox/PreviewViews/SparkView.swift index 5dab962..232694f 100644 --- a/Sandbox/Sandbox/PreviewViews/SparkView.swift +++ b/Sandbox/Sandbox/PreviewViews/SparkView.swift @@ -11,7 +11,7 @@ import Vortex /// A sample view demonstrating the built-in spark preset. struct SparkView: View { var body: some View { - VortexView(.spark.makeUniqueCopy()) { + VortexView(.spark) { Circle() .fill(.white) .frame(width: 16) diff --git a/Sandbox/Sandbox/PreviewViews/SplashView.swift b/Sandbox/Sandbox/PreviewViews/SplashView.swift index 98604fa..72aff9f 100644 --- a/Sandbox/Sandbox/PreviewViews/SplashView.swift +++ b/Sandbox/Sandbox/PreviewViews/SplashView.swift @@ -13,14 +13,14 @@ import Vortex struct SplashView: View { var body: some View { ZStack { - VortexView(.rain.makeUniqueCopy()) { + VortexView(.rain) { Circle() .fill(.white) .frame(width: 32) .tag("circle") } - VortexView(.splash.makeUniqueCopy()) { + VortexView(.splash) { Circle() .fill(.white) .frame(width: 16, height: 16) diff --git a/Sources/Vortex/Helpers/Array-InterpolatedColor.swift b/Sources/Vortex/Helpers/Array-InterpolatedColor.swift index 5bc12ca..cfb4676 100644 --- a/Sources/Vortex/Helpers/Array-InterpolatedColor.swift +++ b/Sources/Vortex/Helpers/Array-InterpolatedColor.swift @@ -6,12 +6,12 @@ // import SwiftUI -extension Array where Element == VortexSystem.Color { +extension Array where Element == VortexSettings.Color { /// Creates a new color by linearly interpolating between other colors in a color array. /// - Parameter amount: How far through the array we should be reading. For example, /// if the array contains white then black and `amount` is set to 0.5, this will return gray. /// - Returns: A new color created by interpolating existing colors inside the array. - func lerp(by amount: Double) -> VortexSystem.Color { + func lerp(by amount: Double) -> VortexSettings.Color { guard isEmpty == false else { fatalError("Attempting to interpolate an empty color array.") } @@ -35,7 +35,7 @@ extension Array where Element == VortexSystem.Color { let interpolatedBlue = lowerColor.blue.lerp(to: upperColor.blue, amount: interpolationFactor) let interpolatedOpacity = lowerColor.opacity.lerp(to: upperColor.opacity, amount: interpolationFactor) - return VortexSystem.Color( + return VortexSettings.Color( red: interpolatedRed, green: interpolatedGreen, blue: interpolatedBlue, diff --git a/Sources/Vortex/Presets/Confetti.swift b/Sources/Vortex/Presets/Confetti.swift index 2078c0e..6fe9e43 100644 --- a/Sources/Vortex/Presets/Confetti.swift +++ b/Sources/Vortex/Presets/Confetti.swift @@ -7,23 +7,39 @@ import SwiftUI -extension VortexSystem { +extension VortexSettings { /// A built-in effect that creates confetti only when a burst is triggered. /// Relies on "square" and "circle" tags being present – using `Rectangle` /// and `Circle` with frames of 16x16 works well. - public static let confetti: VortexSystem = { - VortexSystem( - tags: ["square", "circle"], - birthRate: 0, - lifespan: 4, - speed: 0.5, - speedVariation: 0.5, - angleRange: .degrees(90), - acceleration: [0, 1], - angularSpeedVariation: [4, 4, 4], - colors: .random(.white, .red, .green, .blue, .pink, .orange, .cyan), - size: 0.5, - sizeVariation: 0.5 - ) + public static let confetti = { + var settings = VortexSettings() + settings.tags = ["square", "circle"] + settings.birthRate = 0 + settings.lifespan = 4 + settings.speed = 0.5 + settings.speedVariation = 0.5 + settings.angleRange = .degrees(90) + settings.acceleration = [0, 1] + settings.angularSpeedVariation = [4, 4, 4] + settings.colors = .random(.white, .red, .green, .blue, .pink, .orange, .cyan) + settings.size = 0.5 + settings.sizeVariation = 0.5 + return settings }() } + +@available(macOS 14.0, *) // needed for .onTapGesture +#Preview { + VortexViewReader { proxy in + ZStack { + Text("Tap anywhere to create confetti.") + + VortexView(.confetti) + .onTapGesture { location in + proxy.move(to: location) + proxy.burst() + proxy.particleSystem?.lifespan = 5 + } + } + } +} diff --git a/Sources/Vortex/Presets/Fire.swift b/Sources/Vortex/Presets/Fire.swift index 877dd24..46474b1 100644 --- a/Sources/Vortex/Presets/Fire.swift +++ b/Sources/Vortex/Presets/Fire.swift @@ -7,11 +7,11 @@ import SwiftUI -extension VortexSystem { +extension VortexSettings { /// A built-in fire effect. Relies on a "circle" tag being present, which should be set to use /// `.blendMode(.plusLighter)`. - public static let fire: VortexSystem = { - VortexSystem( + public static let fire: VortexSettings = { + VortexSettings( tags: ["circle"], shape: .box(width: 0.1, height: 0), birthRate: 300, @@ -25,3 +25,23 @@ extension VortexSystem { ) }() } + +#Preview("Demonstrates a modified fire preset") { + /// Here we modify the default fire settings to extend it across the bottom of the screen + let floorOnFire = { + var settings = VortexSettings(basedOn: .fire) + settings.position = [0.5, 1.02] + settings.shape = .box(width: 1.0, height: 0) + settings.birthRate = 600 + return settings + }() + + VortexView(floorOnFire) { + Circle() + .fill(.white) + .frame(width: 32) + .blur(radius: 3) + .blendMode(.plusLighter) + .tag("circle") + } +} diff --git a/Sources/Vortex/Presets/Fireflies.swift b/Sources/Vortex/Presets/Fireflies.swift index 87be447..28656ac 100644 --- a/Sources/Vortex/Presets/Fireflies.swift +++ b/Sources/Vortex/Presets/Fireflies.swift @@ -7,11 +7,11 @@ import SwiftUI -extension VortexSystem { +extension VortexSettings { /// A built-in firefly effect. Relies on a "circle" tag being present, which should be set to use /// `.blendMode(.plusLighter)`. - public static let fireflies: VortexSystem = { - VortexSystem( + public static let fireflies: VortexSettings = { + VortexSettings( tags: ["circle"], shape: .ellipse(radius: 0.5), birthRate: 200, @@ -35,20 +35,25 @@ extension VortexSystem { @Previewable @State var pressingOptionKey = false VortexViewReader { proxy in ZStack(alignment: .bottom) { - if isDragging { - Text("Release your drag to reset the fireflies.") - .padding(.bottom, 20) + let instructions = if isDragging { + "Release your drag to reset the fireflies." + } else if !pressingOptionKey { + "Drag anywhere to repel the fireflies. Or hold the Option Key" } else { - let instructions = if !pressingOptionKey { - "Drag anywhere to repel the fireflies. Or hold the Option Key" - } else { - "Drag anywhere to attract the fireflies" - } - Text(instructions) - .padding(.bottom, 20) + "Drag anywhere to attract the fireflies" } - VortexView(.fireflies) + Text(instructions) + .padding(.bottom, 20) + + VortexView(.fireflies) { + Circle() + .fill(.white) + .frame(width: 32) + .blur(radius: 3) + .blendMode(.plusLighter) + .tag("circle") + } .onModifierKeysChanged(mask: .option) { _, new in // set the view state based on whether the // `new` EventModifiers value contains a value (that would be the option key) diff --git a/Sources/Vortex/Presets/Fireworks.swift b/Sources/Vortex/Presets/Fireworks.swift index ddabed0..9889778 100644 --- a/Sources/Vortex/Presets/Fireworks.swift +++ b/Sources/Vortex/Presets/Fireworks.swift @@ -7,12 +7,12 @@ import SwiftUI -extension VortexSystem { +extension VortexSettings { /// A built-in fireworks effect, using secondary systems that create sparkles and explosions. /// Relies on a "circle" tag being present, which should be set to use /// `.blendMode(.plusLighter)`. - public static let fireworks: VortexSystem = { - let sparkles = VortexSystem( + public static let fireworks: VortexSettings = { + let sparkles = VortexSettings( tags: ["circle"], spawnOccasion: .onUpdate, emissionLimit: 1, @@ -22,7 +22,7 @@ extension VortexSystem { size: 0.05 ) - let explosion = VortexSystem( + let explosion = VortexSettings( tags: ["circle"], spawnOccasion: .onDeath, position: [0.5, 1], @@ -45,9 +45,9 @@ extension VortexSystem { sizeMultiplierAtDeath: 0 ) - let mainSystem = VortexSystem( + let mainSystem = VortexSettings( tags: ["circle"], - secondarySystems: [sparkles, explosion], + secondarySettings: [sparkles, explosion], position: [0.5, 1], birthRate: 2, emissionLimit: 1000, @@ -62,3 +62,14 @@ extension VortexSystem { return mainSystem }() } + +#Preview("Demonstrates multi-stage effects") { + VortexView(.fireworks) { + Circle() + .fill(.white) + .frame(width: 32) + .blur(radius: 5) + .blendMode(.plusLighter) + .tag("circle") + } +} diff --git a/Sources/Vortex/Presets/Magic.swift b/Sources/Vortex/Presets/Magic.swift index 394545a..390b732 100644 --- a/Sources/Vortex/Presets/Magic.swift +++ b/Sources/Vortex/Presets/Magic.swift @@ -7,11 +7,11 @@ import SwiftUI -extension VortexSystem { +extension VortexSettings { /// A built-in magic effect. Relies on a "sparkle" tag being present, which should be set to use /// `.blendMode(.plusLighter)`. - public static let magic: VortexSystem = { - VortexSystem( + public static let magic = + VortexSettings( tags: ["sparkle"], shape: .ring(radius: 0.5), lifespan: 1.5, @@ -24,5 +24,7 @@ extension VortexSystem { sizeVariation: 0.5, sizeMultiplierAtDeath: 0.01 ) - }() +} +#Preview { + VortexView(.magic) } diff --git a/Sources/Vortex/Presets/Rain.swift b/Sources/Vortex/Presets/Rain.swift index fcf1c1b..5af565e 100644 --- a/Sources/Vortex/Presets/Rain.swift +++ b/Sources/Vortex/Presets/Rain.swift @@ -7,10 +7,10 @@ import SwiftUI -extension VortexSystem { +extension VortexSettings { /// A built-in rain effect. Relies on a "circle" tag being present. - public static let rain: VortexSystem = { - VortexSystem( + public static let rain = + VortexSettings( tags: ["circle"], position: [0.5, 0 ], shape: .box(width: 1.8, height: 0), @@ -28,5 +28,7 @@ extension VortexSystem { sizeVariation: 0.05, stretchFactor: 12 ) - }() +} +#Preview("Demonstrate use of 'rain' preset") { + VortexView(.rain) } diff --git a/Sources/Vortex/Presets/Smoke.swift b/Sources/Vortex/Presets/Smoke.swift index a678e63..59fad5d 100644 --- a/Sources/Vortex/Presets/Smoke.swift +++ b/Sources/Vortex/Presets/Smoke.swift @@ -7,10 +7,9 @@ import SwiftUI -extension VortexSystem { +extension VortexSettings { /// A built-in smoke effect. Relies on a "circle" tag being present. - public static let smoke: VortexSystem = { - VortexSystem( + public static let smoke = VortexSettings( tags: ["circle"], shape: .box(width: 0.05, height: 0), lifespan: 3, @@ -22,5 +21,13 @@ extension VortexSystem { sizeVariation: 0.5, sizeMultiplierAtDeath: 2 ) - }() +} +#Preview("Demonstrate use of 'smoke' preset") { + VortexView(.smoke){ + Circle() + .fill(.white) + .frame(width: 64) + .blur(radius: 10) + .tag("circle") + } } diff --git a/Sources/Vortex/Presets/Snow.swift b/Sources/Vortex/Presets/Snow.swift index c91e20b..dcfa7a2 100644 --- a/Sources/Vortex/Presets/Snow.swift +++ b/Sources/Vortex/Presets/Snow.swift @@ -7,10 +7,10 @@ import SwiftUI -extension VortexSystem { +extension VortexSettings { /// A built-in snow effect. Relies on a "circle" tag being present. - public static let snow: VortexSystem = { - VortexSystem( + public static let snow: VortexSettings = { + VortexSettings( tags: ["circle"], position: [0.5, 0], shape: .box(width: 1, height: 0), @@ -28,5 +28,11 @@ extension VortexSystem { #Preview { // Use the snow preset, using the default symbol for the circle tag - VortexView(.snow.makeUniqueCopy()) + VortexView(.snow){ + Circle() + .fill(.white) + .frame(width: 24) + .blur(radius: 5) + .tag("circle") + } } diff --git a/Sources/Vortex/Presets/Spark.swift b/Sources/Vortex/Presets/Spark.swift index be18fe2..a614760 100644 --- a/Sources/Vortex/Presets/Spark.swift +++ b/Sources/Vortex/Presets/Spark.swift @@ -7,11 +7,10 @@ import SwiftUI -extension VortexSystem { +extension VortexSettings { /// A built-in spark effect. Relies on a "circle" tag being present, which should be set to use /// `.blendMode(.plusLighter)`. - public static let spark: VortexSystem = { - VortexSystem( + public static let spark = VortexSettings( tags: ["circle"], birthRate: 150, emissionDuration: 0.2, @@ -28,5 +27,7 @@ extension VortexSystem { sizeVariation: 0.1, stretchFactor: 8 ) - }() +} +#Preview("Spark preset preview") { + VortexView(.spark) } diff --git a/Sources/Vortex/Presets/Splash.swift b/Sources/Vortex/Presets/Splash.swift index 65f018e..92fd32e 100644 --- a/Sources/Vortex/Presets/Splash.swift +++ b/Sources/Vortex/Presets/Splash.swift @@ -7,39 +7,50 @@ import SwiftUI -extension VortexSystem { +extension VortexSettings { /// A built-in splash effect, designed to accompany the rain present. /// Relies on a "circle" tag being present, which should be set to use /// `.blendMode(.plusLighter)`. - public static let splash: VortexSystem = { - let drops = VortexSystem( - tags: ["circle"], - birthRate: 5, - emissionLimit: 10, - speed: 0.4, - speedVariation: 0.1, - angleRange: .degrees(90), - acceleration: [0, 1], - colors: .random( + public static let splash: VortexSettings = { + var drops = VortexSettings() + drops.tags = ["circle"] + drops.birthRate = 5 + drops.emissionLimit = 10 + drops.speed = 0.4 + drops.speedVariation = 0.1 + drops.angleRange = .degrees(90) + drops.acceleration = [0, 1] + drops.colors = .random( Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.7), Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.6), Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.5) - ), - size: 0.2 - ) + ) + drops.size = 0.2 - let mainSystem = VortexSystem( - tags: ["circle"], - secondarySystems: [drops], - position: [0.5, 1], - shape: .box(width: 1, height: 0), - birthRate: 5, - lifespan: 0.001, - speed: 0, - colors: .single(.clear), - size: 0 - ) + var mainSettings = VortexSettings() + mainSettings.tags = ["circle"] + mainSettings.secondarySettings = [drops] + mainSettings.position = [0.5, 1] + mainSettings.shape = .box(width: 1, height: 0) + mainSettings.birthRate = 5 + mainSettings.speed = 0 + mainSettings.colors = .single(.clear) + mainSettings.size = 0 - return mainSystem + return mainSettings }() } + +#Preview("Splash preview with rain") { + ZStack { + VortexView(.rain) { + Circle() + .fill(.white) + .frame(width: 32) + .tag("circle") + } + + // Display the .splash preset using the default "circle" symbol. + VortexView(.splash) + } +} diff --git a/Sources/Vortex/System/Color.swift b/Sources/Vortex/System/Color.swift index 7e2ed00..492fd57 100644 --- a/Sources/Vortex/System/Color.swift +++ b/Sources/Vortex/System/Color.swift @@ -7,7 +7,7 @@ import SwiftUI -extension VortexSystem { +extension VortexSettings { /// A Vortex color struct that gives easy access to its RGBA values, and is also `Codable`. public struct Color: Codable, ExpressibleByArrayLiteral, Hashable { public var red: Double diff --git a/Sources/Vortex/System/ColorMode.swift b/Sources/Vortex/System/ColorMode.swift index 03287ab..8587521 100644 --- a/Sources/Vortex/System/ColorMode.swift +++ b/Sources/Vortex/System/ColorMode.swift @@ -7,7 +7,7 @@ import Foundation -extension VortexSystem { +extension VortexSettings { /// Controls how colors are applied to particles inside a Vortex system. public enum ColorMode: Codable { /// Particles should always be created with a single color. diff --git a/Sources/Vortex/System/Particle.swift b/Sources/Vortex/System/Particle.swift index 89496e7..55a631e 100644 --- a/Sources/Vortex/System/Particle.swift +++ b/Sources/Vortex/System/Particle.swift @@ -40,10 +40,10 @@ extension VortexSystem { var angularSpeed = SIMD3() /// The colors to use for rendering this particle over time. - var colors: [Color] + var colors: [VortexSettings.Color] /// The current color to use for rendering this particle right now. This is recomputed /// every time its system's `update()` method is called. - var currentColor = Color.white + var currentColor = VortexSettings.Color.white } } diff --git a/Sources/Vortex/System/Shape.swift b/Sources/Vortex/System/Shape.swift index 2c28cfc..ab74bef 100644 --- a/Sources/Vortex/System/Shape.swift +++ b/Sources/Vortex/System/Shape.swift @@ -7,7 +7,7 @@ import Foundation -extension VortexSystem { +extension VortexSettings { /// Controls where particles are created inside the particle system. public enum Shape: Codable { /// All particles are created from the center of the particle system. diff --git a/Sources/Vortex/System/SpawnOccasion.swift b/Sources/Vortex/System/SpawnOccasion.swift index da48ff4..6237f8e 100644 --- a/Sources/Vortex/System/SpawnOccasion.swift +++ b/Sources/Vortex/System/SpawnOccasion.swift @@ -7,7 +7,7 @@ import Foundation -extension VortexSystem { +extension VortexSettings { /// Controls when secondary systems are created. public enum SpawnOccasion: Codable { /// Creates a new system at the same time as creating a new particle. diff --git a/Sources/Vortex/System/VortexSettings.swift b/Sources/Vortex/System/VortexSettings.swift new file mode 100644 index 0000000..12506da --- /dev/null +++ b/Sources/Vortex/System/VortexSettings.swift @@ -0,0 +1,299 @@ +// +// Settings.swift +// Vortex +// https://www.github.com/twostraws/Vortex +// See LICENSE for license information. +// + +import SwiftUI + +/// Contains the variables used to configure a Vortex System for use with a `VortexView`. +/// Properties:- +/// - **tags**: The list of possible tags to use for this particle system. This might be the +/// full set of tags passed into a `VortexView`, but might also be a subset if +/// secondary systems use other tags. +/// - secondarySettings: The list of secondary settings for reac secondary system that can be created. +/// Defaults to an empty array. +/// - spawnOccasion: When this particle system should be spawned. +/// This is useful only for secondary systems. Defaults to `.onBirth`. +/// - position: The current position of this particle system, in unit space. +/// Defaults to [0.5, 0.5]. +/// - shape: The shape of this particle system, which controls where particles +/// are created relative to the system's position. Defaults to `.point`. +/// - birthRate: How many particles are created every second. You can use +/// values below 1 here, e.g a birth rate of 0.2 means one particle being created +/// every 5 seconds. Defaults to 100. +/// - emissionLimit: The total number of particles this system should create. +/// A value of `nil` means no limit. Defaults to `nil`. +/// - emissionDuration: How long this system should emit particles for before +/// pausing, measured in seconds. Defaults to 1. +/// - idleDuration: How long this system should wait between particle +/// emissions, measured in seconds. Defaults to 0. +/// - burstCount: How many particles should be emitted when a burst is requested. +/// Defaults to 100. +/// - burstCountVariation: How much variation should be allowed in bursts. +/// Defaults to 0. +/// - lifespan: How long particles should live for, measured in seconds. Defaults +/// to 1. +/// - lifespanVariation: How much variation to allow in particle lifespan. +/// Defaults to 0. +/// - speed: The base rate of movement for particles. A speed of 1 means the +/// system will move from one side to the other in one second. Defaults to 1. +/// - speedVariation: How much variation to allow in particle speed. Defaults +/// to 0. +/// - angle: The base direction to launch new particles, where 0 is directly up. Defaults +/// to 0. +/// - angleRange: How much variation to use in particle launch direction. Defaults to 0. +/// - acceleration: How much acceleration to apply for particle movement. +/// Defaults to 0, meaning that no acceleration is applied. +/// - attractionCenter: A specific point particles should move towards or away +/// from, based on `attractionStrength`. A `nil` value here means +/// no attraction. Defaults to `nil`. +/// - attractionStrength: How fast to move towards `attractionCenter`, +/// when it is not `nil`. Defaults to 0 +/// - dampingFactor: How fast movement speed should be slowed down. Defaults to 0. +/// - angularSpeed: How fast particles should spin. Defaults to `[0, 0, 0]`. +/// - angularSpeedVariation: How much variation to allow in particle spin speed. +/// Defaults to `[0, 0, 0]`. +/// - colors: What colors to use for particles made by this system. If `randomRamp` +/// is used then this system picks one possible color ramp to use. Defaults to +/// `.single(.white)`. +/// - size: How large particles should be drawn, where a value of 1 means 100% +/// the image size. Defaults to 1. +/// - sizeVariation: How much variation to use for particle size. Defaults to 0 +/// - sizeMultiplierAtDeath: How how much bigger or smaller this particle should +/// be by the time it is removed. This is used as a multiplier based on the particle's initial +/// size, so if it starts at size 0.5 and has a `sizeMultiplierAtDeath` of 0.5, the +/// particle will finish at size 0.25. Defaults to 1. +/// - stretchFactor: How much to stretch this particle's image based on its movement +/// speed. Larger values cause more stretching. Defaults to 1 (no stretch). +public struct VortexSettings: Equatable, Hashable, Identifiable, Codable { + /// Unique id. Set as variable to allow decodable conformance without compiler quibbles. + public var id: UUID = UUID() + + /// Equatable conformance + public static func == ( lhs: VortexSettings, rhs: VortexSettings ) -> Bool { + lhs.id == rhs.id + } + /// Hashable conformance + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + /// The current position of this particle system, in unit space. + /// Defaults to the centre. + public var position: SIMD2 = [0.5, 0.5] + + /// The list of possible tags to use for this particle system. This might be the full set of + /// tags passed into a `VortexView`, but might also be a subset if secondary systems + /// use other tags. + /// Defaults to "circle" + public var tags: [String] = ["circle"] + + /// Whether this particle system should actively be emitting right now. + /// Defaults to true + public var isEmitting = true + + /// The list of secondary settings associated with this setting. Empty by default. + public var secondarySettings = [VortexSettings]() + + /// When this particle system should be spawned. This is useful only for secondary systems. + /// Defaults to `.onBirth` + public var spawnOccasion: SpawnOccasion = .onBirth + + // These properties control how particles are created. + /// The shape of this particle system, which controls where particles are created relative to + /// the system's position. + /// Defaults to `.point` + public var shape: Shape = .point + + /// How many particles are created every second. You can use values below 1 here, e.g + /// a birth rate of 0.2 means one particle being created every 5 seconds. + /// Defaults to 100 + public var birthRate: Double = 100 + + /// The total number of particles this system should create. + /// The default value of `nil` means no limit. + public var emissionLimit: Int? = nil + + /// How long this system should emit particles for before pausing, measured in seconds. + /// Defaults to 1 + public var emissionDuration: TimeInterval = 1 + + /// How long this system should wait between particle emissions, measured in seconds. + /// Defaults to 0 + public var idleDuration: TimeInterval = 0 + + /// How many particles should be emitted when a burst is requested. + /// Defaults to 100 + public var burstCount: Int = 100 + + /// How much variation should be allowed in bursts. + /// Defaults to 0 + public var burstCountVariation: Int = .zero + + /// How long particles should live for, measured in seconds. + /// Defaults to 1 + public var lifespan: TimeInterval = 1 + + /// How much variation to allow in particle lifespan. + /// Defaults to 0 + public var lifespanVariation: TimeInterval = 0 + + // These properties control how particles move. + /// The base rate of movement for particles. + /// The default speed of 1 means the system will move + /// from one side to the other in one second. + public var speed: Double = 1 + + /// How much variation to allow in particle speed. + /// Defaults to 0 + public var speedVariation: Double = .zero + + /// The base direction to launch new particles, where 0 is directly up. + /// Defaults to 0 + public var angle: Angle = .zero + + /// How much variation to use in particle launch direction. + /// Defaults to 0 + public var angleRange: Angle = .zero + + /// How much acceleration to apply for particle movement. Set to 0 by default, meaning + /// that no acceleration is applied. + public var acceleration: SIMD2 = [0, 0] + + /// A specific point particles should move towards or away from, based + /// on `attractionStrength`. A `nil` value here means no attraction. + public var attractionCenter: SIMD2? = nil + + /// How fast to move towards `attractionCenter`, when it is not `nil`. + /// Defaults to 0 + public var attractionStrength: Double = .zero + + /// How fast movement speed should be slowed down. + /// Defaults to 0 + public var dampingFactor: Double = .zero + + /// How fast particles should spin. + /// Defaults to zero + public var angularSpeed: SIMD3 = [0, 0, 0] + + /// How much variation to allow in particle spin speed. + /// Defaults to zero + public var angularSpeedVariation: SIMD3 = [0, 0, 0] + + // These properties determine how particles are drawn. + + /// How large particles should be drawn, where a value of 1, the default, means 100% of the image size. + public var size: Double = 1 + + /// How much variation to use for particle size. + /// Defaults to zero + public var sizeVariation: Double = .zero + + /// How how much bigger or smaller this particle should be by the time it is removed. + /// This is used as a multiplier based on the particle's initial size, so if it starts at size + /// 0.5 and has a `sizeMultiplierAtDeath` of 0.5, the particle will finish + /// at size 0.25. + /// Defaults to zero + public var sizeMultiplierAtDeath: Double = .zero + + /// How much to stretch this particle's image based on its movement speed. Larger values + /// cause more stretching. + /// Defaults to zero + public var stretchFactor: Double = .zero + + /// What colors to use for particles made by this system. If `randomRamp` is used + /// then the VortexSystem initialiser will pick one possible color ramp to use. + /// A single, white, color is used by default. + public var colors: ColorMode = .single(.white) + + /// VortexSettings initialisation. + /// - Parameters: None. Uses sensible default values on initialisation, with no parameters required. + public init() {} + + /// Convenient init for VortexSettings initialisation. Allows initialisation based on an existing settings struct, + /// - Parameter basedOn: `VortexSettings` + /// The base settings struct to be used as a base. Defaullt settings will be used if not supplied. + /// e.g. + /// ```swift + /// var newFireSettings = VortexSettings(basedOn: .fire ) + /// ``` + /// The above creates a new VortexSettings struct by copying the `.fire` preset, and allowing further modifications. + /// + public init( basedOn base: VortexSettings = VortexSettings() ) { + // Take a copy of the base struct, and generate new id + var newSettings = base + newSettings.id = UUID() + self = newSettings + } + + /// Formerly used within VortexSystem to make deep copies of the VortexSystem class so that secondary systems functioned correctly. + /// No longer needed, but a vestigial stub is created here for backward compatibility + @available(*, deprecated, message: "Deprecated. This method is no longer required") + public func makeUniqueCopy() -> VortexSettings { + return self + } + + /// Backward compatibility , for those converting from the old VortexSystem initialiser + public init( + tags: [String], + secondarySettings: [VortexSettings] = [], + spawnOccasion: SpawnOccasion = .onBirth, + position: SIMD2 = [0.5, 0.5], + shape: Shape = .point, + birthRate: Double = 100, + emissionLimit: Int? = nil, + emissionDuration: Double = 1, + idleDuration: Double = 0, + burstCount: Int = 100, + burstCountVariation: Int = 0, + lifespan: TimeInterval = 1, + lifespanVariation: TimeInterval = 0, + speed: Double = 1, + speedVariation: Double = 0, + angle: Angle = .zero, + angleRange: Angle = .zero, + acceleration: SIMD2 = [0, 0], + attractionCenter: SIMD2? = nil, + attractionStrength: Double = 0, + dampingFactor: Double = 0, + angularSpeed: SIMD3 = [0, 0, 0], + angularSpeedVariation: SIMD3 = [0, 0, 0], + colors: ColorMode = .single(.white), + size: Double = 1, + sizeVariation: Double = 0, + sizeMultiplierAtDeath: Double = 1, + stretchFactor: Double = 1 + ) { + id = UUID() + self.tags = tags + self.spawnOccasion = spawnOccasion + self.position = position + self.shape = shape + self.birthRate = birthRate + self.emissionLimit = emissionLimit + self.emissionDuration = emissionDuration + self.idleDuration = idleDuration + self.burstCount = burstCount + self.burstCountVariation = burstCountVariation + self.lifespan = lifespan + self.lifespanVariation = lifespanVariation + self.speed = speed + self.speedVariation = speedVariation + self.angle = angle + self.acceleration = acceleration + self.angleRange = angleRange + self.attractionCenter = attractionCenter + self.attractionStrength = attractionStrength + self.dampingFactor = dampingFactor + self.angularSpeed = angularSpeed + self.angularSpeedVariation = angularSpeedVariation + self.colors = colors + self.size = size + self.sizeVariation = sizeVariation + self.sizeMultiplierAtDeath = sizeMultiplierAtDeath + self.stretchFactor = stretchFactor + self.secondarySettings = secondarySettings + } +} diff --git a/Sources/Vortex/System/VortexSystem-Behavior.swift b/Sources/Vortex/System/VortexSystem-Behavior.swift index 0fb4f64..f57d042 100644 --- a/Sources/Vortex/System/VortexSystem-Behavior.swift +++ b/Sources/Vortex/System/VortexSystem-Behavior.swift @@ -24,11 +24,15 @@ extension VortexSystem { let delta = currentTimeInterval - lastUpdate lastUpdate = currentTimeInterval - if isEmitting && lastUpdate - lastIdleTime > emissionDuration { - isEmitting = false + if settings.isEmitting + && lastUpdate - lastIdleTime > settings.emissionDuration + { + settings.isEmitting = false lastIdleTime = lastUpdate - } else if isEmitting == false && lastUpdate - lastIdleTime > idleDuration { - isEmitting = true + } else if settings.isEmitting == false + && lastUpdate - lastIdleTime > settings.idleDuration + { + settings.isEmitting = true lastIdleTime = lastUpdate } @@ -38,10 +42,10 @@ extension VortexSystem { // Push attraction strength down to a small number, otherwise // it's much too strong. - let adjustedAttractionStrength = attractionStrength / 1000 + let adjustedAttractionStrength = settings.attractionStrength / 1000 - if let attractionCenter { - attractionUnitPoint = [attractionCenter.x / drawSize.width, attractionCenter.y / drawSize.height] + if let attractionCenter = settings.attractionCenter { + attractionUnitPoint = [ attractionCenter.x / drawSize.width, attractionCenter.y / drawSize.height] } particles = particles.compactMap { @@ -69,18 +73,18 @@ extension VortexSystem { particle.position.x += particle.speed.x * delta * drawDivisor particle.position.y += particle.speed.y * delta - if dampingFactor != 1 { - let dampingAmount = dampingFactor * delta / lifespan + if settings.dampingFactor != 1 { + let dampingAmount = settings.dampingFactor * delta / settings.lifespan particle.speed -= particle.speed * dampingAmount } - particle.speed += acceleration * delta + particle.speed += settings.acceleration * delta particle.angle += particle.angularSpeed * delta particle.currentColor = particle.colors.lerp(by: lifeProgress) particle.currentSize = particle.initialSize.lerp( - to: particle.initialSize * sizeMultiplierAtDeath, + to: particle.initialSize * settings.sizeMultiplierAtDeath, amount: lifeProgress ) @@ -95,7 +99,7 @@ extension VortexSystem { } private func createParticles(delta: Double) { - outstandingParticles += birthRate * delta + outstandingParticles += settings.birthRate * delta if outstandingParticles >= 1 { let particlesToCreate = Int(outstandingParticles) @@ -122,19 +126,13 @@ extension VortexSystem { /// - Parameter force: When true, this will create a particle even if /// this system has already reached its emission limit. func createParticle(force: Bool = false) { - guard isEmitting else { return } - - if let emissionLimit { - if emissionCount >= emissionLimit && force == false { - return - } - } + guard settings.isEmitting, emissionCount < settings.emissionLimit ?? Int.max || force == true else { return } // We subtract half of pi here to ensure that angle 0 is directly up. - let launchAngle = angle.radians + angleRange.radians.randomSpread() - .pi / 2 - let launchSpeed = speed + speedVariation.randomSpread() - let lifespan = lifespan + lifespanVariation.randomSpread() - let size = size + sizeVariation.randomSpread() + let launchAngle = settings.angle.radians + settings.angleRange.radians.randomSpread() - .pi / 2 + let launchSpeed = settings.speed + settings.speedVariation.randomSpread() + let lifespan = settings.lifespan + settings.lifespanVariation.randomSpread() + let size = settings.size + settings.sizeVariation.randomSpread() let particlePosition = getNewParticlePosition() let speed = SIMD2( @@ -142,11 +140,11 @@ extension VortexSystem { sin(launchAngle) * launchSpeed ) - let spinSpeed = angularSpeed + angularSpeedVariation.randomSpread() + let spinSpeed = settings.angularSpeed + settings.angularSpeedVariation.randomSpread() let colorRamp = getNewParticleColorRamp() let newParticle = Particle( - tag: tags.randomElement() ?? "", + tag: settings.tags.randomElement() ?? "", position: particlePosition, speed: speed, birthTime: lastUpdate, @@ -163,7 +161,7 @@ extension VortexSystem { /// Force a bunch of particles to be created immediately. func burst() { - let particlesToCreate = burstCount + burstCountVariation.randomSpread() + let particlesToCreate = settings.burstCount + settings.burstCountVariation.randomSpread() for _ in 0.. SIMD2 { - switch shape { + switch settings.shape { case .point: - return position + return settings.position case .box(let width, let height): return [ - position.x + width.randomSpread(), - position.y + height.randomSpread() + settings.position.x + width.randomSpread(), + settings.position.y + height.randomSpread(), ] case .ellipse(let radius): @@ -197,22 +197,22 @@ extension VortexSystem { let placement = Double.random(in: 0...radius / 2) return [ - placement * cos(angle) + position.x, - placement * sin(angle) + position.y + placement * cos(angle) + settings.position.x, + placement * sin(angle) + settings.position.y, ] case .ring(let radius): let angle = Double.random(in: 0...(2 * .pi)) return [ - radius / 2 * cos(angle) + position.x, - radius / 2 * sin(angle) + position.y + radius / 2 * cos(angle) + settings.position.x, + radius / 2 * sin(angle) + settings.position.y, ] } } - func getNewParticleColorRamp() -> [Color] { - switch colors { + func getNewParticleColorRamp() -> [VortexSettings.Color] { + switch settings.colors { case .single(let color): return [color] diff --git a/Sources/Vortex/System/VortexSystem.swift b/Sources/Vortex/System/VortexSystem.swift index f5ef3dc..17946be 100644 --- a/Sources/Vortex/System/VortexSystem.swift +++ b/Sources/Vortex/System/VortexSystem.swift @@ -8,17 +8,23 @@ import SwiftUI /// The main particle system generator class that powers Vortex. -public class VortexSystem: Codable, Identifiable, Equatable, Hashable { - /// The subset of properties we need to load and save to handle Codable correctly. - enum CodingKeys: CodingKey { - case tags, secondarySystems, spawnOccasion, position, shape, birthRate, emissionLimit, emissionDuration - case idleDuration, burstCount, burstCountVariation, lifespan, lifespanVariation, speed, speedVariation, angle - case angleRange, acceleration, attractionCenter, attractionStrength, dampingFactor, angularSpeed - case angularSpeedVariation, colors, size, sizeVariation, sizeMultiplierAtDeath, stretchFactor - } - +@dynamicMemberLookup +public class VortexSystem: Identifiable, Equatable, Hashable { + /// A public identifier to satisfy Identifiable public let id = UUID() + + /// Virtual 'add' of the Settings properties to the vortex system for dynamicLookup + /// Getter: + public subscript(dynamicMember keyPath: KeyPath) -> T { + settings[keyPath: keyPath] + } + /// Setter: + public subscript(dynamicMember keyPath: WritableKeyPath) -> T { + get { settings[keyPath: keyPath] } + set { settings[keyPath: keyPath] = newValue } + } + /// Equatable conformance public static func == (lhs: VortexSystem, rhs: VortexSystem) -> Bool { lhs.id == rhs.id @@ -28,7 +34,6 @@ public class VortexSystem: Codable, Identifiable, Equatable, Hashable { hasher.combine(id) } - // These properties are used for managing a live system, rather // than for configuration purposes. /// How many particles are waiting to be created. This is particularly useful when @@ -60,272 +65,19 @@ public class VortexSystem: Codable, Identifiable, Equatable, Hashable { var lastDrawSize = CGSize.zero // These properties control system-wide behavior. - /// The current position of this particle system, in unit space. - public var position: SIMD2 - - /// The list of possible tags to use for this particle system. This might be the full set of - /// tags passed into a `VortexView`, but might also be a subset if secondary systems - /// use other tags. - public var tags = [String]() - - /// Whether this particle system should actively be emitting right now. - public var isEmitting = true - - /// The list of secondary systems that can be created by this system. - public var secondarySystems = [VortexSystem]() - - /// When this particle system should be spawned. This is useful only for secondary systems. - public var spawnOccasion: SpawnOccasion - - // These properties control how particles are created. - /// The shape of this particle system, which controls where particles are created relative to - /// the system's position. - public var shape: Shape - - /// How many particles are created every second. You can use values below 1 here, e.g - /// a birth rate of 0.2 means one particle being created every 5 seconds. - public var birthRate: Double - - /// The total number of particles this system should create. A value of `nil` means no limit. - public var emissionLimit: Int? - - /// How long this system should emit particles for before pausing, measured in seconds. - public var emissionDuration: TimeInterval - - /// How long this system should wait between particle emissions, measured in seconds. - public var idleDuration: TimeInterval - - /// How many particles should be emitted when a burst is requested. - public var burstCount: Int - - /// How much variation should be allowed in bursts. - public var burstCountVariation: Int - - /// How long particles should live for, measured in seconds. - public var lifespan: TimeInterval - - /// How much variation to allow in particle lifespan. - public var lifespanVariation: TimeInterval - - // These properties control how particles move. - /// The base rate of movement for particles. A speed of 1 means the system will move - /// from one side to the other in one second. - public var speed: Double - /// How much variation to allow in particle speed. - public var speedVariation: Double - - /// The base direction to launch new particles, where 0 is directly up. - public var angle: Angle - - /// How much variation to use in particle launch direction. - public var angleRange: Angle - - /// How much acceleration to apply for particle movement. Set to 0 by default, meaning - /// that no acceleration is applied. - public var acceleration: SIMD2 - - /// A specific point particles should move towards or away from, based - /// on `attractionStrength`. A `nil` value here means no attraction. - public var attractionCenter: SIMD2? - - /// How fast to move towards `attractionCenter`, when it is not `nil`. - public var attractionStrength: Double - - /// How fast movement speed should be slowed down. - public var dampingFactor: Double - - /// How fast particles should spin. - public var angularSpeed: SIMD3 - - /// How much variation to allow in particle spin speed. - public var angularSpeedVariation: SIMD3 - - // These properties determine how particles are drawn. - /// What colors to use for particles made by this system. If `randomRamp` is used - /// then this system picks one possible color ramp to use. - public var colors: ColorMode { - didSet { - if case let .randomRamp(allColors) = colors { - self.selectedColorRamp = Int.random(in: 0.. = [0.5, 0.5], - shape: Shape = .point, - birthRate: Double = 100, - emissionLimit: Int? = nil, - emissionDuration: Double = 1, - idleDuration: Double = 0, - burstCount: Int = 100, - burstCountVariation: Int = 0, - lifespan: TimeInterval = 1, - lifespanVariation: TimeInterval = 0, - speed: Double = 1, - speedVariation: Double = 0, - angle: Angle = .zero, - angleRange: Angle = .zero, - acceleration: SIMD2 = [0, 0], - attractionCenter: SIMD2? = nil, - attractionStrength: Double = 0, - dampingFactor: Double = 0, - angularSpeed: SIMD3 = [0, 0, 0], - angularSpeedVariation: SIMD3 = [0, 0, 0], - colors: ColorMode = .single(.white), - size: Double = 1, - sizeVariation: Double = 0, - sizeMultiplierAtDeath: Double = 1, - stretchFactor: Double = 1 - ) { - self.tags = tags - self.secondarySystems = secondarySystems - self.spawnOccasion = spawnOccasion - self.position = position - self.shape = shape - self.birthRate = birthRate - self.emissionLimit = emissionLimit - self.emissionDuration = emissionDuration - self.idleDuration = idleDuration - self.burstCount = burstCount - self.burstCountVariation = burstCountVariation - self.lifespan = lifespan - self.lifespanVariation = lifespanVariation - self.speed = speed - self.speedVariation = speedVariation - self.angle = angle - self.acceleration = acceleration - self.angleRange = angleRange - self.attractionCenter = attractionCenter - self.attractionStrength = attractionStrength - self.dampingFactor = dampingFactor - self.angularSpeed = angularSpeed - self.angularSpeedVariation = angularSpeedVariation - self.colors = colors - self.size = size - self.sizeVariation = sizeVariation - self.sizeMultiplierAtDeath = sizeMultiplierAtDeath - self.stretchFactor = stretchFactor - - if case let .randomRamp(allColors) = colors { + /// The configuration settings for a VortexSystem + public var settings: VortexSettings + + /// Initialise a particle system with a VortexSettings struct + /// - Parameter settings: VortexSettings + /// The settings to be used for this particle system. + public init(_ settings: VortexSettings ) { + self.settings = settings + // Ensure that randomisation is set correctly if settings are copied. + // (This is important when creating a secondary system) + if case .randomRamp(let allColors) = settings.colors { selectedColorRamp = Int.random(in: 0.. VortexSystem { - VortexSystem( - tags: tags, - secondarySystems: secondarySystems, - spawnOccasion: spawnOccasion, - position: position, - shape: shape, - birthRate: birthRate, - emissionLimit: emissionLimit, - emissionDuration: emissionDuration, - idleDuration: idleDuration, - burstCount: burstCount, - burstCountVariation: burstCountVariation, - lifespan: lifespan, - lifespanVariation: lifespanVariation, - speed: speed, - speedVariation: speedVariation, - angle: angle, - angleRange: angleRange, - acceleration: acceleration, - attractionCenter: attractionCenter, - attractionStrength: attractionStrength, - dampingFactor: dampingFactor, - angularSpeed: angularSpeed, - angularSpeedVariation: angularSpeedVariation, - colors: colors, - size: size, - sizeVariation: sizeVariation, - sizeMultiplierAtDeath: sizeMultiplierAtDeath, - stretchFactor: stretchFactor - ) - } } diff --git a/Sources/Vortex/Views/VortexView.swift b/Sources/Vortex/Views/VortexView.swift index c2511b1..0e8db5d 100644 --- a/Sources/Vortex/Views/VortexView.swift +++ b/Sources/Vortex/Views/VortexView.swift @@ -30,27 +30,25 @@ public struct VortexView: View where Symbols: View { .preference(key: VortexSystemPreferenceKey.self, value: particleSystem) } - /// Creates a new VortexView from a pre-configured particle system, along with all the SwiftUI - /// views to render as particles. + /// Creates a new VortexView from a pre-configured particle system, along with any required SwiftUI + /// views needed to render particles. Sensible defaults will be used if no parameters are passed. /// - Parameters: - /// - system: The primary particle system you want to render. - /// - symbols: A closure that should return one or more SwiftUI views to use as particles. - /// If a closure is not supplied, a default group of symbols will be provided; tagged with 'circle', 'triangle' and 'sparkle'. + /// - settings: A vortexSettings struct that should be used to generate a particle system. + /// Typically this will be set using a preset static struct, e.g. `.fire`. Defaults to a simple system. + /// - targetFrameRate: The ideal frame rate for updating particles. Defaults to 60 if not specified. (use 120 on Pro model iPhones/iPads ) + /// - symbols: A closure that should return a tagged group of SwiftUI views to use as particles. Default symbols, used in some Previews, with "circle","square" and "sparkle" tags are provided. public init( - _ system: VortexSystem, + _ settings: VortexSettings = .init(), targetFrameRate: Int = 60, @ViewBuilder symbols: () -> Symbols = { - Group { - Image.circle - .frame(width: 16).blendMode(.plusLighter).tag("circle") - Image.confetti - .frame(width: 16, height: 16).blendMode(.plusLighter).tag("confetti") - Image.sparkle - .frame(width: 16, height: 16).blendMode(.plusLighter).tag("sparkle") + Group { + Circle().fill(.white).blendMode(.plusLighter).frame(width: 16, height: 16).tag("circle") + Rectangle().fill(.white).frame(width: 16, height: 16).tag("square") + Image.sparkle.frame(width: 16, height: 16).blendMode(.plusLighter).tag("sparkle") } } ) { - _particleSystem = State(initialValue: system) + _particleSystem = State( initialValue: VortexSystem(settings)) self.targetFrameRate = targetFrameRate self.symbols = symbols() }