From 7fb6d0da8c3807f5f2712b4a9b1dc2d9168f59c2 Mon Sep 17 00:00:00 2001
From: Discoinferno <007gnail@gmail.com>
Date: Fri, 6 Dec 2024 21:34:56 +0000
Subject: [PATCH 1/5] Introducing VortexSystem.Settings This commit looks
large, but is essentially just an abstraction of the configuration settings
from the VortexSystem into it's own Settings class, with those properties
replaced in VortexSystem with a single property; vortexSettings. The changes
are documented in an the updated README. Settings enables an intuitive
initialisation based on preset settings, enables autocomplete for each
setting (rather than relying on specifying the required parameters of the
init, and getting them all in the right order), and removes the need to ever
call `.makeUniqueCopy`.
The change necessitates changes to VortexSystem of course, but also the VortexSystem behaviour to ensure that the configuration settings are accessed via vortexSettings. (dynamicLookup is also added for convenience).
Included here are also changes to four presets, Confetti, Fire, Fireflies and Fireworks, showing how the settings would be used for each. The others remain (at least for now) with the previous VortexSystem method of initialisation.
Changing the remainder of the presets would take a matter of moments, however I did not want to update too many files at once! (in the interim, some deprecation warnings will be generated by the compiler - see below)
Because of the changes, and in particular because secondary systems are now secondary settings, this could have been a breaking change. However in the interests of backward compatibility, the old inits have been preserved, although they are now marked as deprecated.
This will show warnings in the code, but everything will continue to work.
It is evisaged that after a suitable time, agreed by Paul, the deprecated inits could be removed, and some further code cleanup completed. One such change would be modifying the init of the VortexView to accept a Settings struct as an anonymous parameter, so that
`VortexView(.confetti)` would call the `VortexSystem.Settings.confetti` preset, rather than the current `VortexSystem.confetti` preset, which would be removed.
---
README.md | 199 +++++------
Sources/Vortex/Presets/Confetti.swift | 36 +-
Sources/Vortex/Presets/Fire.swift | 42 ++-
Sources/Vortex/Presets/Fireflies.swift | 37 ++-
Sources/Vortex/Presets/Fireworks.swift | 96 +++---
Sources/Vortex/Settings/Settings.swift | 314 ++++++++++++++++++
.../Vortex/System/VortexSystem-Behavior.swift | 70 ++--
Sources/Vortex/System/VortexSystem.swift | 229 ++++---------
Sources/Vortex/Views/VortexProxy.swift | 2 +-
Sources/Vortex/Views/VortexView.swift | 29 +-
Sources/Vortex/Views/VortexViewReader.swift | 4 +-
11 files changed, 652 insertions(+), 406 deletions(-)
create mode 100644 Sources/Vortex/Settings/Settings.swift
diff --git a/README.md b/README.md
index f522b41..19bec14 100644
--- a/README.md
+++ b/README.md
@@ -39,85 +39,78 @@ This repository contains a cross-platform sample project demonstrating all the p

+There are also a number of Xcode previews created for a selected number of the presets contained in the Presets folder.
## Basic use
-Rendering a Vortex particle system takes two steps:
-
-1. Creating an instance of `VortexSystem`, configured for how you want your particles to behave. This must be given a list of tag names of the particles you want to render.
-2. Adding a `VortexView` to your SwiftUI view hierarchy, passing in the particle system to render, and also all the views that are used for particles, tagged using the same names from step 1.
-
-There are lots of built-in particle system designs, such as rain:
-
+Rendering a Vortex particle system using a preset configuration takes a single line!
```swift
-VortexView(.rain) {
- Circle()
- .fill(.white)
- .frame(width: 32)
- .tag("circle")
-}
+VortexView(.rain)
```
-Fireworks:
-
-```swift
-VortexView(.fireworks) {
- Circle()
- .fill(.white)
- .blendMode(.plusLighter)
- .frame(width: 32)
- .tag("circle")
-}
-```
+There are lots of built-in particle system designs, such as Fireworks (.fireworks) and Fire (.fire)
-And fire:
+You can also easily create custom effects, in two steps:
+1. Creating an instance of `VortexSystem.Settings`, configured for how you want your particles to behave. The easiest way to create Settings is to modify an existing one. (example below)
+2. Call `VortexView` with the settings you just created.
+e.g.
```swift
-VortexView(.fire) {
- Circle()
- .fill(.white)
- .blendMode(.plusLighter)
- .blur(radius: 3)
- .frame(width: 32)
- .tag("circle")
+struct ContentView: View {
+ var body: some View {
+ let fireSettings = VortexSystem.Settings(from: .fire ) { settings in
+ settings.position = [ 0.5, 1.03]
+ settings.shape = .box(width: 1.0, height: 0)
+ settings.birthRate = 600
+ }
+ VortexView(settings: fireSettings)
+ }
}
```
-> [!Note]
-> Each preset is designed to look for one or more tags; please check their documentation below for the correct tags to provide.
+## Next steps
-You can also create completely custom effects, like this:
+All the existing presets use built-in Images that are tagged with "circle", "confetti", or "sparkle"
+You can however use your own views within the particle system, you just need to add them to a trailing closure for VortexView and tag them appropriately.
+e.g.
```swift
struct ContentView: View {
+ let snow = VortexSystem.Settings { settings in
+ settings.tags = ["snow"]
+ settings.position = [0.5, 0]
+ settings.speed = 0.5
+ settings.speedVariation = 0.25
+ settings.lifespan = 3
+ settings.shape = .box(width: 1, height: 0)
+ settings.angle = .degrees(180)
+ settings.angleRange = .degrees(20)
+ settings.size = 0.25
+ settings.sizeVariation = 0.5
+ }
var body: some View {
- VortexView(createSnow()) {
+
+ VortexView(settings: snow) {
Circle()
.fill(.white)
.blur(radius: 5)
.frame(width: 32)
- .tag("circle")
+ .tag("snow")
}
}
-
- func createSnow() -> VortexSystem {
- let system = VortexSystem(tags: ["circle"])
- system.position = [0.5, 0]
- system.speed = 0.5
- system.speedVariation = 0.25
- system.lifespan = 3
- system.shape = .box(width: 1, height: 0)
- system.angle = .degrees(180)
- system.angleRange = .degrees(20)
- system.size = 0.25
- system.sizeVariation = 0.5
- return system
- }
}
```
-> [!Note]
-> `VortexView` does not copy the particle system you provide unless you specifically ask for it using `yourSystem.makeUniqueCopy()`. This allows you to create a particle system once and re-use it in multiple places without losing its state.
+> [!Tip]
+> When creating settings, open the Xcode inspector and show Quick Help (question mark)
+> Each property has help text defined to explain what it is and what the defaults are.
+> You can discover properties from autocomplete (e.g. after typing `settings.`)
+
+
+> [!Note]
+> The Settings that you use has a `tags` property, that must match the `.tag(:String)` property modifier on the view.
+> The Fireworks preset contains an examples where more than one View is used within the particle system.
+
## Programmatic particle control
@@ -130,11 +123,11 @@ For example, this uses the built-in `.confetti` effect, then uses the Vortex pro
```swift
VortexViewReader { proxy in
- VortexView(.confetti) {
+ VortexView(settings: .confetti) {
Rectangle()
.fill(.white)
.frame(width: 16, height: 16)
- .tag("square")
+ .tag("confetti")
Circle()
.fill(.white)
@@ -147,6 +140,7 @@ VortexViewReader { proxy in
```
You can also use the proxy's `attractTo()` method to make particles move towards or away from a specific point, specified in screen coordinates. The exact behavior depends on the value you assign to the `attractionStrength` property of your particle system: positive values move towards your attraction point, whereas negative values move away.
+Check the Xcode Preview of the Fireflies preset to see this in action.
> [!Tip]
> Call `attractTo()` with `nil` as its parameter to clear the attraction point.
@@ -157,12 +151,13 @@ You can also use the proxy's `attractTo()` method to make particles move towards
One of the more advanced Vortex features is the ability create secondary particle systems – for each particle in one system to create a new particle system. This enables creation of multi-stage effects, such as fireworks: one particle launches upwards, setting off sparks as it flies, then exploding into color when it dies.
> [!Important]
-> When creating particle systems with secondary systems inside, both the primary and secondary system can have their own set of tags. However, you must provide all tags from all systems when creating your `ParticleView`.
+> When creating particle systems with secondary systems inside, both the primary and secondary system can have their own set of tags. However, you must provide all tags from all systems when creating the `VortexView`.
## Creating custom particle systems
-The initializer for `VortexSystem` takes a wide range of configuration options to control how your particle systems behave. All but one of these has a sensible default value, allowing you to get started quickly and adjust things on the fly.
+The initializer for `VortexSystem.Settings` has sensible defaults for a wide range of configuration options to control how your particle systems behave.
+For details on each of them, expand the list below.
Details (Click to expand)
@@ -170,7 +165,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` (`[VortexSystem.Settings]`, 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.
@@ -240,18 +235,7 @@ The `.confetti` preset creates a confetti effect where views fly shoot out when
```swift
VortexViewReader { proxy in
- VortexView(.confetti) {
- Rectangle()
- .fill(.white)
- .frame(width: 16, height: 16)
- .tag("square")
-
- Circle()
- .fill(.white)
- .frame(width: 16)
- .tag("circle")
- }
-
+ VortexView(.confetti)
Button("Burst", action: proxy.burst)
}
```
@@ -259,17 +243,10 @@ VortexViewReader { proxy in
### Fire
-The `.fire` preset creates a flame effect. This works better when your particles have a soft edge, and use a `.plusLighter` blend mode, like this:
+The `.fire` preset creates a flame effect.
```swift
-VortexView(.fire) {
- Circle()
- .fill(.white)
- .frame(width: 32)
- .blur(radius: 3)
- .blendMode(.plusLighter)
- .tag("circle")
-}
+VortexView(.fire)
```
@@ -294,14 +271,8 @@ VortexView(.fireflies) {
The `.fireworks` preset creates a three-stage particle effect to simulate exploding fireworks. Each firework is a particle, and also launches new "spark" particles as it flies upwards. When the firework particle is destroyed, it creates an explosion effect in a range of colors.
```swift
-VortexView(.fireworks) {
- Circle()
- .fill(.white)
- .frame(width: 32)
- .blur(radius: 5)
- .blendMode(.plusLighter)
- .tag("circle")
-}
+VortexView(.fireworks)
+
```
@@ -310,11 +281,7 @@ VortexView(.fireworks) {
The `.magic` preset creates a simple ring of particles that fly outwards as they fade out. This works best using the "sparkle" image contained in the Assets folder of this repository, but you can use any other image or shape you prefer.
```swift
-VortexView(.magic) {
- Image(.sparkle)
- .blendMode(.plusLighter)
- .tag("sparkle")
-}
+VortexView(.magic)
```
@@ -323,12 +290,7 @@ VortexView(.magic) {
The `.rain` preset creates a rainfall system by stretching your view based on the rain speed:
```swift
-VortexView(.rain) {
- Circle()
- .fill(.white)
- .frame(width: 32)
- .tag("circle")
-}
+VortexView(.rain)
```
@@ -349,16 +311,10 @@ VortexView(.smoke) {
### Snow
-The `.snow` preset creates a falling snow effect. This works best when your views have soft edges, like this:
+The `.snow` preset creates a falling snow effect.
```swift
-VortexView(.snow) {
- Circle()
- .fill(.white)
- .frame(width: 24)
- .blur(radius: 5)
- .tag("circle")
-}
+VortexView(.snow)
```
@@ -367,12 +323,7 @@ VortexView(.snow) {
The `.spark` preset creates an intermittent spark effect, where sparks fly out for a short time, then pause, then fly out again, etc.
```swift
-VortexView(.spark) {
- Circle()
- .fill(.white)
- .frame(width: 16)
- .tag("circle")
-}
+VortexView(.spark)
```
@@ -382,25 +333,23 @@ The `.splash` present contains raindrop splashes, as if rain were hitting the gr
```swift
ZStack {
- VortexView(.rain) {
- Circle()
- .fill(.white)
- .frame(width: 32)
- .tag("circle")
- }
-
- VortexView(.splash) {
- Circle()
- .fill(.white)
- .frame(width: 16, height: 16)
- .tag("circle")
- }
+ VortexView(.rain)
+ VortexView(.splash)
}
```
+## Change History
+
+### Dec 2025: Introducing VortexSystem.Settings and prebuilt Symbols. ( Contributed by [Andrew Cowley](https://twitter.com/disq0infern0) )
+With this change Vortex particle systems are easier to create. The instructions above have already been modified to include details on the changes.
+Along with this change, the Vortex particle system now has preconfigured symbols (tagged with "circle", "confetti", and "sparkle") for use in your systems.
+These are all 16x16 images, with `.blendMode(.plusLighter)` applied.
+All previously existing code will continue to work without change, however it will now compile with warnings to indicate that the code should be updated to use Settings.
+
+
## Contributing
I welcome all contributions, whether that's adding new particle system presets, fixing up existing code, adding comments, or improving this README – everyone is welcome!
diff --git a/Sources/Vortex/Presets/Confetti.swift b/Sources/Vortex/Presets/Confetti.swift
index 2078c0e..752f0d0 100644
--- a/Sources/Vortex/Presets/Confetti.swift
+++ b/Sources/Vortex/Presets/Confetti.swift
@@ -9,21 +9,25 @@ import SwiftUI
extension VortexSystem {
/// A built-in effect that creates confetti only when a burst is triggered.
- /// Relies on "square" and "circle" tags being present – using `Rectangle`
+ /// Relies on "confetti" 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
- )
- }()
+ /// This declaration is deprecated. A VortexView should be invoked with a VortexSystem.Settings struct directly. See the example below.
+ public static let confetti = VortexSystem(settings: .confetti)
+}
+
+extension VortexSystem.Settings {
+ /// A built-in effect that creates confetti only when a burst is triggered.
+ public static let confetti = VortexSystem.Settings() { settings in
+ settings.tags = ["confetti", "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
+ }
}
diff --git a/Sources/Vortex/Presets/Fire.swift b/Sources/Vortex/Presets/Fire.swift
index 877dd24..1ce2bdf 100644
--- a/Sources/Vortex/Presets/Fire.swift
+++ b/Sources/Vortex/Presets/Fire.swift
@@ -10,18 +10,32 @@ import SwiftUI
extension VortexSystem {
/// 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(
- tags: ["circle"],
- shape: .box(width: 0.1, height: 0),
- birthRate: 300,
- speed: 0.2,
- speedVariation: 0.2,
- angleRange: .degrees(10),
- attractionStrength: 2,
- colors: .ramp(.brown, .brown, .brown, .brown.opacity(0)),
- sizeVariation: 0.5,
- sizeMultiplierAtDeath: 0.1
- )
- }()
+ public static let fire = VortexSystem(settings: .fire)
+}
+
+
+extension VortexSystem.Settings {
+ /// 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.Settings { settings in
+ settings.tags = ["circle"]
+ settings.shape = .box(width: 0.1, height: 0)
+ settings.birthRate = 300
+ settings.speed = 0.2
+ settings.speedVariation = 0.2
+ settings.angleRange = .degrees(10)
+ settings.attractionStrength = 2
+ settings.colors = .ramp(.brown, .brown, .brown, .brown.opacity(0))
+ settings.sizeVariation = 0.5
+ settings.sizeMultiplierAtDeath = 0.1
+ }
+}
+
+#Preview {
+ let floorOnFire = VortexSystem.Settings(from: .fire ) { settings in
+ settings.position = [ 0.5, 1.02]
+ settings.shape = .box(width: 1.0, height: 0)
+ settings.birthRate = 600
+ }
+ VortexView(settings: floorOnFire)
}
diff --git a/Sources/Vortex/Presets/Fireflies.swift b/Sources/Vortex/Presets/Fireflies.swift
index 87be447..f777256 100644
--- a/Sources/Vortex/Presets/Fireflies.swift
+++ b/Sources/Vortex/Presets/Fireflies.swift
@@ -10,20 +10,23 @@ import SwiftUI
extension VortexSystem {
/// 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(
- tags: ["circle"],
- shape: .ellipse(radius: 0.5),
- birthRate: 200,
- lifespan: 2,
- speed: 0,
- speedVariation: 0.25,
- angleRange: .degrees(360),
- colors: .ramp(.yellow, .yellow, .yellow.opacity(0)),
- size: 0.01,
- sizeMultiplierAtDeath: 100
- )
- }()
+ public static let fireflies = VortexSystem(settings: .fireflies)
+}
+
+extension VortexSystem.Settings {
+ /// A built-in firefly effect. Uses the built-in 'circle' image.
+ public static let fireflies = VortexSystem.Settings() { settings in
+ settings.tags = ["circle"]
+ settings.shape = .ellipse(radius: 0.5)
+ settings.birthRate = 200
+ settings.lifespan = 2
+ settings.speed = 0
+ settings.speedVariation = 0.25
+ settings.angleRange = .degrees(360)
+ settings.colors = .ramp(.yellow, .yellow, .yellow.opacity(0))
+ settings.size = 0.01
+ settings.sizeMultiplierAtDeath = 100
+ }
}
/// A Fireflies preview, using the `.fireflies` preset
@@ -48,7 +51,7 @@ extension VortexSystem {
.padding(.bottom, 20)
}
- VortexView(.fireflies)
+ VortexView(settings: .fireflies)
.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)
@@ -59,12 +62,12 @@ extension VortexSystem {
.onChanged { value in
proxy.attractTo(value.location)
proxy.particleSystem?
- .attractionStrength = pressingOptionKey ? 2.5 : -2
+ .vortexSettings.attractionStrength = pressingOptionKey ? 2.5 : -2
isDragging = true
}
.onEnded { _ in
proxy.particleSystem?
- .attractionStrength = 0
+ .vortexSettings.attractionStrength = 0
isDragging = false
}
)
diff --git a/Sources/Vortex/Presets/Fireworks.swift b/Sources/Vortex/Presets/Fireworks.swift
index ddabed0..fea91f6 100644
--- a/Sources/Vortex/Presets/Fireworks.swift
+++ b/Sources/Vortex/Presets/Fireworks.swift
@@ -9,56 +9,66 @@ import SwiftUI
extension VortexSystem {
/// 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
+ /// Relies on the built in circle symbol, or a symbol with the "circle" tag being present, which should be set to use
/// `.blendMode(.plusLighter)`.
- public static let fireworks: VortexSystem = {
- let sparkles = VortexSystem(
- tags: ["circle"],
- spawnOccasion: .onUpdate,
- emissionLimit: 1,
- lifespan: 0.5,
- speed: 0.05,
- angleRange: .degrees(90),
- size: 0.05
- )
+ public static let fireworks = VortexSystem(settings: .fireworks)
+}
- let explosion = VortexSystem(
- tags: ["circle"],
- spawnOccasion: .onDeath,
- position: [0.5, 1],
- birthRate: 100_000,
- emissionLimit: 500,
- speed: 0.5,
- speedVariation: 1,
- angleRange: .degrees(360),
- acceleration: [0, 1.5],
- dampingFactor: 4,
- colors: .randomRamp(
+extension VortexSystem.Settings {
+ /// A built-in fireworks effect, using secondary systems that create sparkles and explosions.
+ /// Relies on a symbol view tagged with "circle" being available to the VortexView. (one such image is built-in)
+ public static let fireworks = VortexSystem.Settings { settings in
+
+ var sparkles = VortexSystem.Settings { sparkle in
+ sparkle.tags = ["sparkle"]
+ sparkle.spawnOccasion = .onUpdate
+ sparkle.emissionLimit = 1
+ sparkle.lifespan = 0.5
+ sparkle.speed = 0.05
+ sparkle.angleRange = .degrees(180)
+ sparkle.size = 0.05
+ }
+
+ var explosions = VortexSystem.Settings { explosion in
+ explosion.tags = ["circle"]
+ explosion.spawnOccasion = .onDeath
+ explosion.position = [0.5, 0.5]
+ explosion.birthRate = 100_000
+ explosion.emissionLimit = 500
+ explosion.speed = 0.5
+ explosion.speedVariation = 1
+ explosion.angleRange = .degrees(360)
+ explosion.acceleration = [0, 1.5]
+ explosion.dampingFactor = 4
+ explosion.colors = .randomRamp(
[.white, .pink, .pink],
[.white, .blue, .blue],
[.white, .green, .green],
[.white, .orange, .orange],
[.white, .cyan, .cyan]
- ),
- size: 0.15,
- sizeVariation: 0.1,
- sizeMultiplierAtDeath: 0
- )
+ )
+ explosion.size = 0.15
+ explosion.sizeVariation = 0.1
+ explosion.sizeMultiplierAtDeath = 0
+ }
- let mainSystem = VortexSystem(
- tags: ["circle"],
- secondarySystems: [sparkles, explosion],
- position: [0.5, 1],
- birthRate: 2,
- emissionLimit: 1000,
- speed: 1.5,
- speedVariation: 0.75,
- angleRange: .degrees(60),
- dampingFactor: 2,
- size: 0.15,
- stretchFactor: 4
- )
+
+ settings.tags = ["circle"]
+ settings.secondarySettings = [sparkles, explosions]
+ settings.position = [0.5, 1]
+ settings.birthRate = 2
+ settings.emissionLimit = 1000
+ settings.speed = 1.5
+ settings.speedVariation = 0.75
+ settings.angleRange = .degrees(60)
+ settings.dampingFactor = 2
+ settings.size = 0.15
+ settings.stretchFactor = 4
+ }
+}
- return mainSystem
- }()
+#Preview("Fireworks") {
+ VortexView(settings: .fireworks)
+ .navigationSubtitle("Demonstrates multi-stage effects")
+ .ignoresSafeArea(edges: .top)
}
diff --git a/Sources/Vortex/Settings/Settings.swift b/Sources/Vortex/Settings/Settings.swift
new file mode 100644
index 0000000..65f3c69
--- /dev/null
+++ b/Sources/Vortex/Settings/Settings.swift
@@ -0,0 +1,314 @@
+//
+// Settings.swift
+// Vortex
+// https://www.github.com/twostraws/Vortex
+// See LICENSE for license information.
+//
+
+import SwiftUI
+
+extension VortexSystem {
+
+ /// 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 Settings: 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: VortexSystem.Settings, rhs: VortexSystem.Settings
+ ) -> 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 = [Settings]()
+
+ /// 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: 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:
+ /// - from: `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 = VortexSystem.Settings(from: .fire ) { settings in
+ /// settings.position = [ 0.5, 1.03]
+ /// settings.shape = .box(width: 1.0, height: 0)
+ /// settings.birthRate = 600
+ /// }
+ /// ```
+ public init(
+ from base: VortexSystem.Settings = VortexSystem.Settings(),
+ _ modifiedBy: @escaping (_: inout VortexSystem.Settings) -> Void
+ ) {
+ // 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 to make deep copies of the VortexSystem class so that secondary systems functioned correctly.
+ /// No longer needed, but created here for backward compatibility if the VortexSystem initialiser taking a Settings
+ /// struct is updated to be identical to the old init that took a VortexSystem initialiser.
+ public func makeUniqueCopy() -> VortexSystem.Settings {
+ return self
+ }
+
+ /// Backward compatibility again:
+ public init(
+ tags: [String],
+ 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
+ }
+ }
+}
diff --git a/Sources/Vortex/System/VortexSystem-Behavior.swift b/Sources/Vortex/System/VortexSystem-Behavior.swift
index 0fb4f64..1713676 100644
--- a/Sources/Vortex/System/VortexSystem-Behavior.swift
+++ b/Sources/Vortex/System/VortexSystem-Behavior.swift
@@ -24,11 +24,11 @@ extension VortexSystem {
let delta = currentTimeInterval - lastUpdate
lastUpdate = currentTimeInterval
- if isEmitting && lastUpdate - lastIdleTime > emissionDuration {
- isEmitting = false
+ if vortexSettings.isEmitting && lastUpdate - lastIdleTime > vortexSettings.emissionDuration {
+ vortexSettings.isEmitting = false
lastIdleTime = lastUpdate
- } else if isEmitting == false && lastUpdate - lastIdleTime > idleDuration {
- isEmitting = true
+ } else if vortexSettings.isEmitting == false && lastUpdate - lastIdleTime > vortexSettings.idleDuration {
+ vortexSettings.isEmitting = true
lastIdleTime = lastUpdate
}
@@ -38,9 +38,9 @@ extension VortexSystem {
// Push attraction strength down to a small number, otherwise
// it's much too strong.
- let adjustedAttractionStrength = attractionStrength / 1000
+ let adjustedAttractionStrength = vortexSettings.attractionStrength / 1000
- if let attractionCenter {
+ if let attractionCenter = vortexSettings.attractionCenter {
attractionUnitPoint = [attractionCenter.x / drawSize.width, attractionCenter.y / drawSize.height]
}
@@ -69,18 +69,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 vortexSettings.dampingFactor != 1 {
+ let dampingAmount = vortexSettings.dampingFactor * delta / vortexSettings.lifespan
particle.speed -= particle.speed * dampingAmount
}
- particle.speed += acceleration * delta
+ particle.speed += vortexSettings.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 * vortexSettings.sizeMultiplierAtDeath,
amount: lifeProgress
)
@@ -95,7 +95,7 @@ extension VortexSystem {
}
private func createParticles(delta: Double) {
- outstandingParticles += birthRate * delta
+ outstandingParticles += vortexSettings.birthRate * delta
if outstandingParticles >= 1 {
let particlesToCreate = Int(outstandingParticles)
@@ -122,19 +122,19 @@ 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 }
+ guard vortexSettings.isEmitting else { return }
- if let emissionLimit {
+ if let emissionLimit = vortexSettings.emissionLimit {
if emissionCount >= emissionLimit && force == false {
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 = vortexSettings.angle.radians + vortexSettings.angleRange.radians.randomSpread() - .pi / 2
+ let launchSpeed = vortexSettings.speed + vortexSettings.speedVariation.randomSpread()
+ let lifespan = vortexSettings.lifespan + vortexSettings.lifespanVariation.randomSpread()
+ let size = vortexSettings.size + vortexSettings.sizeVariation.randomSpread()
let particlePosition = getNewParticlePosition()
let speed = SIMD2(
@@ -142,11 +142,11 @@ extension VortexSystem {
sin(launchAngle) * launchSpeed
)
- let spinSpeed = angularSpeed + angularSpeedVariation.randomSpread()
+ let spinSpeed = vortexSettings.angularSpeed + vortexSettings.angularSpeedVariation.randomSpread()
let colorRamp = getNewParticleColorRamp()
let newParticle = Particle(
- tag: tags.randomElement() ?? "",
+ tag: vortexSettings.tags.randomElement() ?? "",
position: particlePosition,
speed: speed,
birthTime: lastUpdate,
@@ -163,7 +163,7 @@ extension VortexSystem {
/// Force a bunch of particles to be created immediately.
func burst() {
- let particlesToCreate = burstCount + burstCountVariation.randomSpread()
+ let particlesToCreate = vortexSettings.burstCount + vortexSettings.burstCountVariation.randomSpread()
for _ in 0.. SIMD2 {
- switch shape {
+ switch vortexSettings.shape {
case .point:
- return position
+ return vortexSettings.position
case .box(let width, let height):
return [
- position.x + width.randomSpread(),
- position.y + height.randomSpread()
+ vortexSettings.position.x + width.randomSpread(),
+ vortexSettings.position.y + height.randomSpread()
]
case .ellipse(let radius):
@@ -197,22 +205,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) + vortexSettings.position.x,
+ placement * sin(angle) + vortexSettings.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) + vortexSettings.position.x,
+ radius / 2 * sin(angle) + vortexSettings.position.y
]
}
}
func getNewParticleColorRamp() -> [Color] {
- switch colors {
+ switch vortexSettings.colors {
case .single(let color):
return [color]
diff --git a/Sources/Vortex/System/VortexSystem.swift b/Sources/Vortex/System/VortexSystem.swift
index f5ef3dc..b675cea 100644
--- a/Sources/Vortex/System/VortexSystem.swift
+++ b/Sources/Vortex/System/VortexSystem.swift
@@ -8,17 +8,12 @@
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()
+
/// Equatable conformance
public static func == (lhs: VortexSystem, rhs: VortexSystem) -> Bool {
lhs.id == rhs.id
@@ -27,7 +22,10 @@ public class VortexSystem: Codable, Identifiable, Equatable, Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
-
+ /// Virtual 'add' of the Settings properties to the vortex system
+ subscript(dynamicMember keyPath: KeyPath) -> T {
+ vortexSettings[keyPath: keyPath]
+ }
// These properties are used for managing a live system, rather
// than for configuration purposes.
@@ -60,122 +58,36 @@ 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.. VortexSystem {
- VortexSystem(
+ self.vortexSettings = Settings(
tags: tags,
- secondarySystems: secondarySystems,
spawnOccasion: spawnOccasion,
position: position,
shape: shape,
@@ -320,12 +196,53 @@ public class VortexSystem: Codable, Identifiable, Equatable, Hashable {
attractionStrength: attractionStrength,
dampingFactor: dampingFactor,
angularSpeed: angularSpeed,
- angularSpeedVariation: angularSpeedVariation,
+ angularSpeedVariation: angularSpeed,
colors: colors,
size: size,
sizeVariation: sizeVariation,
sizeMultiplierAtDeath: sizeMultiplierAtDeath,
stretchFactor: stretchFactor
)
+
+ if case .randomRamp(let allColors) = colors {
+ selectedColorRamp = Int.random(in: 0.. VortexSystem {
+ VortexSystem(
+ tags: vortexSettings.tags,
+ secondarySystems: secondarySystems,
+ spawnOccasion: vortexSettings.spawnOccasion,
+ position: vortexSettings.position,
+ shape: vortexSettings.shape,
+ birthRate: vortexSettings.birthRate,
+ emissionLimit: vortexSettings.emissionLimit,
+ emissionDuration: vortexSettings.emissionDuration,
+ idleDuration: vortexSettings.idleDuration,
+ burstCount: vortexSettings.burstCount,
+ burstCountVariation: vortexSettings.burstCountVariation,
+ lifespan: vortexSettings.lifespan,
+ lifespanVariation: vortexSettings.lifespanVariation,
+ speed: vortexSettings.speed,
+ speedVariation: vortexSettings.speedVariation,
+ angle: vortexSettings.angle,
+ angleRange: vortexSettings.angleRange,
+ acceleration: vortexSettings.acceleration,
+ attractionCenter: vortexSettings.attractionCenter,
+ attractionStrength: vortexSettings.attractionStrength,
+ dampingFactor: vortexSettings.dampingFactor,
+ angularSpeed: vortexSettings.angularSpeed,
+ angularSpeedVariation: vortexSettings.angularSpeedVariation,
+ colors: vortexSettings.colors,
+ size: vortexSettings.size,
+ sizeVariation: vortexSettings.sizeVariation,
+ sizeMultiplierAtDeath: vortexSettings.sizeMultiplierAtDeath,
+ stretchFactor: vortexSettings.stretchFactor
+ )
}
}
diff --git a/Sources/Vortex/Views/VortexProxy.swift b/Sources/Vortex/Views/VortexProxy.swift
index 3694548..d94482a 100644
--- a/Sources/Vortex/Views/VortexProxy.swift
+++ b/Sources/Vortex/Views/VortexProxy.swift
@@ -26,7 +26,7 @@ public struct VortexProxy {
public func move(to newPosition: CGPoint) {
guard let particleSystem else { return }
- particleSystem.position = [
+ particleSystem.vortexSettings.position = [
newPosition.x / particleSystem.lastDrawSize.width,
newPosition.y / particleSystem.lastDrawSize.height
]
diff --git a/Sources/Vortex/Views/VortexView.swift b/Sources/Vortex/Views/VortexView.swift
index c2511b1..d77ee68 100644
--- a/Sources/Vortex/Views/VortexView.swift
+++ b/Sources/Vortex/Views/VortexView.swift
@@ -35,7 +35,8 @@ public struct VortexView: View where Symbols: View {
/// - 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'.
+ /// If a closure is not supplied, a default group of symbols will be provided; tagged with 'circle', 'confetti' and 'sparkle'.
+ @available(*, deprecated, message: "Deprecated. Invoke the VortexView with a VortexSystem.Settings struct")
public init(
_ system: VortexSystem,
targetFrameRate: Int = 60,
@@ -55,6 +56,32 @@ public struct VortexView: View where Symbols: View {
self.symbols = symbols()
}
+ /// 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:
+ /// - 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.
+ /// If not specified, a default group of three views tagged with `.circle`,`.confetti` and `.sparkle` will be used.
+ public init(
+ settings: VortexSystem.Settings = .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("triangle")
+ Image.sparkle
+ .frame(width: 16, height: 16).blendMode(.plusLighter).tag("sparkle")
+ }
+ }
+ ) {
+ _particleSystem = State( initialValue: VortexSystem(settings: settings) )
+ self.targetFrameRate = targetFrameRate
+ self.symbols = symbols()
+ }
/// Draws one particle system inside the canvas.
/// - Parameters:
/// - particleSystem: The particle system to draw.
diff --git a/Sources/Vortex/Views/VortexViewReader.swift b/Sources/Vortex/Views/VortexViewReader.swift
index 41a11cc..8a1b193 100644
--- a/Sources/Vortex/Views/VortexViewReader.swift
+++ b/Sources/Vortex/Views/VortexViewReader.swift
@@ -26,9 +26,9 @@ public struct VortexViewReader: View {
nearestVortexSystem?.burst()
} attractTo: { point in
if let point {
- nearestVortexSystem?.attractionCenter = SIMD2(point.x, point.y)
+ nearestVortexSystem?.vortexSettings.attractionCenter = SIMD2(point.x, point.y)
} else {
- nearestVortexSystem?.attractionCenter = nil
+ nearestVortexSystem?.vortexSettings.attractionCenter = nil
}
}
From 766c1a2f039ba68f7e9d088222842b2258c8e7d9 Mon Sep 17 00:00:00 2001
From: Discoinferno <007gnail@gmail.com>
Date: Mon, 9 Dec 2024 00:31:27 +0000
Subject: [PATCH 2/5] Introducing VortexSettings This commit is essentially an
abstraction of the configuration settings from the VortexSystem into it's own
VortexSettings type, with those properties replaced in VortexSystem with a
single property; settings. (Prebuilt/default symbols have been removed from
the previous PR). The changes are documented in an the updated README.
Settings enables an intuitive initialisation based on preset settings. Using
type ahead, it enables discovery of each setting, and does not rely on
specifying all the required parameters of the init, and getting them all in
the right order, It also removes the need to ever call `.makeUniqueCopy`.
The change necessitates changes to VortexSystem of course, but also the VortexProxy, ViewReader and VortexSystem-Behaviour to ensure that the configuration settings are correctly accessed via settings. (dynamicLookup is also added to VortexSystem for convenience).
---
README.md | 109 +++-
Sources/Vortex/Presets/Confetti.swift | 42 +-
Sources/Vortex/Presets/Fire.swift | 52 +-
Sources/Vortex/Presets/Fireflies.swift | 59 +-
Sources/Vortex/Presets/Fireworks.swift | 49 +-
Sources/Vortex/Presets/Magic.swift | 38 +-
Sources/Vortex/Presets/Rain.swift | 48 +-
Sources/Vortex/Presets/Smoke.swift | 38 +-
Sources/Vortex/Presets/Snow.swift | 40 +-
Sources/Vortex/Presets/Spark.swift | 47 +-
Sources/Vortex/Presets/Splash.swift | 78 ++-
Sources/Vortex/Settings/Settings.swift | 596 +++++++++---------
.../Vortex/System/VortexSystem-Behavior.swift | 128 ++--
Sources/Vortex/System/VortexSystem.swift | 184 +-----
Sources/Vortex/Views/VortexProxy.swift | 2 +-
Sources/Vortex/Views/VortexView.swift | 16 +-
Sources/Vortex/Views/VortexViewReader.swift | 4 +-
17 files changed, 735 insertions(+), 795 deletions(-)
diff --git a/README.md b/README.md
index 19bec14..82470d9 100644
--- a/README.md
+++ b/README.md
@@ -43,35 +43,48 @@ There are also a number of Xcode previews created for a selected number of the p
## Basic use
-Rendering a Vortex particle system using a preset configuration takes a single line!
+Rendering a Vortex particle system using a preset configuration takes just a few lines of code;-
```swift
-VortexView(.rain)
+VortexView(.rain) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .tag("circle")
+}
```
There are lots of built-in particle system designs, such as Fireworks (.fireworks) and Fire (.fire)
You can also easily create custom effects, in two steps:
-1. Creating an instance of `VortexSystem.Settings`, configured for how you want your particles to behave. The easiest way to create Settings is to modify an existing one. (example below)
+1. Creating an instance of `VortexSettings`, configured for how you want your particles to behave. The easiest way to create Settings is to modify an existing one. (example below)
2. Call `VortexView` with the settings you just created.
e.g.
```swift
struct ContentView: View {
var body: some View {
- let fireSettings = VortexSystem.Settings(from: .fire ) { settings in
+ let fireSettings = VortexSettings(basedOn: .fire ) { settings in
settings.position = [ 0.5, 1.03]
settings.shape = .box(width: 1.0, height: 0)
settings.birthRate = 600
}
- VortexView(settings: fireSettings)
+ VortexView(fireSettings) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .blur(radius: 3)
+ .blendMode(.plusLighter)
+ .tag("circle")
+ }
}
}
```
+The tag modifier that is added to the view is very important. It links the VortexSettings, which describes the behaviour of the particle system, to the particle views that are drawn. The `.tag` modifier on the view must match one of the `tags` in the settings.
## Next steps
-All the existing presets use built-in Images that are tagged with "circle", "confetti", or "sparkle"
-You can however use your own views within the particle system, you just need to add them to a trailing closure for VortexView and tag them appropriately.
+All the existing presets reference views that are tagged with "circle", "square", or "sparkle"
+You can of course create use your own views within the particle system, you just need to add them to a trailing closure for VortexView and tag them appropriately.
e.g.
```swift
@@ -89,7 +102,6 @@ struct ContentView: View {
settings.sizeVariation = 0.5
}
var body: some View {
-
VortexView(settings: snow) {
Circle()
.fill(.white)
@@ -123,11 +135,11 @@ For example, this uses the built-in `.confetti` effect, then uses the Vortex pro
```swift
VortexViewReader { proxy in
- VortexView(settings: .confetti) {
+ VortexView(.confetti) {
Rectangle()
.fill(.white)
.frame(width: 16, height: 16)
- .tag("confetti")
+ .tag("square")
Circle()
.fill(.white)
@@ -151,7 +163,7 @@ Check the Xcode Preview of the Fireflies preset to see this in action.
One of the more advanced Vortex features is the ability create secondary particle systems – for each particle in one system to create a new particle system. This enables creation of multi-stage effects, such as fireworks: one particle launches upwards, setting off sparks as it flies, then exploding into color when it dies.
> [!Important]
-> When creating particle systems with secondary systems inside, both the primary and secondary system can have their own set of tags. However, you must provide all tags from all systems when creating the `VortexView`.
+> When creating particle systems with secondary settings, both the primary and secondary system can have their own set of tags. However, you must provide all tags from all systems when creating the `VortexView`.
## Creating custom particle systems
@@ -235,7 +247,17 @@ The `.confetti` preset creates a confetti effect where views fly shoot out when
```swift
VortexViewReader { proxy in
- VortexView(.confetti)
+ VortexView(.confetti) {
+ Rectangle()
+ .fill(.white)
+ .frame(width: 16, height: 16)
+ .tag("square")
+
+ Circle()
+ .fill(.white)
+ .frame(width: 16)
+ .tag("circle")
+ }
Button("Burst", action: proxy.burst)
}
```
@@ -246,7 +268,14 @@ VortexViewReader { proxy in
The `.fire` preset creates a flame effect.
```swift
-VortexView(.fire)
+VortexView(.fire) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .blur(radius: 3)
+ .blendMode(.plusLighter)
+ .tag("circle")
+}
```
@@ -271,8 +300,14 @@ VortexView(.fireflies) {
The `.fireworks` preset creates a three-stage particle effect to simulate exploding fireworks. Each firework is a particle, and also launches new "spark" particles as it flies upwards. When the firework particle is destroyed, it creates an explosion effect in a range of colors.
```swift
-VortexView(.fireworks)
-
+VortexView(.fireworks) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .blur(radius: 5)
+ .blendMode(.plusLighter)
+ .tag("circle")
+}
```
@@ -281,16 +316,24 @@ VortexView(.fireworks)
The `.magic` preset creates a simple ring of particles that fly outwards as they fade out. This works best using the "sparkle" image contained in the Assets folder of this repository, but you can use any other image or shape you prefer.
```swift
-VortexView(.magic)
+VortexView(.magic) {
+ Image.sparkle
+ .blendMode(.plusLighter)
+ .tag("sparkle")
+}
```
-
### Rain
The `.rain` preset creates a rainfall system by stretching your view based on the rain speed:
```swift
-VortexView(.rain)
+VortexView(.rain) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .tag("circle")
+}
```
@@ -314,16 +357,26 @@ VortexView(.smoke) {
The `.snow` preset creates a falling snow effect.
```swift
-VortexView(.snow)
+VortexView(.snow) {
+ Circle()
+ .fill(.white)
+ .frame(width: 24)
+ .blur(radius: 5)
+ .tag("circle")
+}
```
-
### Spark
The `.spark` preset creates an intermittent spark effect, where sparks fly out for a short time, then pause, then fly out again, etc.
```swift
-VortexView(.spark)
+VortexView(.spark) {
+ Circle()
+ .fill(.white)
+ .frame(width: 16)
+ .tag("circle")
+}
```
@@ -333,8 +386,18 @@ The `.splash` present contains raindrop splashes, as if rain were hitting the gr
```swift
ZStack {
- VortexView(.rain)
- VortexView(.splash)
+ VortexView(.rain) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .tag("circle")
+ }
+ VortexView(.splash){
+ Circle()
+ .fill(.white)
+ .frame(width: 16)
+ .tag("circle")
+ }
}
```
diff --git a/Sources/Vortex/Presets/Confetti.swift b/Sources/Vortex/Presets/Confetti.swift
index 752f0d0..d39c4ab 100644
--- a/Sources/Vortex/Presets/Confetti.swift
+++ b/Sources/Vortex/Presets/Confetti.swift
@@ -7,18 +7,10 @@
import SwiftUI
-extension VortexSystem {
+extension VortexSettings {
/// A built-in effect that creates confetti only when a burst is triggered.
- /// Relies on "confetti" and "circle" tags being present – using `Rectangle`
- /// and `Circle` with frames of 16x16 works well.
- /// This declaration is deprecated. A VortexView should be invoked with a VortexSystem.Settings struct directly. See the example below.
- public static let confetti = VortexSystem(settings: .confetti)
-}
-
-extension VortexSystem.Settings {
- /// A built-in effect that creates confetti only when a burst is triggered.
- public static let confetti = VortexSystem.Settings() { settings in
- settings.tags = ["confetti", "circle"]
+ public static let confetti = VortexSettings { settings in
+ settings.tags = ["square", "circle"]
settings.birthRate = 0
settings.lifespan = 4
settings.speed = 0.5
@@ -26,8 +18,34 @@ extension VortexSystem.Settings {
settings.angleRange = .degrees(90)
settings.acceleration = [0, 1]
settings.angularSpeedVariation = [4, 4, 4]
- settings.colors = .random(.white, .red, .green, .blue, .pink, .orange, .cyan)
+ settings.colors = .random(
+ .white, .red, .green, .blue, .pink, .orange, .cyan)
settings.size = 0.5
settings.sizeVariation = 0.5
}
}
+
+@available(macOS 14.0, *)
+#Preview("Demonstrate on demand bursts") {
+ VortexViewReader { proxy in
+ ZStack {
+ Text("Tap anywhere to create confetti.")
+
+ VortexView(.confetti.makeUniqueCopy()) {
+ Rectangle()
+ .fill(.white)
+ .frame(width: 16, height: 16)
+ .tag("square")
+ Circle()
+ .fill(.white)
+ .frame(width: 16)
+ .tag("circle")
+ }
+ .onTapGesture { location in
+ proxy.move(to: location)
+ proxy.burst()
+ }
+ }
+ }
+}
+
diff --git a/Sources/Vortex/Presets/Fire.swift b/Sources/Vortex/Presets/Fire.swift
index 1ce2bdf..9781ceb 100644
--- a/Sources/Vortex/Presets/Fire.swift
+++ b/Sources/Vortex/Presets/Fire.swift
@@ -7,17 +7,10 @@
import SwiftUI
-extension VortexSystem {
- /// A built-in fire effect. Relies on a "circle" tag being present, which should be set to use
+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(settings: .fire)
-}
-
-
-extension VortexSystem.Settings {
- /// 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.Settings { settings in
+ public static let fire = VortexSettings { settings in
settings.tags = ["circle"]
settings.shape = .box(width: 0.1, height: 0)
settings.birthRate = 300
@@ -31,11 +24,38 @@ extension VortexSystem.Settings {
}
}
-#Preview {
- let floorOnFire = VortexSystem.Settings(from: .fire ) { settings in
- settings.position = [ 0.5, 1.02]
+#Preview("Demonstrates the fire preset with attraction") {
+ /// Here we modify the default fire settings to extend it across the bottom of the screen
+ let floorOnFire:VortexSettings = {
+ 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")
+ }
+}
+
+#Preview("Demonstrates the fire preset with attraction") {
+ /// Here we modify the default fire settings to extend it across the bottom of the screen
+ let floorOnFire = VortexSettings(basedOn: .fire) { settings in
+ settings.position = [0.5, 1.02]
settings.shape = .box(width: 1.0, height: 0)
- settings.birthRate = 600
- }
- VortexView(settings: floorOnFire)
+ settings.birthRate = 600
+ }
+ 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 f777256..50a8a07 100644
--- a/Sources/Vortex/Presets/Fireflies.swift
+++ b/Sources/Vortex/Presets/Fireflies.swift
@@ -7,15 +7,9 @@
import SwiftUI
-extension VortexSystem {
- /// 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(settings: .fireflies)
-}
-
-extension VortexSystem.Settings {
+extension VortexSettings {
/// A built-in firefly effect. Uses the built-in 'circle' image.
- public static let fireflies = VortexSystem.Settings() { settings in
+ public static let fireflies = VortexSettings() { settings in
settings.tags = ["circle"]
settings.shape = .ellipse(radius: 0.5)
settings.birthRate = 200
@@ -32,7 +26,7 @@ extension VortexSystem.Settings {
/// A Fireflies preview, using the `.fireflies` preset
/// macOS 15 is required for the `.onModifierKeysChanged` method that is used to capture the Option key being pressed.
@available(macOS 15.0, *)
-#Preview("Fireflies") {
+#Preview("Demonstrates use of attraction and repulsion") {
@Previewable @State var isDragging = false
/// A state value indicating whether the Option key is being held down
@Previewable @State var pressingOptionKey = false
@@ -51,28 +45,31 @@ extension VortexSystem.Settings {
.padding(.bottom, 20)
}
- VortexView(settings: .fireflies)
- .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)
- pressingOptionKey = !new.isEmpty
- }
- .gesture(
- DragGesture(minimumDistance: 0)
- .onChanged { value in
- proxy.attractTo(value.location)
- proxy.particleSystem?
- .vortexSettings.attractionStrength = pressingOptionKey ? 2.5 : -2
- isDragging = true
- }
- .onEnded { _ in
- proxy.particleSystem?
- .vortexSettings.attractionStrength = 0
- isDragging = false
- }
- )
+ 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)
+ pressingOptionKey = !new.isEmpty
+ }
+ .gesture(
+ DragGesture(minimumDistance: 0)
+ .onChanged { value in
+ proxy.attractTo(value.location)
+ proxy.particleSystem?.settings.attractionStrength = pressingOptionKey ? 2.5 : -2
+ isDragging = true
+ }
+ .onEnded { _ in
+ proxy.particleSystem?.settings.attractionStrength = 0
+ isDragging = false
+ }
+ )
}
}
- .navigationSubtitle("Demonstrates use of attraction and repulsion")
- .ignoresSafeArea(edges: .top)
}
diff --git a/Sources/Vortex/Presets/Fireworks.swift b/Sources/Vortex/Presets/Fireworks.swift
index fea91f6..b7cb64c 100644
--- a/Sources/Vortex/Presets/Fireworks.swift
+++ b/Sources/Vortex/Presets/Fireworks.swift
@@ -7,19 +7,22 @@
import SwiftUI
-extension VortexSystem {
- /// A built-in fireworks effect, using secondary systems that create sparkles and explosions.
- /// Relies on the built in circle symbol, or a symbol with the "circle" tag being present, which should be set to use
- /// `.blendMode(.plusLighter)`.
- public static let fireworks = VortexSystem(settings: .fireworks)
-}
-
-extension VortexSystem.Settings {
+extension VortexSettings {
/// A built-in fireworks effect, using secondary systems that create sparkles and explosions.
/// Relies on a symbol view tagged with "circle" being available to the VortexView. (one such image is built-in)
- public static let fireworks = VortexSystem.Settings { settings in
+ public static let fireworks = VortexSettings { settings in
+ settings.tags = ["circle"]
+ settings.position = [0.5, 1]
+ settings.birthRate = 2
+ settings.emissionLimit = 1000
+ settings.speed = 1.5
+ settings.speedVariation = 0.75
+ settings.angleRange = .degrees(60)
+ settings.dampingFactor = 2
+ settings.size = 0.15
+ settings.stretchFactor = 4
- var sparkles = VortexSystem.Settings { sparkle in
+ var sparkles = VortexSettings { sparkle in
sparkle.tags = ["sparkle"]
sparkle.spawnOccasion = .onUpdate
sparkle.emissionLimit = 1
@@ -29,7 +32,7 @@ extension VortexSystem.Settings {
sparkle.size = 0.05
}
- var explosions = VortexSystem.Settings { explosion in
+ var explosions = VortexSettings { explosion in
explosion.tags = ["circle"]
explosion.spawnOccasion = .onDeath
explosion.position = [0.5, 0.5]
@@ -52,23 +55,17 @@ extension VortexSystem.Settings {
explosion.sizeMultiplierAtDeath = 0
}
-
- settings.tags = ["circle"]
settings.secondarySettings = [sparkles, explosions]
- settings.position = [0.5, 1]
- settings.birthRate = 2
- settings.emissionLimit = 1000
- settings.speed = 1.5
- settings.speedVariation = 0.75
- settings.angleRange = .degrees(60)
- settings.dampingFactor = 2
- settings.size = 0.15
- settings.stretchFactor = 4
}
}
-#Preview("Fireworks") {
- VortexView(settings: .fireworks)
- .navigationSubtitle("Demonstrates multi-stage effects")
- .ignoresSafeArea(edges: .top)
+#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..9b558d4 100644
--- a/Sources/Vortex/Presets/Magic.swift
+++ b/Sources/Vortex/Presets/Magic.swift
@@ -7,22 +7,28 @@
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(
- tags: ["sparkle"],
- shape: .ring(radius: 0.5),
- lifespan: 1.5,
- speed: 0,
- speedVariation: 0.2,
- angleRange: .degrees(360),
- angularSpeedVariation: [0, 0, 10],
- colors: .random(.red, .pink, .orange, .blue, .green, .white),
- size: 0.5,
- sizeVariation: 0.5,
- sizeMultiplierAtDeath: 0.01
- )
- }()
+ static let magic = VortexSettings { settings in
+ settings.tags = ["sparkle"]
+ settings.shape = .ring(radius: 0.5)
+ settings.lifespan = 1.5
+ settings.speed = 0
+ settings.speedVariation = 0.2
+ settings.angleRange = .degrees(360)
+ settings.angularSpeedVariation = [0, 0, 10]
+ settings.colors = .random(.red, .pink, .orange, .blue, .green, .white)
+ settings.size = 0.5
+ settings.sizeVariation = 0.5
+ settings.sizeMultiplierAtDeath = 0.01
+ }
+}
+
+#Preview("Demonstrates the magic preset") {
+ VortexView(.magic) {
+ Image.sparkle
+ .blendMode(.plusLighter)
+ .tag("sparkle")
+ }
}
diff --git a/Sources/Vortex/Presets/Rain.swift b/Sources/Vortex/Presets/Rain.swift
index fcf1c1b..7e044b6 100644
--- a/Sources/Vortex/Presets/Rain.swift
+++ b/Sources/Vortex/Presets/Rain.swift
@@ -7,26 +7,34 @@
import SwiftUI
-extension VortexSystem {
+extension VortexSettings {
/// A built-in rain effect. Relies on a "circle" tag being present.
- public static let rain: VortexSystem = {
- VortexSystem(
- tags: ["circle"],
- position: [0.5, 0 ],
- shape: .box(width: 1.8, height: 0),
- birthRate: 400,
- lifespan: 0.5,
- speed: 4.5,
- speedVariation: 2,
- angle: .degrees(190),
- colors: .random(
- Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.6),
- Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.5),
- Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.4)
- ),
- size: 0.09,
- sizeVariation: 0.05,
- stretchFactor: 12
+ public static let rain = VortexSettings { settings in
+ settings.tags = ["circle"]
+ settings.position = [0.5, 0 ]
+ settings.shape = .box(width: 1.8, height: 0)
+ settings.birthRate = 400
+ settings.lifespan = 0.5
+ settings.speed = 4.5
+ settings.speedVariation = 2
+ settings.angle = .degrees(190)
+ settings.colors = .random(
+ VortexSystem.Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.6),
+ VortexSystem.Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.5),
+ VortexSystem.Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.4)
)
- }()
+ settings.size = 0.09
+ settings.sizeVariation = 0.05
+ settings.stretchFactor = 12
+ }
+
+}
+
+#Preview("Demonstrates the rain preset") {
+ VortexView(.rain) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .tag("circle")
+ }
}
diff --git a/Sources/Vortex/Presets/Smoke.swift b/Sources/Vortex/Presets/Smoke.swift
index a678e63..ce503ed 100644
--- a/Sources/Vortex/Presets/Smoke.swift
+++ b/Sources/Vortex/Presets/Smoke.swift
@@ -7,20 +7,28 @@
import SwiftUI
-extension VortexSystem {
+extension VortexSettings {
/// A built-in smoke effect. Relies on a "circle" tag being present.
- public static let smoke: VortexSystem = {
- VortexSystem(
- tags: ["circle"],
- shape: .box(width: 0.05, height: 0),
- lifespan: 3,
- speed: 0.1,
- speedVariation: 0.1,
- angleRange: .degrees(10),
- colors: .ramp(.gray, .gray.opacity(0)),
- size: 0.5,
- sizeVariation: 0.5,
- sizeMultiplierAtDeath: 2
- )
- }()
+ static let smoke = VortexSettings { settings in
+ settings.tags = ["circle"]
+ settings.shape = .box(width: 0.05, height: 0)
+ settings.lifespan = 3
+ settings.speed = 0.1
+ settings.speedVariation = 0.1
+ settings.angleRange = .degrees(10)
+ settings.colors = .ramp(.gray, .gray.opacity(0))
+ settings.size = 0.5
+ settings.sizeVariation = 0.5
+ settings.sizeMultiplierAtDeath = 2
+ }
+}
+
+#Preview("Demonstrates the 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..c445030 100644
--- a/Sources/Vortex/Presets/Snow.swift
+++ b/Sources/Vortex/Presets/Snow.swift
@@ -7,26 +7,30 @@
import SwiftUI
-extension VortexSystem {
+extension VortexSettings {
/// A built-in snow effect. Relies on a "circle" tag being present.
- public static let snow: VortexSystem = {
- VortexSystem(
- tags: ["circle"],
- position: [0.5, 0],
- shape: .box(width: 1, height: 0),
- birthRate: 50,
- lifespan: 10,
- speed: 0.2,
- speedVariation: 0.2,
- angle: .degrees(180),
- angleRange: .degrees(20),
- size: 0.25,
- sizeVariation: 0.4
- )
- }()
+ static let snow = VortexSettings { settings in
+ settings.tags = ["circle"]
+ settings.position = [0.5, 0]
+ settings.shape = .box(width: 1, height: 0)
+ settings.birthRate = 50
+ settings.lifespan = 10
+ settings.speed = 0.2
+ settings.speedVariation = 0.2
+ settings.angle = .degrees(180)
+ settings.angleRange = .degrees(20)
+ settings.size = 0.25
+ settings.sizeVariation = 0.4
+ }
}
-#Preview {
+#Preview("Demonstrates the snow preset") {
// 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..7960818 100644
--- a/Sources/Vortex/Presets/Spark.swift
+++ b/Sources/Vortex/Presets/Spark.swift
@@ -7,26 +7,33 @@
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(
- tags: ["circle"],
- birthRate: 150,
- emissionDuration: 0.2,
- idleDuration: 0.5,
- lifespan: 1.5,
- speed: 1.25,
- speedVariation: 0.2,
- angle: .degrees(330),
- angleRange: .degrees(20),
- acceleration: [0, 3],
- dampingFactor: 4,
- colors: .ramp(.white, .yellow, .yellow.opacity(0)),
- size: 0.1,
- sizeVariation: 0.1,
- stretchFactor: 8
- )
- }()
+ static let spark = VortexSettings { settings in
+ settings.tags = ["circle"]
+ settings.birthRate = 150
+ settings.emissionDuration = 0.2
+ settings.idleDuration = 0.5
+ settings.lifespan = 1.5
+ settings.speed = 1.25
+ settings.speedVariation = 0.2
+ settings.angle = .degrees(330)
+ settings.angleRange = .degrees(20)
+ settings.acceleration = [0, 3]
+ settings.dampingFactor = 4
+ settings.colors = .ramp(.white, .yellow, .yellow.opacity(0))
+ settings.size = 0.1
+ settings.sizeVariation = 0.1
+ settings.stretchFactor = 8
+ }
+}
+
+#Preview("Demonstrates the spark preset") {
+ VortexView(.spark) {
+ Circle()
+ .fill(.white)
+ .frame(width: 16)
+ .tag("circle")
+ }
}
diff --git a/Sources/Vortex/Presets/Splash.swift b/Sources/Vortex/Presets/Splash.swift
index 65f018e..b16782f 100644
--- a/Sources/Vortex/Presets/Splash.swift
+++ b/Sources/Vortex/Presets/Splash.swift
@@ -7,39 +7,55 @@
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(
- 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
- )
-
- 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
- )
+ static let splash = VortexSettings { settings in
+
+ var drops = VortexSettings { drops in
+ 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(
+ VortexSystem.Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.7),
+ VortexSystem.Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.6),
+ VortexSystem.Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.5)
+ )
+ drops.size = 0.2
+ drops.position = [0.5,1]
+ }
+
+ settings.tags = ["circle"]
+ settings.secondarySettings = [drops]
+ settings.position = [0.5, 1]
+ settings.shape = .box(width: 1, height: 0)
+ settings.birthRate = 5
+ settings.lifespan = 0.001
+ settings.speed = 0
+ settings.colors = .single(.clear)
+ settings.size = 0
+ }
+}
- return mainSystem
- }()
+#Preview("Demonstrates a combination rain/splash preset") {
+ ZStack {
+ VortexView(.rain) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .tag("circle")
+ }
+ VortexView(.splash) {
+ Circle()
+ .fill(.white)
+ .frame(width: 16)
+ .tag("circle")
+ }
+ }
}
+
diff --git a/Sources/Vortex/Settings/Settings.swift b/Sources/Vortex/Settings/Settings.swift
index 65f3c69..b5f6297 100644
--- a/Sources/Vortex/Settings/Settings.swift
+++ b/Sources/Vortex/Settings/Settings.swift
@@ -7,308 +7,298 @@
import SwiftUI
-extension VortexSystem {
-
- /// 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 Settings: 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: VortexSystem.Settings, rhs: VortexSystem.Settings
- ) -> 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 = [Settings]()
-
- /// 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: 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:
- /// - from: `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 = VortexSystem.Settings(from: .fire ) { settings in
- /// settings.position = [ 0.5, 1.03]
- /// settings.shape = .box(width: 1.0, height: 0)
- /// settings.birthRate = 600
- /// }
- /// ```
- public init(
- from base: VortexSystem.Settings = VortexSystem.Settings(),
- _ modifiedBy: @escaping (_: inout VortexSystem.Settings) -> Void
- ) {
- // 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 to make deep copies of the VortexSystem class so that secondary systems functioned correctly.
- /// No longer needed, but created here for backward compatibility if the VortexSystem initialiser taking a Settings
- /// struct is updated to be identical to the old init that took a VortexSystem initialiser.
- public func makeUniqueCopy() -> VortexSystem.Settings {
- return self
- }
-
- /// Backward compatibility again:
- public init(
- tags: [String],
- 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
- }
+/// 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
}
}
diff --git a/Sources/Vortex/System/VortexSystem-Behavior.swift b/Sources/Vortex/System/VortexSystem-Behavior.swift
index 1713676..5d27ef8 100644
--- a/Sources/Vortex/System/VortexSystem-Behavior.swift
+++ b/Sources/Vortex/System/VortexSystem-Behavior.swift
@@ -17,46 +17,46 @@ extension VortexSystem {
func update(date: Date, drawSize: CGSize) {
lastDrawSize = drawSize
updateSecondarySystems(date: date, drawSize: drawSize)
-
+
let drawDivisor = drawSize.height / drawSize.width
let currentTimeInterval = date.timeIntervalSince1970
-
+
let delta = currentTimeInterval - lastUpdate
lastUpdate = currentTimeInterval
-
- if vortexSettings.isEmitting && lastUpdate - lastIdleTime > vortexSettings.emissionDuration {
- vortexSettings.isEmitting = false
+
+ if settings.isEmitting && lastUpdate - lastIdleTime > settings.emissionDuration {
+ settings.isEmitting = false
lastIdleTime = lastUpdate
- } else if vortexSettings.isEmitting == false && lastUpdate - lastIdleTime > vortexSettings.idleDuration {
- vortexSettings.isEmitting = true
+ } else if settings.isEmitting == false && lastUpdate - lastIdleTime > settings.idleDuration {
+ settings.isEmitting = true
lastIdleTime = lastUpdate
}
-
+
createParticles(delta: delta)
-
+
var attractionUnitPoint: SIMD2?
-
+
// Push attraction strength down to a small number, otherwise
// it's much too strong.
- let adjustedAttractionStrength = vortexSettings.attractionStrength / 1000
-
- if let attractionCenter = vortexSettings.attractionCenter {
+ let adjustedAttractionStrength = settings.attractionStrength / 1000
+
+ if let attractionCenter = settings.attractionCenter {
attractionUnitPoint = [attractionCenter.x / drawSize.width, attractionCenter.y / drawSize.height]
}
-
+
particles = particles.compactMap {
var particle = $0
let age = currentTimeInterval - particle.birthTime
let lifeProgress = age / particle.lifespan
-
+
// Apply attraction force to particle's existing velocity.
if let attractionUnitPoint {
let gap = attractionUnitPoint - particle.position
let distance = sqrt((gap * gap).sum())
-
+
if distance > 0 {
let normalized = gap / distance
-
+
// Increase the magnitude the closer we get, adding a small
// amount to avoid a slingshot / over-attraction.
let movementMagnitude = adjustedAttractionStrength / (distance * distance + 0.0025)
@@ -64,26 +64,26 @@ extension VortexSystem {
particle.position += movement
}
}
-
+
// Update particle position
particle.position.x += particle.speed.x * delta * drawDivisor
particle.position.y += particle.speed.y * delta
-
- if vortexSettings.dampingFactor != 1 {
- let dampingAmount = vortexSettings.dampingFactor * delta / vortexSettings.lifespan
+
+ if settings.dampingFactor != 1 {
+ let dampingAmount = settings.dampingFactor * delta / settings.lifespan
particle.speed -= particle.speed * dampingAmount
}
-
- particle.speed += vortexSettings.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 * vortexSettings.sizeMultiplierAtDeath,
+ to: particle.initialSize * settings.sizeMultiplierAtDeath,
amount: lifeProgress
)
-
+
if age >= particle.lifespan {
spawn(from: particle, event: .onDeath)
return nil
@@ -93,48 +93,44 @@ extension VortexSystem {
}
}
}
-
+
private func createParticles(delta: Double) {
- outstandingParticles += vortexSettings.birthRate * delta
-
+ outstandingParticles += settings.birthRate * delta
+
if outstandingParticles >= 1 {
let particlesToCreate = Int(outstandingParticles)
-
+
for _ in 0.. 0 {
activeSecondarySystems.remove(activeSecondarySystem)
}
}
}
-
+
/// Used to create a single particle.
/// - 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 vortexSettings.isEmitting else { return }
-
- if let emissionLimit = vortexSettings.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 = vortexSettings.angle.radians + vortexSettings.angleRange.radians.randomSpread() - .pi / 2
- let launchSpeed = vortexSettings.speed + vortexSettings.speedVariation.randomSpread()
- let lifespan = vortexSettings.lifespan + vortexSettings.lifespanVariation.randomSpread()
- let size = vortexSettings.size + vortexSettings.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 +138,11 @@ extension VortexSystem {
sin(launchAngle) * launchSpeed
)
- let spinSpeed = vortexSettings.angularSpeed + vortexSettings.angularSpeedVariation.randomSpread()
+ let spinSpeed = settings.angularSpeed + settings.angularSpeedVariation.randomSpread()
let colorRamp = getNewParticleColorRamp()
let newParticle = Particle(
- tag: vortexSettings.tags.randomElement() ?? "",
+ tag: settings.tags.randomElement() ?? "",
position: particlePosition,
speed: speed,
birthTime: lastUpdate,
@@ -163,7 +159,7 @@ extension VortexSystem {
/// Force a bunch of particles to be created immediately.
func burst() {
- let particlesToCreate = vortexSettings.burstCount + vortexSettings.burstCountVariation.randomSpread()
+ let particlesToCreate = settings.burstCount + settings.burstCountVariation.randomSpread()
for _ in 0.. SIMD2 {
- switch vortexSettings.shape {
+ switch settings.shape {
case .point:
- return vortexSettings.position
+ return settings.position
case .box(let width, let height):
return [
- vortexSettings.position.x + width.randomSpread(),
- vortexSettings.position.y + height.randomSpread()
+ settings.position.x + width.randomSpread(),
+ settings.position.y + height.randomSpread()
]
case .ellipse(let radius):
@@ -205,22 +193,22 @@ extension VortexSystem {
let placement = Double.random(in: 0...radius / 2)
return [
- placement * cos(angle) + vortexSettings.position.x,
- placement * sin(angle) + vortexSettings.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) + vortexSettings.position.x,
- radius / 2 * sin(angle) + vortexSettings.position.y
+ radius / 2 * cos(angle) + settings.position.x,
+ radius / 2 * sin(angle) + settings.position.y
]
}
}
func getNewParticleColorRamp() -> [Color] {
- switch vortexSettings.colors {
+ switch settings.colors {
case .single(let color):
return [color]
diff --git a/Sources/Vortex/System/VortexSystem.swift b/Sources/Vortex/System/VortexSystem.swift
index b675cea..e79e363 100644
--- a/Sources/Vortex/System/VortexSystem.swift
+++ b/Sources/Vortex/System/VortexSystem.swift
@@ -23,8 +23,8 @@ public class VortexSystem: Identifiable, Equatable, Hashable {
hasher.combine(id)
}
/// Virtual 'add' of the Settings properties to the vortex system
- subscript(dynamicMember keyPath: KeyPath) -> T {
- vortexSettings[keyPath: keyPath]
+ subscript(dynamicMember keyPath: KeyPath) -> T {
+ settings[keyPath: keyPath]
}
// These properties are used for managing a live system, rather
@@ -60,189 +60,17 @@ public class VortexSystem: Identifiable, Equatable, Hashable {
// These properties control system-wide behavior.
/// The configuration settings for a VortexSystem
- var vortexSettings: VortexSystem.Settings
+ var settings: VortexSettings
- /// Backwards compatibility only - use is deprecated
- /// The secondary systems to be used in the particle system
- var secondarySystems: [VortexSystem] = []
-
/// Initialise a particle system with a VortexSettings struct
/// - Parameter settings: VortexSettings
/// The settings to be used for this particle system.
- public init( settings: VortexSystem.Settings ) {
- self.vortexSettings = settings
+ 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 let .randomRamp(allColors) = vortexSettings.colors {
- 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.secondarySystems = secondarySystems
- self.vortexSettings = Settings(
- tags: tags,
- 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: angularSpeed,
- colors: colors,
- size: size,
- sizeVariation: sizeVariation,
- sizeMultiplierAtDeath: sizeMultiplierAtDeath,
- stretchFactor: stretchFactor
- )
-
- if case .randomRamp(let allColors) = colors {
+ if case let .randomRamp(allColors) = settings.colors {
selectedColorRamp = Int.random(in: 0.. VortexSystem {
- VortexSystem(
- tags: vortexSettings.tags,
- secondarySystems: secondarySystems,
- spawnOccasion: vortexSettings.spawnOccasion,
- position: vortexSettings.position,
- shape: vortexSettings.shape,
- birthRate: vortexSettings.birthRate,
- emissionLimit: vortexSettings.emissionLimit,
- emissionDuration: vortexSettings.emissionDuration,
- idleDuration: vortexSettings.idleDuration,
- burstCount: vortexSettings.burstCount,
- burstCountVariation: vortexSettings.burstCountVariation,
- lifespan: vortexSettings.lifespan,
- lifespanVariation: vortexSettings.lifespanVariation,
- speed: vortexSettings.speed,
- speedVariation: vortexSettings.speedVariation,
- angle: vortexSettings.angle,
- angleRange: vortexSettings.angleRange,
- acceleration: vortexSettings.acceleration,
- attractionCenter: vortexSettings.attractionCenter,
- attractionStrength: vortexSettings.attractionStrength,
- dampingFactor: vortexSettings.dampingFactor,
- angularSpeed: vortexSettings.angularSpeed,
- angularSpeedVariation: vortexSettings.angularSpeedVariation,
- colors: vortexSettings.colors,
- size: vortexSettings.size,
- sizeVariation: vortexSettings.sizeVariation,
- sizeMultiplierAtDeath: vortexSettings.sizeMultiplierAtDeath,
- stretchFactor: vortexSettings.stretchFactor
- )
- }
}
diff --git a/Sources/Vortex/Views/VortexProxy.swift b/Sources/Vortex/Views/VortexProxy.swift
index d94482a..4a8bf25 100644
--- a/Sources/Vortex/Views/VortexProxy.swift
+++ b/Sources/Vortex/Views/VortexProxy.swift
@@ -26,7 +26,7 @@ public struct VortexProxy {
public func move(to newPosition: CGPoint) {
guard let particleSystem else { return }
- particleSystem.vortexSettings.position = [
+ particleSystem.settings.position = [
newPosition.x / particleSystem.lastDrawSize.width,
newPosition.y / particleSystem.lastDrawSize.height
]
diff --git a/Sources/Vortex/Views/VortexView.swift b/Sources/Vortex/Views/VortexView.swift
index d77ee68..d8b115e 100644
--- a/Sources/Vortex/Views/VortexView.swift
+++ b/Sources/Vortex/Views/VortexView.swift
@@ -63,22 +63,12 @@ public struct VortexView: View where Symbols: View {
/// 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.
- /// If not specified, a default group of three views tagged with `.circle`,`.confetti` and `.sparkle` will be used.
public init(
- settings: VortexSystem.Settings = .init(),
+ _ 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("triangle")
- Image.sparkle
- .frame(width: 16, height: 16).blendMode(.plusLighter).tag("sparkle")
- }
- }
+ @ViewBuilder symbols: () -> Symbols
) {
- _particleSystem = State( initialValue: VortexSystem(settings: settings) )
+ _particleSystem = State( initialValue: VortexSystem(settings) )
self.targetFrameRate = targetFrameRate
self.symbols = symbols()
}
diff --git a/Sources/Vortex/Views/VortexViewReader.swift b/Sources/Vortex/Views/VortexViewReader.swift
index 8a1b193..cd3fb6c 100644
--- a/Sources/Vortex/Views/VortexViewReader.swift
+++ b/Sources/Vortex/Views/VortexViewReader.swift
@@ -26,9 +26,9 @@ public struct VortexViewReader: View {
nearestVortexSystem?.burst()
} attractTo: { point in
if let point {
- nearestVortexSystem?.vortexSettings.attractionCenter = SIMD2(point.x, point.y)
+ nearestVortexSystem?.settings.attractionCenter = SIMD2(point.x, point.y)
} else {
- nearestVortexSystem?.vortexSettings.attractionCenter = nil
+ nearestVortexSystem?.settings.attractionCenter = nil
}
}
From 9f8752dc6e8df98cf7ac7b350ccc0f41504ab918 Mon Sep 17 00:00:00 2001
From: Discoinferno <007gnail@gmail.com>
Date: Mon, 9 Dec 2024 00:31:27 +0000
Subject: [PATCH 3/5] Introducing VortexSettings This commit is essentially an
abstraction of the configuration settings from the VortexSystem into it's own
VortexSettings type, with those properties replaced in VortexSystem with a
single property; settings. (Prebuilt/default symbols have been removed from
the previous PR). The changes are documented in an the updated README.
Settings enables an intuitive initialisation based on preset settings. Using
type ahead, it enables discovery of each setting, and does not rely on
specifying all the required parameters of the init, and getting them all in
the right order, It also removes the need to ever call `.makeUniqueCopy`.
The change necessitates changes to VortexSystem of course, but also the VortexProxy, ViewReader and VortexSystem-Behaviour to ensure that the configuration settings are correctly accessed via settings. (dynamicLookup is also added to VortexSystem for convenience).
---
README.md | 109 +++-
Sources/Vortex/Presets/Confetti.swift | 42 +-
Sources/Vortex/Presets/Fire.swift | 33 +-
Sources/Vortex/Presets/Fireflies.swift | 59 +-
Sources/Vortex/Presets/Fireworks.swift | 49 +-
Sources/Vortex/Presets/Magic.swift | 38 +-
Sources/Vortex/Presets/Rain.swift | 48 +-
Sources/Vortex/Presets/Smoke.swift | 38 +-
Sources/Vortex/Presets/Snow.swift | 40 +-
Sources/Vortex/Presets/Spark.swift | 47 +-
Sources/Vortex/Presets/Splash.swift | 78 ++-
Sources/Vortex/Settings/Settings.swift | 596 +++++++++---------
.../Vortex/System/VortexSystem-Behavior.swift | 128 ++--
Sources/Vortex/System/VortexSystem.swift | 184 +-----
Sources/Vortex/Views/VortexProxy.swift | 2 +-
Sources/Vortex/Views/VortexView.swift | 16 +-
Sources/Vortex/Views/VortexViewReader.swift | 4 +-
17 files changed, 716 insertions(+), 795 deletions(-)
diff --git a/README.md b/README.md
index 19bec14..82470d9 100644
--- a/README.md
+++ b/README.md
@@ -43,35 +43,48 @@ There are also a number of Xcode previews created for a selected number of the p
## Basic use
-Rendering a Vortex particle system using a preset configuration takes a single line!
+Rendering a Vortex particle system using a preset configuration takes just a few lines of code;-
```swift
-VortexView(.rain)
+VortexView(.rain) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .tag("circle")
+}
```
There are lots of built-in particle system designs, such as Fireworks (.fireworks) and Fire (.fire)
You can also easily create custom effects, in two steps:
-1. Creating an instance of `VortexSystem.Settings`, configured for how you want your particles to behave. The easiest way to create Settings is to modify an existing one. (example below)
+1. Creating an instance of `VortexSettings`, configured for how you want your particles to behave. The easiest way to create Settings is to modify an existing one. (example below)
2. Call `VortexView` with the settings you just created.
e.g.
```swift
struct ContentView: View {
var body: some View {
- let fireSettings = VortexSystem.Settings(from: .fire ) { settings in
+ let fireSettings = VortexSettings(basedOn: .fire ) { settings in
settings.position = [ 0.5, 1.03]
settings.shape = .box(width: 1.0, height: 0)
settings.birthRate = 600
}
- VortexView(settings: fireSettings)
+ VortexView(fireSettings) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .blur(radius: 3)
+ .blendMode(.plusLighter)
+ .tag("circle")
+ }
}
}
```
+The tag modifier that is added to the view is very important. It links the VortexSettings, which describes the behaviour of the particle system, to the particle views that are drawn. The `.tag` modifier on the view must match one of the `tags` in the settings.
## Next steps
-All the existing presets use built-in Images that are tagged with "circle", "confetti", or "sparkle"
-You can however use your own views within the particle system, you just need to add them to a trailing closure for VortexView and tag them appropriately.
+All the existing presets reference views that are tagged with "circle", "square", or "sparkle"
+You can of course create use your own views within the particle system, you just need to add them to a trailing closure for VortexView and tag them appropriately.
e.g.
```swift
@@ -89,7 +102,6 @@ struct ContentView: View {
settings.sizeVariation = 0.5
}
var body: some View {
-
VortexView(settings: snow) {
Circle()
.fill(.white)
@@ -123,11 +135,11 @@ For example, this uses the built-in `.confetti` effect, then uses the Vortex pro
```swift
VortexViewReader { proxy in
- VortexView(settings: .confetti) {
+ VortexView(.confetti) {
Rectangle()
.fill(.white)
.frame(width: 16, height: 16)
- .tag("confetti")
+ .tag("square")
Circle()
.fill(.white)
@@ -151,7 +163,7 @@ Check the Xcode Preview of the Fireflies preset to see this in action.
One of the more advanced Vortex features is the ability create secondary particle systems – for each particle in one system to create a new particle system. This enables creation of multi-stage effects, such as fireworks: one particle launches upwards, setting off sparks as it flies, then exploding into color when it dies.
> [!Important]
-> When creating particle systems with secondary systems inside, both the primary and secondary system can have their own set of tags. However, you must provide all tags from all systems when creating the `VortexView`.
+> When creating particle systems with secondary settings, both the primary and secondary system can have their own set of tags. However, you must provide all tags from all systems when creating the `VortexView`.
## Creating custom particle systems
@@ -235,7 +247,17 @@ The `.confetti` preset creates a confetti effect where views fly shoot out when
```swift
VortexViewReader { proxy in
- VortexView(.confetti)
+ VortexView(.confetti) {
+ Rectangle()
+ .fill(.white)
+ .frame(width: 16, height: 16)
+ .tag("square")
+
+ Circle()
+ .fill(.white)
+ .frame(width: 16)
+ .tag("circle")
+ }
Button("Burst", action: proxy.burst)
}
```
@@ -246,7 +268,14 @@ VortexViewReader { proxy in
The `.fire` preset creates a flame effect.
```swift
-VortexView(.fire)
+VortexView(.fire) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .blur(radius: 3)
+ .blendMode(.plusLighter)
+ .tag("circle")
+}
```
@@ -271,8 +300,14 @@ VortexView(.fireflies) {
The `.fireworks` preset creates a three-stage particle effect to simulate exploding fireworks. Each firework is a particle, and also launches new "spark" particles as it flies upwards. When the firework particle is destroyed, it creates an explosion effect in a range of colors.
```swift
-VortexView(.fireworks)
-
+VortexView(.fireworks) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .blur(radius: 5)
+ .blendMode(.plusLighter)
+ .tag("circle")
+}
```
@@ -281,16 +316,24 @@ VortexView(.fireworks)
The `.magic` preset creates a simple ring of particles that fly outwards as they fade out. This works best using the "sparkle" image contained in the Assets folder of this repository, but you can use any other image or shape you prefer.
```swift
-VortexView(.magic)
+VortexView(.magic) {
+ Image.sparkle
+ .blendMode(.plusLighter)
+ .tag("sparkle")
+}
```
-
### Rain
The `.rain` preset creates a rainfall system by stretching your view based on the rain speed:
```swift
-VortexView(.rain)
+VortexView(.rain) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .tag("circle")
+}
```
@@ -314,16 +357,26 @@ VortexView(.smoke) {
The `.snow` preset creates a falling snow effect.
```swift
-VortexView(.snow)
+VortexView(.snow) {
+ Circle()
+ .fill(.white)
+ .frame(width: 24)
+ .blur(radius: 5)
+ .tag("circle")
+}
```
-
### Spark
The `.spark` preset creates an intermittent spark effect, where sparks fly out for a short time, then pause, then fly out again, etc.
```swift
-VortexView(.spark)
+VortexView(.spark) {
+ Circle()
+ .fill(.white)
+ .frame(width: 16)
+ .tag("circle")
+}
```
@@ -333,8 +386,18 @@ The `.splash` present contains raindrop splashes, as if rain were hitting the gr
```swift
ZStack {
- VortexView(.rain)
- VortexView(.splash)
+ VortexView(.rain) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .tag("circle")
+ }
+ VortexView(.splash){
+ Circle()
+ .fill(.white)
+ .frame(width: 16)
+ .tag("circle")
+ }
}
```
diff --git a/Sources/Vortex/Presets/Confetti.swift b/Sources/Vortex/Presets/Confetti.swift
index 752f0d0..d39c4ab 100644
--- a/Sources/Vortex/Presets/Confetti.swift
+++ b/Sources/Vortex/Presets/Confetti.swift
@@ -7,18 +7,10 @@
import SwiftUI
-extension VortexSystem {
+extension VortexSettings {
/// A built-in effect that creates confetti only when a burst is triggered.
- /// Relies on "confetti" and "circle" tags being present – using `Rectangle`
- /// and `Circle` with frames of 16x16 works well.
- /// This declaration is deprecated. A VortexView should be invoked with a VortexSystem.Settings struct directly. See the example below.
- public static let confetti = VortexSystem(settings: .confetti)
-}
-
-extension VortexSystem.Settings {
- /// A built-in effect that creates confetti only when a burst is triggered.
- public static let confetti = VortexSystem.Settings() { settings in
- settings.tags = ["confetti", "circle"]
+ public static let confetti = VortexSettings { settings in
+ settings.tags = ["square", "circle"]
settings.birthRate = 0
settings.lifespan = 4
settings.speed = 0.5
@@ -26,8 +18,34 @@ extension VortexSystem.Settings {
settings.angleRange = .degrees(90)
settings.acceleration = [0, 1]
settings.angularSpeedVariation = [4, 4, 4]
- settings.colors = .random(.white, .red, .green, .blue, .pink, .orange, .cyan)
+ settings.colors = .random(
+ .white, .red, .green, .blue, .pink, .orange, .cyan)
settings.size = 0.5
settings.sizeVariation = 0.5
}
}
+
+@available(macOS 14.0, *)
+#Preview("Demonstrate on demand bursts") {
+ VortexViewReader { proxy in
+ ZStack {
+ Text("Tap anywhere to create confetti.")
+
+ VortexView(.confetti.makeUniqueCopy()) {
+ Rectangle()
+ .fill(.white)
+ .frame(width: 16, height: 16)
+ .tag("square")
+ Circle()
+ .fill(.white)
+ .frame(width: 16)
+ .tag("circle")
+ }
+ .onTapGesture { location in
+ proxy.move(to: location)
+ proxy.burst()
+ }
+ }
+ }
+}
+
diff --git a/Sources/Vortex/Presets/Fire.swift b/Sources/Vortex/Presets/Fire.swift
index 1ce2bdf..7d1c940 100644
--- a/Sources/Vortex/Presets/Fire.swift
+++ b/Sources/Vortex/Presets/Fire.swift
@@ -7,17 +7,10 @@
import SwiftUI
-extension VortexSystem {
- /// A built-in fire effect. Relies on a "circle" tag being present, which should be set to use
+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(settings: .fire)
-}
-
-
-extension VortexSystem.Settings {
- /// 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.Settings { settings in
+ public static let fire = VortexSettings { settings in
settings.tags = ["circle"]
settings.shape = .box(width: 0.1, height: 0)
settings.birthRate = 300
@@ -31,11 +24,19 @@ extension VortexSystem.Settings {
}
}
-#Preview {
- let floorOnFire = VortexSystem.Settings(from: .fire ) { settings in
- settings.position = [ 0.5, 1.02]
+#Preview("Demonstrates the fire preset with attraction") {
+ /// Here we modify the default fire settings to extend it across the bottom of the screen
+ let floorOnFire = VortexSettings(basedOn: .fire) { settings in
+ settings.position = [0.5, 1.02]
settings.shape = .box(width: 1.0, height: 0)
- settings.birthRate = 600
- }
- VortexView(settings: floorOnFire)
+ settings.birthRate = 600
+ }
+ 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 f777256..50a8a07 100644
--- a/Sources/Vortex/Presets/Fireflies.swift
+++ b/Sources/Vortex/Presets/Fireflies.swift
@@ -7,15 +7,9 @@
import SwiftUI
-extension VortexSystem {
- /// 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(settings: .fireflies)
-}
-
-extension VortexSystem.Settings {
+extension VortexSettings {
/// A built-in firefly effect. Uses the built-in 'circle' image.
- public static let fireflies = VortexSystem.Settings() { settings in
+ public static let fireflies = VortexSettings() { settings in
settings.tags = ["circle"]
settings.shape = .ellipse(radius: 0.5)
settings.birthRate = 200
@@ -32,7 +26,7 @@ extension VortexSystem.Settings {
/// A Fireflies preview, using the `.fireflies` preset
/// macOS 15 is required for the `.onModifierKeysChanged` method that is used to capture the Option key being pressed.
@available(macOS 15.0, *)
-#Preview("Fireflies") {
+#Preview("Demonstrates use of attraction and repulsion") {
@Previewable @State var isDragging = false
/// A state value indicating whether the Option key is being held down
@Previewable @State var pressingOptionKey = false
@@ -51,28 +45,31 @@ extension VortexSystem.Settings {
.padding(.bottom, 20)
}
- VortexView(settings: .fireflies)
- .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)
- pressingOptionKey = !new.isEmpty
- }
- .gesture(
- DragGesture(minimumDistance: 0)
- .onChanged { value in
- proxy.attractTo(value.location)
- proxy.particleSystem?
- .vortexSettings.attractionStrength = pressingOptionKey ? 2.5 : -2
- isDragging = true
- }
- .onEnded { _ in
- proxy.particleSystem?
- .vortexSettings.attractionStrength = 0
- isDragging = false
- }
- )
+ 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)
+ pressingOptionKey = !new.isEmpty
+ }
+ .gesture(
+ DragGesture(minimumDistance: 0)
+ .onChanged { value in
+ proxy.attractTo(value.location)
+ proxy.particleSystem?.settings.attractionStrength = pressingOptionKey ? 2.5 : -2
+ isDragging = true
+ }
+ .onEnded { _ in
+ proxy.particleSystem?.settings.attractionStrength = 0
+ isDragging = false
+ }
+ )
}
}
- .navigationSubtitle("Demonstrates use of attraction and repulsion")
- .ignoresSafeArea(edges: .top)
}
diff --git a/Sources/Vortex/Presets/Fireworks.swift b/Sources/Vortex/Presets/Fireworks.swift
index fea91f6..b7cb64c 100644
--- a/Sources/Vortex/Presets/Fireworks.swift
+++ b/Sources/Vortex/Presets/Fireworks.swift
@@ -7,19 +7,22 @@
import SwiftUI
-extension VortexSystem {
- /// A built-in fireworks effect, using secondary systems that create sparkles and explosions.
- /// Relies on the built in circle symbol, or a symbol with the "circle" tag being present, which should be set to use
- /// `.blendMode(.plusLighter)`.
- public static let fireworks = VortexSystem(settings: .fireworks)
-}
-
-extension VortexSystem.Settings {
+extension VortexSettings {
/// A built-in fireworks effect, using secondary systems that create sparkles and explosions.
/// Relies on a symbol view tagged with "circle" being available to the VortexView. (one such image is built-in)
- public static let fireworks = VortexSystem.Settings { settings in
+ public static let fireworks = VortexSettings { settings in
+ settings.tags = ["circle"]
+ settings.position = [0.5, 1]
+ settings.birthRate = 2
+ settings.emissionLimit = 1000
+ settings.speed = 1.5
+ settings.speedVariation = 0.75
+ settings.angleRange = .degrees(60)
+ settings.dampingFactor = 2
+ settings.size = 0.15
+ settings.stretchFactor = 4
- var sparkles = VortexSystem.Settings { sparkle in
+ var sparkles = VortexSettings { sparkle in
sparkle.tags = ["sparkle"]
sparkle.spawnOccasion = .onUpdate
sparkle.emissionLimit = 1
@@ -29,7 +32,7 @@ extension VortexSystem.Settings {
sparkle.size = 0.05
}
- var explosions = VortexSystem.Settings { explosion in
+ var explosions = VortexSettings { explosion in
explosion.tags = ["circle"]
explosion.spawnOccasion = .onDeath
explosion.position = [0.5, 0.5]
@@ -52,23 +55,17 @@ extension VortexSystem.Settings {
explosion.sizeMultiplierAtDeath = 0
}
-
- settings.tags = ["circle"]
settings.secondarySettings = [sparkles, explosions]
- settings.position = [0.5, 1]
- settings.birthRate = 2
- settings.emissionLimit = 1000
- settings.speed = 1.5
- settings.speedVariation = 0.75
- settings.angleRange = .degrees(60)
- settings.dampingFactor = 2
- settings.size = 0.15
- settings.stretchFactor = 4
}
}
-#Preview("Fireworks") {
- VortexView(settings: .fireworks)
- .navigationSubtitle("Demonstrates multi-stage effects")
- .ignoresSafeArea(edges: .top)
+#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..9b558d4 100644
--- a/Sources/Vortex/Presets/Magic.swift
+++ b/Sources/Vortex/Presets/Magic.swift
@@ -7,22 +7,28 @@
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(
- tags: ["sparkle"],
- shape: .ring(radius: 0.5),
- lifespan: 1.5,
- speed: 0,
- speedVariation: 0.2,
- angleRange: .degrees(360),
- angularSpeedVariation: [0, 0, 10],
- colors: .random(.red, .pink, .orange, .blue, .green, .white),
- size: 0.5,
- sizeVariation: 0.5,
- sizeMultiplierAtDeath: 0.01
- )
- }()
+ static let magic = VortexSettings { settings in
+ settings.tags = ["sparkle"]
+ settings.shape = .ring(radius: 0.5)
+ settings.lifespan = 1.5
+ settings.speed = 0
+ settings.speedVariation = 0.2
+ settings.angleRange = .degrees(360)
+ settings.angularSpeedVariation = [0, 0, 10]
+ settings.colors = .random(.red, .pink, .orange, .blue, .green, .white)
+ settings.size = 0.5
+ settings.sizeVariation = 0.5
+ settings.sizeMultiplierAtDeath = 0.01
+ }
+}
+
+#Preview("Demonstrates the magic preset") {
+ VortexView(.magic) {
+ Image.sparkle
+ .blendMode(.plusLighter)
+ .tag("sparkle")
+ }
}
diff --git a/Sources/Vortex/Presets/Rain.swift b/Sources/Vortex/Presets/Rain.swift
index fcf1c1b..7e044b6 100644
--- a/Sources/Vortex/Presets/Rain.swift
+++ b/Sources/Vortex/Presets/Rain.swift
@@ -7,26 +7,34 @@
import SwiftUI
-extension VortexSystem {
+extension VortexSettings {
/// A built-in rain effect. Relies on a "circle" tag being present.
- public static let rain: VortexSystem = {
- VortexSystem(
- tags: ["circle"],
- position: [0.5, 0 ],
- shape: .box(width: 1.8, height: 0),
- birthRate: 400,
- lifespan: 0.5,
- speed: 4.5,
- speedVariation: 2,
- angle: .degrees(190),
- colors: .random(
- Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.6),
- Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.5),
- Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.4)
- ),
- size: 0.09,
- sizeVariation: 0.05,
- stretchFactor: 12
+ public static let rain = VortexSettings { settings in
+ settings.tags = ["circle"]
+ settings.position = [0.5, 0 ]
+ settings.shape = .box(width: 1.8, height: 0)
+ settings.birthRate = 400
+ settings.lifespan = 0.5
+ settings.speed = 4.5
+ settings.speedVariation = 2
+ settings.angle = .degrees(190)
+ settings.colors = .random(
+ VortexSystem.Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.6),
+ VortexSystem.Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.5),
+ VortexSystem.Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.4)
)
- }()
+ settings.size = 0.09
+ settings.sizeVariation = 0.05
+ settings.stretchFactor = 12
+ }
+
+}
+
+#Preview("Demonstrates the rain preset") {
+ VortexView(.rain) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .tag("circle")
+ }
}
diff --git a/Sources/Vortex/Presets/Smoke.swift b/Sources/Vortex/Presets/Smoke.swift
index a678e63..ce503ed 100644
--- a/Sources/Vortex/Presets/Smoke.swift
+++ b/Sources/Vortex/Presets/Smoke.swift
@@ -7,20 +7,28 @@
import SwiftUI
-extension VortexSystem {
+extension VortexSettings {
/// A built-in smoke effect. Relies on a "circle" tag being present.
- public static let smoke: VortexSystem = {
- VortexSystem(
- tags: ["circle"],
- shape: .box(width: 0.05, height: 0),
- lifespan: 3,
- speed: 0.1,
- speedVariation: 0.1,
- angleRange: .degrees(10),
- colors: .ramp(.gray, .gray.opacity(0)),
- size: 0.5,
- sizeVariation: 0.5,
- sizeMultiplierAtDeath: 2
- )
- }()
+ static let smoke = VortexSettings { settings in
+ settings.tags = ["circle"]
+ settings.shape = .box(width: 0.05, height: 0)
+ settings.lifespan = 3
+ settings.speed = 0.1
+ settings.speedVariation = 0.1
+ settings.angleRange = .degrees(10)
+ settings.colors = .ramp(.gray, .gray.opacity(0))
+ settings.size = 0.5
+ settings.sizeVariation = 0.5
+ settings.sizeMultiplierAtDeath = 2
+ }
+}
+
+#Preview("Demonstrates the 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..c445030 100644
--- a/Sources/Vortex/Presets/Snow.swift
+++ b/Sources/Vortex/Presets/Snow.swift
@@ -7,26 +7,30 @@
import SwiftUI
-extension VortexSystem {
+extension VortexSettings {
/// A built-in snow effect. Relies on a "circle" tag being present.
- public static let snow: VortexSystem = {
- VortexSystem(
- tags: ["circle"],
- position: [0.5, 0],
- shape: .box(width: 1, height: 0),
- birthRate: 50,
- lifespan: 10,
- speed: 0.2,
- speedVariation: 0.2,
- angle: .degrees(180),
- angleRange: .degrees(20),
- size: 0.25,
- sizeVariation: 0.4
- )
- }()
+ static let snow = VortexSettings { settings in
+ settings.tags = ["circle"]
+ settings.position = [0.5, 0]
+ settings.shape = .box(width: 1, height: 0)
+ settings.birthRate = 50
+ settings.lifespan = 10
+ settings.speed = 0.2
+ settings.speedVariation = 0.2
+ settings.angle = .degrees(180)
+ settings.angleRange = .degrees(20)
+ settings.size = 0.25
+ settings.sizeVariation = 0.4
+ }
}
-#Preview {
+#Preview("Demonstrates the snow preset") {
// 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..7960818 100644
--- a/Sources/Vortex/Presets/Spark.swift
+++ b/Sources/Vortex/Presets/Spark.swift
@@ -7,26 +7,33 @@
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(
- tags: ["circle"],
- birthRate: 150,
- emissionDuration: 0.2,
- idleDuration: 0.5,
- lifespan: 1.5,
- speed: 1.25,
- speedVariation: 0.2,
- angle: .degrees(330),
- angleRange: .degrees(20),
- acceleration: [0, 3],
- dampingFactor: 4,
- colors: .ramp(.white, .yellow, .yellow.opacity(0)),
- size: 0.1,
- sizeVariation: 0.1,
- stretchFactor: 8
- )
- }()
+ static let spark = VortexSettings { settings in
+ settings.tags = ["circle"]
+ settings.birthRate = 150
+ settings.emissionDuration = 0.2
+ settings.idleDuration = 0.5
+ settings.lifespan = 1.5
+ settings.speed = 1.25
+ settings.speedVariation = 0.2
+ settings.angle = .degrees(330)
+ settings.angleRange = .degrees(20)
+ settings.acceleration = [0, 3]
+ settings.dampingFactor = 4
+ settings.colors = .ramp(.white, .yellow, .yellow.opacity(0))
+ settings.size = 0.1
+ settings.sizeVariation = 0.1
+ settings.stretchFactor = 8
+ }
+}
+
+#Preview("Demonstrates the spark preset") {
+ VortexView(.spark) {
+ Circle()
+ .fill(.white)
+ .frame(width: 16)
+ .tag("circle")
+ }
}
diff --git a/Sources/Vortex/Presets/Splash.swift b/Sources/Vortex/Presets/Splash.swift
index 65f018e..b16782f 100644
--- a/Sources/Vortex/Presets/Splash.swift
+++ b/Sources/Vortex/Presets/Splash.swift
@@ -7,39 +7,55 @@
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(
- 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
- )
-
- 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
- )
+ static let splash = VortexSettings { settings in
+
+ var drops = VortexSettings { drops in
+ 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(
+ VortexSystem.Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.7),
+ VortexSystem.Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.6),
+ VortexSystem.Color(red: 0.7, green: 0.7, blue: 1, opacity: 0.5)
+ )
+ drops.size = 0.2
+ drops.position = [0.5,1]
+ }
+
+ settings.tags = ["circle"]
+ settings.secondarySettings = [drops]
+ settings.position = [0.5, 1]
+ settings.shape = .box(width: 1, height: 0)
+ settings.birthRate = 5
+ settings.lifespan = 0.001
+ settings.speed = 0
+ settings.colors = .single(.clear)
+ settings.size = 0
+ }
+}
- return mainSystem
- }()
+#Preview("Demonstrates a combination rain/splash preset") {
+ ZStack {
+ VortexView(.rain) {
+ Circle()
+ .fill(.white)
+ .frame(width: 32)
+ .tag("circle")
+ }
+ VortexView(.splash) {
+ Circle()
+ .fill(.white)
+ .frame(width: 16)
+ .tag("circle")
+ }
+ }
}
+
diff --git a/Sources/Vortex/Settings/Settings.swift b/Sources/Vortex/Settings/Settings.swift
index 65f3c69..b5f6297 100644
--- a/Sources/Vortex/Settings/Settings.swift
+++ b/Sources/Vortex/Settings/Settings.swift
@@ -7,308 +7,298 @@
import SwiftUI
-extension VortexSystem {
-
- /// 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 Settings: 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: VortexSystem.Settings, rhs: VortexSystem.Settings
- ) -> 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 = [Settings]()
-
- /// 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: 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:
- /// - from: `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 = VortexSystem.Settings(from: .fire ) { settings in
- /// settings.position = [ 0.5, 1.03]
- /// settings.shape = .box(width: 1.0, height: 0)
- /// settings.birthRate = 600
- /// }
- /// ```
- public init(
- from base: VortexSystem.Settings = VortexSystem.Settings(),
- _ modifiedBy: @escaping (_: inout VortexSystem.Settings) -> Void
- ) {
- // 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 to make deep copies of the VortexSystem class so that secondary systems functioned correctly.
- /// No longer needed, but created here for backward compatibility if the VortexSystem initialiser taking a Settings
- /// struct is updated to be identical to the old init that took a VortexSystem initialiser.
- public func makeUniqueCopy() -> VortexSystem.Settings {
- return self
- }
-
- /// Backward compatibility again:
- public init(
- tags: [String],
- 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
- }
+/// 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
}
}
diff --git a/Sources/Vortex/System/VortexSystem-Behavior.swift b/Sources/Vortex/System/VortexSystem-Behavior.swift
index 1713676..5d27ef8 100644
--- a/Sources/Vortex/System/VortexSystem-Behavior.swift
+++ b/Sources/Vortex/System/VortexSystem-Behavior.swift
@@ -17,46 +17,46 @@ extension VortexSystem {
func update(date: Date, drawSize: CGSize) {
lastDrawSize = drawSize
updateSecondarySystems(date: date, drawSize: drawSize)
-
+
let drawDivisor = drawSize.height / drawSize.width
let currentTimeInterval = date.timeIntervalSince1970
-
+
let delta = currentTimeInterval - lastUpdate
lastUpdate = currentTimeInterval
-
- if vortexSettings.isEmitting && lastUpdate - lastIdleTime > vortexSettings.emissionDuration {
- vortexSettings.isEmitting = false
+
+ if settings.isEmitting && lastUpdate - lastIdleTime > settings.emissionDuration {
+ settings.isEmitting = false
lastIdleTime = lastUpdate
- } else if vortexSettings.isEmitting == false && lastUpdate - lastIdleTime > vortexSettings.idleDuration {
- vortexSettings.isEmitting = true
+ } else if settings.isEmitting == false && lastUpdate - lastIdleTime > settings.idleDuration {
+ settings.isEmitting = true
lastIdleTime = lastUpdate
}
-
+
createParticles(delta: delta)
-
+
var attractionUnitPoint: SIMD2?
-
+
// Push attraction strength down to a small number, otherwise
// it's much too strong.
- let adjustedAttractionStrength = vortexSettings.attractionStrength / 1000
-
- if let attractionCenter = vortexSettings.attractionCenter {
+ let adjustedAttractionStrength = settings.attractionStrength / 1000
+
+ if let attractionCenter = settings.attractionCenter {
attractionUnitPoint = [attractionCenter.x / drawSize.width, attractionCenter.y / drawSize.height]
}
-
+
particles = particles.compactMap {
var particle = $0
let age = currentTimeInterval - particle.birthTime
let lifeProgress = age / particle.lifespan
-
+
// Apply attraction force to particle's existing velocity.
if let attractionUnitPoint {
let gap = attractionUnitPoint - particle.position
let distance = sqrt((gap * gap).sum())
-
+
if distance > 0 {
let normalized = gap / distance
-
+
// Increase the magnitude the closer we get, adding a small
// amount to avoid a slingshot / over-attraction.
let movementMagnitude = adjustedAttractionStrength / (distance * distance + 0.0025)
@@ -64,26 +64,26 @@ extension VortexSystem {
particle.position += movement
}
}
-
+
// Update particle position
particle.position.x += particle.speed.x * delta * drawDivisor
particle.position.y += particle.speed.y * delta
-
- if vortexSettings.dampingFactor != 1 {
- let dampingAmount = vortexSettings.dampingFactor * delta / vortexSettings.lifespan
+
+ if settings.dampingFactor != 1 {
+ let dampingAmount = settings.dampingFactor * delta / settings.lifespan
particle.speed -= particle.speed * dampingAmount
}
-
- particle.speed += vortexSettings.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 * vortexSettings.sizeMultiplierAtDeath,
+ to: particle.initialSize * settings.sizeMultiplierAtDeath,
amount: lifeProgress
)
-
+
if age >= particle.lifespan {
spawn(from: particle, event: .onDeath)
return nil
@@ -93,48 +93,44 @@ extension VortexSystem {
}
}
}
-
+
private func createParticles(delta: Double) {
- outstandingParticles += vortexSettings.birthRate * delta
-
+ outstandingParticles += settings.birthRate * delta
+
if outstandingParticles >= 1 {
let particlesToCreate = Int(outstandingParticles)
-
+
for _ in 0.. 0 {
activeSecondarySystems.remove(activeSecondarySystem)
}
}
}
-
+
/// Used to create a single particle.
/// - 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 vortexSettings.isEmitting else { return }
-
- if let emissionLimit = vortexSettings.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 = vortexSettings.angle.radians + vortexSettings.angleRange.radians.randomSpread() - .pi / 2
- let launchSpeed = vortexSettings.speed + vortexSettings.speedVariation.randomSpread()
- let lifespan = vortexSettings.lifespan + vortexSettings.lifespanVariation.randomSpread()
- let size = vortexSettings.size + vortexSettings.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 +138,11 @@ extension VortexSystem {
sin(launchAngle) * launchSpeed
)
- let spinSpeed = vortexSettings.angularSpeed + vortexSettings.angularSpeedVariation.randomSpread()
+ let spinSpeed = settings.angularSpeed + settings.angularSpeedVariation.randomSpread()
let colorRamp = getNewParticleColorRamp()
let newParticle = Particle(
- tag: vortexSettings.tags.randomElement() ?? "",
+ tag: settings.tags.randomElement() ?? "",
position: particlePosition,
speed: speed,
birthTime: lastUpdate,
@@ -163,7 +159,7 @@ extension VortexSystem {
/// Force a bunch of particles to be created immediately.
func burst() {
- let particlesToCreate = vortexSettings.burstCount + vortexSettings.burstCountVariation.randomSpread()
+ let particlesToCreate = settings.burstCount + settings.burstCountVariation.randomSpread()
for _ in 0.. SIMD2 {
- switch vortexSettings.shape {
+ switch settings.shape {
case .point:
- return vortexSettings.position
+ return settings.position
case .box(let width, let height):
return [
- vortexSettings.position.x + width.randomSpread(),
- vortexSettings.position.y + height.randomSpread()
+ settings.position.x + width.randomSpread(),
+ settings.position.y + height.randomSpread()
]
case .ellipse(let radius):
@@ -205,22 +193,22 @@ extension VortexSystem {
let placement = Double.random(in: 0...radius / 2)
return [
- placement * cos(angle) + vortexSettings.position.x,
- placement * sin(angle) + vortexSettings.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) + vortexSettings.position.x,
- radius / 2 * sin(angle) + vortexSettings.position.y
+ radius / 2 * cos(angle) + settings.position.x,
+ radius / 2 * sin(angle) + settings.position.y
]
}
}
func getNewParticleColorRamp() -> [Color] {
- switch vortexSettings.colors {
+ switch settings.colors {
case .single(let color):
return [color]
diff --git a/Sources/Vortex/System/VortexSystem.swift b/Sources/Vortex/System/VortexSystem.swift
index b675cea..e79e363 100644
--- a/Sources/Vortex/System/VortexSystem.swift
+++ b/Sources/Vortex/System/VortexSystem.swift
@@ -23,8 +23,8 @@ public class VortexSystem: Identifiable, Equatable, Hashable {
hasher.combine(id)
}
/// Virtual 'add' of the Settings properties to the vortex system
- subscript(dynamicMember keyPath: KeyPath) -> T {
- vortexSettings[keyPath: keyPath]
+ subscript(dynamicMember keyPath: KeyPath) -> T {
+ settings[keyPath: keyPath]
}
// These properties are used for managing a live system, rather
@@ -60,189 +60,17 @@ public class VortexSystem: Identifiable, Equatable, Hashable {
// These properties control system-wide behavior.
/// The configuration settings for a VortexSystem
- var vortexSettings: VortexSystem.Settings
+ var settings: VortexSettings
- /// Backwards compatibility only - use is deprecated
- /// The secondary systems to be used in the particle system
- var secondarySystems: [VortexSystem] = []
-
/// Initialise a particle system with a VortexSettings struct
/// - Parameter settings: VortexSettings
/// The settings to be used for this particle system.
- public init( settings: VortexSystem.Settings ) {
- self.vortexSettings = settings
+ 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 let .randomRamp(allColors) = vortexSettings.colors {
- 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.secondarySystems = secondarySystems
- self.vortexSettings = Settings(
- tags: tags,
- 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: angularSpeed,
- colors: colors,
- size: size,
- sizeVariation: sizeVariation,
- sizeMultiplierAtDeath: sizeMultiplierAtDeath,
- stretchFactor: stretchFactor
- )
-
- if case .randomRamp(let allColors) = colors {
+ if case let .randomRamp(allColors) = settings.colors {
selectedColorRamp = Int.random(in: 0.. VortexSystem {
- VortexSystem(
- tags: vortexSettings.tags,
- secondarySystems: secondarySystems,
- spawnOccasion: vortexSettings.spawnOccasion,
- position: vortexSettings.position,
- shape: vortexSettings.shape,
- birthRate: vortexSettings.birthRate,
- emissionLimit: vortexSettings.emissionLimit,
- emissionDuration: vortexSettings.emissionDuration,
- idleDuration: vortexSettings.idleDuration,
- burstCount: vortexSettings.burstCount,
- burstCountVariation: vortexSettings.burstCountVariation,
- lifespan: vortexSettings.lifespan,
- lifespanVariation: vortexSettings.lifespanVariation,
- speed: vortexSettings.speed,
- speedVariation: vortexSettings.speedVariation,
- angle: vortexSettings.angle,
- angleRange: vortexSettings.angleRange,
- acceleration: vortexSettings.acceleration,
- attractionCenter: vortexSettings.attractionCenter,
- attractionStrength: vortexSettings.attractionStrength,
- dampingFactor: vortexSettings.dampingFactor,
- angularSpeed: vortexSettings.angularSpeed,
- angularSpeedVariation: vortexSettings.angularSpeedVariation,
- colors: vortexSettings.colors,
- size: vortexSettings.size,
- sizeVariation: vortexSettings.sizeVariation,
- sizeMultiplierAtDeath: vortexSettings.sizeMultiplierAtDeath,
- stretchFactor: vortexSettings.stretchFactor
- )
- }
}
diff --git a/Sources/Vortex/Views/VortexProxy.swift b/Sources/Vortex/Views/VortexProxy.swift
index d94482a..4a8bf25 100644
--- a/Sources/Vortex/Views/VortexProxy.swift
+++ b/Sources/Vortex/Views/VortexProxy.swift
@@ -26,7 +26,7 @@ public struct VortexProxy {
public func move(to newPosition: CGPoint) {
guard let particleSystem else { return }
- particleSystem.vortexSettings.position = [
+ particleSystem.settings.position = [
newPosition.x / particleSystem.lastDrawSize.width,
newPosition.y / particleSystem.lastDrawSize.height
]
diff --git a/Sources/Vortex/Views/VortexView.swift b/Sources/Vortex/Views/VortexView.swift
index d77ee68..d8b115e 100644
--- a/Sources/Vortex/Views/VortexView.swift
+++ b/Sources/Vortex/Views/VortexView.swift
@@ -63,22 +63,12 @@ public struct VortexView: View where Symbols: View {
/// 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.
- /// If not specified, a default group of three views tagged with `.circle`,`.confetti` and `.sparkle` will be used.
public init(
- settings: VortexSystem.Settings = .init(),
+ _ 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("triangle")
- Image.sparkle
- .frame(width: 16, height: 16).blendMode(.plusLighter).tag("sparkle")
- }
- }
+ @ViewBuilder symbols: () -> Symbols
) {
- _particleSystem = State( initialValue: VortexSystem(settings: settings) )
+ _particleSystem = State( initialValue: VortexSystem(settings) )
self.targetFrameRate = targetFrameRate
self.symbols = symbols()
}
diff --git a/Sources/Vortex/Views/VortexViewReader.swift b/Sources/Vortex/Views/VortexViewReader.swift
index 8a1b193..cd3fb6c 100644
--- a/Sources/Vortex/Views/VortexViewReader.swift
+++ b/Sources/Vortex/Views/VortexViewReader.swift
@@ -26,9 +26,9 @@ public struct VortexViewReader: View {
nearestVortexSystem?.burst()
} attractTo: { point in
if let point {
- nearestVortexSystem?.vortexSettings.attractionCenter = SIMD2(point.x, point.y)
+ nearestVortexSystem?.settings.attractionCenter = SIMD2(point.x, point.y)
} else {
- nearestVortexSystem?.vortexSettings.attractionCenter = nil
+ nearestVortexSystem?.settings.attractionCenter = nil
}
}
From 968c8ce5ac5dc5f0f3f92cbc6b7201ed3879e07f Mon Sep 17 00:00:00 2001
From: Discoinferno <007gnail@gmail.com>
Date: Mon, 9 Dec 2024 16:52:05 +0000
Subject: [PATCH 4/5] fire preset
---
Sources/Vortex/Presets/Fire.swift | 1 +
1 file changed, 1 insertion(+)
diff --git a/Sources/Vortex/Presets/Fire.swift b/Sources/Vortex/Presets/Fire.swift
index 7d1c940..00063d4 100644
--- a/Sources/Vortex/Presets/Fire.swift
+++ b/Sources/Vortex/Presets/Fire.swift
@@ -24,6 +24,7 @@ extension VortexSettings {
}
}
+
#Preview("Demonstrates the fire preset with attraction") {
/// Here we modify the default fire settings to extend it across the bottom of the screen
let floorOnFire = VortexSettings(basedOn: .fire) { settings in
From 91470b9bcfe93cbe1502bd578a62e124431c75b6 Mon Sep 17 00:00:00 2001
From: Discoinferno <007gnail@gmail.com>
Date: Mon, 9 Dec 2024 17:00:16 +0000
Subject: [PATCH 5/5] fire preset - remove duplicate preview
---
Sources/Vortex/Presets/Fire.swift | 19 -------------------
1 file changed, 19 deletions(-)
diff --git a/Sources/Vortex/Presets/Fire.swift b/Sources/Vortex/Presets/Fire.swift
index 9781ceb..7d1c940 100644
--- a/Sources/Vortex/Presets/Fire.swift
+++ b/Sources/Vortex/Presets/Fire.swift
@@ -24,25 +24,6 @@ extension VortexSettings {
}
}
-#Preview("Demonstrates the fire preset with attraction") {
- /// Here we modify the default fire settings to extend it across the bottom of the screen
- let floorOnFire:VortexSettings = {
- 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")
- }
-}
-
#Preview("Demonstrates the fire preset with attraction") {
/// Here we modify the default fire settings to extend it across the bottom of the screen
let floorOnFire = VortexSettings(basedOn: .fire) { settings in