From ac7fa0f302d5071e17335c1780870294d1b97ff1 Mon Sep 17 00:00:00 2001 From: Discoinferno <007gnail@gmail.com> Date: Sun, 15 Dec 2024 21:42:36 +0000 Subject: [PATCH 1/3] Code tidy for Text display of instructions --- Sources/Vortex/Presets/Fireflies.swift | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Sources/Vortex/Presets/Fireflies.swift b/Sources/Vortex/Presets/Fireflies.swift index 87be447..05701f7 100644 --- a/Sources/Vortex/Presets/Fireflies.swift +++ b/Sources/Vortex/Presets/Fireflies.swift @@ -35,19 +35,17 @@ 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" } + Text(instructions) + .padding(.bottom, 20) + VortexView(.fireflies) .onModifierKeysChanged(mask: .option) { _, new in // set the view state based on whether the From 574d724ff883ee6d690b4c2416402c2504b4ddfd Mon Sep 17 00:00:00 2001 From: Discoinferno <007gnail@gmail.com> Date: Tue, 17 Dec 2024 21:33:38 +0000 Subject: [PATCH 2/3] Introducing VortexSettings This change extracts the system wide parameters of a vortex system into its own struct, referenced from within the VortexSystem by the new variable `settings`. Doing the above removes the need for invoking .makeUniqueCopy() on the VortexSystem The seemingly large change is, after creating of VortexSettings, mostly changing references from VortexSystem to the new VortexSettings, except for the following:- - Shape, Color, ColorMode and SpawnOccasion are relocated to be within VortexSettings, as they relate to parameters in VortexSettings with those types. - The VortexView init now accepts a type of VortexSettings. - Presets and the Sandbox have been updated to use the VortexSettings initialiser, and for the presets, Xcode previews have been added. - secondarySystems is replaced by secondarySettings, and the spawn method in VortexSystem-Behaviour is updated as a result. - Readable and writable dynamic lookup has been to VortexSystem to minimize the impact on existing code. (References to secondarySystems will have to be updated) [ Aside: Odd shaped initialisers from previous PR's have been removed.] --- Sources/Vortex/System/VortexSettings.swift | 304 +++++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 Sources/Vortex/System/VortexSettings.swift diff --git a/Sources/Vortex/System/VortexSettings.swift b/Sources/Vortex/System/VortexSettings.swift new file mode 100644 index 0000000..f8955be --- /dev/null +++ b/Sources/Vortex/System/VortexSettings.swift @@ -0,0 +1,304 @@ +// +// 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) + } + + // These properties control system-wide behavior. + /// 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: VortexSystem.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: VortexSystem.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: VortexSystem.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, copies it into a new struct and modifiiesf it via a supplied closure + /// - Parameters: + /// - basedOn: `VortexSettings` + /// The base settings struct to be used as a base. Defaullt settings will be used if not supplied. + /// - : @escaping (inout VortexSettings)->Void + /// An anonymous closure which will modify the settings supplied in the first parameter + /// e.g. + /// ```swift + /// let newFireSettings = VortexSettings(from: .fire ) + /// ``` + public init( + basedOn base: VortexSettings = VortexSettings(), + _ modifiedBy: @escaping (_: inout VortexSettings) -> Void = {_ in} + ) { + // Take a copy of the base struct, and generate new id + var newSettings = base + newSettings.id = UUID() + // Amend newSettings by calling the supplied closure + modifiedBy(&newSettings) + self = newSettings + } + + /// Formerly used within VortexSystem to make deep copies of the VortexSystem class so that secondary systems functioned correctly. + /// No longer needed, but created here for backward compatibility + @available(*, deprecated, message: "Deprecated. This method is no longer required") + public func makeUniqueCopy() -> VortexSettings { + return self + } + + /// Backward compatibility again, for those converting from the old VortexSystem initialiser + public init( + tags: [String], + spawnOccasion: VortexSystem.SpawnOccasion = .onBirth, + position: SIMD2 = [0.5, 0.5], + shape: VortexSystem.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: VortexSystem.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 + } +} \ No newline at end of file From a54a0be92bab023211df7f38b10b21afc412241b Mon Sep 17 00:00:00 2001 From: Discoinferno <007gnail@gmail.com> Date: Wed, 18 Dec 2024 11:44:19 +0000 Subject: [PATCH 3/3] VortexSettings implementation - previous commit unsaved? --- README.md | 7 +- .../Sandbox/PreviewViews/ConfettiView.swift | 2 +- Sandbox/Sandbox/PreviewViews/FireView.swift | 2 +- .../Sandbox/PreviewViews/FirefliesView.swift | 2 +- .../Sandbox/PreviewViews/FireworksView.swift | 2 +- Sandbox/Sandbox/PreviewViews/MagicView.swift | 2 +- Sandbox/Sandbox/PreviewViews/RainView.swift | 2 +- Sandbox/Sandbox/PreviewViews/SmokeView.swift | 2 +- Sandbox/Sandbox/PreviewViews/SnowView.swift | 2 +- Sandbox/Sandbox/PreviewViews/SparkView.swift | 2 +- Sandbox/Sandbox/PreviewViews/SplashView.swift | 4 +- .../Helpers/Array-InterpolatedColor.swift | 6 +- Sources/Vortex/Presets/Confetti.swift | 46 ++- Sources/Vortex/Presets/Fire.swift | 26 +- Sources/Vortex/Presets/Fireflies.swift | 15 +- Sources/Vortex/Presets/Fireworks.swift | 23 +- Sources/Vortex/Presets/Magic.swift | 10 +- Sources/Vortex/Presets/Rain.swift | 10 +- Sources/Vortex/Presets/Smoke.swift | 15 +- Sources/Vortex/Presets/Snow.swift | 14 +- Sources/Vortex/Presets/Spark.swift | 9 +- Sources/Vortex/Presets/Splash.swift | 63 ++-- Sources/Vortex/System/Color.swift | 2 +- Sources/Vortex/System/ColorMode.swift | 2 +- Sources/Vortex/System/Particle.swift | 4 +- Sources/Vortex/System/Shape.swift | 2 +- Sources/Vortex/System/SpawnOccasion.swift | 2 +- Sources/Vortex/System/VortexSettings.swift | 39 +-- .../Vortex/System/VortexSystem-Behavior.swift | 84 ++--- Sources/Vortex/System/VortexSystem.swift | 300 ++---------------- Sources/Vortex/Views/VortexView.swift | 26 +- 31 files changed, 278 insertions(+), 449 deletions(-) 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 05701f7..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, @@ -46,7 +46,14 @@ extension VortexSystem { Text(instructions) .padding(.bottom, 20) - VortexView(.fireflies) + 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 index f8955be..12506da 100644 --- a/Sources/Vortex/System/VortexSettings.swift +++ b/Sources/Vortex/System/VortexSettings.swift @@ -80,7 +80,6 @@ public struct VortexSettings: Equatable, Hashable, Identifiable, Codable { hasher.combine(id) } - // These properties control system-wide behavior. /// The current position of this particle system, in unit space. /// Defaults to the centre. public var position: SIMD2 = [0.5, 0.5] @@ -100,13 +99,13 @@ public struct VortexSettings: Equatable, Hashable, Identifiable, Codable { /// When this particle system should be spawned. This is useful only for secondary systems. /// Defaults to `.onBirth` - public var spawnOccasion: VortexSystem.SpawnOccasion = .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: VortexSystem.Shape = .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. @@ -207,47 +206,42 @@ public struct VortexSettings: Equatable, Hashable, Identifiable, Codable { /// 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: VortexSystem.ColorMode = .single(.white) + 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, copies it into a new struct and modifiiesf it via a supplied closure - /// - Parameters: - /// - basedOn: `VortexSettings` + /// 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. - /// - : @escaping (inout VortexSettings)->Void - /// An anonymous closure which will modify the settings supplied in the first parameter /// e.g. /// ```swift - /// let newFireSettings = VortexSettings(from: .fire ) + /// var newFireSettings = VortexSettings(basedOn: .fire ) /// ``` - public init( - basedOn base: VortexSettings = VortexSettings(), - _ modifiedBy: @escaping (_: inout VortexSettings) -> Void = {_ in} - ) { + /// 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() - // Amend newSettings by calling the supplied closure - modifiedBy(&newSettings) self = newSettings } /// Formerly used within VortexSystem to make deep copies of the VortexSystem class so that secondary systems functioned correctly. - /// No longer needed, but created here for backward compatibility + /// 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 again, for those converting from the old VortexSystem initialiser + /// Backward compatibility , for those converting from the old VortexSystem initialiser public init( tags: [String], - spawnOccasion: VortexSystem.SpawnOccasion = .onBirth, + secondarySettings: [VortexSettings] = [], + spawnOccasion: SpawnOccasion = .onBirth, position: SIMD2 = [0.5, 0.5], - shape: VortexSystem.Shape = .point, + shape: Shape = .point, birthRate: Double = 100, emissionLimit: Int? = nil, emissionDuration: Double = 1, @@ -266,7 +260,7 @@ public struct VortexSettings: Equatable, Hashable, Identifiable, Codable { dampingFactor: Double = 0, angularSpeed: SIMD3 = [0, 0, 0], angularSpeedVariation: SIMD3 = [0, 0, 0], - colors: VortexSystem.ColorMode = .single(.white), + colors: ColorMode = .single(.white), size: Double = 1, sizeVariation: Double = 0, sizeMultiplierAtDeath: Double = 1, @@ -300,5 +294,6 @@ public struct VortexSettings: Equatable, Hashable, Identifiable, Codable { self.sizeVariation = sizeVariation self.sizeMultiplierAtDeath = sizeMultiplierAtDeath self.stretchFactor = stretchFactor + self.secondarySettings = secondarySettings } -} \ No newline at end of file +} 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() }