├── README.md └── TextParticle ├── TextParticle.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── minsang.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── minsang.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── TextParticle ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── ContentView.swift ├── Model └── Particle.swift ├── ParticleAnimation.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json └── TextParticleApp.swift /README.md: -------------------------------------------------------------------------------- 1 | **Text to Particle** 2 | 3 | Simple particle to text effect powered by ImageRenderer. This is basically sampling pixel data and put dots in random position. And then slowly move them to original position as timer fires up. 4 | 5 | Inspired by some website that I saw the other day - got help from ChatGPT and Claude Sonnet 3.5 6 | -------------------------------------------------------------------------------- /TextParticle/TextParticle.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 70; 7 | objects = { 8 | 9 | /* Begin PBXFileReference section */ 10 | AB4FD2692C52006F0088E344 /* TextParticle.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TextParticle.app; sourceTree = BUILT_PRODUCTS_DIR; }; 11 | /* End PBXFileReference section */ 12 | 13 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 14 | AB4FD26B2C52006F0088E344 /* TextParticle */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = TextParticle; sourceTree = ""; }; 15 | /* End PBXFileSystemSynchronizedRootGroup section */ 16 | 17 | /* Begin PBXFrameworksBuildPhase section */ 18 | AB4FD2662C52006F0088E344 /* Frameworks */ = { 19 | isa = PBXFrameworksBuildPhase; 20 | buildActionMask = 2147483647; 21 | files = ( 22 | ); 23 | runOnlyForDeploymentPostprocessing = 0; 24 | }; 25 | /* End PBXFrameworksBuildPhase section */ 26 | 27 | /* Begin PBXGroup section */ 28 | AB4FD2602C52006F0088E344 = { 29 | isa = PBXGroup; 30 | children = ( 31 | AB4FD26B2C52006F0088E344 /* TextParticle */, 32 | AB4FD26A2C52006F0088E344 /* Products */, 33 | ); 34 | sourceTree = ""; 35 | }; 36 | AB4FD26A2C52006F0088E344 /* Products */ = { 37 | isa = PBXGroup; 38 | children = ( 39 | AB4FD2692C52006F0088E344 /* TextParticle.app */, 40 | ); 41 | name = Products; 42 | sourceTree = ""; 43 | }; 44 | /* End PBXGroup section */ 45 | 46 | /* Begin PBXNativeTarget section */ 47 | AB4FD2682C52006F0088E344 /* TextParticle */ = { 48 | isa = PBXNativeTarget; 49 | buildConfigurationList = AB4FD2772C5200700088E344 /* Build configuration list for PBXNativeTarget "TextParticle" */; 50 | buildPhases = ( 51 | AB4FD2652C52006F0088E344 /* Sources */, 52 | AB4FD2662C52006F0088E344 /* Frameworks */, 53 | AB4FD2672C52006F0088E344 /* Resources */, 54 | ); 55 | buildRules = ( 56 | ); 57 | dependencies = ( 58 | ); 59 | fileSystemSynchronizedGroups = ( 60 | AB4FD26B2C52006F0088E344 /* TextParticle */, 61 | ); 62 | name = TextParticle; 63 | packageProductDependencies = ( 64 | ); 65 | productName = TextParticle; 66 | productReference = AB4FD2692C52006F0088E344 /* TextParticle.app */; 67 | productType = "com.apple.product-type.application"; 68 | }; 69 | /* End PBXNativeTarget section */ 70 | 71 | /* Begin PBXProject section */ 72 | AB4FD2612C52006F0088E344 /* Project object */ = { 73 | isa = PBXProject; 74 | attributes = { 75 | BuildIndependentTargetsInParallel = 1; 76 | LastSwiftUpdateCheck = 1600; 77 | LastUpgradeCheck = 1600; 78 | TargetAttributes = { 79 | AB4FD2682C52006F0088E344 = { 80 | CreatedOnToolsVersion = 16.0; 81 | }; 82 | }; 83 | }; 84 | buildConfigurationList = AB4FD2642C52006F0088E344 /* Build configuration list for PBXProject "TextParticle" */; 85 | compatibilityVersion = "Xcode 15.0"; 86 | developmentRegion = en; 87 | hasScannedForEncodings = 0; 88 | knownRegions = ( 89 | en, 90 | Base, 91 | ); 92 | mainGroup = AB4FD2602C52006F0088E344; 93 | productRefGroup = AB4FD26A2C52006F0088E344 /* Products */; 94 | projectDirPath = ""; 95 | projectRoot = ""; 96 | targets = ( 97 | AB4FD2682C52006F0088E344 /* TextParticle */, 98 | ); 99 | }; 100 | /* End PBXProject section */ 101 | 102 | /* Begin PBXResourcesBuildPhase section */ 103 | AB4FD2672C52006F0088E344 /* Resources */ = { 104 | isa = PBXResourcesBuildPhase; 105 | buildActionMask = 2147483647; 106 | files = ( 107 | ); 108 | runOnlyForDeploymentPostprocessing = 0; 109 | }; 110 | /* End PBXResourcesBuildPhase section */ 111 | 112 | /* Begin PBXSourcesBuildPhase section */ 113 | AB4FD2652C52006F0088E344 /* Sources */ = { 114 | isa = PBXSourcesBuildPhase; 115 | buildActionMask = 2147483647; 116 | files = ( 117 | ); 118 | runOnlyForDeploymentPostprocessing = 0; 119 | }; 120 | /* End PBXSourcesBuildPhase section */ 121 | 122 | /* Begin XCBuildConfiguration section */ 123 | AB4FD2752C5200700088E344 /* Debug */ = { 124 | isa = XCBuildConfiguration; 125 | buildSettings = { 126 | ALWAYS_SEARCH_USER_PATHS = NO; 127 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 128 | CLANG_ANALYZER_NONNULL = YES; 129 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 130 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 131 | CLANG_ENABLE_MODULES = YES; 132 | CLANG_ENABLE_OBJC_ARC = YES; 133 | CLANG_ENABLE_OBJC_WEAK = YES; 134 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 135 | CLANG_WARN_BOOL_CONVERSION = YES; 136 | CLANG_WARN_COMMA = YES; 137 | CLANG_WARN_CONSTANT_CONVERSION = YES; 138 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 139 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 140 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 141 | CLANG_WARN_EMPTY_BODY = YES; 142 | CLANG_WARN_ENUM_CONVERSION = YES; 143 | CLANG_WARN_INFINITE_RECURSION = YES; 144 | CLANG_WARN_INT_CONVERSION = YES; 145 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 146 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 147 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 148 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 149 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 150 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 151 | CLANG_WARN_STRICT_PROTOTYPES = YES; 152 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 153 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 154 | CLANG_WARN_UNREACHABLE_CODE = YES; 155 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 156 | COPY_PHASE_STRIP = NO; 157 | DEBUG_INFORMATION_FORMAT = dwarf; 158 | ENABLE_STRICT_OBJC_MSGSEND = YES; 159 | ENABLE_TESTABILITY = YES; 160 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 161 | GCC_C_LANGUAGE_STANDARD = gnu17; 162 | GCC_DYNAMIC_NO_PIC = NO; 163 | GCC_NO_COMMON_BLOCKS = YES; 164 | GCC_OPTIMIZATION_LEVEL = 0; 165 | GCC_PREPROCESSOR_DEFINITIONS = ( 166 | "DEBUG=1", 167 | "$(inherited)", 168 | ); 169 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 170 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 171 | GCC_WARN_UNDECLARED_SELECTOR = YES; 172 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 173 | GCC_WARN_UNUSED_FUNCTION = YES; 174 | GCC_WARN_UNUSED_VARIABLE = YES; 175 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 176 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 177 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 178 | MTL_FAST_MATH = YES; 179 | ONLY_ACTIVE_ARCH = YES; 180 | SDKROOT = iphoneos; 181 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 182 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 183 | }; 184 | name = Debug; 185 | }; 186 | AB4FD2762C5200700088E344 /* Release */ = { 187 | isa = XCBuildConfiguration; 188 | buildSettings = { 189 | ALWAYS_SEARCH_USER_PATHS = NO; 190 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 191 | CLANG_ANALYZER_NONNULL = YES; 192 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 193 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 194 | CLANG_ENABLE_MODULES = YES; 195 | CLANG_ENABLE_OBJC_ARC = YES; 196 | CLANG_ENABLE_OBJC_WEAK = YES; 197 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 198 | CLANG_WARN_BOOL_CONVERSION = YES; 199 | CLANG_WARN_COMMA = YES; 200 | CLANG_WARN_CONSTANT_CONVERSION = YES; 201 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 202 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 203 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 204 | CLANG_WARN_EMPTY_BODY = YES; 205 | CLANG_WARN_ENUM_CONVERSION = YES; 206 | CLANG_WARN_INFINITE_RECURSION = YES; 207 | CLANG_WARN_INT_CONVERSION = YES; 208 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 209 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 210 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 211 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 212 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 213 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 214 | CLANG_WARN_STRICT_PROTOTYPES = YES; 215 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 216 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 217 | CLANG_WARN_UNREACHABLE_CODE = YES; 218 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 219 | COPY_PHASE_STRIP = NO; 220 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 221 | ENABLE_NS_ASSERTIONS = NO; 222 | ENABLE_STRICT_OBJC_MSGSEND = YES; 223 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 224 | GCC_C_LANGUAGE_STANDARD = gnu17; 225 | GCC_NO_COMMON_BLOCKS = YES; 226 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 227 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 228 | GCC_WARN_UNDECLARED_SELECTOR = YES; 229 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 230 | GCC_WARN_UNUSED_FUNCTION = YES; 231 | GCC_WARN_UNUSED_VARIABLE = YES; 232 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 233 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 234 | MTL_ENABLE_DEBUG_INFO = NO; 235 | MTL_FAST_MATH = YES; 236 | SDKROOT = iphoneos; 237 | SWIFT_COMPILATION_MODE = wholemodule; 238 | VALIDATE_PRODUCT = YES; 239 | }; 240 | name = Release; 241 | }; 242 | AB4FD2782C5200700088E344 /* Debug */ = { 243 | isa = XCBuildConfiguration; 244 | buildSettings = { 245 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 246 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 247 | CODE_SIGN_STYLE = Automatic; 248 | CURRENT_PROJECT_VERSION = 1; 249 | DEVELOPMENT_ASSET_PATHS = "\"TextParticle/Preview Content\""; 250 | DEVELOPMENT_TEAM = 66X955TH34; 251 | ENABLE_PREVIEWS = YES; 252 | GENERATE_INFOPLIST_FILE = YES; 253 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 254 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 255 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 256 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 257 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 258 | LD_RUNPATH_SEARCH_PATHS = ( 259 | "$(inherited)", 260 | "@executable_path/Frameworks", 261 | ); 262 | MARKETING_VERSION = 1.0; 263 | PRODUCT_BUNDLE_IDENTIFIER = minsang.TextParticle; 264 | PRODUCT_NAME = "$(TARGET_NAME)"; 265 | SWIFT_EMIT_LOC_STRINGS = YES; 266 | SWIFT_VERSION = 5.0; 267 | TARGETED_DEVICE_FAMILY = "1,2"; 268 | }; 269 | name = Debug; 270 | }; 271 | AB4FD2792C5200700088E344 /* Release */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 275 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 276 | CODE_SIGN_STYLE = Automatic; 277 | CURRENT_PROJECT_VERSION = 1; 278 | DEVELOPMENT_ASSET_PATHS = "\"TextParticle/Preview Content\""; 279 | DEVELOPMENT_TEAM = 66X955TH34; 280 | ENABLE_PREVIEWS = YES; 281 | GENERATE_INFOPLIST_FILE = YES; 282 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 283 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 284 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 285 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 286 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 287 | LD_RUNPATH_SEARCH_PATHS = ( 288 | "$(inherited)", 289 | "@executable_path/Frameworks", 290 | ); 291 | MARKETING_VERSION = 1.0; 292 | PRODUCT_BUNDLE_IDENTIFIER = minsang.TextParticle; 293 | PRODUCT_NAME = "$(TARGET_NAME)"; 294 | SWIFT_EMIT_LOC_STRINGS = YES; 295 | SWIFT_VERSION = 5.0; 296 | TARGETED_DEVICE_FAMILY = "1,2"; 297 | }; 298 | name = Release; 299 | }; 300 | /* End XCBuildConfiguration section */ 301 | 302 | /* Begin XCConfigurationList section */ 303 | AB4FD2642C52006F0088E344 /* Build configuration list for PBXProject "TextParticle" */ = { 304 | isa = XCConfigurationList; 305 | buildConfigurations = ( 306 | AB4FD2752C5200700088E344 /* Debug */, 307 | AB4FD2762C5200700088E344 /* Release */, 308 | ); 309 | defaultConfigurationIsVisible = 0; 310 | defaultConfigurationName = Release; 311 | }; 312 | AB4FD2772C5200700088E344 /* Build configuration list for PBXNativeTarget "TextParticle" */ = { 313 | isa = XCConfigurationList; 314 | buildConfigurations = ( 315 | AB4FD2782C5200700088E344 /* Debug */, 316 | AB4FD2792C5200700088E344 /* Release */, 317 | ); 318 | defaultConfigurationIsVisible = 0; 319 | defaultConfigurationName = Release; 320 | }; 321 | /* End XCConfigurationList section */ 322 | }; 323 | rootObject = AB4FD2612C52006F0088E344 /* Project object */; 324 | } 325 | -------------------------------------------------------------------------------- /TextParticle/TextParticle.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TextParticle/TextParticle.xcodeproj/project.xcworkspace/xcuserdata/minsang.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radiofun/ParticleToText/224ba261a35dadec88dfecf3f95bea4cbe3ca980/TextParticle/TextParticle.xcodeproj/project.xcworkspace/xcuserdata/minsang.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /TextParticle/TextParticle.xcodeproj/xcuserdata/minsang.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | TextParticle.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TextParticle/TextParticle/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 | -------------------------------------------------------------------------------- /TextParticle/TextParticle/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /TextParticle/TextParticle/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TextParticle/TextParticle/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKit 3 | 4 | struct ParticleView: View { 5 | 6 | @State private var text : String = "a" 7 | @FocusState private var onfocus: Bool 8 | 9 | 10 | var body: some View { 11 | 12 | ZStack{ 13 | VStack { 14 | ParticleTextAnimation(text: text) 15 | .ignoresSafeArea() 16 | .opacity(onfocus ? 0.3 : 1) 17 | } 18 | 19 | VStack{ 20 | Text("Text to Particle") 21 | .foregroundColor(.primary) 22 | .padding() 23 | .font(.system(size: 14, design: .rounded)) 24 | .bold() 25 | 26 | TextField("...", text:$text) 27 | .foregroundColor(.primary) 28 | .padding() 29 | .font(.system(size: 20, design: .rounded)) 30 | .bold() 31 | .background(.primary.opacity(0.1)) 32 | .cornerRadius(20) 33 | .frame(width:200) 34 | .contentShape(Rectangle()) 35 | .multilineTextAlignment(.center) 36 | .focused($onfocus) 37 | } 38 | .offset(y: onfocus ? 0 : 330) 39 | 40 | } 41 | } 42 | } 43 | 44 | 45 | 46 | #Preview { 47 | ParticleView() 48 | } 49 | -------------------------------------------------------------------------------- /TextParticle/TextParticle/Model/Particle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Particle.swift 3 | // TextParticle 4 | // 5 | // Created by Minsang Choi on 7/24/24. 6 | // 7 | import SwiftUI 8 | 9 | struct Particle { 10 | 11 | var x: Double 12 | var y: Double 13 | let baseX: Double 14 | let baseY: Double 15 | let density: Double 16 | var isStopped = false 17 | 18 | 19 | mutating func update(dragPosition: CGPoint?, dragVelocity: CGSize?) { 20 | 21 | let dx = baseX - x 22 | let dy = baseY - y 23 | let distance = sqrt(dx * dx + dy * dy) 24 | let forceDirectionX = dx / distance 25 | let forceDirectionY = dy / distance 26 | 27 | let maxDistance: Double = 280 28 | let force = (maxDistance - distance) / maxDistance 29 | let directionX = forceDirectionX * force * density 30 | let directionY = forceDirectionY * force * density 31 | 32 | 33 | // Apply slow movement when close to the base position 34 | if distance < 30 { 35 | x += directionX * 0.01 36 | y += directionY * 0.01 37 | } else { 38 | if distance < maxDistance { 39 | x += directionX * 2.5 40 | y += directionY * 2.5 41 | } else { 42 | if x != baseX { 43 | let dx = x - baseX 44 | x -= dx / 10 45 | } 46 | 47 | if y != baseY { 48 | let dy = y - baseY 49 | y -= dy / 10 50 | } 51 | } 52 | } 53 | 54 | // React to drag gesture with increased responsiveness 55 | 56 | if let dragPosition = dragPosition { 57 | 58 | let dragDx = x - dragPosition.x 59 | let dragDy = y - dragPosition.y 60 | 61 | var velocityF = 0.0 62 | 63 | if let dragVelocity = dragVelocity { 64 | velocityF = max(abs(dragVelocity.width),abs(dragVelocity.height)) 65 | } 66 | 67 | let dragDistance = sqrt(dragDx * dragDx + dragDy * dragDy) 68 | let dragForce = (200 - min(dragDistance, 200)) / 200 + velocityF * 0.00005 69 | 70 | x += dragDx * dragForce * 0.5 71 | y += dragDy * dragForce * 0.5 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /TextParticle/TextParticle/ParticleAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticleAnimation.swift 3 | // TextParticle 4 | // 5 | // Created by Minsang Choi on 7/24/24. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | 11 | struct ParticleTextAnimation: View { 12 | 13 | let text: String 14 | let particleCount = 1000 15 | 16 | @State private var particles: [Particle] = [] 17 | @State private var dragPosition: CGPoint? 18 | @State private var dragVelocity: CGSize? 19 | @State private var size: CGSize = .zero 20 | 21 | 22 | let timer = Timer.publish(every: 1/120, on: .main, in: .common).autoconnect() 23 | 24 | var body: some View { 25 | 26 | Canvas { context, size in 27 | 28 | context.blendMode = .normal 29 | 30 | for particle in particles { 31 | let path = Path(ellipseIn: CGRect(x: particle.x, y: particle.y, width: 2, height: 2)) 32 | context.fill(path, with: .color(.primary.opacity(0.7))) 33 | } 34 | } 35 | 36 | .onReceive(timer){ _ in 37 | updateParticles() 38 | } 39 | .onChange(of:text){ 40 | createParticles() 41 | } 42 | .onAppear { 43 | createParticles() 44 | } 45 | .gesture( 46 | DragGesture(minimumDistance: 0) 47 | .onChanged { value in 48 | dragPosition = value.location 49 | dragVelocity = value.velocity 50 | triggerHapticFeedback() 51 | } 52 | 53 | .onEnded { value in 54 | dragPosition = nil 55 | dragVelocity = nil 56 | updateParticles() 57 | } 58 | 59 | ) 60 | 61 | .background(.background) 62 | .overlay( 63 | GeometryReader { geometry in 64 | Color.clear 65 | .onAppear { 66 | size = geometry.size 67 | createParticles() 68 | } 69 | } 70 | ) 71 | } 72 | 73 | 74 | 75 | private func createParticles() { 76 | 77 | let renderer = ImageRenderer(content: Text(text) 78 | .font(.system(size: 240, design: .rounded)) 79 | .bold()) 80 | 81 | renderer.scale = 1.0 82 | 83 | guard let image = renderer.uiImage else { return } 84 | guard let cgImage = image.cgImage else { return } 85 | 86 | let width = Int(image.size.width) 87 | let height = Int(image.size.height) 88 | 89 | guard let pixelData = cgImage.dataProvider?.data, let data = CFDataGetBytePtr(pixelData) else { return } 90 | 91 | let offsetX = (size.width - CGFloat(width)) / 2 92 | let offsetY = (size.height - CGFloat(height)) / 2 93 | 94 | 95 | particles = (0..