├── .gitignore ├── LICENSE.md ├── Package.swift ├── ParticlizedDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── ParticlizedDemo.xcscheme ├── ParticlizedDemo ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── oregon.imageset │ │ ├── Contents.json │ │ └── oregon.jpeg ├── ContentViews │ ├── Algo │ │ ├── AlgoContentView.swift │ │ └── AlgoTextParticle.sks │ ├── Oregon │ │ ├── OregonContentView.swift │ │ ├── OregonImageParticle.sks │ │ └── OregonTextParticle.sks │ └── Sale │ │ ├── OffTextParticle.sks │ │ ├── SaleContentView.swift │ │ └── SaleTextParticle.sks ├── ParticlizedDemo.entitlements ├── ParticlizedDemoApp.swift └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── README.md ├── Sources └── Particlized │ ├── Particlized.swift │ ├── ParticlizedImage.swift │ └── ParticlizedText.swift └── Tests └── ParticlizedTests └── ParticlizedTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,swiftpackagemanager,swiftpm,objective-c,xcode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,swiftpackagemanager,swiftpm,objective-c,xcode 3 | 4 | ### Objective-C ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## User settings 10 | xcuserdata/ 11 | 12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 13 | *.xcscmblueprint 14 | *.xccheckout 15 | 16 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 17 | build/ 18 | DerivedData/ 19 | *.moved-aside 20 | *.pbxuser 21 | !default.pbxuser 22 | *.mode1v3 23 | !default.mode1v3 24 | *.mode2v3 25 | !default.mode2v3 26 | *.perspectivev3 27 | !default.perspectivev3 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | 32 | ## App packaging 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | # CocoaPods 38 | # We recommend against adding the Pods directory to your .gitignore. However 39 | # you should judge for yourself, the pros and cons are mentioned at: 40 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 41 | # Pods/ 42 | # Add this line if you want to avoid checking in source code from the Xcode workspace 43 | # *.xcworkspace 44 | 45 | # Carthage 46 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 47 | # Carthage/Checkouts 48 | 49 | Carthage/Build/ 50 | 51 | # fastlane 52 | # It is recommended to not store the screenshots in the git repo. 53 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 54 | # For more information about the recommended setup visit: 55 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 56 | 57 | fastlane/report.xml 58 | fastlane/Preview.html 59 | fastlane/screenshots/**/*.png 60 | fastlane/test_output 61 | 62 | # Code Injection 63 | # After new code Injection tools there's a generated folder /iOSInjectionProject 64 | # https://github.com/johnno1962/injectionforxcode 65 | 66 | iOSInjectionProject/ 67 | 68 | ### Objective-C Patch ### 69 | 70 | ### Swift ### 71 | # Xcode 72 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 73 | 74 | 75 | 76 | 77 | 78 | 79 | ## Playgrounds 80 | timeline.xctimeline 81 | playground.xcworkspace 82 | 83 | # Swift Package Manager 84 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 85 | # Packages/ 86 | # Package.pins 87 | # Package.resolved 88 | # *.xcodeproj 89 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 90 | # hence it is not needed unless you have added a package configuration file to your project 91 | .swiftpm 92 | 93 | .build/ 94 | 95 | # CocoaPods 96 | # We recommend against adding the Pods directory to your .gitignore. However 97 | # you should judge for yourself, the pros and cons are mentioned at: 98 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 99 | # Pods/ 100 | # Add this line if you want to avoid checking in source code from the Xcode workspace 101 | # *.xcworkspace 102 | 103 | # Carthage 104 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 105 | # Carthage/Checkouts 106 | 107 | 108 | # Accio dependency management 109 | Dependencies/ 110 | .accio/ 111 | 112 | # fastlane 113 | # It is recommended to not store the screenshots in the git repo. 114 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 115 | # For more information about the recommended setup visit: 116 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 117 | 118 | 119 | # Code Injection 120 | # After new code Injection tools there's a generated folder /iOSInjectionProject 121 | # https://github.com/johnno1962/injectionforxcode 122 | 123 | 124 | ### SwiftPackageManager ### 125 | Packages 126 | xcuserdata 127 | 128 | ### SwiftPM ### 129 | 130 | 131 | ### Xcode ### 132 | 133 | ## Xcode 8 and earlier 134 | 135 | ### Xcode Patch ### 136 | *.xcodeproj/* 137 | !*.xcodeproj/project.pbxproj 138 | !*.xcodeproj/xcshareddata/ 139 | !*.xcodeproj/project.xcworkspace/ 140 | !*.xcworkspace/contents.xcworkspacedata 141 | /*.gcno 142 | **/xcshareddata/WorkspaceSettings.xcsettings 143 | 144 | # End of https://www.toptal.com/developers/gitignore/api/swift,swiftpackagemanager,swiftpm,objective-c,xcode -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Aleksei Gusachenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Particlized", 8 | platforms: [.iOS(.v17)], 9 | products: [ 10 | .library( 11 | name: "Particlized", 12 | targets: ["Particlized"] 13 | ), 14 | ], 15 | targets: [ 16 | .target( 17 | name: "Particlized", 18 | path: "Sources" 19 | ), 20 | .testTarget( 21 | name: "ParticlizedTests", 22 | dependencies: ["Particlized"] 23 | ), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /ParticlizedDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5DBC0F632BE252FA002A406D /* AlgoContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DBC0F622BE252FA002A406D /* AlgoContentView.swift */; }; 11 | 5DBC0F662BE2537E002A406D /* OregonImageParticle.sks in Resources */ = {isa = PBXBuildFile; fileRef = 5DBC0F642BE2537E002A406D /* OregonImageParticle.sks */; }; 12 | 5DBC0F672BE2537E002A406D /* OregonTextParticle.sks in Resources */ = {isa = PBXBuildFile; fileRef = 5DBC0F652BE2537E002A406D /* OregonTextParticle.sks */; }; 13 | 5DBC0F6B2BE25CDB002A406D /* OregonContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DBC0F6A2BE25CDA002A406D /* OregonContentView.swift */; }; 14 | 5DBC0F6D2BE25EDD002A406D /* AlgoTextParticle.sks in Resources */ = {isa = PBXBuildFile; fileRef = 5DBC0F6C2BE25EDD002A406D /* AlgoTextParticle.sks */; }; 15 | 5DBC0F712BE2691D002A406D /* SaleTextParticle.sks in Resources */ = {isa = PBXBuildFile; fileRef = 5DBC0F6F2BE2691D002A406D /* SaleTextParticle.sks */; }; 16 | 5DBC0F722BE2691D002A406D /* SaleContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DBC0F702BE2691D002A406D /* SaleContentView.swift */; }; 17 | 5DDA8C582BE410EF003D3D41 /* OffTextParticle.sks in Resources */ = {isa = PBXBuildFile; fileRef = 5DDA8C572BE410EF003D3D41 /* OffTextParticle.sks */; }; 18 | 5DE8CEB42BE00CB6008038D4 /* ParticlizedDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE8CEB32BE00CB6008038D4 /* ParticlizedDemoApp.swift */; }; 19 | 5DE8CEB82BE00CB7008038D4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5DE8CEB72BE00CB7008038D4 /* Assets.xcassets */; }; 20 | 5DE8CEBC2BE00CB7008038D4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5DE8CEBB2BE00CB7008038D4 /* Preview Assets.xcassets */; }; 21 | 5DFBA9292BE00ECA00A65FCC /* Particlized in Frameworks */ = {isa = PBXBuildFile; productRef = 5DFBA9282BE00ECA00A65FCC /* Particlized */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 5D93753E2BE00CF000B8BCFD /* Particlized */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Particlized; path = ../Particlized; sourceTree = ""; }; 26 | 5DBC0F622BE252FA002A406D /* AlgoContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlgoContentView.swift; sourceTree = ""; }; 27 | 5DBC0F642BE2537E002A406D /* OregonImageParticle.sks */ = {isa = PBXFileReference; lastKnownFileType = file.sks; path = OregonImageParticle.sks; sourceTree = ""; }; 28 | 5DBC0F652BE2537E002A406D /* OregonTextParticle.sks */ = {isa = PBXFileReference; lastKnownFileType = file.sks; path = OregonTextParticle.sks; sourceTree = ""; }; 29 | 5DBC0F6A2BE25CDA002A406D /* OregonContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OregonContentView.swift; path = ParticlizedDemo/ContentViews/Oregon/OregonContentView.swift; sourceTree = SOURCE_ROOT; }; 30 | 5DBC0F6C2BE25EDD002A406D /* AlgoTextParticle.sks */ = {isa = PBXFileReference; lastKnownFileType = file.sks; path = AlgoTextParticle.sks; sourceTree = ""; }; 31 | 5DBC0F6F2BE2691D002A406D /* SaleTextParticle.sks */ = {isa = PBXFileReference; lastKnownFileType = file.sks; path = SaleTextParticle.sks; sourceTree = ""; }; 32 | 5DBC0F702BE2691D002A406D /* SaleContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaleContentView.swift; sourceTree = ""; }; 33 | 5DDA8C572BE410EF003D3D41 /* OffTextParticle.sks */ = {isa = PBXFileReference; lastKnownFileType = file.sks; path = OffTextParticle.sks; sourceTree = ""; }; 34 | 5DE8CEB02BE00CB6008038D4 /* ParticlizedDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ParticlizedDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 5DE8CEB32BE00CB6008038D4 /* ParticlizedDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticlizedDemoApp.swift; sourceTree = ""; }; 36 | 5DE8CEB72BE00CB7008038D4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | 5DE8CEB92BE00CB7008038D4 /* ParticlizedDemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ParticlizedDemo.entitlements; sourceTree = ""; }; 38 | 5DE8CEBB2BE00CB7008038D4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | 5DE8CEAD2BE00CB6008038D4 /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | 5DFBA9292BE00ECA00A65FCC /* Particlized in Frameworks */, 47 | ); 48 | runOnlyForDeploymentPostprocessing = 0; 49 | }; 50 | /* End PBXFrameworksBuildPhase section */ 51 | 52 | /* Begin PBXGroup section */ 53 | 5DBC0F5D2BE25280002A406D /* ContentViews */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | 5DBC0F5F2BE2528F002A406D /* Algo */, 57 | 5DBC0F5E2BE25288002A406D /* Oregon */, 58 | 5DBC0F6E2BE268FF002A406D /* Sale */, 59 | ); 60 | path = ContentViews; 61 | sourceTree = ""; 62 | }; 63 | 5DBC0F5E2BE25288002A406D /* Oregon */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 5DBC0F6A2BE25CDA002A406D /* OregonContentView.swift */, 67 | 5DBC0F642BE2537E002A406D /* OregonImageParticle.sks */, 68 | 5DBC0F652BE2537E002A406D /* OregonTextParticle.sks */, 69 | ); 70 | path = Oregon; 71 | sourceTree = ""; 72 | }; 73 | 5DBC0F5F2BE2528F002A406D /* Algo */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 5DBC0F622BE252FA002A406D /* AlgoContentView.swift */, 77 | 5DBC0F6C2BE25EDD002A406D /* AlgoTextParticle.sks */, 78 | ); 79 | path = Algo; 80 | sourceTree = ""; 81 | }; 82 | 5DBC0F6E2BE268FF002A406D /* Sale */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 5DBC0F702BE2691D002A406D /* SaleContentView.swift */, 86 | 5DBC0F6F2BE2691D002A406D /* SaleTextParticle.sks */, 87 | 5DDA8C572BE410EF003D3D41 /* OffTextParticle.sks */, 88 | ); 89 | path = Sale; 90 | sourceTree = ""; 91 | }; 92 | 5DE8CEA72BE00CB6008038D4 = { 93 | isa = PBXGroup; 94 | children = ( 95 | 5DE8CEB22BE00CB6008038D4 /* ParticlizedDemo */, 96 | 5D93753E2BE00CF000B8BCFD /* Particlized */, 97 | 5DE8CEB12BE00CB6008038D4 /* Products */, 98 | 5DFBA9252BE00E2200A65FCC /* Frameworks */, 99 | ); 100 | sourceTree = ""; 101 | }; 102 | 5DE8CEB12BE00CB6008038D4 /* Products */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 5DE8CEB02BE00CB6008038D4 /* ParticlizedDemo.app */, 106 | ); 107 | name = Products; 108 | sourceTree = ""; 109 | }; 110 | 5DE8CEB22BE00CB6008038D4 /* ParticlizedDemo */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 5DE8CEB32BE00CB6008038D4 /* ParticlizedDemoApp.swift */, 114 | 5DBC0F5D2BE25280002A406D /* ContentViews */, 115 | 5DE8CEB72BE00CB7008038D4 /* Assets.xcassets */, 116 | 5DE8CEB92BE00CB7008038D4 /* ParticlizedDemo.entitlements */, 117 | 5DE8CEBA2BE00CB7008038D4 /* Preview Content */, 118 | ); 119 | path = ParticlizedDemo; 120 | sourceTree = ""; 121 | }; 122 | 5DE8CEBA2BE00CB7008038D4 /* Preview Content */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 5DE8CEBB2BE00CB7008038D4 /* Preview Assets.xcassets */, 126 | ); 127 | path = "Preview Content"; 128 | sourceTree = ""; 129 | }; 130 | 5DFBA9252BE00E2200A65FCC /* Frameworks */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | ); 134 | name = Frameworks; 135 | sourceTree = ""; 136 | }; 137 | /* End PBXGroup section */ 138 | 139 | /* Begin PBXNativeTarget section */ 140 | 5DE8CEAF2BE00CB6008038D4 /* ParticlizedDemo */ = { 141 | isa = PBXNativeTarget; 142 | buildConfigurationList = 5DE8CEBF2BE00CB7008038D4 /* Build configuration list for PBXNativeTarget "ParticlizedDemo" */; 143 | buildPhases = ( 144 | 5DE8CEAC2BE00CB6008038D4 /* Sources */, 145 | 5DE8CEAD2BE00CB6008038D4 /* Frameworks */, 146 | 5DE8CEAE2BE00CB6008038D4 /* Resources */, 147 | ); 148 | buildRules = ( 149 | ); 150 | dependencies = ( 151 | ); 152 | name = ParticlizedDemo; 153 | packageProductDependencies = ( 154 | 5DFBA9282BE00ECA00A65FCC /* Particlized */, 155 | ); 156 | productName = ParticlizedDemo; 157 | productReference = 5DE8CEB02BE00CB6008038D4 /* ParticlizedDemo.app */; 158 | productType = "com.apple.product-type.application"; 159 | }; 160 | /* End PBXNativeTarget section */ 161 | 162 | /* Begin PBXProject section */ 163 | 5DE8CEA82BE00CB6008038D4 /* Project object */ = { 164 | isa = PBXProject; 165 | attributes = { 166 | BuildIndependentTargetsInParallel = 1; 167 | LastSwiftUpdateCheck = 1530; 168 | LastUpgradeCheck = 1530; 169 | TargetAttributes = { 170 | 5DE8CEAF2BE00CB6008038D4 = { 171 | CreatedOnToolsVersion = 15.3; 172 | }; 173 | }; 174 | }; 175 | buildConfigurationList = 5DE8CEAB2BE00CB6008038D4 /* Build configuration list for PBXProject "ParticlizedDemo" */; 176 | compatibilityVersion = "Xcode 14.0"; 177 | developmentRegion = en; 178 | hasScannedForEncodings = 0; 179 | knownRegions = ( 180 | en, 181 | Base, 182 | ); 183 | mainGroup = 5DE8CEA72BE00CB6008038D4; 184 | productRefGroup = 5DE8CEB12BE00CB6008038D4 /* Products */; 185 | projectDirPath = ""; 186 | projectRoot = ""; 187 | targets = ( 188 | 5DE8CEAF2BE00CB6008038D4 /* ParticlizedDemo */, 189 | ); 190 | }; 191 | /* End PBXProject section */ 192 | 193 | /* Begin PBXResourcesBuildPhase section */ 194 | 5DE8CEAE2BE00CB6008038D4 /* Resources */ = { 195 | isa = PBXResourcesBuildPhase; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | 5DE8CEBC2BE00CB7008038D4 /* Preview Assets.xcassets in Resources */, 199 | 5DE8CEB82BE00CB7008038D4 /* Assets.xcassets in Resources */, 200 | 5DDA8C582BE410EF003D3D41 /* OffTextParticle.sks in Resources */, 201 | 5DBC0F672BE2537E002A406D /* OregonTextParticle.sks in Resources */, 202 | 5DBC0F662BE2537E002A406D /* OregonImageParticle.sks in Resources */, 203 | 5DBC0F6D2BE25EDD002A406D /* AlgoTextParticle.sks in Resources */, 204 | 5DBC0F712BE2691D002A406D /* SaleTextParticle.sks in Resources */, 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | }; 208 | /* End PBXResourcesBuildPhase section */ 209 | 210 | /* Begin PBXSourcesBuildPhase section */ 211 | 5DE8CEAC2BE00CB6008038D4 /* Sources */ = { 212 | isa = PBXSourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 5DBC0F632BE252FA002A406D /* AlgoContentView.swift in Sources */, 216 | 5DBC0F722BE2691D002A406D /* SaleContentView.swift in Sources */, 217 | 5DE8CEB42BE00CB6008038D4 /* ParticlizedDemoApp.swift in Sources */, 218 | 5DBC0F6B2BE25CDB002A406D /* OregonContentView.swift in Sources */, 219 | ); 220 | runOnlyForDeploymentPostprocessing = 0; 221 | }; 222 | /* End PBXSourcesBuildPhase section */ 223 | 224 | /* Begin XCBuildConfiguration section */ 225 | 5DE8CEBD2BE00CB7008038D4 /* Debug */ = { 226 | isa = XCBuildConfiguration; 227 | buildSettings = { 228 | ALWAYS_SEARCH_USER_PATHS = NO; 229 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 230 | CLANG_ANALYZER_NONNULL = YES; 231 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 232 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 233 | CLANG_ENABLE_MODULES = YES; 234 | CLANG_ENABLE_OBJC_ARC = YES; 235 | CLANG_ENABLE_OBJC_WEAK = YES; 236 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 237 | CLANG_WARN_BOOL_CONVERSION = YES; 238 | CLANG_WARN_COMMA = YES; 239 | CLANG_WARN_CONSTANT_CONVERSION = YES; 240 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 241 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 242 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 243 | CLANG_WARN_EMPTY_BODY = YES; 244 | CLANG_WARN_ENUM_CONVERSION = YES; 245 | CLANG_WARN_INFINITE_RECURSION = YES; 246 | CLANG_WARN_INT_CONVERSION = YES; 247 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 248 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 249 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 250 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 251 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 252 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 253 | CLANG_WARN_STRICT_PROTOTYPES = YES; 254 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 255 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 256 | CLANG_WARN_UNREACHABLE_CODE = YES; 257 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 258 | COPY_PHASE_STRIP = NO; 259 | DEBUG_INFORMATION_FORMAT = dwarf; 260 | ENABLE_STRICT_OBJC_MSGSEND = YES; 261 | ENABLE_TESTABILITY = YES; 262 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 263 | GCC_C_LANGUAGE_STANDARD = gnu17; 264 | GCC_DYNAMIC_NO_PIC = NO; 265 | GCC_NO_COMMON_BLOCKS = YES; 266 | GCC_OPTIMIZATION_LEVEL = 0; 267 | GCC_PREPROCESSOR_DEFINITIONS = ( 268 | "DEBUG=1", 269 | "$(inherited)", 270 | ); 271 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 272 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 273 | GCC_WARN_UNDECLARED_SELECTOR = YES; 274 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 275 | GCC_WARN_UNUSED_FUNCTION = YES; 276 | GCC_WARN_UNUSED_VARIABLE = YES; 277 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 278 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 279 | MTL_FAST_MATH = YES; 280 | ONLY_ACTIVE_ARCH = YES; 281 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 282 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 283 | }; 284 | name = Debug; 285 | }; 286 | 5DE8CEBE2BE00CB7008038D4 /* Release */ = { 287 | isa = XCBuildConfiguration; 288 | buildSettings = { 289 | ALWAYS_SEARCH_USER_PATHS = NO; 290 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 291 | CLANG_ANALYZER_NONNULL = YES; 292 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 293 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 294 | CLANG_ENABLE_MODULES = YES; 295 | CLANG_ENABLE_OBJC_ARC = YES; 296 | CLANG_ENABLE_OBJC_WEAK = YES; 297 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 298 | CLANG_WARN_BOOL_CONVERSION = YES; 299 | CLANG_WARN_COMMA = YES; 300 | CLANG_WARN_CONSTANT_CONVERSION = YES; 301 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 302 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 303 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 304 | CLANG_WARN_EMPTY_BODY = YES; 305 | CLANG_WARN_ENUM_CONVERSION = YES; 306 | CLANG_WARN_INFINITE_RECURSION = YES; 307 | CLANG_WARN_INT_CONVERSION = YES; 308 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 309 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 310 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 311 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 312 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 313 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 314 | CLANG_WARN_STRICT_PROTOTYPES = YES; 315 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 316 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 317 | CLANG_WARN_UNREACHABLE_CODE = YES; 318 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 319 | COPY_PHASE_STRIP = NO; 320 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 321 | ENABLE_NS_ASSERTIONS = NO; 322 | ENABLE_STRICT_OBJC_MSGSEND = YES; 323 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 324 | GCC_C_LANGUAGE_STANDARD = gnu17; 325 | GCC_NO_COMMON_BLOCKS = YES; 326 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 327 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 328 | GCC_WARN_UNDECLARED_SELECTOR = YES; 329 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 330 | GCC_WARN_UNUSED_FUNCTION = YES; 331 | GCC_WARN_UNUSED_VARIABLE = YES; 332 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 333 | MTL_ENABLE_DEBUG_INFO = NO; 334 | MTL_FAST_MATH = YES; 335 | SWIFT_COMPILATION_MODE = wholemodule; 336 | }; 337 | name = Release; 338 | }; 339 | 5DE8CEC02BE00CB7008038D4 /* Debug */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 343 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 344 | CODE_SIGN_ENTITLEMENTS = ParticlizedDemo/ParticlizedDemo.entitlements; 345 | CODE_SIGN_STYLE = Automatic; 346 | CURRENT_PROJECT_VERSION = 1; 347 | DEVELOPMENT_ASSET_PATHS = "\"ParticlizedDemo/Preview Content\""; 348 | DEVELOPMENT_TEAM = TVHAQD8T2B; 349 | ENABLE_HARDENED_RUNTIME = YES; 350 | ENABLE_PREVIEWS = YES; 351 | GENERATE_INFOPLIST_FILE = YES; 352 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 353 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 354 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 355 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 356 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 357 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 358 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 359 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 360 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 361 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 362 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 363 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 364 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 365 | MACOSX_DEPLOYMENT_TARGET = 14.0; 366 | MARKETING_VERSION = 1.0; 367 | PRODUCT_BUNDLE_IDENTIFIER = com.jmstajim.ParticlizedDemo; 368 | PRODUCT_NAME = "$(TARGET_NAME)"; 369 | SDKROOT = auto; 370 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 371 | SWIFT_EMIT_LOC_STRINGS = YES; 372 | SWIFT_VERSION = 5.0; 373 | TARGETED_DEVICE_FAMILY = "1,2"; 374 | }; 375 | name = Debug; 376 | }; 377 | 5DE8CEC12BE00CB7008038D4 /* Release */ = { 378 | isa = XCBuildConfiguration; 379 | buildSettings = { 380 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 381 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 382 | CODE_SIGN_ENTITLEMENTS = ParticlizedDemo/ParticlizedDemo.entitlements; 383 | CODE_SIGN_STYLE = Automatic; 384 | CURRENT_PROJECT_VERSION = 1; 385 | DEVELOPMENT_ASSET_PATHS = "\"ParticlizedDemo/Preview Content\""; 386 | DEVELOPMENT_TEAM = TVHAQD8T2B; 387 | ENABLE_HARDENED_RUNTIME = YES; 388 | ENABLE_PREVIEWS = YES; 389 | GENERATE_INFOPLIST_FILE = YES; 390 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 391 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 392 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 393 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 394 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 395 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 396 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 397 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 398 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 399 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 400 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 401 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 402 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 403 | MACOSX_DEPLOYMENT_TARGET = 14.0; 404 | MARKETING_VERSION = 1.0; 405 | PRODUCT_BUNDLE_IDENTIFIER = com.jmstajim.ParticlizedDemo; 406 | PRODUCT_NAME = "$(TARGET_NAME)"; 407 | SDKROOT = auto; 408 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 409 | SWIFT_EMIT_LOC_STRINGS = YES; 410 | SWIFT_VERSION = 5.0; 411 | TARGETED_DEVICE_FAMILY = "1,2"; 412 | }; 413 | name = Release; 414 | }; 415 | /* End XCBuildConfiguration section */ 416 | 417 | /* Begin XCConfigurationList section */ 418 | 5DE8CEAB2BE00CB6008038D4 /* Build configuration list for PBXProject "ParticlizedDemo" */ = { 419 | isa = XCConfigurationList; 420 | buildConfigurations = ( 421 | 5DE8CEBD2BE00CB7008038D4 /* Debug */, 422 | 5DE8CEBE2BE00CB7008038D4 /* Release */, 423 | ); 424 | defaultConfigurationIsVisible = 0; 425 | defaultConfigurationName = Release; 426 | }; 427 | 5DE8CEBF2BE00CB7008038D4 /* Build configuration list for PBXNativeTarget "ParticlizedDemo" */ = { 428 | isa = XCConfigurationList; 429 | buildConfigurations = ( 430 | 5DE8CEC02BE00CB7008038D4 /* Debug */, 431 | 5DE8CEC12BE00CB7008038D4 /* Release */, 432 | ); 433 | defaultConfigurationIsVisible = 0; 434 | defaultConfigurationName = Release; 435 | }; 436 | /* End XCConfigurationList section */ 437 | 438 | /* Begin XCSwiftPackageProductDependency section */ 439 | 5DFBA9282BE00ECA00A65FCC /* Particlized */ = { 440 | isa = XCSwiftPackageProductDependency; 441 | productName = Particlized; 442 | }; 443 | /* End XCSwiftPackageProductDependency section */ 444 | }; 445 | rootObject = 5DE8CEA82BE00CB6008038D4 /* Project object */; 446 | } 447 | -------------------------------------------------------------------------------- /ParticlizedDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ParticlizedDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ParticlizedDemo.xcodeproj/xcshareddata/xcschemes/ParticlizedDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /ParticlizedDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ParticlizedDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ParticlizedDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ParticlizedDemo/Assets.xcassets/oregon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "oregon.jpeg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ParticlizedDemo/Assets.xcassets/oregon.imageset/oregon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmstajim/Particlized/1674c17eb20d03f61621f6cf788e601825f0f1dc/ParticlizedDemo/Assets.xcassets/oregon.imageset/oregon.jpeg -------------------------------------------------------------------------------- /ParticlizedDemo/ContentViews/Algo/AlgoContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlgoContentView.swift 3 | // ParticlizedDemo 4 | // 5 | // Created by Aleksei Gusachenko on 01.05.2024. 6 | // 7 | 8 | import SwiftUI 9 | import Particlized 10 | import SpriteKit 11 | 12 | struct AlgoContentView: View { 13 | private let scene: SKScene = { 14 | $0.anchorPoint = CGPoint(x: 0.5, y: 0.5) 15 | $0.scaleMode = .resizeFill 16 | $0.backgroundColor = .black 17 | return $0 18 | }(SKScene()) 19 | 20 | private let text = ParticlizedText( 21 | text: """ 22 | DATA STRUCTURES 23 | ‾‾‾ ALGORITHMS ‾‾‾ 24 | """, 25 | font: UIFont(name: "AcademyEngravedLetPlain", size: 25)!, 26 | textColor: UIColor(red: 255 / 255, green: 21 / 255, blue: 21 / 255, alpha: 1), 27 | emitterNode: .init(fileNamed: "AlgoTextParticle.sks")!, 28 | numberOfPixelsPerNode: 1, 29 | nodeSkipPercentageChance: 0 30 | ) 31 | 32 | private let radialGravity: SKFieldNode = { 33 | $0.isEnabled = false 34 | $0.strength = -2 35 | $0.falloff = -0.5 36 | $0.region = .init(radius: 30) 37 | return $0 38 | }(SKFieldNode.radialGravityField()) 39 | 40 | private let turbulence: SKFieldNode = { 41 | $0.isEnabled = false 42 | $0.strength = 10 43 | $0.falloff = -1 44 | return $0 45 | }(SKFieldNode.turbulenceField(withSmoothness: 0.1, animationSpeed: 100)) 46 | 47 | private let linearGravity: SKFieldNode = { 48 | $0.isEnabled = false 49 | $0.strength = 1 50 | return $0 51 | }(SKFieldNode.linearGravityField(withVector: .init(x: 0, y: -1, z: 0))) 52 | 53 | private let turbulenceForLinearGravity: SKFieldNode = { 54 | $0.isEnabled = false 55 | return $0 56 | }(SKFieldNode.turbulenceField(withSmoothness: 0.1, animationSpeed: 1)) 57 | 58 | var body: some View { 59 | ZStack { 60 | SpriteView(scene: scene) 61 | .onAppear(perform: { 62 | scene.addChild(text) 63 | scene.addChild(radialGravity) 64 | scene.addChild(turbulence) 65 | scene.addChild(linearGravity) 66 | scene.addChild(turbulenceForLinearGravity) 67 | }) 68 | } 69 | .onTapGesture { 70 | turbulence.isEnabled = false 71 | } 72 | .gesture( 73 | DragGesture(minimumDistance: 5) 74 | .onChanged { value in 75 | let fieldLocation = scene.convertPoint(fromView: value.location) 76 | radialGravity.position = fieldLocation 77 | radialGravity.isEnabled = true 78 | } 79 | .onEnded { dragGestureValue in 80 | radialGravity.isEnabled = false 81 | } 82 | 83 | ) 84 | .onLongPressGesture( 85 | perform: { 86 | turbulence.isEnabled = true 87 | } 88 | ) 89 | .simultaneousGesture( 90 | TapGesture(count: 2) 91 | .onEnded { _ in 92 | linearGravity.isEnabled.toggle() 93 | turbulenceForLinearGravity.isEnabled.toggle() 94 | } 95 | ) 96 | .simultaneousGesture( 97 | TapGesture(count: 3) 98 | .onEnded { _ in 99 | text.isEmitting.toggle() 100 | } 101 | ) 102 | .ignoresSafeArea() 103 | } 104 | } 105 | 106 | #Preview { 107 | AlgoContentView() 108 | } 109 | -------------------------------------------------------------------------------- /ParticlizedDemo/ContentViews/Algo/AlgoTextParticle.sks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmstajim/Particlized/1674c17eb20d03f61621f6cf788e601825f0f1dc/ParticlizedDemo/ContentViews/Algo/AlgoTextParticle.sks -------------------------------------------------------------------------------- /ParticlizedDemo/ContentViews/Oregon/OregonContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // ParticlizedDemo 4 | // 5 | // Created by Aleksei Gusachenko on 29.04.2024. 6 | // 7 | 8 | import SwiftUI 9 | import Particlized 10 | import SpriteKit 11 | 12 | struct OregonContentView: View { 13 | private let scene: SKScene = { 14 | $0.anchorPoint = CGPoint(x: 0.5, y: 0.5) 15 | $0.scaleMode = .resizeFill 16 | $0.backgroundColor = .white 17 | return $0 18 | }(SKScene()) 19 | 20 | private let text = ParticlizedText( 21 | text: "Oregon 🦫", 22 | font: UIFont(name: "SnellRoundhand", size: 40)!, 23 | textColor: .red, 24 | emitterNode: .init(fileNamed: "OregonTextParticle.sks")!, 25 | numberOfPixelsPerNode: 2, 26 | nodeSkipPercentageChance: 0 27 | ) 28 | 29 | private let image = ParticlizedImage( 30 | image: UIImage(named: "oregon")!, 31 | emitterNode: .init(fileNamed: "OregonImageParticle.sks")!, 32 | numberOfPixelsPerNode: 6, 33 | nodeSkipPercentageChance: 0 34 | ) 35 | 36 | private let radialGravity: SKFieldNode = { 37 | $0.isEnabled = false 38 | $0.strength = 5 39 | $0.falloff = 1 40 | return $0 41 | }(SKFieldNode.radialGravityField()) 42 | 43 | private let noise: SKFieldNode = { 44 | $0.isEnabled = false 45 | $0.strength = 4 46 | $0.falloff = 1 47 | return $0 48 | }(SKFieldNode.noiseField(withSmoothness: 1, animationSpeed: 10)) 49 | 50 | private let linearGravity: SKFieldNode = { 51 | $0.isEnabled = false 52 | $0.strength = 1 53 | return $0 54 | }(SKFieldNode.linearGravityField(withVector: .init(x: 0, y: 1, z: 0))) 55 | 56 | private let turbulence: SKFieldNode = { 57 | $0.isEnabled = false 58 | return $0 59 | }(SKFieldNode.turbulenceField(withSmoothness: 0.4, animationSpeed: 1)) 60 | 61 | var body: some View { 62 | ZStack { 63 | SpriteView(scene: scene) 64 | .onAppear(perform: { 65 | scene.addChild(image) 66 | scene.addChild(text) 67 | text.position = .init(x: 0, y: -220) 68 | scene.addChild(radialGravity) 69 | scene.addChild(noise) 70 | scene.addChild(linearGravity) 71 | scene.addChild(turbulence) 72 | }) 73 | } 74 | .onTapGesture { 75 | noise.isEnabled = false 76 | } 77 | .gesture( 78 | DragGesture(minimumDistance: 5) 79 | .onChanged { value in 80 | let fieldLocation = scene.convertPoint(fromView: value.location) 81 | radialGravity.position = fieldLocation 82 | radialGravity.isEnabled = true 83 | } 84 | .onEnded { dragGestureValue in 85 | radialGravity.isEnabled = false 86 | } 87 | 88 | ) 89 | .onLongPressGesture( 90 | perform: { 91 | noise.isEnabled = true 92 | } 93 | ) 94 | .simultaneousGesture( 95 | TapGesture(count: 2) 96 | .onEnded { _ in 97 | linearGravity.isEnabled.toggle() 98 | turbulence.isEnabled.toggle() 99 | } 100 | ) 101 | .simultaneousGesture( 102 | TapGesture(count: 3) 103 | .onEnded { _ in 104 | text.isEmitting.toggle() 105 | image.isEmitting.toggle() 106 | } 107 | ) 108 | .ignoresSafeArea() 109 | } 110 | } 111 | 112 | #Preview { 113 | OregonContentView() 114 | } 115 | -------------------------------------------------------------------------------- /ParticlizedDemo/ContentViews/Oregon/OregonImageParticle.sks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmstajim/Particlized/1674c17eb20d03f61621f6cf788e601825f0f1dc/ParticlizedDemo/ContentViews/Oregon/OregonImageParticle.sks -------------------------------------------------------------------------------- /ParticlizedDemo/ContentViews/Oregon/OregonTextParticle.sks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmstajim/Particlized/1674c17eb20d03f61621f6cf788e601825f0f1dc/ParticlizedDemo/ContentViews/Oregon/OregonTextParticle.sks -------------------------------------------------------------------------------- /ParticlizedDemo/ContentViews/Sale/OffTextParticle.sks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmstajim/Particlized/1674c17eb20d03f61621f6cf788e601825f0f1dc/ParticlizedDemo/ContentViews/Sale/OffTextParticle.sks -------------------------------------------------------------------------------- /ParticlizedDemo/ContentViews/Sale/SaleContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SaleContentView.swift 3 | // ParticlizedDemo 4 | // 5 | // Created by Aleksei Gusachenko on 01.05.2024. 6 | // 7 | 8 | import SwiftUI 9 | import Particlized 10 | import SpriteKit 11 | 12 | struct SaleContentView: View { 13 | private let scene: SKScene = { 14 | $0.anchorPoint = CGPoint(x: 0.5, y: 0.5) 15 | $0.scaleMode = .resizeFill 16 | $0.backgroundColor = .white 17 | return $0 18 | }(SKScene()) 19 | 20 | private let saleText = ParticlizedText( 21 | text: "Sale", 22 | font: UIFont(name: "MuktaMahee-Bold", size: 120)!, 23 | textColor: nil, 24 | emitterNode: .init(fileNamed: "SaleTextParticle.sks")!, 25 | numberOfPixelsPerNode: 1, 26 | nodeSkipPercentageChance: 50 27 | ) 28 | 29 | private let offText = ParticlizedText( 30 | text: "50% off", 31 | font: UIFont(name: "SnellRoundhand", size: 60)!, 32 | textColor: .black, 33 | emitterNode: .init(fileNamed: "OffTextParticle.sks")!, 34 | numberOfPixelsPerNode: 1, 35 | nodeSkipPercentageChance: 20, 36 | isEmittingOnStart: false 37 | ) 38 | 39 | private let radialGravity: SKFieldNode = { 40 | $0.isEnabled = false 41 | $0.strength = -2 42 | $0.falloff = -0.5 43 | $0.region = .init(radius: 30) 44 | return $0 45 | }(SKFieldNode.radialGravityField()) 46 | 47 | private let turbulence: SKFieldNode = { 48 | $0.isEnabled = false 49 | $0.strength = 10 50 | $0.falloff = -1 51 | return $0 52 | }(SKFieldNode.turbulenceField(withSmoothness: 0.1, animationSpeed: 100)) 53 | 54 | private let linearGravity: SKFieldNode = { 55 | $0.isEnabled = false 56 | $0.strength = 1 57 | return $0 58 | }(SKFieldNode.linearGravityField(withVector: .init(x: 1, y: 0, z: 0))) 59 | 60 | private let turbulenceForLinearGravity: SKFieldNode = { 61 | $0.isEnabled = false 62 | return $0 63 | }(SKFieldNode.turbulenceField(withSmoothness: 0.1, animationSpeed: 1)) 64 | 65 | var body: some View { 66 | ZStack { 67 | SpriteView(scene: scene) 68 | .onAppear(perform: { 69 | scene.addChild(saleText) 70 | scene.addChild(offText) 71 | scene.addChild(radialGravity) 72 | scene.addChild(turbulence) 73 | scene.addChild(linearGravity) 74 | scene.addChild(turbulenceForLinearGravity) 75 | }) 76 | } 77 | .onTapGesture { 78 | turbulence.isEnabled = false 79 | } 80 | .gesture( 81 | DragGesture(minimumDistance: 5) 82 | .onChanged { value in 83 | let fieldLocation = scene.convertPoint(fromView: value.location) 84 | radialGravity.position = fieldLocation 85 | radialGravity.isEnabled = true 86 | } 87 | .onEnded { dragGestureValue in 88 | radialGravity.isEnabled = false 89 | } 90 | 91 | ) 92 | .onLongPressGesture( 93 | perform: { 94 | turbulence.isEnabled = true 95 | } 96 | ) 97 | .simultaneousGesture( 98 | TapGesture(count: 2) 99 | .onEnded { _ in 100 | linearGravity.isEnabled.toggle() 101 | turbulenceForLinearGravity.isEnabled.toggle() 102 | } 103 | ) 104 | .simultaneousGesture( 105 | TapGesture(count: 3) 106 | .onEnded { _ in 107 | offText.isEmitting.toggle() 108 | saleText.isEmitting.toggle() 109 | linearGravity.isEnabled = false 110 | turbulenceForLinearGravity.isEnabled = false 111 | } 112 | ) 113 | .ignoresSafeArea() 114 | } 115 | } 116 | 117 | #Preview { 118 | AlgoContentView() 119 | } 120 | -------------------------------------------------------------------------------- /ParticlizedDemo/ContentViews/Sale/SaleTextParticle.sks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmstajim/Particlized/1674c17eb20d03f61621f6cf788e601825f0f1dc/ParticlizedDemo/ContentViews/Sale/SaleTextParticle.sks -------------------------------------------------------------------------------- /ParticlizedDemo/ParticlizedDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ParticlizedDemo/ParticlizedDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticlizedDemoApp.swift 3 | // ParticlizedDemo 4 | // 5 | // Created by Aleksei Gusachenko on 29.04.2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct ParticlizedDemoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | OregonContentView() 15 | // AlgoContentView() 16 | // SaleContentView() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ParticlizedDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Particlized 2 | 3 | Particlized is a Swift library that enables developers to easily turn text, emoji, or images into particles aka SKEmitterNodes. 4 | 5 | [See more examples on YouTube](https://youtu.be/JRN9YDiMbXU) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Features 15 | 16 | - **ParticlizedText:** turn text and emoji into particles. 17 | - **ParticlizedImage:** turn image into particles. 18 | 19 | ## Installation 20 | 21 | ### Swift Package Manager 22 | 23 | 1. In Xcode, go to `File` > `Swift Packages` > `Add Package Dependency...` 24 | 2. Enter the repository URL (https://github.com/jmstajim/Particlized.git). 25 | 3. Follow the steps to specify versioning, branch, or tag. 26 | 4. Click `Finish`. 27 | 28 | ### CocoaPods 29 | 30 | *TODO* 31 | 32 | ## Usage 33 | 34 | 1. Import Particlized module into your view controller: 35 | 36 | ```swift 37 | import Particlized 38 | ``` 39 | 40 | 2. Create a Particlized instance: 41 | 42 | ```swift 43 | let text = ParticlizedText( 44 | text: "Oregon 🦫", 45 | font: UIFont(name: "SnellRoundhand", size: 40)!, 46 | textColor: .red, 47 | emitterNode: .init(fileNamed: "TextParticle.sks")!, 48 | numberOfPixelsPerNode: 2, 49 | nodeSkipPercentageChance: 0 50 | ) 51 | ``` 52 | or 53 | 54 | ```swift 55 | let image = ParticlizedImage( 56 | image: UIImage(named: "oregon")!, 57 | emitterNode: .init(fileNamed: "ImageParticle.sks")!, 58 | numberOfPixelsPerNode: 6, 59 | nodeSkipPercentageChance: 0 60 | ) 61 | ``` 62 | 63 | 3. Add the Particlized object to your SKScene: 64 | 65 | ```swift 66 | scene.addChild(text) 67 | ``` 68 | 69 | ```swift 70 | scene.addChild(image) 71 | ``` 72 | 73 | ## Customization 74 | 75 | The behavior of the Particlized object is overridden from SKEmitterNode, but has not been tested and may not work as expected. 76 | 77 | ## Example 78 | 79 | To see Particlized in action, check out the included Demo project. 80 | Check ParticlizedDemoApp.swift to choose a scene. 81 | 82 | Gestures to try: 83 | * Tap gesture x1, x2, x3 times 84 | * Long press gesture 85 | * Drag gesture 86 | 87 | ## Limitations 88 | 89 | By default, SKEmitterNodes are created for each pixel. Be mindful of device resources. 90 | 91 | ## License 92 | 93 | Particlized is available under the MIT license. See the LICENSE file for more info. 94 | 95 | ## Support 96 | 97 | For any questions, issues, or feature requests, please open an issue on GitHub 98 | 99 | or reach out to [gusachenkoalexius@gmail.com](mailto:gusachenkoalexius@gmail.com) or [LinkedIn](https://www.linkedin.com/in/jmstajim/). 100 | -------------------------------------------------------------------------------- /Sources/Particlized/Particlized.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Particlized.swift 3 | // 4 | // 5 | // Created by Aleksei Gusachenko on 30.04.2024. 6 | // 7 | 8 | import SpriteKit 9 | 10 | public class Particlized: SKEmitterNode { 11 | 12 | /// Object identifier 13 | public let id: String 14 | 15 | /// The original SKEmitterNode that is used to create the Particlized object 16 | public let emitterNode: SKEmitterNode 17 | 18 | /// Number of pixels per node 19 | /// A value of 5 means that there will be 1 node represented in a 5 x 5 pixel square 20 | /// From 1 to ∞ 21 | public let numberOfPixelsPerNode: Int 22 | 23 | /// Percentage chance of skipping node creation 24 | /// From 0 to 100 25 | /// 0 means nothing will be skipped 26 | /// 100 means everything will be skipped 27 | public let nodeSkipPercentageChance: UInt8 28 | 29 | /// Is emitting on start 30 | public let isEmittingOnStart: Bool 31 | 32 | /// Particlized object processing queue 33 | public lazy var queue = DispatchQueue(label: "com.particlized.\(id)", qos: .userInteractive) 34 | 35 | public init( 36 | id: String, 37 | emitterNode: SKEmitterNode, 38 | numberOfPixelsPerNode: Int, 39 | nodeSkipPercentageChance: UInt8, 40 | isEmittingOnStart: Bool 41 | ) { 42 | self.id = id 43 | self.emitterNode = emitterNode 44 | self.numberOfPixelsPerNode = numberOfPixelsPerNode < 1 ? 1 : numberOfPixelsPerNode 45 | self.nodeSkipPercentageChance = nodeSkipPercentageChance 46 | self.isEmittingOnStart = isEmittingOnStart 47 | super.init() 48 | } 49 | 50 | required init?(coder aDecoder: NSCoder) { 51 | fatalError("init(coder:) has not been implemented") 52 | } 53 | 54 | /// Does the Particlized object emit any particles? 55 | public var isEmitting: Bool { 56 | get { 57 | queue.sync { 58 | guard let node = self.children.last as? SKEmitterNode else { return false } 59 | return node.particleBirthRate != 0 60 | } 61 | } 62 | set { 63 | queue.async { 64 | if newValue { 65 | self.startEmitting() 66 | } else { 67 | self.stopEmitting() 68 | } 69 | } 70 | } 71 | } 72 | 73 | public func startEmitting() { 74 | queue.async { 75 | self.children.forEach { node in 76 | (node as? SKEmitterNode)?.particleBirthRate = self.emitterNode.particleBirthRate 77 | } 78 | } 79 | } 80 | 81 | public func stopEmitting() { 82 | queue.async { 83 | self.children.forEach { node in 84 | (node as? SKEmitterNode)?.particleBirthRate = 0 85 | } 86 | } 87 | } 88 | 89 | // MARK: - SKEmitterNode overrides 90 | 91 | public override func advanceSimulationTime(_ sec: TimeInterval) { 92 | queue.async { 93 | self.children.forEach { node in 94 | (node as? SKEmitterNode)?.advanceSimulationTime(sec) 95 | } 96 | } 97 | } 98 | 99 | public override func resetSimulation() { 100 | queue.async { 101 | self.children.forEach { node in 102 | (node as? SKEmitterNode)?.resetSimulation() 103 | } 104 | } 105 | } 106 | 107 | public override var particleTexture: SKTexture? { 108 | get { queue.sync { emitterNode.particleTexture } } 109 | set { 110 | queue.async { 111 | self.emitterNode.particleTexture = newValue 112 | self.children.forEach { node in 113 | (node as? SKEmitterNode)?.particleTexture = newValue 114 | } 115 | } 116 | } 117 | } 118 | 119 | public override var particleBlendMode: SKBlendMode { 120 | get { queue.sync { emitterNode.particleBlendMode } } 121 | set { 122 | queue.async { 123 | self.emitterNode.particleBlendMode = newValue 124 | self.children.forEach { node in 125 | (node as? SKEmitterNode)?.particleBlendMode = newValue 126 | } 127 | } 128 | } 129 | } 130 | 131 | public override var particleColor: UIColor { 132 | get { queue.sync { emitterNode.particleColor } } 133 | set { 134 | queue.async { 135 | self.particleColor = newValue 136 | self.children.forEach { node in 137 | (node as? SKEmitterNode)?.particleColor = newValue 138 | } 139 | } 140 | } 141 | } 142 | 143 | public override var particleColorRedRange: CGFloat { 144 | get { queue.sync { emitterNode.particleColorRedRange } } 145 | set { 146 | queue.async { 147 | self.emitterNode.particleColorRedRange = newValue 148 | self.children.forEach { node in 149 | (node as? SKEmitterNode)?.particleColorRedRange = newValue 150 | } 151 | } 152 | } 153 | } 154 | 155 | public override var particleColorGreenRange: CGFloat { 156 | get { queue.sync { emitterNode.particleColorGreenRange } } 157 | set { 158 | queue.async { 159 | self.emitterNode.particleColorGreenRange = newValue 160 | self.children.forEach { node in 161 | (node as? SKEmitterNode)?.particleColorGreenRange = newValue 162 | } 163 | } 164 | } 165 | } 166 | 167 | public override var particleColorBlueRange: CGFloat { 168 | get { queue.sync { emitterNode.particleColorBlueRange } } 169 | set { 170 | queue.async { 171 | self.emitterNode.particleColorBlueRange = newValue 172 | self.children.forEach { node in 173 | (node as? SKEmitterNode)?.particleColorBlueRange = newValue 174 | } 175 | } 176 | } 177 | } 178 | 179 | public override var particleColorAlphaRange: CGFloat { 180 | get { queue.sync { emitterNode.particleColorAlphaRange } } 181 | set { 182 | queue.async { 183 | self.emitterNode.particleColorAlphaRange = newValue 184 | self.children.forEach { node in 185 | (node as? SKEmitterNode)?.particleColorAlphaRange = newValue 186 | } 187 | } 188 | } 189 | } 190 | 191 | public override var particleColorRedSpeed: CGFloat { 192 | get { queue.sync { emitterNode.particleColorRedSpeed } } 193 | set { 194 | queue.async { 195 | self.emitterNode.particleColorRedSpeed = newValue 196 | self.children.forEach { node in 197 | (node as? SKEmitterNode)?.particleColorRedSpeed = newValue 198 | } 199 | } 200 | } 201 | } 202 | 203 | public override var particleColorGreenSpeed: CGFloat { 204 | get { queue.sync { emitterNode.particleColorGreenSpeed } } 205 | set { 206 | queue.async { 207 | self.emitterNode.particleColorGreenSpeed = newValue 208 | self.children.forEach { node in 209 | (node as? SKEmitterNode)?.particleColorGreenSpeed = newValue 210 | } 211 | } 212 | } 213 | } 214 | 215 | public override var particleColorBlueSpeed: CGFloat { 216 | get { queue.sync { emitterNode.particleColorBlueSpeed } } 217 | set { 218 | queue.async { 219 | self.emitterNode.particleColorBlueSpeed = newValue 220 | self.children.forEach { node in 221 | (node as? SKEmitterNode)?.particleColorBlueSpeed = newValue 222 | } 223 | } 224 | } 225 | } 226 | 227 | public override var particleColorAlphaSpeed: CGFloat { 228 | get { queue.sync { emitterNode.particleColorAlphaSpeed } } 229 | set { 230 | queue.async { 231 | self.emitterNode.particleColorAlphaSpeed = newValue 232 | self.children.forEach { node in 233 | (node as? SKEmitterNode)?.particleColorAlphaSpeed = newValue 234 | } 235 | } 236 | } 237 | } 238 | 239 | public override var particleColorSequence: SKKeyframeSequence? { 240 | get { queue.sync { emitterNode.particleColorSequence } } 241 | set { 242 | queue.async { 243 | self.emitterNode.particleColorSequence = newValue 244 | self.children.forEach { node in 245 | (node as? SKEmitterNode)?.particleColorSequence = newValue 246 | } 247 | } 248 | } 249 | } 250 | 251 | public override var particleColorBlendFactor: CGFloat { 252 | get { queue.sync { emitterNode.particleColorBlendFactor } } 253 | set { 254 | queue.async { 255 | self.emitterNode.particleColorBlendFactor = newValue 256 | self.children.forEach { node in 257 | (node as? SKEmitterNode)?.particleColorBlendFactor = newValue 258 | } 259 | } 260 | } 261 | } 262 | 263 | public override var particleColorBlendFactorRange: CGFloat { 264 | get { queue.sync { emitterNode.particleColorBlendFactorRange } } 265 | set { 266 | queue.async { 267 | self.emitterNode.particleColorBlendFactorRange = newValue 268 | self.children.forEach { node in 269 | (node as? SKEmitterNode)?.particleColorBlendFactorRange = newValue 270 | } 271 | } 272 | } 273 | } 274 | 275 | public override var particleColorBlendFactorSpeed: CGFloat { 276 | get { queue.sync { emitterNode.particleColorBlendFactorSpeed } } 277 | set { 278 | queue.async { 279 | self.emitterNode.particleColorBlendFactorSpeed = newValue 280 | self.children.forEach { node in 281 | (node as? SKEmitterNode)?.particleColorBlendFactorSpeed = newValue 282 | } 283 | } 284 | } 285 | } 286 | 287 | public override var particleColorBlendFactorSequence: SKKeyframeSequence? { 288 | get { queue.sync { emitterNode.particleColorBlendFactorSequence } } 289 | set { 290 | queue.async { 291 | self.emitterNode.particleColorBlendFactorSequence = newValue 292 | self.children.forEach { node in 293 | (node as? SKEmitterNode)?.particleColorBlendFactorSequence = newValue 294 | } 295 | } 296 | } 297 | } 298 | 299 | public override var particlePosition: CGPoint { 300 | get { queue.sync { emitterNode.particlePosition } } 301 | set { 302 | queue.async { 303 | self.emitterNode.particlePosition = newValue 304 | self.children.forEach { node in 305 | (node as? SKEmitterNode)?.particlePosition = newValue 306 | } 307 | } 308 | } 309 | } 310 | 311 | public override var particlePositionRange: CGVector { 312 | get { queue.sync { emitterNode.particlePositionRange } } 313 | set { 314 | queue.async { 315 | self.emitterNode.particlePositionRange = newValue 316 | self.children.forEach { node in 317 | (node as? SKEmitterNode)?.particlePositionRange = newValue 318 | } 319 | } 320 | } 321 | } 322 | 323 | public override var particleSpeed: CGFloat { 324 | get { queue.sync { emitterNode.particleSpeed } } 325 | set { 326 | queue.async { 327 | self.emitterNode.particleSpeed = newValue 328 | self.children.forEach { node in 329 | (node as? SKEmitterNode)?.particleSpeed = newValue 330 | } 331 | } 332 | } 333 | } 334 | 335 | public override var particleSpeedRange: CGFloat { 336 | get { queue.sync { emitterNode.particleSpeedRange } } 337 | set { 338 | queue.async { 339 | self.emitterNode.particleSpeedRange = newValue 340 | self.children.forEach { node in 341 | (node as? SKEmitterNode)?.particleSpeedRange = newValue 342 | } 343 | } 344 | } 345 | } 346 | 347 | public override var emissionAngle: CGFloat { 348 | get { queue.sync { emitterNode.emissionAngle } } 349 | set { 350 | queue.async { 351 | self.emitterNode.emissionAngle = newValue 352 | self.children.forEach { node in 353 | (node as? SKEmitterNode)?.emissionAngle = newValue 354 | } 355 | } 356 | } 357 | } 358 | 359 | public override var emissionAngleRange: CGFloat { 360 | get { queue.sync { emitterNode.emissionAngleRange } } 361 | set { 362 | queue.async { 363 | self.emitterNode.emissionAngleRange = newValue 364 | self.children.forEach { node in 365 | (node as? SKEmitterNode)?.emissionAngleRange = newValue 366 | } 367 | } 368 | } 369 | } 370 | 371 | public override var xAcceleration: CGFloat { 372 | get { queue.sync { emitterNode.xAcceleration } } 373 | set { 374 | queue.async { 375 | self.emitterNode.xAcceleration = newValue 376 | self.children.forEach { node in 377 | (node as? SKEmitterNode)?.xAcceleration = newValue 378 | } 379 | } 380 | } 381 | } 382 | 383 | public override var yAcceleration: CGFloat { 384 | get { queue.sync { emitterNode.yAcceleration } } 385 | set { 386 | queue.async { 387 | self.emitterNode.yAcceleration = newValue 388 | self.children.forEach { node in 389 | (node as? SKEmitterNode)?.yAcceleration = newValue 390 | } 391 | } 392 | } 393 | } 394 | 395 | public override var particleBirthRate: CGFloat { 396 | get { queue.sync { emitterNode.particleBirthRate } } 397 | set { 398 | queue.async { 399 | self.emitterNode.particleBirthRate = newValue 400 | self.children.forEach { node in 401 | (node as? SKEmitterNode)?.particleBirthRate = newValue 402 | } 403 | } 404 | } 405 | } 406 | 407 | public override var numParticlesToEmit: Int { 408 | get { queue.sync { emitterNode.numParticlesToEmit } } 409 | set { 410 | queue.async { 411 | self.emitterNode.numParticlesToEmit = newValue 412 | self.children.forEach { node in 413 | (node as? SKEmitterNode)?.numParticlesToEmit = newValue 414 | } 415 | } 416 | } 417 | } 418 | 419 | public override var particleLifetime: CGFloat { 420 | get { queue.sync { emitterNode.particleLifetime } } 421 | set { 422 | queue.async { 423 | self.emitterNode.particleLifetime = newValue 424 | self.children.forEach { node in 425 | (node as? SKEmitterNode)?.particleLifetime = newValue 426 | } 427 | } 428 | } 429 | } 430 | 431 | public override var particleLifetimeRange: CGFloat { 432 | get { queue.sync { emitterNode.particleLifetimeRange } } 433 | set { 434 | queue.async { 435 | self.emitterNode.particleLifetimeRange = newValue 436 | self.children.forEach { node in 437 | (node as? SKEmitterNode)?.particleLifetimeRange = newValue 438 | } 439 | } 440 | } 441 | } 442 | 443 | public override var particleRotation: CGFloat { 444 | get { queue.sync { emitterNode.particleRotation } } 445 | set { 446 | queue.async { 447 | self.emitterNode.particleRotation = newValue 448 | self.children.forEach { node in 449 | (node as? SKEmitterNode)?.particleRotation = newValue 450 | } 451 | } 452 | } 453 | } 454 | 455 | public override var particleRotationRange: CGFloat { 456 | get { queue.sync { emitterNode.particleRotationRange } } 457 | set { 458 | queue.async { 459 | self.emitterNode.particleRotationRange = newValue 460 | self.children.forEach { node in 461 | (node as? SKEmitterNode)?.particleRotationRange = newValue 462 | } 463 | } 464 | } 465 | } 466 | 467 | public override var particleRotationSpeed: CGFloat { 468 | get { queue.sync { emitterNode.particleRotationSpeed } } 469 | set { 470 | queue.async { 471 | self.emitterNode.particleRotationSpeed = newValue 472 | self.children.forEach { node in 473 | (node as? SKEmitterNode)?.particleRotationSpeed = newValue 474 | } 475 | } 476 | } 477 | } 478 | 479 | public override var particleSize: CGSize { 480 | get { queue.sync { emitterNode.particleSize } } 481 | set { 482 | queue.async { 483 | self.emitterNode.particleSize = newValue 484 | self.children.forEach { node in 485 | (node as? SKEmitterNode)?.particleSize = newValue 486 | } 487 | } 488 | } 489 | } 490 | 491 | public override var particleScale: CGFloat { 492 | get { queue.sync { emitterNode.particleScale } } 493 | set { 494 | queue.async { 495 | self.emitterNode.particleScale = newValue 496 | self.children.forEach { node in 497 | (node as? SKEmitterNode)?.particleScale = newValue 498 | } 499 | } 500 | } 501 | } 502 | 503 | public override var particleScaleRange: CGFloat { 504 | get { queue.sync { emitterNode.particleScaleRange } } 505 | set { 506 | queue.async { 507 | self.emitterNode.particleScaleRange = newValue 508 | self.children.forEach { node in 509 | (node as? SKEmitterNode)?.particleScaleRange = newValue 510 | } 511 | } 512 | } 513 | } 514 | 515 | public override var particleScaleSpeed: CGFloat { 516 | get { queue.sync { emitterNode.particleScaleSpeed } } 517 | set { 518 | queue.async { 519 | self.emitterNode.particleScaleSpeed = newValue 520 | self.children.forEach { node in 521 | (node as? SKEmitterNode)?.particleScaleSpeed = newValue 522 | } 523 | } 524 | } 525 | } 526 | 527 | public override var particleScaleSequence: SKKeyframeSequence? { 528 | get { queue.sync { emitterNode.particleScaleSequence } } 529 | set { 530 | queue.async { 531 | self.emitterNode.particleScaleSequence = newValue 532 | self.children.forEach { node in 533 | (node as? SKEmitterNode)?.particleScaleSequence = newValue 534 | } 535 | } 536 | } 537 | } 538 | 539 | public override var particleAlpha: CGFloat { 540 | get { queue.sync { emitterNode.particleAlpha } } 541 | set { 542 | queue.async { 543 | self.emitterNode.particleAlpha = newValue 544 | self.children.forEach { node in 545 | (node as? SKEmitterNode)?.particleAlpha = newValue 546 | } 547 | } 548 | } 549 | } 550 | 551 | public override var particleAlphaRange: CGFloat { 552 | get { queue.sync { emitterNode.particleAlphaRange } } 553 | set { 554 | queue.async { 555 | self.emitterNode.particleAlphaRange = newValue 556 | self.children.forEach { node in 557 | (node as? SKEmitterNode)?.particleAlphaRange = newValue 558 | } 559 | } 560 | } 561 | } 562 | 563 | public override var particleAlphaSpeed: CGFloat { 564 | get { queue.sync { emitterNode.particleAlphaSpeed } } 565 | set { 566 | queue.async { 567 | self.emitterNode.particleAlphaSpeed = newValue 568 | self.children.forEach { node in 569 | (node as? SKEmitterNode)?.particleAlphaSpeed = newValue 570 | } 571 | } 572 | } 573 | } 574 | 575 | 576 | public override var particleAlphaSequence: SKKeyframeSequence? { 577 | get { queue.sync { emitterNode.particleAlphaSequence } } 578 | set { 579 | queue.async { 580 | self.emitterNode.particleAlphaSequence = newValue 581 | self.children.forEach { node in 582 | (node as? SKEmitterNode)?.particleAlphaSequence = newValue 583 | } 584 | } 585 | } 586 | } 587 | 588 | public override var fieldBitMask: UInt32 { 589 | get { queue.sync { emitterNode.fieldBitMask } } 590 | set { 591 | queue.async { 592 | self.emitterNode.fieldBitMask = newValue 593 | self.children.forEach { node in 594 | (node as? SKEmitterNode)?.fieldBitMask = newValue 595 | } 596 | } 597 | } 598 | } 599 | 600 | public override var shader: SKShader? { 601 | get { queue.sync { emitterNode.shader } } 602 | set { 603 | queue.async { 604 | self.emitterNode.shader = newValue 605 | self.children.forEach { node in 606 | (node as? SKEmitterNode)?.shader = newValue 607 | } 608 | } 609 | } 610 | } 611 | 612 | public override var particleZPosition: CGFloat { 613 | get { queue.sync { emitterNode.particleZPosition } } 614 | set { 615 | queue.async { 616 | self.emitterNode.particleZPosition = newValue 617 | self.children.forEach { node in 618 | (node as? SKEmitterNode)?.particleZPosition = newValue 619 | } 620 | } 621 | } 622 | } 623 | 624 | @available(iOS 9.0, *) 625 | public override var particleRenderOrder: SKParticleRenderOrder { 626 | get { queue.sync { emitterNode.particleRenderOrder } } 627 | set { 628 | queue.async { 629 | self.emitterNode.particleRenderOrder = newValue 630 | self.children.forEach { node in 631 | (node as? SKEmitterNode)?.particleRenderOrder = newValue 632 | } 633 | } 634 | } 635 | } 636 | } 637 | -------------------------------------------------------------------------------- /Sources/Particlized/ParticlizedImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticlizeImage.swift 3 | // 4 | // 5 | // Created by Aleksei Gusachenko on 28.04.2024. 6 | // 7 | 8 | import SpriteKit 9 | 10 | /// Turn image into particles 11 | public final class ParticlizedImage: Particlized { 12 | 13 | /// Original image 14 | public let image: UIImage 15 | 16 | public init( 17 | id: String = UUID().uuidString, 18 | image: UIImage, 19 | emitterNode: SKEmitterNode, 20 | numberOfPixelsPerNode: Int = 1, 21 | nodeSkipPercentageChance: UInt8 = 0, 22 | isEmittingOnStart: Bool = true 23 | ) { 24 | self.image = image 25 | super.init( 26 | id: id, 27 | emitterNode: emitterNode, 28 | numberOfPixelsPerNode: numberOfPixelsPerNode, 29 | nodeSkipPercentageChance: nodeSkipPercentageChance, 30 | isEmittingOnStart: isEmittingOnStart 31 | ) 32 | 33 | queue.async { 34 | self.createParticles() 35 | } 36 | } 37 | 38 | required init?(coder aDecoder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | private func createParticles() { 43 | guard 44 | let cgImage = image.cgImage, 45 | let pixelData = cgImage.dataProvider?.data, 46 | let data = CFDataGetBytePtr(pixelData) 47 | else { return } 48 | let textImageWidth = cgImage.width 49 | let textImageHeight = cgImage.height 50 | 51 | let halfTextImageWidth = textImageWidth / 2 52 | let halfTextImageHeight = textImageHeight / 2 53 | 54 | let bytesPerPixel = cgImage.bitsPerPixel / 8 55 | let bytesPerRow = cgImage.bytesPerRow 56 | 57 | for x in 0.. nodeSkipPercentageChance 63 | 64 | guard shouldCreateParticle else { continue } 65 | 66 | guard let color = self.pixelColor(data: data, bytesPerPixel: bytesPerPixel, bytesPerRow: bytesPerRow, x: x, y: y) 67 | else { continue } 68 | 69 | self.createPaticle( 70 | x: CGFloat(x) - CGFloat(halfTextImageWidth), 71 | y: CGFloat(-y) + CGFloat(halfTextImageHeight), 72 | color: color 73 | ) 74 | } 75 | } 76 | } 77 | 78 | @inline(__always) private func pixelColor( 79 | data: UnsafePointer, 80 | bytesPerPixel: Int, 81 | bytesPerRow: Int, 82 | x: Int, 83 | y: Int 84 | ) -> UIColor? { 85 | let pixelByteOffset: Int = (bytesPerPixel * x) + (bytesPerRow * y) 86 | let a = CGFloat(data[pixelByteOffset+3]) / CGFloat(255.0) 87 | guard a > 0 else { return nil } 88 | let r = CGFloat(data[pixelByteOffset]) / CGFloat(255.0) 89 | let g = CGFloat(data[pixelByteOffset+1]) / CGFloat(255.0) 90 | let b = CGFloat(data[pixelByteOffset+2]) / CGFloat(255.0) 91 | return UIColor(ciColor: .init(red: r, green: g, blue: b, alpha: a)) 92 | } 93 | 94 | @inline(__always) private func createPaticle(x: CGFloat, y: CGFloat, color: UIColor) { 95 | let emitterNode = emitterNode.copy() as! SKEmitterNode 96 | emitterNode.particleColor = color 97 | emitterNode.particleColorSequence = nil 98 | emitterNode.position = CGPoint(x: x, y: y) 99 | DispatchQueue.main.async { 100 | self.addChild(emitterNode) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/Particlized/ParticlizedText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticlizedText.swift 3 | // 4 | // 5 | // Created by Aleksei Gusachenko on 28.04.2024. 6 | // 7 | 8 | import SpriteKit 9 | 10 | /// Turn text and emoji into particles 11 | public final class ParticlizedText: Particlized { 12 | public let text: String 13 | public let font: UIFont 14 | 15 | /// To use colors from the .sks file, set the property to nil. 16 | /// For the emoji to work, set the color. 17 | public let textColor: UIColor? 18 | 19 | public init( 20 | id: String = UUID().uuidString, 21 | text: String, 22 | font: UIFont, 23 | textColor: UIColor? = nil, 24 | emitterNode: SKEmitterNode, 25 | numberOfPixelsPerNode: Int = 1, 26 | nodeSkipPercentageChance: UInt8 = 0, 27 | isEmittingOnStart: Bool = true 28 | ) { 29 | self.text = text 30 | self.font = UIFont(name: font.fontName, size: font.pointSize / UIScreen.main.scale)! // TODO: remove UIScreen 31 | self.textColor = textColor 32 | super.init( 33 | id: id, 34 | emitterNode: emitterNode, 35 | numberOfPixelsPerNode: numberOfPixelsPerNode, 36 | nodeSkipPercentageChance: nodeSkipPercentageChance, 37 | isEmittingOnStart: isEmittingOnStart 38 | ) 39 | 40 | queue.async { 41 | self.createParticles() 42 | } 43 | } 44 | 45 | required init?(coder aDecoder: NSCoder) { 46 | fatalError("init(coder:) has not been implemented") 47 | } 48 | 49 | private func createParticles() { 50 | let textImage = makeImageFromText() 51 | guard 52 | let cgImage = textImage.cgImage, 53 | let pixelData = cgImage.dataProvider?.data, 54 | let data = CFDataGetBytePtr(pixelData) 55 | else { return } 56 | let textImageWidth = cgImage.width 57 | let textImageHeight = cgImage.height 58 | 59 | let halfTextImageWidth = textImageWidth / 2 60 | let halfTextImageHeight = textImageHeight / 2 61 | 62 | let bytesPerPixel = cgImage.bitsPerPixel / 8 63 | let bytesPerRow = cgImage.bytesPerRow 64 | 65 | // TODO: I don’t understand why the offset changes depending on whether the text contains emoji and number or not 66 | let containsEmojiOrNumber = 67 | text.unicodeScalars.contains(where: { $0.properties.isEmoji }) 68 | && !text.contains(where: { $0.isNumber }) 69 | 70 | let redOffset = containsEmojiOrNumber ? 2 : 0 71 | let blueOffset = containsEmojiOrNumber ? 0 : 2 72 | 73 | for x in 0.. nodeSkipPercentageChance 79 | 80 | guard shouldCreateParticle else { continue } 81 | 82 | guard let color = self.pixelColor( 83 | data: data, 84 | bytesPerPixel: bytesPerPixel, 85 | bytesPerRow: bytesPerRow, 86 | x: x, 87 | y: y, 88 | redOffset: redOffset, 89 | blueOffset: blueOffset 90 | ) 91 | else { continue } 92 | 93 | self.createPaticle( 94 | x: CGFloat(x) - CGFloat(halfTextImageWidth), 95 | y: CGFloat(-y) + CGFloat(halfTextImageHeight), 96 | color: color, 97 | containsEmojiOrNumber: containsEmojiOrNumber 98 | ) 99 | } 100 | } 101 | } 102 | 103 | private func makeImageFromText() -> UIImage { 104 | let paragraphStyle = NSMutableParagraphStyle() 105 | paragraphStyle.alignment = .center 106 | 107 | let fontAttributes = [ 108 | NSAttributedString.Key.font: self.font, 109 | NSAttributedString.Key.paragraphStyle: paragraphStyle, 110 | NSAttributedString.Key.foregroundColor: textColor ?? .red 111 | ] 112 | let attributeString = NSAttributedString(string: text, attributes: fontAttributes) 113 | var textSize = attributeString.size() 114 | if font.fontDescriptor.symbolicTraits == .classScripts { 115 | textSize.width += 20 116 | } 117 | 118 | let textRect = CGRect(origin: .zero, size: textSize) 119 | 120 | let renderer = UIGraphicsImageRenderer(bounds: textRect) 121 | let image = renderer.image { context in 122 | attributeString.draw(with: textRect, options: [ 123 | .usesLineFragmentOrigin 124 | ], context: nil) 125 | } 126 | return image 127 | } 128 | 129 | @inline(__always) private func pixelColor( 130 | data: UnsafePointer, 131 | bytesPerPixel: Int, 132 | bytesPerRow: Int, 133 | x: Int, 134 | y: Int, 135 | redOffset: Int, 136 | blueOffset: Int 137 | ) -> UIColor? { 138 | let pixelByteOffset: Int = (bytesPerPixel * x) + (bytesPerRow * y) 139 | let a = CGFloat(data[pixelByteOffset + 3]) / CGFloat(255.0) 140 | guard a > 0 else { return nil } 141 | let r = CGFloat(data[pixelByteOffset + redOffset]) / CGFloat(255.0) 142 | let g = CGFloat(data[pixelByteOffset + 1]) / CGFloat(255.0) 143 | let b = CGFloat(data[pixelByteOffset + blueOffset]) / CGFloat(255.0) 144 | return UIColor(ciColor: .init(red: r, green: g, blue: b, alpha: a)) 145 | } 146 | 147 | @inline(__always) private func createPaticle(x: CGFloat, y: CGFloat, color: UIColor, containsEmojiOrNumber: Bool) { 148 | let emitterNode = emitterNode.copy() as! SKEmitterNode 149 | if containsEmojiOrNumber { 150 | emitterNode.particleColor = color 151 | } else { 152 | emitterNode.particleColor = textColor ?? color 153 | } 154 | if textColor != nil { 155 | emitterNode.particleColorSequence = nil 156 | } 157 | 158 | emitterNode.position = CGPoint(x: x, y: y) 159 | if !isEmittingOnStart { 160 | emitterNode.particleBirthRate = 0 161 | } 162 | 163 | DispatchQueue.main.async { 164 | self.addChild(emitterNode) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Tests/ParticlizedTests/ParticlizedTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Particlized 3 | 4 | final class ParticlizedTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documentation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | --------------------------------------------------------------------------------