├── .gitignore ├── BDGradientNodeDemo.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── BDGradientNodeDemo.xccheckout ├── BDGradientNodeDemo ├── AppDelegate.swift ├── BDGradientNode.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── GradientScene.swift ├── GradientViewController.swift ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-40.png │ │ ├── Icon-40@2x.png │ │ ├── Icon-40@3x.png │ │ ├── Icon-60@2x.png │ │ ├── Icon-60@3x.png │ │ ├── Icon-76.png │ │ ├── Icon-76@2x.png │ │ ├── Icon-Small.png │ │ ├── Icon-Small@2x.png │ │ └── Icon-Small@3x.png │ ├── Spaceship.imageset │ │ ├── Contents.json │ │ └── Spaceship.png │ └── gamutlogo.imageset │ │ ├── Contents.json │ │ └── gamutlogo.png └── Info.plist ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | build/* 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | !default.xcworkspace 12 | xcuserdata 13 | profile 14 | *.moved-aside 15 | 16 | # OS X 17 | .DS_Store 18 | Icon? 19 | 20 | # Windows 21 | 22 | desktop.ini 23 | 24 | # Thumbnails 25 | ._* 26 | 27 | # Files that might appear on external disk 28 | .Spotlight-V100 29 | .Trashes 30 | -------------------------------------------------------------------------------- /BDGradientNodeDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3CAC01291AC75E17002EB9EA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CAC01281AC75E17002EB9EA /* AppDelegate.swift */; }; 11 | 3CAC012D1AC75E17002EB9EA /* GradientScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CAC012C1AC75E17002EB9EA /* GradientScene.swift */; }; 12 | 3CAC012F1AC75E17002EB9EA /* GradientViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CAC012E1AC75E17002EB9EA /* GradientViewController.swift */; }; 13 | 3CAC01321AC75E17002EB9EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3CAC01301AC75E17002EB9EA /* Main.storyboard */; }; 14 | 3CAC01341AC75E17002EB9EA /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3CAC01331AC75E17002EB9EA /* Images.xcassets */; }; 15 | 3CAC01371AC75E17002EB9EA /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3CAC01351AC75E17002EB9EA /* LaunchScreen.xib */; }; 16 | 3CAC014D1AC75FC8002EB9EA /* BDGradientNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CAC014C1AC75FC8002EB9EA /* BDGradientNode.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXFileReference section */ 20 | 3CAC01231AC75E17002EB9EA /* BDGradientNodeDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BDGradientNodeDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 3CAC01271AC75E17002EB9EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 22 | 3CAC01281AC75E17002EB9EA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23 | 3CAC012C1AC75E17002EB9EA /* GradientScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientScene.swift; sourceTree = ""; }; 24 | 3CAC012E1AC75E17002EB9EA /* GradientViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientViewController.swift; sourceTree = ""; }; 25 | 3CAC01311AC75E17002EB9EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | 3CAC01331AC75E17002EB9EA /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 27 | 3CAC01361AC75E17002EB9EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 28 | 3CAC014C1AC75FC8002EB9EA /* BDGradientNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BDGradientNode.swift; sourceTree = ""; }; 29 | /* End PBXFileReference section */ 30 | 31 | /* Begin PBXFrameworksBuildPhase section */ 32 | 3CAC01201AC75E17002EB9EA /* Frameworks */ = { 33 | isa = PBXFrameworksBuildPhase; 34 | buildActionMask = 2147483647; 35 | files = ( 36 | ); 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXFrameworksBuildPhase section */ 40 | 41 | /* Begin PBXGroup section */ 42 | 3CAC011A1AC75E17002EB9EA = { 43 | isa = PBXGroup; 44 | children = ( 45 | 3CAC01251AC75E17002EB9EA /* BDGradientNodeDemo */, 46 | 3CAC01241AC75E17002EB9EA /* Products */, 47 | ); 48 | sourceTree = ""; 49 | }; 50 | 3CAC01241AC75E17002EB9EA /* Products */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | 3CAC01231AC75E17002EB9EA /* BDGradientNodeDemo.app */, 54 | ); 55 | name = Products; 56 | sourceTree = ""; 57 | }; 58 | 3CAC01251AC75E17002EB9EA /* BDGradientNodeDemo */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3CAC01281AC75E17002EB9EA /* AppDelegate.swift */, 62 | 3CAC014C1AC75FC8002EB9EA /* BDGradientNode.swift */, 63 | 3CAC012C1AC75E17002EB9EA /* GradientScene.swift */, 64 | 3CAC012E1AC75E17002EB9EA /* GradientViewController.swift */, 65 | 3CAC01301AC75E17002EB9EA /* Main.storyboard */, 66 | 3CAC01331AC75E17002EB9EA /* Images.xcassets */, 67 | 3CAC01351AC75E17002EB9EA /* LaunchScreen.xib */, 68 | 3CAC01261AC75E17002EB9EA /* Supporting Files */, 69 | ); 70 | path = BDGradientNodeDemo; 71 | sourceTree = ""; 72 | }; 73 | 3CAC01261AC75E17002EB9EA /* Supporting Files */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | 3CAC01271AC75E17002EB9EA /* Info.plist */, 77 | ); 78 | name = "Supporting Files"; 79 | sourceTree = ""; 80 | }; 81 | /* End PBXGroup section */ 82 | 83 | /* Begin PBXNativeTarget section */ 84 | 3CAC01221AC75E17002EB9EA /* BDGradientNodeDemo */ = { 85 | isa = PBXNativeTarget; 86 | buildConfigurationList = 3CAC01461AC75E18002EB9EA /* Build configuration list for PBXNativeTarget "BDGradientNodeDemo" */; 87 | buildPhases = ( 88 | 3CAC011F1AC75E17002EB9EA /* Sources */, 89 | 3CAC01201AC75E17002EB9EA /* Frameworks */, 90 | 3CAC01211AC75E17002EB9EA /* Resources */, 91 | ); 92 | buildRules = ( 93 | ); 94 | dependencies = ( 95 | ); 96 | name = BDGradientNodeDemo; 97 | productName = BDGradientNodeDemo; 98 | productReference = 3CAC01231AC75E17002EB9EA /* BDGradientNodeDemo.app */; 99 | productType = "com.apple.product-type.application"; 100 | }; 101 | /* End PBXNativeTarget section */ 102 | 103 | /* Begin PBXProject section */ 104 | 3CAC011B1AC75E17002EB9EA /* Project object */ = { 105 | isa = PBXProject; 106 | attributes = { 107 | LastSwiftMigration = 0720; 108 | LastUpgradeCheck = 0630; 109 | ORGANIZATIONNAME = "Braindrizzle Studio"; 110 | TargetAttributes = { 111 | 3CAC01221AC75E17002EB9EA = { 112 | CreatedOnToolsVersion = 6.3; 113 | DevelopmentTeam = FC75JJPE59; 114 | }; 115 | }; 116 | }; 117 | buildConfigurationList = 3CAC011E1AC75E17002EB9EA /* Build configuration list for PBXProject "BDGradientNodeDemo" */; 118 | compatibilityVersion = "Xcode 3.2"; 119 | developmentRegion = English; 120 | hasScannedForEncodings = 0; 121 | knownRegions = ( 122 | en, 123 | Base, 124 | ); 125 | mainGroup = 3CAC011A1AC75E17002EB9EA; 126 | productRefGroup = 3CAC01241AC75E17002EB9EA /* Products */; 127 | projectDirPath = ""; 128 | projectRoot = ""; 129 | targets = ( 130 | 3CAC01221AC75E17002EB9EA /* BDGradientNodeDemo */, 131 | ); 132 | }; 133 | /* End PBXProject section */ 134 | 135 | /* Begin PBXResourcesBuildPhase section */ 136 | 3CAC01211AC75E17002EB9EA /* Resources */ = { 137 | isa = PBXResourcesBuildPhase; 138 | buildActionMask = 2147483647; 139 | files = ( 140 | 3CAC01371AC75E17002EB9EA /* LaunchScreen.xib in Resources */, 141 | 3CAC01341AC75E17002EB9EA /* Images.xcassets in Resources */, 142 | 3CAC01321AC75E17002EB9EA /* Main.storyboard in Resources */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXResourcesBuildPhase section */ 147 | 148 | /* Begin PBXSourcesBuildPhase section */ 149 | 3CAC011F1AC75E17002EB9EA /* Sources */ = { 150 | isa = PBXSourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | 3CAC012D1AC75E17002EB9EA /* GradientScene.swift in Sources */, 154 | 3CAC014D1AC75FC8002EB9EA /* BDGradientNode.swift in Sources */, 155 | 3CAC012F1AC75E17002EB9EA /* GradientViewController.swift in Sources */, 156 | 3CAC01291AC75E17002EB9EA /* AppDelegate.swift in Sources */, 157 | ); 158 | runOnlyForDeploymentPostprocessing = 0; 159 | }; 160 | /* End PBXSourcesBuildPhase section */ 161 | 162 | /* Begin PBXVariantGroup section */ 163 | 3CAC01301AC75E17002EB9EA /* Main.storyboard */ = { 164 | isa = PBXVariantGroup; 165 | children = ( 166 | 3CAC01311AC75E17002EB9EA /* Base */, 167 | ); 168 | name = Main.storyboard; 169 | sourceTree = ""; 170 | }; 171 | 3CAC01351AC75E17002EB9EA /* LaunchScreen.xib */ = { 172 | isa = PBXVariantGroup; 173 | children = ( 174 | 3CAC01361AC75E17002EB9EA /* Base */, 175 | ); 176 | name = LaunchScreen.xib; 177 | sourceTree = ""; 178 | }; 179 | /* End PBXVariantGroup section */ 180 | 181 | /* Begin XCBuildConfiguration section */ 182 | 3CAC01441AC75E18002EB9EA /* Debug */ = { 183 | isa = XCBuildConfiguration; 184 | buildSettings = { 185 | ALWAYS_SEARCH_USER_PATHS = NO; 186 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 187 | CLANG_CXX_LIBRARY = "libc++"; 188 | CLANG_ENABLE_MODULES = YES; 189 | CLANG_ENABLE_OBJC_ARC = YES; 190 | CLANG_WARN_BOOL_CONVERSION = YES; 191 | CLANG_WARN_CONSTANT_CONVERSION = YES; 192 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 193 | CLANG_WARN_EMPTY_BODY = YES; 194 | CLANG_WARN_ENUM_CONVERSION = YES; 195 | CLANG_WARN_INT_CONVERSION = YES; 196 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 197 | CLANG_WARN_UNREACHABLE_CODE = YES; 198 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 199 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 200 | COPY_PHASE_STRIP = NO; 201 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 202 | ENABLE_STRICT_OBJC_MSGSEND = YES; 203 | GCC_C_LANGUAGE_STANDARD = gnu99; 204 | GCC_DYNAMIC_NO_PIC = NO; 205 | GCC_NO_COMMON_BLOCKS = YES; 206 | GCC_OPTIMIZATION_LEVEL = 0; 207 | GCC_PREPROCESSOR_DEFINITIONS = ( 208 | "DEBUG=1", 209 | "$(inherited)", 210 | ); 211 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 212 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 213 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 214 | GCC_WARN_UNDECLARED_SELECTOR = YES; 215 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 216 | GCC_WARN_UNUSED_FUNCTION = YES; 217 | GCC_WARN_UNUSED_VARIABLE = YES; 218 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 219 | MTL_ENABLE_DEBUG_INFO = YES; 220 | ONLY_ACTIVE_ARCH = YES; 221 | SDKROOT = iphoneos; 222 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 223 | TARGETED_DEVICE_FAMILY = "1,2"; 224 | }; 225 | name = Debug; 226 | }; 227 | 3CAC01451AC75E18002EB9EA /* Release */ = { 228 | isa = XCBuildConfiguration; 229 | buildSettings = { 230 | ALWAYS_SEARCH_USER_PATHS = NO; 231 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 232 | CLANG_CXX_LIBRARY = "libc++"; 233 | CLANG_ENABLE_MODULES = YES; 234 | CLANG_ENABLE_OBJC_ARC = YES; 235 | CLANG_WARN_BOOL_CONVERSION = YES; 236 | CLANG_WARN_CONSTANT_CONVERSION = YES; 237 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 238 | CLANG_WARN_EMPTY_BODY = YES; 239 | CLANG_WARN_ENUM_CONVERSION = YES; 240 | CLANG_WARN_INT_CONVERSION = YES; 241 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 242 | CLANG_WARN_UNREACHABLE_CODE = YES; 243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 244 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 245 | COPY_PHASE_STRIP = NO; 246 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 247 | ENABLE_NS_ASSERTIONS = NO; 248 | ENABLE_STRICT_OBJC_MSGSEND = YES; 249 | GCC_C_LANGUAGE_STANDARD = gnu99; 250 | GCC_NO_COMMON_BLOCKS = YES; 251 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 252 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 253 | GCC_WARN_UNDECLARED_SELECTOR = YES; 254 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 255 | GCC_WARN_UNUSED_FUNCTION = YES; 256 | GCC_WARN_UNUSED_VARIABLE = YES; 257 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 258 | MTL_ENABLE_DEBUG_INFO = NO; 259 | SDKROOT = iphoneos; 260 | TARGETED_DEVICE_FAMILY = "1,2"; 261 | VALIDATE_PRODUCT = YES; 262 | }; 263 | name = Release; 264 | }; 265 | 3CAC01471AC75E18002EB9EA /* Debug */ = { 266 | isa = XCBuildConfiguration; 267 | buildSettings = { 268 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 269 | CODE_SIGN_IDENTITY = "iPhone Developer"; 270 | INFOPLIST_FILE = BDGradientNodeDemo/Info.plist; 271 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 272 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 273 | PRODUCT_NAME = "$(TARGET_NAME)"; 274 | }; 275 | name = Debug; 276 | }; 277 | 3CAC01481AC75E18002EB9EA /* Release */ = { 278 | isa = XCBuildConfiguration; 279 | buildSettings = { 280 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 281 | CODE_SIGN_IDENTITY = "iPhone Developer"; 282 | INFOPLIST_FILE = BDGradientNodeDemo/Info.plist; 283 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 284 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 285 | PRODUCT_NAME = "$(TARGET_NAME)"; 286 | }; 287 | name = Release; 288 | }; 289 | /* End XCBuildConfiguration section */ 290 | 291 | /* Begin XCConfigurationList section */ 292 | 3CAC011E1AC75E17002EB9EA /* Build configuration list for PBXProject "BDGradientNodeDemo" */ = { 293 | isa = XCConfigurationList; 294 | buildConfigurations = ( 295 | 3CAC01441AC75E18002EB9EA /* Debug */, 296 | 3CAC01451AC75E18002EB9EA /* Release */, 297 | ); 298 | defaultConfigurationIsVisible = 0; 299 | defaultConfigurationName = Release; 300 | }; 301 | 3CAC01461AC75E18002EB9EA /* Build configuration list for PBXNativeTarget "BDGradientNodeDemo" */ = { 302 | isa = XCConfigurationList; 303 | buildConfigurations = ( 304 | 3CAC01471AC75E18002EB9EA /* Debug */, 305 | 3CAC01481AC75E18002EB9EA /* Release */, 306 | ); 307 | defaultConfigurationIsVisible = 0; 308 | defaultConfigurationName = Release; 309 | }; 310 | /* End XCConfigurationList section */ 311 | }; 312 | rootObject = 3CAC011B1AC75E17002EB9EA /* Project object */; 313 | } 314 | -------------------------------------------------------------------------------- /BDGradientNodeDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BDGradientNodeDemo.xcodeproj/project.xcworkspace/xcshareddata/BDGradientNodeDemo.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | 3A3A850D-95B2-4CCE-A298-B652E7CDD8CA 9 | IDESourceControlProjectName 10 | BDGradientNodeDemo 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | A6AEA5E92AEC9CC7C78C06450326F8A5555A3AA9 14 | https://github.com/braindrizzlestudio/BDGradientNode.git 15 | 16 | IDESourceControlProjectPath 17 | BDGradientNodeDemo.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | A6AEA5E92AEC9CC7C78C06450326F8A5555A3AA9 21 | ../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/braindrizzlestudio/BDGradientNode.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | A6AEA5E92AEC9CC7C78C06450326F8A5555A3AA9 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | A6AEA5E92AEC9CC7C78C06450326F8A5555A3AA9 36 | IDESourceControlWCCName 37 | BDGradientNodeDemo 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /BDGradientNodeDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // BDGradientNodeDemo 4 | // 5 | // Created by Braindrizzle Studio. 6 | // http://braindrizzlestudio.com 7 | // Copyright (c) 2015 Braindrizzle Studio. All rights reserved. 8 | // 9 | // This application is a demo of BDGradientNode. 10 | // 11 | 12 | import UIKit 13 | 14 | @UIApplicationMain 15 | class AppDelegate: UIResponder, UIApplicationDelegate { 16 | 17 | var window: UIWindow? 18 | 19 | 20 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 21 | // Override point for customization after application launch. 22 | return true 23 | } 24 | 25 | func applicationWillResignActive(application: UIApplication) { 26 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 27 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 28 | } 29 | 30 | func applicationDidEnterBackground(application: UIApplication) { 31 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | func applicationWillEnterForeground(application: UIApplication) { 36 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 37 | } 38 | 39 | func applicationDidBecomeActive(application: UIApplication) { 40 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 41 | } 42 | 43 | func applicationWillTerminate(application: UIApplication) { 44 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 45 | } 46 | 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /BDGradientNodeDemo/BDGradientNode.swift: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2015 Braindrizzle Studio 6 | http://braindrizzlestudio.com 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | 26 | Version: 1.3.1 27 | 28 | */ 29 | 30 | /* 31 | 32 | NOTE: Because of a change in Swift 2 / iOS 9, apps now use Metal rather than OpenGL by default. For this version of BDGradientNodes to work you must add PrefersOpenGL = YES to your info.plist, as described in https://forums.developer.apple.com/message/71424#71424 33 | 34 | */ 35 | 36 | 37 | import SpriteKit 38 | 39 | class BDGradientNode : SKSpriteNode { 40 | 41 | 42 | // MARK: - Properties 43 | 44 | 45 | /// The type of gradient of the instantiated node: gamut, linear, radial, or sweep. (Read Only.) 46 | private(set) var gradientType = "" 47 | 48 | 49 | // MARK: Uniforms 50 | 51 | 52 | // This array will be filled with the appropriate uniforms and passed to the shader. 53 | private var uniforms = [SKUniform]() 54 | 55 | private let u_blending = SKUniform(name: "u_blending", float: 0.5) 56 | /// (All Gradients) A number between 0.0 and 1.0. If blending == 0.0, the shader adds no color to the texture; if blending == 1.0, the passed colors are solid; otherwise, the passed colors are blended with those of the texture using the given blending weight. Default is 0.5. 57 | var blending : Float = 0.5 { 58 | didSet { 59 | u_blending.floatValue = blending 60 | } 61 | } 62 | 63 | private let u_center = SKUniform(name: "u_center", floatVector2: GLKVector2Make(0.5, 0.5)) 64 | /// (Gamut and Sweep) The center of the sweeping gradient in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the texture and (1.0, 1.0) is the top right. Default is (0.5, 0.5). 65 | var center = CGPoint(x: 0.5, y: 0.5) { 66 | didSet { 67 | u_center.floatVector2Value = GLKVector2Make(Float(center.x), Float(center.y)) 68 | } 69 | } 70 | 71 | private var u_colors = [SKUniform]() 72 | private var backupColors = [UIColor]() 73 | /// (Linear, Radial, and Sweep) The gradient's colors. This can only be updated with an array that contains the same number of colors as the initializaed gradient. 74 | var colors = [UIColor]() { 75 | didSet { 76 | if backupColors.count != colors.count { colors = backupColors } 77 | else { updateColors() } 78 | } 79 | willSet { 80 | backupColors = colors 81 | } 82 | } 83 | 84 | private let u_discardOutsideGradient = SKUniform(name: "u_discardOutsideGradient", float: 1.0) 85 | /// (Gamut, Radial, and Sweep) If true, the non-gradient areas will not be drawn. In particular: outside the radius of the gamut, sweep, or outer radial circle, and inside the inner radial circle; if false, the texture colors will be visible. 86 | var discardOutsideGradient = true { 87 | didSet { 88 | if discardOutsideGradient == true { u_discardOutsideGradient.floatValue = 1.0 } 89 | if discardOutsideGradient == false { u_discardOutsideGradient.floatValue = 0.0 } 90 | } 91 | } 92 | 93 | private let u_endPoint = SKUniform(name: "u_endPoint", floatVector2: GLKVector2Make(0.5, 0.0)) 94 | /// (Linear) The point from which the gradient will start. If this is not nil then startPoint must also have a value. If it is nil then it will default to the bottom center of the texture (0.5, 1.0). 95 | var endPoint = CGPoint(x: 0.5, y: 1.0) { 96 | didSet { 97 | u_endPoint.floatVector2Value = GLKVector2Make(Float(endPoint.x), Float(endPoint.y)) 98 | updateLocations() 99 | } 100 | } 101 | 102 | private let u_firstCenter = SKUniform(name: "u_firstCenter", floatVector2: GLKVector2Make(0.5, 0.5)) 103 | /// (Radial) The center of the first circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the texture and (1.0, 1.0) is the top right. Default is (0.5, 0.5). 104 | var firstCenter = CGPoint(x: 0.5, y: 0.5) { 105 | didSet { 106 | u_firstCenter.floatVector2Value = GLKVector2Make(Float(firstCenter.x), Float(firstCenter.y)) 107 | } 108 | } 109 | 110 | private let u_firstRadius = SKUniform(name: "u_firstRadius", float: 0.1) 111 | /// (Radial) The radius of the first circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the texture and (1.0, 1.0) is the top right. Default is 0.1. 112 | var firstRadius : Float = 0.1 { 113 | didSet { 114 | u_firstRadius.floatValue = firstRadius 115 | } 116 | } 117 | 118 | private let u_keepTextureShape = SKUniform(name: "u_keepTextureShape", float: 1.0) 119 | /// (All Gradients) If true, the resulting node will have the shape of the given texture by only drawing where the texture alpha channel is non-zero; if false it will fill the given size. 120 | var keepTextureShape = true { 121 | didSet { 122 | if keepTextureShape == true { u_keepTextureShape.floatValue = 1.0 } 123 | if keepTextureShape == false { u_keepTextureShape.floatValue = 0.0 } 124 | } 125 | } 126 | 127 | private var u_locations = [SKUniform]() 128 | /// (Linear, Radial, and Sweep) An array of monotonically increasing color locations, where 0.0 is the start and 1.0 is the end; neither 0.0 nor 1.0 should be included in this array. The first color in colors is automatically at 0.0; the last is automatically at 1.0; the rest are at the locations in this array. If the number of locations is not appropriate for the initialized gradient, or the values are out of range, locations will be set to nil and the colors will be evenly spread out. Linear and Radial must have (number of colors - 2) locations; Sweep must have (number of colors - 1) locations. 129 | var locations : [Float]? { 130 | didSet { 131 | updateLocations() 132 | } 133 | } 134 | 135 | private let u_radius = SKUniform(name: "u_radius", float: 1.0) 136 | /// (Gamut and Sweep) The radius of the gradient circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the sprite and (1.0, 1.0) is the top right. Default is 1.0. 137 | var radius : Float = 0.5 { 138 | didSet { 139 | u_radius.floatValue = radius 140 | } 141 | } 142 | 143 | private let u_secondCenter = SKUniform(name: "u_secondCenter", floatVector2: GLKVector2Make(0.5, 0.5)) 144 | /// (Radial Gradient) The center of the second circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the sprite and (1.0, 1.0) is the top right. Default is (0.5, 0.5). 145 | var secondCenter = CGPoint(x: 0.5, y: 0.5) { 146 | didSet { 147 | u_secondCenter.floatVector2Value = GLKVector2Make(Float(secondCenter.x), Float(secondCenter.y)) 148 | } 149 | } 150 | 151 | private let u_secondRadius = SKUniform(name: "u_secondRadius", float: 0.0) 152 | /// (Radial) The radius of the second circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the sprite and (1.0, 1.0) is the top right. Default is 0.5. 153 | var secondRadius : Float = 0.5 { 154 | didSet { 155 | u_secondRadius.floatValue = secondRadius 156 | } 157 | } 158 | 159 | private let u_startAngle = SKUniform(name: "u_startAngle", float: 0.0) 160 | /// (Gamut and Sweep) The angle at which the first color of the gradient will start (red for gamut) in radians between 0 and 2Pi, where 0 is to the right along the x axis and the colors proceed counter-clockwise. Default is 0. 161 | var startAngle : Float = 0.0 { 162 | didSet { 163 | u_startAngle.floatValue = startAngle / Float(2 * M_PI) % 1.0 164 | } 165 | } 166 | 167 | private let u_startPoint = SKUniform(name: "u_startPoint", floatVector2: GLKVector2Make(0.5, 0.0)) 168 | /// (Linear) The point from which the gradient will start. If this is not nil then endPoint must also have a value. If it is nil then it will default to the bottom center of the texture (0.5, 0.0). 169 | var startPoint = CGPoint(x: 0.5, y: 0.0) { 170 | didSet { 171 | u_startPoint.floatVector2Value = GLKVector2Make(Float(startPoint.x), Float(startPoint.y)) 172 | updateLocations() 173 | } 174 | } 175 | 176 | 177 | 178 | // MARK: - Initialization 179 | 180 | 181 | /// Use the appropriate convenience initializer to get your BDGradientNode; this isn't the initializer you're looking for. 182 | internal override init (texture: SKTexture?, color: UIColor, size: CGSize) { 183 | 184 | super.init(texture: texture, color: color, size: size) 185 | } 186 | 187 | internal required init?(coder aDecoder: NSCoder) { 188 | fatalError("init(coder:) has not been implemented") 189 | } 190 | 191 | 192 | 193 | // MARK: Gamut Gradients 194 | 195 | 196 | /** 197 | 198 | An SKSpriteNode with the gamut of the spectrum. 199 | 200 | - parameter texture: The texture to be shaded. 201 | 202 | - parameter center: The center of the sweeping gradient in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the texture and (1.0, 1.0) is the top right. Default is (0.5, 0.5). 203 | 204 | - parameter radius: The radius of the gradient circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the sprite and (1.0, 1.0) is the top right. Default is 0.5. 205 | 206 | - parameter startAngle: The angle at which the red in the gradient will start in radians between 0 and 2Pi, where 0 is to the right along the x axis and the colors proceed counter-clockwise. Default is 0. 207 | 208 | - parameter blending: A number between 0.0 and 1.0. If blending == 0.0, the shader adds no color to the texture; if blending == 1.0, the passed colors are solid; otherwise, the passed colors are blended with those of the texture using the given blending weight. Default is 0.5. 209 | 210 | - parameter discardOutsideGradient: If true, the non-gradient areas will not be drawn. In particular: outside the radius of the gamut, sweep, or outer radial circle, and inside the inner radial circle; if false, the texture colors will be visible. 211 | 212 | - parameter keepTextureShape: If true, the resulting node will have the shape of the given texture by only drawing where the texture alpha channel is non-zero; if false it will fill the given size. Note that this value will be ignored if blending is true. 213 | 214 | - parameter size: The desired size of the node. 215 | 216 | */ 217 | convenience init (gamutGradientWithTexture texture: SKTexture, center: CGPoint?, radius: Float?, startAngle: Float?, blending: Float, discardOutsideGradient: Bool, keepTextureShape: Bool, size: CGSize) { 218 | 219 | self.init(texture: texture, color: UIColor.clearColor(), size: size) 220 | 221 | gradientType = "gamut" 222 | 223 | // Setup Shader 224 | shader = gamutGradientShader(center: center, radius: radius, startAngle: startAngle, blending: blending, discardOutsideGradient: discardOutsideGradient, keepTextureShape: keepTextureShape) 225 | shader?.uniforms = uniforms 226 | } 227 | 228 | 229 | 230 | // MARK: Linear Gradient 231 | 232 | 233 | /** 234 | 235 | A SKSpriteNode with a linear gradient from bottom to top in the given texture. 236 | 237 | - parameter texture: The texture to be shaded. 238 | 239 | - parameter colors: An array of two or more colors. 240 | 241 | - parameter locations: An array of monotonically increasing color locations, where 0.0 is the start and 1.0 is the end; neither 0.0 nor 1.0 should be included in this array. The first color in colors is automatically at 0.0; the last is automatically at 1.0; the rest are at the locations in this array. For that reason: locations must contain colors.count - 2 Floats. If the array is nil or the number of locations is different than required: colors will be spread out evenly. 242 | 243 | - parameter startPoint: The point from which the gradient will start. If this is not nil then endPoint must also have a value. If it is nil then it will default to the bottom center of the texture (0.5, 0.0). 244 | 245 | - parameter endPoint: The point from which the gradient will start. If this is not nil then startPoint must also have a value. If it is nil then it will default to the bottom center of the texture (0.5, 1.0). 246 | 247 | - parameter blending: A number between 0.0 and 1.0. If blending == 0.0, the shader adds no color to the texture; if blending == 1.0, the passed colors are solid; otherwise, the passed colors are blended with those of the texture using the given blending weight. Default is 0.5. 248 | 249 | - parameter keepTextureShape: If true, the resulting node will have the shape of the given texture by only drawing where the texture alpha channel is non-zero; if false it will fill the given size. Note that this value will be ignored if blending is true. 250 | 251 | - parameter size: The desired size of the node. 252 | 253 | */ 254 | convenience init (linearGradientWithTexture texture: SKTexture, colors: [UIColor], locations: [Float]?, startPoint: CGPoint?, endPoint: CGPoint?, blending: Float, keepTextureShape: Bool, size: CGSize) { 255 | 256 | self.init(texture: texture, color: UIColor.clearColor(), size: size) 257 | 258 | gradientType = "linear" 259 | self.colors = colors 260 | 261 | shader = linearGradientShader(colors: colors, locations: locations, startPoint: startPoint, endPoint: endPoint, blending: blending, keepTextureShape: keepTextureShape) 262 | shader?.uniforms = uniforms 263 | } 264 | 265 | 266 | 267 | // MARK: Radial Gradient 268 | 269 | 270 | /** 271 | 272 | An SKSpriteNode with a radial gradient between the two specified circles. At least one circle must have a radius specified. Note that the coordinate system for the firstCenter and secondCenter has its anchor point (0.0, 0.0) at the bottom left of the texture; (texture.size().width, texture.size().height) is at the top right. 273 | 274 | - parameter texture: The texture to be shaded. 275 | 276 | - parameter colors: An array of two or more colors. 277 | 278 | - parameter locations: An array of monotonically increasing color locations, where 0.0 is the start and 1.0 is the end; neither 0.0 nor 1.0 should be included in this array. The first color in colors is automatically at 0.0; the last is automatically at 1.0; the rest are at the locations in this array. For that reason: locations must contain colors.count - 2 Floats. If the array is nil or the number of locations is different than required: colors will be spread out evenly. 279 | 280 | - parameter firstCenter: The center of the first circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the texture and (1.0, 1.0) is the top right. Default is (0.5, 0.5). 281 | 282 | - parameter firstRadius: The radius of the first circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the texture and (1.0, 1.0) is the top right. Default is 0.1. 283 | 284 | - parameter secondCenter: The center of the second circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the sprite and (1.0, 1.0) is the top right. Default is (0.5, 0.5). 285 | 286 | - parameter secondRadius: The radius of the second circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the sprite and (1.0, 1.0) is the top right. Default is 0.5. 287 | 288 | - parameter blending: A number between 0.0 and 1.0. If blending == 0.0, the shader adds no color to the texture; if blending == 1.0, the passed colors are solid; otherwise, the passed colors are blended with those of the texture using the given blending weight. Default is 0.5. 289 | 290 | - parameter discardOutsideGradient: If true, the non-gradient areas will not be drawn. In particular: outside the radius of the gamut, sweep, or outer radial circle, and inside the inner radial circle; if false, the texture colors will be visible. 291 | 292 | - parameter keepTextureShape: If true, the resulting node will have the shape of the given texture by only drawing where the texture alpha channel is non-zero; if false it will fill the given size. Note that this value will be ignored if blending is true. 293 | 294 | - parameter size: The desired size of the node. Note: if this size is not the size of the passed texture then the "circles" will be ellipses. 295 | 296 | */ 297 | convenience init (radialGradientWithTexture texture: SKTexture, colors: [UIColor], locations: [Float]?, firstCenter: CGPoint?, firstRadius: Float?, secondCenter: CGPoint?, secondRadius: Float?, blending: Float, discardOutsideGradient: Bool, keepTextureShape: Bool, size: CGSize) { 298 | 299 | self.init(texture: texture, color: UIColor.clearColor(), size: size) 300 | 301 | gradientType = "radial" 302 | self.colors = colors 303 | 304 | shader = radialGradientShader(colors: colors, locations: locations, firstCenter: firstCenter, firstRadius: firstRadius, secondCenter: secondCenter, secondRadius: secondRadius, blending: blending, discardOutsideGradient: discardOutsideGradient, keepTextureShape: keepTextureShape) 305 | shader?.uniforms = uniforms 306 | } 307 | 308 | 309 | 310 | // MARK: Sweep Gradient 311 | 312 | 313 | /** 314 | 315 | An SKSpriteNode shaded wtih a sweeping gradient (i.e. a gradient determined by angle). 316 | 317 | - parameter texture: The texture to be shaded. 318 | 319 | - parameter colors: An array of two or more colors. 320 | 321 | - parameter locations: An array of monotonically increasing color locations, where 0.0 is the start and 1.0 is the end; 0.0 = 1.0; neither 0.0 nor 1.0 should be included in this array. The first color in colors is automatically at startAngle; the rest are at these locations. For that reason: locations must contain colors.count - 1 Floats. If the array is nil or the number of locations is different than required: colors will be spread out evenly. 322 | 323 | - parameter center: The center of the sweeping gradient in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the texture and (1.0, 1.0) is the top right. Default is (0.5, 0.5). 324 | 325 | - parameter radius: The radius of the gradient circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the sprite and (1.0, 1.0) is the top right. Default is 0.5. 326 | 327 | - parameter startAngle: The angle at which the first color of the gradient will start in radians between 0 and 2Pi, where 0 is to the right along the x axis and the colors proceed counter-clockwise. Default is 0. 328 | 329 | - parameter blending: A number between 0.0 and 1.0. If blending == 0.0, the shader adds no color to the texture; if blending == 1.0, the passed colors are solid; otherwise, the passed colors are blended with those of the texture using the given blending weight. Default is 0.5. 330 | 331 | - parameter discardOutsideGradient: If true, the non-gradient areas will not be drawn. In particular: outside the radius of the gamut, sweep, or outer radial circle, and inside the inner radial circle; if false, the texture colors will be visible. 332 | 333 | - parameter keepTextureShape: If true, the resulting node will have the shape of the given texture by only drawing where the texture alpha channel is non-zero; if false it will fill the given size. Note that this value will be ignored if blending is true. 334 | 335 | - parameter size: The desired size of the node. 336 | 337 | */ 338 | convenience init (sweepGradientWithTexture texture: SKTexture, colors: [UIColor], locations: [Float]?, center: CGPoint?, radius: Float?, startAngle: Float?, blending: Float, discardOutsideGradient: Bool, keepTextureShape: Bool, size: CGSize) { 339 | 340 | self.init(texture: texture, color: UIColor.clearColor(), size: size) 341 | 342 | gradientType = "sweep" 343 | self.colors = colors 344 | 345 | shader = sweepGradientShader(colors: colors, locations: locations, center: center, radius: radius, startAngle: startAngle, blending: blending, discardOutsideGradient: discardOutsideGradient, keepTextureShape: keepTextureShape) 346 | shader?.uniforms = uniforms 347 | } 348 | 349 | 350 | 351 | // MARK: - Shaders 352 | 353 | 354 | // MARK: Gamut Gradient 355 | 356 | 357 | /** 358 | 359 | Returns a shader that produces a circle with the gamut of color. 360 | 361 | - parameter center: The center of the sweeping gradient in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the texture and (1.0, 1.0) is the top right. Default is (0.5, 0.5). 362 | 363 | - parameter radius: The radius of the gradient circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the sprite and (1.0, 1.0) is the top right. Default is 0.5. 364 | 365 | - parameter startAngle: The angle at which the red in the gradient will start in radians between 0 and 2Pi, where 0 is to the right along the x axis and the colors proceed counter-clockwise. Default is 0. 366 | 367 | - parameter blending: A number between 0.0 and 1.0. If blending == 0.0, the shader adds no color to the texture; if blending == 1.0, the passed colors are solid; otherwise, the passed colors are blended with those of the texture using the given blending weight. Default is 0.5. 368 | 369 | - parameter discardOutsideGradient: If true, the non-gradient areas will not be drawn. In particular: outside the radius of the gamut, sweep, or outer radial circle, and inside the inner radial circle; if false, the texture colors will be visible. 370 | 371 | - parameter keepTextureShape: If true, the resulting node will have the shape of the given texture by only drawing where the texture alpha channel is non-zero; if false it will fill the given size. Note that this value will be ignored if blending is true. 372 | 373 | - returns: A shader that produces a circle with the gamut of color. This shader makes use of the hsv2rgb method from http://stackoverflow.com/a/17897228/605869 374 | 375 | */ 376 | private func gamutGradientShader(center center: CGPoint?, radius: Float?, startAngle: Float?, blending: Float, discardOutsideGradient: Bool, keepTextureShape: Bool) -> SKShader { 377 | 378 | 379 | // Sanitization 380 | 381 | // blending 382 | self.blending = min(max(blending, 0.0), 1.0) 383 | uniforms.append(u_blending) 384 | 385 | // center 386 | if center != nil { 387 | self.center = CGPoint(x: min(max(center!.x, 0.0), 1.0), y: min(max(center!.y, 0.0), 1.0)) 388 | } 389 | uniforms.append(u_center) 390 | 391 | // discardOutsideGradient 392 | self.discardOutsideGradient = discardOutsideGradient 393 | uniforms.append(u_discardOutsideGradient) 394 | 395 | // keepTextureShape 396 | self.keepTextureShape = keepTextureShape 397 | uniforms.append(u_keepTextureShape) 398 | 399 | // radius 400 | if radius != nil { 401 | self.radius = min(max(radius!, 0.0), 1.0) 402 | } 403 | uniforms.append(u_radius) 404 | 405 | // startAngle 406 | if startAngle != nil { 407 | self.startAngle = startAngle! 408 | } 409 | uniforms.append(u_startAngle) 410 | 411 | 412 | // Shader Creation 413 | 414 | let gamutGradientShader = "highp float; vec3 hsv2rgb(vec3 c) { vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); } float M_PI = 3.1415926535897932384626433832795; vec4 color; void main() { vec2 coord = v_tex_coord.xy - u_center; if (u_keepTextureShape == 1.0) { vec4 textureColor = texture2D(u_texture, v_tex_coord); if (textureColor.w == 0.0) { discard; } } if (distance(coord, vec2(0.0, 0.0)) > u_radius) { if (u_discardOutsideGradient == 1.0) { discard; } else { gl_FragColor = texture2D(u_texture, v_tex_coord); return; } } if (u_blending == 0.0) { gl_FragColor = texture2D(u_texture, v_tex_coord); return; } float angle = atan(coord.y, coord.x); angle = mod(angle / (2.0 * M_PI) - u_startAngle, 1.0); float distanceFromCenter = length(coord); color = vec4(hsv2rgb(vec3(angle, distanceFromCenter, 1.0)), 1.0); if (u_blending < 1.0) { color = mix(color, texture2D(u_texture, v_tex_coord), 1.0 - u_blending); } gl_FragColor = color; }" 415 | 416 | 417 | return SKShader(source: gamutGradientShader) 418 | } 419 | 420 | 421 | 422 | // MARK: Linear Gradient 423 | 424 | 425 | /** 426 | 427 | Returns an SKShader that creates a linear gradient in the node's texture, from bottom to top, of the colors given. 428 | 429 | - parameter colors: An array of two or more colors. 430 | 431 | - parameter locations: An array of monotonically increasing color locations, where 0.0 is the start and 1.0 is the end; neither 0.0 nor 1.0 should be included in this array. The first color in colors is automatically at 0.0; the last is automatically at 1.0; the rest are at the locations in this array. For that reason: locations must contain colors.count - 2 Floats. If the array is nil or the number of locations is different than required: colors will be spread out evenly. 432 | 433 | - parameter startPoint: The point from which the gradient will start. If this is not nil then endPoint must also have a value. If it is nil then it will default to the bottom center of the texture (0.5, 0.0). 434 | 435 | - parameter endPoint: The point from which the gradient will start. If this is not nil then startPoint must also have a value. If it is nil then it will default to the bottom center of the texture (0.5, 1.0). 436 | 437 | - parameter blending: A number between 0.0 and 1.0. If blending == 0.0, the shader adds no color to the texture; if blending == 1.0, the passed colors are solid; otherwise, the passed colors are blended with those of the texture using the given blending weight. Default is 0.5. 438 | 439 | - parameter keepTextureShape: If true, the resulting node will have the shape of the given texture by only drawing where the texture alpha channel is non-zero; if false it will fill the given size. Note that this value will be ignored if blending is true. 440 | 441 | - returns: An SKShader that will produce a linear gradient. Note: the current implementation simply uses the GLSL mix function, which is a linear interpolation. For colors near to each other on the spectrum the results are fine; for very different colors the results can be pretty ugly. If you need a gradient between very different colors then you can simply add more colors in between to narrow the relative distances. 442 | 443 | */ 444 | private func linearGradientShader(colors colors: [UIColor], locations: [Float]?, startPoint: CGPoint?, endPoint: CGPoint?, blending: Float, keepTextureShape: Bool) -> SKShader { 445 | 446 | 447 | // Sanitization and Uniforms 448 | 449 | // If there aren't at least two colors: return an empty shader. 450 | if colors.count < 2 { return SKShader() } 451 | 452 | 453 | // blending 454 | self.blending = min(max(blending, 0.0), 1.0) 455 | uniforms.append(u_blending) 456 | 457 | 458 | // Colors in CGFloat-array form 459 | var colorFloats = [[Float]]() 460 | for color in colors { 461 | colorFloats.append(colorToRGBAComponentFloatArray(color)) 462 | } 463 | 464 | 465 | // Color uniforms 466 | for (index, color) in colorFloats.enumerate() { 467 | 468 | let vector4 = GLKVector4Make(color[0], color[1], color[2], color[3]) 469 | let colorUniform = SKUniform(name: "u_color\(index)", floatVector4: vector4) 470 | u_colors.append(colorUniform) 471 | uniforms.append(colorUniform) 472 | } 473 | 474 | 475 | // keepTextureShape 476 | self.keepTextureShape = keepTextureShape 477 | uniforms.append(u_keepTextureShape) 478 | 479 | 480 | // Locations 481 | var locationArray = [Float]() 482 | if locations != nil && locations!.count == colors.count - 2 { 483 | 484 | var lastValue : Float = 0.000001 485 | var newValue : Float = 0.0 486 | for location in locations! { 487 | 488 | newValue = min(max(location, lastValue), 1.0) 489 | locationArray.append(newValue) 490 | lastValue = newValue + 0.000001 491 | } 492 | 493 | } else { 494 | 495 | for var i = 0; i < colors.count - 2; i++ { 496 | 497 | let location = (Float(i) + 1.0) / (Float(colors.count) - 1.0) 498 | locationArray.append(location) 499 | } 500 | } 501 | self.locations = locationArray 502 | 503 | 504 | // Start and end points 505 | var start = CGPoint(x: 0.5, y: 0.0) 506 | var end = CGPoint(x: 0.5, y: 1.0) 507 | if startPoint != nil && endPoint != nil { 508 | 509 | start = CGPoint(x: min(max(startPoint!.x, 0.0), 1.0), y: min(max(startPoint!.y, 0.0), 1.0)) 510 | end = CGPoint(x: min(max(endPoint!.x, 0.0), 1.0), y: min(max(endPoint!.y, 0.0), 1.0)) 511 | } 512 | self.startPoint = start 513 | uniforms.append(u_startPoint) 514 | self.endPoint = end 515 | uniforms.append(u_endPoint) 516 | 517 | 518 | // Location uniforms 519 | let vector = CGPoint(x: self.endPoint.x - self.startPoint.x, y: self.endPoint.y - self.startPoint.y) 520 | for (index, location) in locationArray.enumerate() { 521 | 522 | let vector2 = GLKVector2Make(Float(self.startPoint.x) + location * Float(vector.x), Float(self.startPoint.y) + location * Float(vector.y)) 523 | let locationUniform = SKUniform(name: "u_stop\(index + 1)", floatVector2: vector2) 524 | u_locations.append(locationUniform) 525 | uniforms.append(locationUniform) 526 | } 527 | 528 | 529 | // Shader Creation 530 | 531 | 532 | var linearGradientShader = "highp float; vec4 color; void main (void) { vec2 coord = v_tex_coord; if (u_keepTextureShape == 1.0) { vec4 textureColor = texture2D(u_texture, coord); if (textureColor.w == 0.0) { discard; } } if (u_blending == 0.0) { gl_FragColor = texture2D(u_texture, v_tex_coord); return; } vec2 vector = vec2(u_endPoint.x - u_startPoint.x, u_endPoint.y - u_startPoint.y); if (u_blending < 1.0) { color = mix(color, texture2D(u_texture, v_tex_coord), 1.0 - u_blending); } gl_FragColor = color; }" 533 | 534 | var stringRange : NSRange 535 | var string = "" 536 | 537 | 538 | // Add the gradients 539 | stringRange = (linearGradientShader as NSString).rangeOfString("vec2 vector = vec2(u_endPoint.x - u_startPoint.x, u_endPoint.y - u_startPoint.y); ") 540 | if colors.count == 2 { 541 | 542 | string = "color = mix(u_color0, u_color1, smoothstep(dot(u_startPoint, vector), dot(u_endPoint, vector), dot(coord, vector))); " 543 | linearGradientShader = linearGradientShader.insert(string: string, atIndex: stringRange.location + stringRange.length) 544 | 545 | } else { 546 | 547 | for var i = colors.count - 1; i > 0 ; i-- { 548 | 549 | if i == 1 { 550 | 551 | string = "color = mix(u_color0, u_color1, smoothstep(dot(u_startPoint, vector), dot(u_stop\(i), vector), dot(coord, vector))); " 552 | 553 | } else if i == colors.count - 1 { 554 | 555 | string = "color = mix(color, u_color\(i), smoothstep(dot(u_stop\(i - 1), vector), dot(u_endPoint, vector), dot(coord, vector))); " 556 | 557 | } else { 558 | 559 | string = "color = mix(color, u_color\(i), smoothstep(dot(u_stop\(i - 1), vector), dot(u_stop\(i), vector), dot(coord, vector))); " 560 | } 561 | 562 | linearGradientShader = linearGradientShader.insert(string: string, atIndex: stringRange.location + stringRange.length) 563 | } 564 | } 565 | 566 | 567 | return SKShader(source: linearGradientShader) 568 | } 569 | 570 | 571 | 572 | // MARK: Radial Gradient 573 | 574 | 575 | /** 576 | 577 | An SKShader for a radial gradient between two specified circles. At least one circle must have a radius specified. 578 | 579 | - parameter colors: An array of two or more colors. 580 | 581 | - parameter locations: An array of monotonically increasing color locations, where 0.0 is the start and 1.0 is the end; neither 0.0 nor 1.0 should be included in this array. The first color in colors is automatically at 0.0; the last is automatically at 1.0; the rest are at the locations in this array. For that reason: locations must contain colors.count - 2 Floats. If the array is nil or the number of locations is different than required: colors will be spread out evenly. 582 | 583 | - parameter firstCenter: The center of the first circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the texture and (1.0, 1.0) is the top right. Default is (0.5, 0.5). 584 | 585 | - parameter firstRadius: The radius of the first circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the texture and (1.0, 1.0) is the top right. Default is 0.1. 586 | 587 | - parameter secondCenter: The center of the second circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the sprite and (1.0, 1.0) is the top right. Default is (0.5, 0.5). 588 | 589 | - parameter secondRadius: The radius of the second circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the sprite and (1.0, 1.0) is the top right. Default is 0.5. 590 | 591 | - parameter blending: A number between 0.0 and 1.0. If blending == 0.0, the shader adds no color to the texture; if blending == 1.0, the passed colors are solid; otherwise, the passed colors are blended with those of the texture using the given blending weight. Default is 0.5. 592 | 593 | - parameter discardOutsideGradient: If true, the non-gradient areas will not be drawn. In particular: outside the radius of the gamut, sweep, or outer radial circle, and inside the inner radial circle; if false, the texture colors will be visible. 594 | 595 | - parameter keepTextureShape: If true, the resulting node will have the shape of the given texture by only drawing where the texture alpha channel is non-zero; if false it will fill the given size. Note that this value will be ignored if blending is true. 596 | 597 | - returns: An SKShader that produced a radial gradient with colors begining at the first circle and ending at the second. 598 | 599 | */ 600 | private func radialGradientShader (colors colors: [UIColor], locations: [Float]?, firstCenter: CGPoint?, firstRadius: Float?, secondCenter: CGPoint?, secondRadius: Float?, blending: Float, discardOutsideGradient: Bool, keepTextureShape: Bool) -> SKShader { 601 | 602 | 603 | // Sanitizaton 604 | 605 | // If there aren't at least two colors: return an empty shader. 606 | if colors.count < 2 { return SKShader() } 607 | 608 | 609 | // blending 610 | self.blending = min(max(blending, 0.0), 1.0) 611 | uniforms.append(u_blending) 612 | 613 | 614 | // discardOutsideGradient 615 | self.discardOutsideGradient = discardOutsideGradient 616 | uniforms.append(u_discardOutsideGradient) 617 | 618 | 619 | // keepTextureShape 620 | self.keepTextureShape = keepTextureShape 621 | uniforms.append(u_keepTextureShape) 622 | 623 | 624 | // Centers 625 | 626 | var center0 = CGPoint(x: 0.5, y: 0.5) 627 | if firstCenter != nil { 628 | center0.x = min(max(firstCenter!.x, 0.0), 1.0) 629 | center0.y = min(max(firstCenter!.y, 0.0), 1.0) 630 | } 631 | var center1 = CGPoint(x: 0.5, y: 0.5) 632 | if secondCenter != nil { 633 | center1.x = min(max(secondCenter!.x, 0.0), 1.0) 634 | center1.y = min(max(secondCenter!.y, 0.0), 1.0) 635 | } 636 | self.firstCenter = center0 637 | uniforms.append(u_firstCenter) 638 | self.secondCenter = center1 639 | uniforms.append(u_secondCenter) 640 | 641 | 642 | // Colors in CGFloat-array form 643 | var colorFloats = [[Float]]() 644 | for color in colors { 645 | colorFloats.insert(colorToRGBAComponentFloatArray(color), atIndex: 0) 646 | } 647 | 648 | 649 | // Color uniforms 650 | for (index, color) in colorFloats.enumerate() { 651 | 652 | let vector4 = GLKVector4Make(color[0], color[1], color[2], color[3]) 653 | let colorUniform = SKUniform(name: "u_color\(index)", floatVector4: vector4) 654 | u_colors.append(colorUniform) 655 | uniforms.append(colorUniform) 656 | } 657 | 658 | 659 | // Locations 660 | // colors.count - 2 is the expected number of locations. If the number is different: spread out evenly. 661 | var locationArray = [Float]() 662 | if locations != nil && locations!.count == colors.count - 2 { 663 | 664 | var lastValue : Float = 0.000001 665 | var newValue : Float = 0.0 666 | for location in locations! { 667 | 668 | newValue = min(max(location, lastValue), 1.0) 669 | locationArray.append(newValue) 670 | lastValue = newValue + 0.000001 671 | } 672 | 673 | } else { 674 | 675 | for var i = 1; i < colors.count - 1; i++ { 676 | 677 | let location = Float(i) / Float(colors.count - 1) 678 | locationArray.append(location) 679 | } 680 | } 681 | self.locations = locationArray 682 | 683 | 684 | // Location uniforms 685 | for (index, location) in locationArray.enumerate() { 686 | 687 | let locationUniform = SKUniform(name: "u_location\(index + 1)", float: location) 688 | u_locations.append(locationUniform) 689 | uniforms.append(locationUniform) 690 | } 691 | 692 | 693 | // Radii 694 | if firstRadius != nil { self.firstRadius = min(max(firstRadius!, 0.0), 1.0) } 695 | uniforms.append(u_firstRadius) 696 | if secondRadius != nil { self.secondRadius = min(max(secondRadius!, 0.0), 1.0) } 697 | uniforms.append(u_secondRadius) 698 | 699 | 700 | // Shader Creation 701 | 702 | var radialGradientShader = "highp float; vec4 color; float center0X = u_firstCenter.x; float center0Y = u_firstCenter.y; float center1X = u_secondCenter.x; float center1Y = u_secondCenter.y; float location0 = 0.0; void main() { if (u_keepTextureShape == 1.0) { vec4 textureColor = texture2D(u_texture, v_tex_coord); if (textureColor.w == 0.0) { discard; } } float coordX = v_tex_coord.x; float coordY = v_tex_coord.y; float root = sqrt(u_secondRadius * u_secondRadius * ((center0X - coordX) * (center0X - coordX) + (center0Y - coordY) * (center0Y - coordY)) - 2.0 * u_firstRadius * u_secondRadius * ((center0X - coordX) * (center1X - coordX) + (center0Y - coordY) * (center1Y - coordY)) + u_firstRadius * u_firstRadius * ((center1X - coordX) * (center1X - coordX) + (center1Y - coordY) * (center1Y - coordY)) - ((center1X * center0Y - coordX * center0Y - center0X * center1Y + coordX * center1Y + center0X * coordY - center1X * coordY) * (center1X * center0Y - coordX * center0Y - center0X * center1Y + coordX * center1Y + center0X * coordY - center1X * coordY))); float t; if(distance(v_tex_coord.xy, vec2(center1X, center1Y)) > u_secondRadius) { t = (-u_secondRadius * (u_firstRadius - u_secondRadius) + (center0X - center1X) * (center1X - coordX) + (center0Y - center1Y) * (center1Y - coordY) + root) / ((u_firstRadius - u_secondRadius) * (u_firstRadius - u_secondRadius) - (center0X - center1X) * (center0X - center1X) - (center0Y - center1Y) * (center0Y - center1Y)); } else { t = (-u_secondRadius * (u_firstRadius - u_secondRadius) + (center0X - center1X) * (center1X - coordX) + (center0Y - center1Y) * (center1Y - coordY) - root) / ((u_firstRadius - u_secondRadius) * (u_firstRadius - u_secondRadius) - (center0X - center1X) * (center0X - center1X) - (center0Y - center1Y) * (center0Y - center1Y)); } if (t > 0.0 && t <= 1.0) { if (u_blending == 0.0) { gl_FragColor = texture2D(u_texture, v_tex_coord); return; } if (u_blending < 1.0) { color = mix(color, texture2D(u_texture, v_tex_coord), 1.0 - u_blending); } gl_FragColor = color; } else { if (u_discardOutsideGradient == 1.0) { discard; } else { gl_FragColor = texture2D(u_texture, v_tex_coord); return; } } }" 703 | 704 | 705 | var stringRange : NSRange 706 | var string = "" 707 | 708 | 709 | // Add the gradients 710 | if colors.count == 2 { 711 | 712 | stringRange = (radialGradientShader as NSString).rangeOfString("if (u_blending == 0.0) { gl_FragColor = texture2D(u_texture, v_tex_coord); return; } ") 713 | string = "color = mix(u_color0, u_color1, smoothstep(location0, location1, t)); " 714 | radialGradientShader = radialGradientShader.insert(string: string, atIndex: stringRange.location + stringRange.length) 715 | 716 | } else { 717 | 718 | stringRange = (radialGradientShader as NSString).rangeOfString("if (u_blending == 0.0) { gl_FragColor = texture2D(u_texture, v_tex_coord); return; } ") 719 | 720 | for var i = colors.count - 1; i > 1 ; i-- { 721 | 722 | if i == colors.count - 1 { 723 | 724 | string = "color = mix(color, u_color\(i), smoothstep(u_location\(i - 1), location\(i), t)); " 725 | radialGradientShader = radialGradientShader.insert(string: string, atIndex: stringRange.location + stringRange.length) 726 | 727 | } else { 728 | 729 | string = "color = mix(color, u_color\(i), smoothstep(u_location\(i - 1), u_location\(i), t)); " 730 | radialGradientShader = radialGradientShader.insert(string: string, atIndex: stringRange.location + stringRange.length) 731 | } 732 | } 733 | 734 | string = "vec4 color = mix(u_color0, u_color1, smoothstep(location0, u_location1, t)); " 735 | radialGradientShader = radialGradientShader.insert(string: string, atIndex: stringRange.location + stringRange.length) 736 | } 737 | 738 | 739 | // Add the final locations 740 | stringRange = (radialGradientShader as NSString).rangeOfString("float location0 = 0.0; ") 741 | string = "float location\(colors.count - 1) = 1.0; " 742 | radialGradientShader = radialGradientShader.insert(string: string, atIndex: stringRange.location + stringRange.length) 743 | 744 | return SKShader(source: radialGradientShader) 745 | } 746 | 747 | 748 | 749 | // MARK: Sweep Gradient 750 | 751 | 752 | /** 753 | 754 | Returns an SKShader that creates a sweep gradient, starting from the left along the x axis and moving counter-clockwise, of the colors given in the texture of the node. 755 | 756 | - parameter colors: An array of two or more colors. 757 | 758 | - parameter locations: An array of monotonically increasing color locations, where 0.0 is the start and 1.0 is the end; 0.0 = 1.0; neither 0.0 nor 1.0 should be included in this array. The first color in colors is automatically at startAngle; the rest are at these locations. For that reason: locations must contain colors.count - 1 Floats. If the array is nil or the number of locations is different than required: colors will be spread out evenly. 759 | 760 | - parameter center: The center of the sweeping gradient in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the texture and (1.0, 1.0) is the top right. Default is (0.5, 0.5). 761 | 762 | - parameter radius: The radius of the gradient circle in the coordinate system of (0.0, 0.0) to (1.0, 1.0), where (0.0, 0.0) is the bottom left corner of the sprite and (1.0, 1.0) is the top right. Default is 0.5. 763 | 764 | - parameter startAngle: The angle at which the first color of the gradient will start in radians between 0 and 2Pi, where 0 is to the right along the x axis and the colors proceed counter-clockwise. Default is 0. 765 | 766 | - parameter blending: A number between 0.0 and 1.0. If blending == 0.0, the shader adds no color to the texture; if blending == 1.0, the passed colors are solid; otherwise, the passed colors are blended with those of the texture using the given blending weight. Default is 0.5. 767 | 768 | - parameter discardOutsideGradient: If true, the non-gradient areas will not be drawn. In particular: outside the radius of the gamut, sweep, or outer radial circle, and inside the inner radial circle; if false, the texture colors will be visible. 769 | 770 | - parameter keepTextureShape: If true, the resulting node will have the shape of the given texture by only drawing where the texture alpha channel is non-zero; if false it will fill the given size. Note that this value will be ignored if blending is true. 771 | 772 | - returns: An SKShader that will produce a sweep gradient. Note: the current implementation simply uses the GLSL mix function, which is a linear interpolation. For colors near to each other on the spectrum the results are fine; for very different colors the results can be pretty ugly. If you need a gradient between very different colors then you can simply add more clors in between to narrow the relative distances. 773 | 774 | */ 775 | private func sweepGradientShader(colors colors: [UIColor], locations: [Float]?, center: CGPoint?, radius: Float?, startAngle: Float?, blending: Float, discardOutsideGradient: Bool, keepTextureShape: Bool) -> SKShader { 776 | 777 | 778 | // Sanitization 779 | 780 | // If there aren't at least two colors: return an empty shader. 781 | if colors.count < 2 { return SKShader() } 782 | 783 | 784 | // blending 785 | self.blending = min(max(blending, 0.0), 1.0) 786 | uniforms.append(u_blending) 787 | 788 | 789 | // discardOutsideGradient 790 | self.discardOutsideGradient = discardOutsideGradient 791 | uniforms.append(u_discardOutsideGradient) 792 | 793 | 794 | // center 795 | if center != nil { 796 | self.center = CGPoint(x: min(max(center!.x, 0.0), 1.0), y: min(max(center!.y, 0.0), 1.0)) 797 | } 798 | uniforms.append(u_center) 799 | 800 | 801 | // Colors in Float-array form 802 | var colorFloats = [[Float]]() 803 | for color in colors { 804 | colorFloats.append(colorToRGBAComponentFloatArray(color)) 805 | } 806 | 807 | 808 | // Color uniforms 809 | for (index, color) in colorFloats.enumerate() { 810 | 811 | let vector4 = GLKVector4Make(color[0], color[1], color[2], color[3]) 812 | let colorUniform = SKUniform(name: "u_color\(index)", floatVector4: vector4) 813 | u_colors.append(colorUniform) 814 | uniforms.append(colorUniform) 815 | } 816 | 817 | 818 | self.keepTextureShape = keepTextureShape 819 | uniforms.append(u_keepTextureShape) 820 | 821 | 822 | // Locations 823 | // colors.count - 1 is the expected number of locations. If the number is different: spread out evenly. 824 | var locationArray = [Float]() 825 | if locations != nil && locations!.count == colors.count - 1 { 826 | 827 | var lastValue : Float = 0.000001 828 | var newValue : Float = 0.0 829 | for location in locations! { 830 | 831 | newValue = min(max(location, lastValue), 1.0) 832 | locationArray.append(newValue) 833 | lastValue = newValue + 0.000001 834 | } 835 | } else { 836 | 837 | for var i = 1; i < colors.count; i++ { 838 | 839 | let location = Float(i) / Float(colors.count) 840 | locationArray.append(location) 841 | } 842 | } 843 | self.locations = locationArray 844 | 845 | 846 | // Location uniforms 847 | for (index, location) in locationArray.enumerate() { 848 | 849 | let locationUniform = SKUniform(name: "u_location\(index + 1)", float: location) 850 | u_locations.append(locationUniform) 851 | uniforms.append(locationUniform) 852 | } 853 | 854 | 855 | // radius 856 | if radius != nil { 857 | self.radius = min(max(radius!, 0.0), 1.0) 858 | } 859 | uniforms.append(u_radius) 860 | 861 | 862 | // startAngle 863 | if startAngle != nil { 864 | self.startAngle = startAngle! 865 | } 866 | uniforms.append(u_startAngle) 867 | 868 | 869 | // Shader Construction 870 | 871 | var sweepGradientShader = "highp float; float M_PI = 3.1415926535897932384626433832795; float location0 = 0.0; void main() { vec2 coord = v_tex_coord.xy - u_center; if (distance(coord, vec2(0.0, 0.0)) > u_radius) { if (u_discardOutsideGradient == 1.0) { discard; } else { gl_FragColor = texture2D(u_texture, v_tex_coord); return; } } if (u_keepTextureShape == 1.0) { vec4 textureColor = texture2D(u_texture, v_tex_coord); if (textureColor.w == 0.0) { discard; } } if (u_blending == 0.0) { gl_FragColor = texture2D(u_texture, v_tex_coord); return; } float angle = atan(coord.y, coord.x); angle = mod(angle / (2.0 * M_PI) - u_startAngle, 1.0); vec4 color = mix(u_color0, u_color1, smoothstep(location0, u_location1, angle)); if (u_blending < 1.0) { color = mix(color, texture2D(u_texture, v_tex_coord), 1.0 - u_blending); } gl_FragColor = color; }" 872 | 873 | 874 | var stringRange : NSRange 875 | var string = "" 876 | 877 | 878 | // Add the last gradient 879 | string = "color = mix(color, u_color0, smoothstep(u_location\(colors.count - 1), location0 + 1.0, angle)); " 880 | stringRange = (sweepGradientShader as NSString).rangeOfString("vec4 color = mix(u_color0, u_color1, smoothstep(location0, u_location1, angle)); ") 881 | sweepGradientShader = sweepGradientShader.insert(string: string, atIndex: stringRange.location + stringRange.length) 882 | 883 | 884 | // Add the gradients 885 | stringRange = (sweepGradientShader as NSString).rangeOfString("vec4 color = mix(u_color0, u_color1, smoothstep(location0, u_location1, angle)); ") 886 | for var i = colors.count - 1; i > 1; i-- { 887 | 888 | string = "color = mix(color, u_color\(i), smoothstep(u_location\(i - 1), u_location\(i), angle)); " 889 | sweepGradientShader = sweepGradientShader.insert(string: string, atIndex: stringRange.location + stringRange.length) 890 | } 891 | 892 | 893 | return SKShader(source: sweepGradientShader) 894 | } 895 | 896 | 897 | 898 | // MARK: - Helpers 899 | 900 | 901 | /** 902 | 903 | Translates a UIColor into an array of its RGBA components. 904 | 905 | - parameter color: A color. 906 | 907 | - returns: An array of the RGBA components, with values from 0.0 to 1.0, of the given color. 908 | 909 | */ 910 | private func colorToRGBAComponentFloatArray (color: UIColor) -> [Float] { 911 | 912 | var red = CGFloat(0.0), green = CGFloat(0.0), blue = CGFloat(0.0), alpha = CGFloat(0.0) 913 | 914 | let components = color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) 915 | 916 | return [Float(red), Float(green), Float(blue), Float(alpha)] 917 | } 918 | 919 | 920 | /** 921 | 922 | Sets up self.locations with default locations. 923 | 924 | */ 925 | private func defaultLocations () { 926 | 927 | var locationArray = [Float]() 928 | 929 | if gradientType == "linear" || gradientType == "radial" { 930 | for var i = 0; i < colors.count - 2; i++ { 931 | 932 | let location = (Float(i) + 1.0) / (Float(colors.count) - 1.0) 933 | locationArray.append(location) 934 | } 935 | 936 | } else if gradientType == "sweep" { 937 | 938 | for var i = 0; i < colors.count - 1; i++ { 939 | 940 | let location = (Float(i) + 1.0) / (Float(colors.count) - 1.0) 941 | locationArray.append(location) 942 | } 943 | } 944 | 945 | if locationArray.count != 0 { locations = locationArray } 946 | } 947 | 948 | 949 | /** 950 | 951 | Updates the uniforms in u_colors with the values in self.colors. 952 | 953 | */ 954 | private func updateColors () { 955 | 956 | if !u_colors.isEmpty && u_colors.count == colors.count { 957 | if gradientType != "gamut" { 958 | for var i = 0; i < colors.count; i++ { 959 | let colorArray = colorToRGBAComponentFloatArray(colors[i]) 960 | let vector4 = GLKVector4Make(colorArray[0], colorArray[1], colorArray[2], colorArray[3]) 961 | u_colors[i].floatVector4Value = vector4 962 | } 963 | } 964 | } 965 | } 966 | 967 | 968 | /** 969 | 970 | Updates the uniforms in u_locations with the values in self.locations. 971 | 972 | */ 973 | private func updateLocations () { 974 | 975 | // Sanitization 976 | 977 | if locations != nil { 978 | 979 | if gradientType == "linear" || gradientType == "radial" { if locations!.count != colors.count - 2 { defaultLocations(); return } } 980 | else if gradientType == "sweep" { if locations!.count != colors.count - 1 { defaultLocations(); return } } 981 | else { 982 | for location in locations! { 983 | if location <= 0 || location >= 1 { defaultLocations(); return } 984 | } 985 | } 986 | 987 | } else { 988 | defaultLocations() 989 | } 990 | 991 | 992 | // Update uniforms 993 | 994 | if !u_locations.isEmpty { 995 | if gradientType == "linear" { 996 | 997 | let vector = CGPoint(x: endPoint.x - startPoint.x, y: endPoint.y - startPoint.y) 998 | for var i = 0; i < u_locations.count; i++ { 999 | let vector2 = GLKVector2Make(Float(startPoint.x) + locations![i] * Float(vector.x), Float(startPoint.y) + locations![i] * Float(vector.y)) 1000 | u_locations[i].floatVector2Value = vector2 1001 | } 1002 | 1003 | } else if gradientType == "radial" || gradientType == "sweep" { 1004 | for var i = 0; i < u_locations.count; i++ { 1005 | u_locations[i].floatValue = locations![i] 1006 | } 1007 | } 1008 | } 1009 | } 1010 | } 1011 | 1012 | 1013 | 1014 | // MARK: - Extensions 1015 | 1016 | 1017 | extension String { 1018 | 1019 | /** 1020 | 1021 | Creates a new String by inserting a String at the given index into the messaged String. Note that I don't believe that this method is unicode-safe. 1022 | 1023 | - parameter string: The string to insert. 1024 | 1025 | - parameter atIndex: The index at which the string should be inserted. 1026 | 1027 | - returns: The original string with the new string inserted at the given index. 1028 | 1029 | */ 1030 | func insert(string string: String, atIndex index: Int) -> String { 1031 | 1032 | return String(self.characters.prefix(index)) + string + String(self.characters.suffix(self.characters.count - index)) 1033 | } 1034 | } -------------------------------------------------------------------------------- /BDGradientNodeDemo/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /BDGradientNodeDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /BDGradientNodeDemo/GradientScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientScene.swift 3 | // BDGradientNodeDemo 4 | // 5 | // Created by Braindrizzle Studio. 6 | // http://braindrizzlestudio.com 7 | // Copyright (c) 2015 Braindrizzle Studio. All rights reserved. 8 | // 9 | // This application is a demo of BDGradientNode. 10 | // 11 | 12 | 13 | import SpriteKit 14 | 15 | class GradientScene : SKScene { 16 | 17 | 18 | // MARK: - Properties 19 | 20 | 21 | // The pretty Apple blue for text/strokes 22 | var blue = UIColor(red: 0.0, green: 122/255, blue: 1.0, alpha: 1.0) 23 | 24 | // The currently displayed texture 25 | var currentTexture = SKTexture(imageNamed: "Spaceship") 26 | 27 | // Our BDGradientNode 28 | var gradientNode : BDGradientNode! = BDGradientNode() 29 | 30 | // The size of the BDGradientNode 31 | var nodeSize = CGSizeZero 32 | 33 | 34 | // The current number of colors. Changes the label when changed. 35 | var numberOfColors = 6 { 36 | didSet { 37 | (view?.viewWithTag(99) as! UILabel).text = "\(numberOfColors)" 38 | } 39 | } 40 | 41 | 42 | 43 | // MARK: - ViewController 44 | 45 | 46 | override func didMoveToView(view: SKView) { 47 | 48 | backgroundColor = UIColor.whiteColor() 49 | 50 | if UIDevice.currentDevice().userInterfaceIdiom == .Pad { 51 | nodeSize = CGSize(width: self.size.width * 2 / 3, height: self.size.width * 2 / 3) 52 | } else { 53 | nodeSize = CGSize(width: self.size.width - 10, height: self.size.width - 10) 54 | } 55 | 56 | colorsButtonPressed() 57 | 58 | setupUI() 59 | 60 | gamutGradientButtonPressed() 61 | } 62 | 63 | 64 | func setupUI () { 65 | 66 | setupButtons() 67 | setupLabels() 68 | setupSliders() 69 | } 70 | 71 | 72 | 73 | // MARK: - Buttons and Labels 74 | 75 | 76 | func setupButtons () { 77 | 78 | // Animation 79 | setupAnimationButton() 80 | 81 | // Colors 82 | setupColorsButton() 83 | setupNumberOfColorsButtons() 84 | 85 | // Gradients 86 | setupGamutGradientButton() 87 | setupLinearGradientButton() 88 | setupRadialGradientButton() 89 | setupSweepGradientButton() 90 | 91 | // Locations 92 | setupLocationsButton() 93 | 94 | // Options 95 | setupDiscardOutsideGradientButton() 96 | setupKeepTextureShapeButton() 97 | } 98 | 99 | 100 | 101 | // MARK: Animation 102 | 103 | 104 | func setupAnimationButton () { 105 | 106 | let origin = convertPointToView(CGPoint(x: self.size.width * 15 / 21, y: self.size.height - self.size.width * 0.2 / 21)) 107 | let size = CGSize(width: self.size.width * 5 / 21, height: self.size.height * 1 / 21) 108 | let frame = CGRect(origin: origin, size: size) 109 | let button = setupButton(frame: frame, title: "Animate: No", action: "animateButtonPressed") 110 | button.tag = 30 111 | } 112 | 113 | 114 | 115 | // MARK: Colors 116 | 117 | 118 | func setupColorsButton () { 119 | 120 | let origin = convertPointToView(CGPoint(x: self.size.width * 1 / 21, y: self.size.height - nodeSize.height * 12.5 / 10)) 121 | let size = CGSize(width: self.size.width * 4 / 21, height: self.size.height * 1 / 21) 122 | let frame = CGRect(origin: origin, size: size) 123 | let button = setupButton(frame: frame, title: "Colors", action: "colorsButtonPressed") 124 | button.tag = 96 125 | } 126 | 127 | 128 | func setupNumberOfColorsButtons () { 129 | 130 | // Minus 131 | 132 | let minusorigin = convertPointToView(CGPoint(x: self.size.width * 1 / 21, y: self.size.height - nodeSize.height * 13.5 / 10)) 133 | let MinusSize = CGSize(width: self.size.height * 1 / 21, height: self.size.height * 1 / 21) 134 | let MinusFrame = CGRect(origin: minusorigin, size: MinusSize) 135 | let minusButton = setupButton(frame: MinusFrame, title: "-", action: "minusColorsButtonPressed") 136 | minusButton.tag = 98 137 | 138 | // Number of Colors Label 139 | let numberOrigin = convertPointToView(CGPoint(x: self.size.width * 4.5 / 21, y: self.size.height - nodeSize.height * 13.5 / 10)) 140 | let numberSize = CGSize(width: self.size.height * 1 / 21, height: self.size.height * 1 / 21) 141 | let numberFrame = CGRect(origin: numberOrigin, size: numberSize) 142 | let numberOfButtonLabel = setupLabel(frame: numberFrame, text: "\(numberOfColors)") 143 | numberOfButtonLabel.tag = 99 144 | 145 | // Plus 146 | let plusOrigin = convertPointToView(CGPoint(x: self.size.width * 8 / 21, y: self.size.height - nodeSize.height * 13.5 / 10)) 147 | let plusSize = CGSize(width: self.size.height * 1 / 21, height: self.size.height * 1 / 21) 148 | let plusFrame = CGRect(origin: plusOrigin, size: plusSize) 149 | let plusButton = setupButton(frame: plusFrame, title: "+", action: "plusColorsButtonPressed") 150 | plusButton.tag = 97 151 | } 152 | 153 | 154 | 155 | // MARK: Gradients 156 | 157 | 158 | func setupGamutGradientButton () { 159 | 160 | let origin = convertPointToView(CGPoint(x: self.size.width * 1 / 21, y: self.size.height - nodeSize.height * 11.5 / 10)) 161 | let size = CGSize(width: self.size.width * 4 / 21, height: self.size.height * 1 / 21) 162 | let frame = CGRect(origin: origin, size: size) 163 | setupButton(frame: frame, title: "Gamut", action: "gamutGradientButtonPressed") 164 | } 165 | 166 | 167 | func setupLinearGradientButton () { 168 | 169 | let origin = convertPointToView(CGPoint(x: self.size.width * 6 / 21, y: self.size.height - nodeSize.height * 11.5 / 10)) 170 | let size = CGSize(width: self.size.width * 4 / 21, height: self.size.height * 1 / 21) 171 | let frame = CGRect(origin: origin, size: size) 172 | setupButton(frame: frame, title: "Linear", action: "linearGradientButtonPressed") 173 | } 174 | 175 | 176 | func setupRadialGradientButton () { 177 | 178 | let origin = convertPointToView(CGPoint(x: self.size.width * 11 / 21, y: self.size.height - nodeSize.height * 11.5 / 10)) 179 | let size = CGSize(width: self.size.width * 4 / 21, height: self.size.height * 1 / 21) 180 | let frame = CGRect(origin: origin, size: size) 181 | setupButton(frame: frame, title: "Radial", action: "radialGradientButtonPressed") 182 | } 183 | 184 | 185 | func setupSweepGradientButton () { 186 | 187 | let origin = convertPointToView(CGPoint(x: self.size.width * 16 / 21, y: self.size.height - nodeSize.height * 11.5 / 10)) 188 | let size = CGSize(width: self.size.width * 4 / 21, height: self.size.height * 1 / 21) 189 | let frame = CGRect(origin: origin, size: size) 190 | setupButton(frame: frame, title: "Sweep", action: "sweepGradientButtonPressed") 191 | } 192 | 193 | 194 | 195 | // MARK: Locations 196 | 197 | var locationsButtonStatus = "default" 198 | func setupLocationsButton () { 199 | 200 | let origin = convertPointToView(CGPoint(x: self.size.width * 6 / 21, y: self.size.height - nodeSize.height * 12.5 / 10)) 201 | let size = CGSize(width: self.size.width * 4 / 21, height: self.size.height * 1 / 21) 202 | let frame = CGRect(origin: origin, size: size) 203 | let button = setupButton(frame: frame, title: "Locations", action: "locationsButtonPressed") 204 | button.titleLabel?.adjustsFontSizeToFitWidth = true 205 | button.tag = 93 206 | } 207 | 208 | 209 | 210 | // MARK: Options 211 | 212 | 213 | func setupDiscardOutsideGradientButton () { 214 | 215 | let origin = convertPointToView(CGPoint(x: self.size.width * 11 / 21, y: self.size.height - nodeSize.height * 12.5 / 10)) 216 | let size = CGSize(width: self.size.width * 9 / 21, height: self.size.height * 1 / 21) 217 | let frame = CGRect(origin: origin, size: size) 218 | let button = setupButton(frame: frame, title: "Discard Outside: Yes", action: "discardOutsideGradientButtonPressed") 219 | button.tag = 92 220 | } 221 | 222 | 223 | func setupKeepTextureShapeButton () { 224 | 225 | let origin = convertPointToView(CGPoint(x: self.size.width * 11 / 21, y: self.size.height - nodeSize.height * 13.5 / 10)) 226 | let size = CGSize(width: self.size.width * 9 / 21, height: self.size.height * 1 / 21) 227 | let frame = CGRect(origin: origin, size: size) 228 | let button = setupButton(frame: frame, title: "Keep Shape: Yes", action: "keepTextureShapeButtonPressed") 229 | button.tag = 94 230 | } 231 | 232 | 233 | 234 | // MARK: Labels 235 | 236 | 237 | func setupLabels() { 238 | 239 | setupCenterLabel() 240 | setupCentersDragLabel() 241 | setupLinkLabel() 242 | setupStartEndDragLabel() 243 | } 244 | 245 | 246 | func setupCenterLabel () { 247 | 248 | let origin = convertPointToView(CGPoint(x: self.size.width * 1 / 21, y: self.size.height - nodeSize.height * 11 / 10)) 249 | let size = CGSize(width: self.size.width * 20 / 21, height: self.size.height * 0.5 / 21) 250 | let frame = CGRect(origin: origin, size: size) 251 | let label = UILabel(frame: frame) 252 | label.text = "Drag the image to move the center!" 253 | label.adjustsFontSizeToFitWidth = true 254 | label.font = UIFont(name: "Georgia", size: 10) 255 | label.textAlignment = .Center 256 | label.textColor = blue 257 | label.numberOfLines = 0 258 | label.tag = 20 259 | view?.addSubview(label) 260 | } 261 | 262 | 263 | func setupCentersDragLabel () { 264 | 265 | let origin = convertPointToView(CGPoint(x: self.size.width * 1 / 21, y: self.size.height - nodeSize.height * 11 / 10)) 266 | let size = CGSize(width: self.size.width * 20 / 21, height: self.size.height * 0.5 / 21) 267 | let frame = CGRect(origin: origin, size: size) 268 | let label = UILabel(frame: frame) 269 | label.text = "Drag the two circles to move their centers!" 270 | label.adjustsFontSizeToFitWidth = true 271 | label.font = UIFont(name: "Georgia", size: 10) 272 | label.hidden = true 273 | label.textAlignment = .Center 274 | label.textColor = blue 275 | label.numberOfLines = 0 276 | label.tag = 22 277 | view?.addSubview(label) 278 | } 279 | 280 | 281 | func setupLinkLabel () { 282 | 283 | let origin = convertPointToView(CGPoint(x: self.size.width * 1 / 21, y: self.size.height * 21 / 21)) 284 | let size = CGSize(width: self.size.width * 9 / 21, height: self.size.height * 1 / 21) 285 | let frame = CGRect(origin: origin, size: size) 286 | let text = UITextView(frame: frame) 287 | text.editable = false 288 | text.dataDetectorTypes = .All 289 | text.text = "braindrizzlestudio.com" 290 | text.textAlignment = .Center 291 | text.textColor = blue 292 | text.tag = 10 293 | view?.addSubview(text) 294 | if let textView = view?.viewWithTag(10) as? UITextView { 295 | view?.bringSubviewToFront(textView) 296 | } 297 | } 298 | 299 | 300 | func setupStartEndDragLabel () { 301 | 302 | let origin = convertPointToView(CGPoint(x: self.size.width * 1 / 21, y: self.size.height - nodeSize.height * 11 / 10)) 303 | let size = CGSize(width: self.size.width * 20 / 21, height: self.size.height * 0.5 / 21) 304 | let frame = CGRect(origin: origin, size: size) 305 | let label = UILabel(frame: frame) 306 | label.text = "Drag the top and bottom to move the start/end points!" 307 | label.adjustsFontSizeToFitWidth = true 308 | label.font = UIFont(name: "Georgia", size: 10) 309 | label.hidden = true 310 | label.textAlignment = .Center 311 | label.textColor = blue 312 | label.numberOfLines = 0 313 | label.tag = 21 314 | view?.addSubview(label) 315 | } 316 | 317 | 318 | 319 | // MARK: Button and Label Makers 320 | 321 | 322 | func setupButton (frame frame: CGRect, title: String, action: Selector) -> UIButton { 323 | 324 | let button = UIButton(frame: frame) 325 | 326 | button.layer.cornerRadius = 8.0 327 | button.layer.borderWidth = 0.5 328 | button.layer.borderColor = blue.CGColor 329 | 330 | button.setTitle(title, forState: .Normal) 331 | button.setTitleColor(blue, forState: .Normal) 332 | button.titleLabel!.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody) 333 | 334 | button.titleLabel!.adjustsFontSizeToFitWidth = true 335 | 336 | button.addTarget(self, action: action, forControlEvents: .TouchUpInside) 337 | 338 | view?.addSubview(button) 339 | 340 | return button 341 | } 342 | 343 | 344 | func setupLabel (frame frame: CGRect, text: String) -> UILabel { 345 | 346 | let label = UILabel(frame: frame) 347 | 348 | label.layer.cornerRadius = 8.0 349 | label.layer.borderWidth = 0.5 350 | label.layer.borderColor = blue.CGColor 351 | 352 | label.text = text 353 | label.adjustsFontSizeToFitWidth = true 354 | label.textColor = blue 355 | label.textAlignment = .Center 356 | 357 | view?.addSubview(label) 358 | 359 | return label 360 | } 361 | 362 | 363 | 364 | // MARK: - Sliders 365 | 366 | 367 | func setupSliders () { 368 | 369 | setupBlendingSlider() 370 | setupFirstRadiusSlider() 371 | setupSecondRadiusSlider() 372 | setupStartAngleSlider() 373 | } 374 | 375 | 376 | func setupBlendingSlider () { 377 | 378 | let origin = convertPointToView(CGPoint(x: self.size.width * 1 / 21, y: self.size.height - nodeSize.height * 17 / 10)) 379 | let size = CGSize(width: self.size.width * 9 / 21, height: self.size.height * 1 / 21) 380 | let frame = CGRect(origin: origin, size: size) 381 | let slider = UISlider(frame: frame) 382 | slider.minimumValue = Float(0.0) 383 | slider.maximumValue = Float(1.0) 384 | slider.maximumTrackTintColor = UIColor.darkGrayColor() 385 | slider.value = gradientNode.secondRadius 386 | slider.addTarget(self, action: "blendingSliderChanged", forControlEvents: .ValueChanged) 387 | slider.tag = 56 388 | view?.addSubview(slider) 389 | 390 | let subOrigin = convertPointToView(CGPoint(x: self.size.width * 1 / 21, y: self.size.height - nodeSize.height * 16.5 / 10)) 391 | let subSize = CGSize(width: self.size.width * 9 / 21, height: self.size.height * 0.5 / 21) 392 | let subFrame = CGRect(origin: subOrigin, size: subSize) 393 | let label = UILabel(frame: subFrame) 394 | label.text = "Blending" 395 | label.textColor = blue 396 | label.textAlignment = .Center 397 | label.adjustsFontSizeToFitWidth = true 398 | label.numberOfLines = 0 399 | label.tag = 57 400 | view?.addSubview(label) 401 | } 402 | 403 | 404 | func setupFirstRadiusSlider () { 405 | 406 | let origin = convertPointToView(CGPoint(x: self.size.width * 11 / 21, y: self.size.height - nodeSize.height * 15.5 / 10)) 407 | let size = CGSize(width: self.size.width * 9 / 21, height: self.size.height * 1 / 21) 408 | let frame = CGRect(origin: origin, size: size) 409 | let slider = UISlider(frame: frame) 410 | slider.minimumValue = Float(0.0) 411 | slider.maximumValue = Float(1.0) 412 | slider.maximumTrackTintColor = UIColor.darkGrayColor() 413 | slider.value = gradientNode.firstRadius 414 | slider.addTarget(self, action: "firstRadiusSliderChanged", forControlEvents: .ValueChanged) 415 | slider.tag = 52 416 | view?.addSubview(slider) 417 | 418 | let subOrigin = convertPointToView(CGPoint(x: self.size.width * 11 / 21, y: self.size.height - nodeSize.height * 15 / 10)) 419 | let subSize = CGSize(width: self.size.width * 9 / 21, height: self.size.height * 0.5 / 21) 420 | let subFrame = CGRect(origin: subOrigin, size: subSize) 421 | let label = UILabel(frame: subFrame) 422 | label.text = "First Radius" 423 | label.textColor = blue 424 | label.textAlignment = .Center 425 | label.adjustsFontSizeToFitWidth = true 426 | label.numberOfLines = 0 427 | label.tag = 53 428 | view?.addSubview(label) 429 | } 430 | 431 | 432 | func setupSecondRadiusSlider () { 433 | 434 | let origin = convertPointToView(CGPoint(x: self.size.width * 11 / 21, y: self.size.height - nodeSize.height * 17 / 10)) 435 | let size = CGSize(width: self.size.width * 9 / 21, height: self.size.height * 1 / 21) 436 | let frame = CGRect(origin: origin, size: size) 437 | let slider = UISlider(frame: frame) 438 | slider.minimumValue = Float(0.0) 439 | slider.maximumValue = Float(1.0) 440 | slider.maximumTrackTintColor = UIColor.darkGrayColor() 441 | slider.value = gradientNode.secondRadius 442 | slider.addTarget(self, action: "secondRadiusSliderChanged", forControlEvents: .ValueChanged) 443 | slider.tag = 54 444 | view?.addSubview(slider) 445 | 446 | let subOrigin = convertPointToView(CGPoint(x: self.size.width * 11 / 21, y: self.size.height - nodeSize.height * 16.5 / 10)) 447 | let subSize = CGSize(width: self.size.width * 9 / 21, height: self.size.height * 0.5 / 21) 448 | let subFrame = CGRect(origin: subOrigin, size: subSize) 449 | let label = UILabel(frame: subFrame) 450 | label.text = "Second Radius" 451 | label.textColor = blue 452 | label.textAlignment = .Center 453 | label.adjustsFontSizeToFitWidth = true 454 | label.numberOfLines = 0 455 | label.tag = 55 456 | view?.addSubview(label) 457 | } 458 | 459 | 460 | func setupStartAngleSlider () { 461 | 462 | let origin = convertPointToView(CGPoint(x: self.size.width * 1 / 21, y: self.size.height - nodeSize.height * 15.5 / 10)) 463 | let size = CGSize(width: self.size.width * 9 / 21, height: self.size.height * 1 / 21) 464 | let frame = CGRect(origin: origin, size: size) 465 | let slider = UISlider(frame: frame) 466 | slider.minimumValue = Float(0.0) 467 | slider.maximumValue = Float(2 * M_PI) 468 | slider.maximumTrackTintColor = UIColor.darkGrayColor() 469 | slider.value = gradientNode.startAngle 470 | slider.addTarget(self, action: "startAngleSliderChanged", forControlEvents: .ValueChanged) 471 | slider.tag = 50 472 | view?.addSubview(slider) 473 | 474 | let subOrigin = convertPointToView(CGPoint(x: self.size.width * 1 / 21, y: self.size.height - nodeSize.height * 15 / 10)) 475 | let subSize = CGSize(width: self.size.width * 9 / 21, height: self.size.height * 0.5 / 21) 476 | let subFrame = CGRect(origin: subOrigin, size: subSize) 477 | let label = UILabel(frame: subFrame) 478 | label.text = "Start Angle" 479 | label.textColor = blue 480 | label.textAlignment = .Center 481 | label.adjustsFontSizeToFitWidth = true 482 | label.numberOfLines = 0 483 | label.tag = 51 484 | view?.addSubview(label) 485 | } 486 | 487 | 488 | 489 | // MARK: - Actions 490 | 491 | 492 | // MARK: Animation 493 | 494 | 495 | func animateButtonPressed () { 496 | 497 | if let animateButton = view?.viewWithTag(30) as? UIButton { 498 | 499 | switch animateButton.titleLabel!.text! { 500 | 501 | case "Animate: Yes": 502 | animateButton.setTitle("Animate: No", forState: .Normal) 503 | gradientNode.removeAllActions() 504 | 505 | case "Animate: No": 506 | animateButton.setTitle("Animate: Yes", forState: .Normal) 507 | 508 | switch gradientNode.gradientType { 509 | case "gamut": gamutAnimation() 510 | case "linear": linearAnimation() 511 | case "radial": radialAnimation() 512 | case "sweep": sweepAnimation() 513 | default: return 514 | } 515 | default: return 516 | } 517 | 518 | 519 | } 520 | } 521 | 522 | 523 | func gamutAnimation () { 524 | 525 | 526 | self.gradientNode.center.x += 0.001 527 | self.gradientNode.center.y += 0.001 528 | 529 | let angleAction = SKAction.runBlock { 530 | 531 | self.gradientNode.startAngle = (self.gradientNode.startAngle + 0.05) % Float(2 * M_PI) 532 | if let slider = self.view?.viewWithTag(50) as? UISlider { 533 | slider.value = self.gradientNode.startAngle 534 | } 535 | } 536 | 537 | var blendingMultiplier : Float = 1.0 538 | let blendingAction = SKAction.runBlock { 539 | 540 | if self.gradientNode.blending > 1.0 || self.gradientNode.blending < 0.0 { blendingMultiplier *= -1.0 } 541 | self.gradientNode.blending += blendingMultiplier * 0.01 542 | if let slider = self.view?.viewWithTag(56) as? UISlider { 543 | slider.value = self.gradientNode.blending 544 | } 545 | } 546 | 547 | let centerAction = SKAction.runBlock { 548 | 549 | let angle : CGFloat = 0.03 550 | self.gradientNode.center = self.rotatePoint(self.gradientNode.center, byAngle: angle) 551 | 552 | let multiplier = CGFloat(sin(self.gradientNode.startAngle) / 1000) 553 | var normalizedPoint = self.gradientNode.center 554 | normalizedPoint.x -= 0.5 555 | normalizedPoint.y -= 0.5 556 | let length = sqrt(pow(normalizedPoint.x, 2) + pow(normalizedPoint.y, 2)) 557 | normalizedPoint.x /= length 558 | normalizedPoint.y /= length 559 | 560 | self.gradientNode.center.x += multiplier * normalizedPoint.x 561 | self.gradientNode.center.y -= multiplier * normalizedPoint.y 562 | } 563 | 564 | let radiusAction = SKAction.runBlock { 565 | 566 | let change = Float(sin(self.angleOfPoint(self.gradientNode.center))) / 100 567 | self.gradientNode.radius += change 568 | if let slider = self.view?.viewWithTag(52) as? UISlider { 569 | slider.value = self.gradientNode.radius 570 | } 571 | } 572 | 573 | let delayAction = SKAction.waitForDuration(0.05) 574 | 575 | let actionGroup = SKAction.group([angleAction, blendingAction, centerAction, radiusAction, delayAction]) 576 | 577 | gradientNode.runAction(SKAction.repeatActionForever(actionGroup)) 578 | } 579 | 580 | 581 | func linearAnimation () { 582 | 583 | var blendingMultiplier : Float = 1.0 584 | let blendingAction = SKAction.runBlock { 585 | 586 | if self.gradientNode.blending > 1.0 || self.gradientNode.blending < 0.0 { blendingMultiplier *= -1.0 } 587 | self.gradientNode.blending += blendingMultiplier * 0.01 588 | if let slider = self.view?.viewWithTag(56) as? UISlider { 589 | slider.value = self.gradientNode.blending 590 | } 591 | } 592 | 593 | let startAction = SKAction.runBlock { 594 | 595 | let angle : CGFloat = 0.03 596 | self.gradientNode.startPoint = self.rotatePoint(self.gradientNode.startPoint, byAngle: angle) 597 | 598 | let multiplier = sin(self.angleOfPoint(self.gradientNode.startPoint)) / 100 599 | var normalizedPoint = self.gradientNode.startPoint 600 | normalizedPoint.x -= 0.5 601 | normalizedPoint.y -= 0.5 602 | let length = sqrt(pow(normalizedPoint.x, 2) + pow(normalizedPoint.y, 2)) 603 | normalizedPoint.x /= length 604 | normalizedPoint.y /= length 605 | 606 | self.gradientNode.startPoint.x += multiplier * normalizedPoint.x 607 | self.gradientNode.startPoint.y += multiplier * normalizedPoint.y 608 | } 609 | 610 | let endAction = SKAction.runBlock { 611 | 612 | let angle : CGFloat = 0.03 613 | self.gradientNode.endPoint = self.rotatePoint(self.gradientNode.endPoint, byAngle: angle) 614 | 615 | let multiplier = sin(self.angleOfPoint(self.gradientNode.endPoint)) / 100 616 | var normalizedPoint = self.gradientNode.endPoint 617 | normalizedPoint.x -= 0.5 618 | normalizedPoint.y -= 0.5 619 | let length = sqrt(pow(normalizedPoint.x, 2) + pow(normalizedPoint.y, 2)) 620 | normalizedPoint.x /= length 621 | normalizedPoint.y /= length 622 | 623 | self.gradientNode.endPoint.x -= multiplier * normalizedPoint.x 624 | self.gradientNode.endPoint.y -= multiplier * normalizedPoint.y 625 | } 626 | 627 | let delayAction = SKAction.waitForDuration(0.05) 628 | 629 | let actionGroup = SKAction.group([blendingAction, startAction, endAction, delayAction]) 630 | 631 | gradientNode.runAction(SKAction.repeatActionForever(actionGroup)) 632 | } 633 | 634 | 635 | func radialAnimation () { 636 | 637 | self.gradientNode.firstCenter.x += 0.001 638 | self.gradientNode.firstCenter.y += 0.001 639 | self.gradientNode.secondCenter.x -= 0.001 640 | self.gradientNode.secondCenter.y -= 0.001 641 | 642 | var blendingMultiplier : Float = 1.0 643 | let blendingAction = SKAction.runBlock { 644 | 645 | if self.gradientNode.blending > 1.0 || self.gradientNode.blending < 0.0 { blendingMultiplier *= -1.0 } 646 | self.gradientNode.blending += blendingMultiplier * 0.01 647 | if let slider = self.view?.viewWithTag(56) as? UISlider { 648 | slider.value = self.gradientNode.blending 649 | } 650 | } 651 | 652 | let firstCenterAction = SKAction.runBlock { 653 | 654 | let angle : CGFloat = 0.03 655 | self.gradientNode.firstCenter = self.rotatePoint(self.gradientNode.firstCenter, byAngle: angle) 656 | 657 | let multiplier = sin(self.angleOfPoint(self.gradientNode.firstCenter)) / 100 658 | var normalizedPoint = self.gradientNode.firstCenter 659 | normalizedPoint.x -= 0.5 660 | normalizedPoint.y -= 0.5 661 | let length = sqrt(pow(normalizedPoint.x, 2) + pow(normalizedPoint.y, 2)) 662 | normalizedPoint.x /= length 663 | normalizedPoint.y /= length 664 | 665 | self.gradientNode.firstCenter.x += multiplier * normalizedPoint.x 666 | self.gradientNode.firstCenter.y += multiplier * normalizedPoint.y 667 | } 668 | 669 | let firstRadiusAction = SKAction.runBlock { 670 | 671 | let change = Float(sin(self.angleOfPoint(self.gradientNode.firstCenter))) / 100 672 | self.gradientNode.firstRadius = min(max(self.gradientNode.firstRadius + change, 0.0), 1.0) 673 | if let slider = self.view?.viewWithTag(52) as? UISlider { 674 | slider.value = self.gradientNode.firstRadius 675 | } 676 | } 677 | 678 | let secondCenterAction = SKAction.runBlock { 679 | 680 | let angle : CGFloat = 0.03 681 | self.gradientNode.secondCenter = self.rotatePoint(self.gradientNode.secondCenter, byAngle: angle) 682 | 683 | let multiplier = sin(self.angleOfPoint(self.gradientNode.secondCenter)) / 100 684 | var normalizedPoint = self.gradientNode.secondCenter 685 | normalizedPoint.x -= 0.5 686 | normalizedPoint.y -= 0.5 687 | let length = sqrt(pow(normalizedPoint.x, 2) + pow(normalizedPoint.y, 2)) 688 | normalizedPoint.x /= length 689 | normalizedPoint.y /= length 690 | 691 | self.gradientNode.secondCenter.x -= multiplier * normalizedPoint.x 692 | self.gradientNode.secondCenter.y -= multiplier * normalizedPoint.y 693 | } 694 | 695 | let secondRadiusAction = SKAction.runBlock { 696 | 697 | let change = Float(sin(self.angleOfPoint(self.gradientNode.secondCenter))) / 100 698 | self.gradientNode.secondRadius = min(max(self.gradientNode.secondRadius + change, 0.0), 1.0) 699 | if let slider = self.view?.viewWithTag(54) as? UISlider { 700 | slider.value = self.gradientNode.secondRadius 701 | } 702 | } 703 | 704 | let delayAction = SKAction.waitForDuration(0.05) 705 | 706 | let actionGroup = SKAction.group([blendingAction, firstCenterAction, firstRadiusAction, secondCenterAction, secondRadiusAction, delayAction]) 707 | 708 | gradientNode.runAction(SKAction.repeatActionForever(actionGroup)) 709 | } 710 | 711 | 712 | func sweepAnimation () { 713 | 714 | self.gradientNode.center.x += 0.001 715 | self.gradientNode.center.y += 0.001 716 | 717 | let angleAction = SKAction.runBlock { 718 | 719 | self.gradientNode.startAngle = (self.gradientNode.startAngle + 0.05) % Float(2 * M_PI) 720 | if let slider = self.view?.viewWithTag(50) as? UISlider { 721 | slider.value = self.gradientNode.startAngle 722 | } 723 | } 724 | 725 | var blendingMultiplier : Float = 1.0 726 | let blendingAction = SKAction.runBlock { 727 | 728 | if self.gradientNode.blending > 1.0 || self.gradientNode.blending < 0.0 { blendingMultiplier *= -1.0 } 729 | self.gradientNode.blending += blendingMultiplier * 0.01 730 | if let slider = self.view?.viewWithTag(56) as? UISlider { 731 | slider.value = self.gradientNode.blending 732 | } 733 | } 734 | 735 | let centerAction = SKAction.runBlock { 736 | 737 | let angle : CGFloat = 0.03 738 | self.gradientNode.center = self.rotatePoint(self.gradientNode.center, byAngle: angle) 739 | 740 | let multiplier = CGFloat(sin(self.gradientNode.startAngle) / 1000) 741 | var normalizedPoint = self.gradientNode.center 742 | normalizedPoint.x -= 0.5 743 | normalizedPoint.y -= 0.5 744 | let length = sqrt(pow(normalizedPoint.x, 2) + pow(normalizedPoint.y, 2)) 745 | normalizedPoint.x /= length 746 | normalizedPoint.y /= length 747 | 748 | self.gradientNode.center.x += multiplier * normalizedPoint.x 749 | self.gradientNode.center.y -= multiplier * normalizedPoint.y 750 | } 751 | 752 | let radiusAction = SKAction.runBlock { 753 | 754 | let change = Float(sin(self.angleOfPoint(self.gradientNode.center))) / 100 755 | self.gradientNode.radius += change 756 | if let slider = self.view?.viewWithTag(52) as? UISlider { 757 | slider.value = self.gradientNode.radius 758 | } 759 | } 760 | 761 | let delayAction = SKAction.waitForDuration(0.05) 762 | 763 | let actionGroup = SKAction.group([angleAction, blendingAction, centerAction, radiusAction, delayAction]) 764 | 765 | gradientNode.runAction(SKAction.repeatActionForever(actionGroup)) 766 | } 767 | 768 | 769 | 770 | // MARK: Colors 771 | 772 | 773 | func colorsButtonPressed () { 774 | 775 | gradientNode.colors = randomColorArray(numberOfColors) 776 | } 777 | 778 | 779 | func minusColorsButtonPressed () { 780 | 781 | if numberOfColors == 2 { return } 782 | if numberOfColors == 3 { disableButtonForTag(98) } 783 | 784 | numberOfColors = numberOfColors - 1 785 | resetCurrentNode() 786 | } 787 | 788 | 789 | func plusColorsButtonPressed () { 790 | 791 | if numberOfColors == 2 { enableButtonForTag(98) } 792 | 793 | numberOfColors = numberOfColors + 1 794 | resetCurrentNode() 795 | } 796 | 797 | 798 | 799 | // MARK: Gradients 800 | 801 | 802 | func gamutGradientButtonPressed () { 803 | 804 | if gradientNode.gradientType == "gamut" { return } 805 | switchToGradient("gamut") 806 | } 807 | 808 | 809 | func linearGradientButtonPressed () { 810 | 811 | if gradientNode.gradientType == "linear" && gradientNode.colors.count == numberOfColors { return } 812 | switchToGradient("linear") 813 | } 814 | 815 | 816 | func radialGradientButtonPressed () { 817 | 818 | if gradientNode.gradientType == "radial" && gradientNode.colors.count == numberOfColors { return } 819 | switchToGradient("radial") 820 | } 821 | 822 | 823 | func sweepGradientButtonPressed () { 824 | 825 | if gradientNode.gradientType == "sweep" && gradientNode.colors.count == numberOfColors { return } 826 | switchToGradient("sweep") 827 | } 828 | 829 | 830 | func switchToGradient(gradient: String) { 831 | 832 | let blending = gradientNode.blending 833 | let center = gradientNode.center 834 | var colors : [UIColor] 835 | if gradientNode.colors.count != numberOfColors { colors = randomColorArray(numberOfColors)} 836 | else {colors = gradientNode.colors } 837 | let discardOutsideGradient = gradientNode.discardOutsideGradient 838 | let endPoint = gradientNode.endPoint 839 | let firstCenter = gradientNode.firstCenter 840 | let firstRadius = gradientNode.firstRadius 841 | let keepTextureShape = gradientNode.keepTextureShape 842 | let radius = gradientNode.radius 843 | let secondCenter = gradientNode.secondCenter 844 | let secondRadius = gradientNode.secondRadius 845 | let startAngle = gradientNode.startAngle 846 | let startPoint = gradientNode.startPoint 847 | 848 | gradientNode.removeFromParent() 849 | gradientNode = nil 850 | 851 | switch gradient { 852 | 853 | case "gamut": gradientNode = BDGradientNode(gamutGradientWithTexture: currentTexture, center: center, radius: radius, startAngle: startAngle, blending: blending, discardOutsideGradient: discardOutsideGradient, keepTextureShape: keepTextureShape, size: nodeSize) 854 | case "linear": gradientNode = BDGradientNode(linearGradientWithTexture: currentTexture, colors: colors, locations: nil, startPoint: startPoint, endPoint: endPoint, blending: blending, keepTextureShape: keepTextureShape, size: nodeSize) 855 | case "radial": gradientNode = BDGradientNode(radialGradientWithTexture: currentTexture, colors: colors, locations: nil, firstCenter: firstCenter, firstRadius: firstRadius, secondCenter: secondCenter, secondRadius: secondRadius, blending: blending, discardOutsideGradient: discardOutsideGradient, keepTextureShape: keepTextureShape, size: nodeSize) 856 | case "sweep": gradientNode = BDGradientNode(sweepGradientWithTexture: currentTexture, colors: colors, locations: nil, center: center, radius: radius, startAngle: startAngle, blending: blending, discardOutsideGradient: discardOutsideGradient, keepTextureShape: keepTextureShape, size: nodeSize) 857 | default: return 858 | } 859 | 860 | gradientNode.position = CGPoint(x: self.size.width / 2, y: self.size.height - nodeSize.height / 2 - nodeSize.height * 1 / 10) 861 | addChild(gradientNode) 862 | adjustUIForGradient(gradient) 863 | } 864 | 865 | 866 | 867 | // MARK: Locations 868 | 869 | // If the gradient is using default locations, add random locations; if random, add default. 870 | func locationsButtonPressed () { 871 | 872 | if let locations = gradientNode.locations { 873 | 874 | switch locationsButtonStatus { 875 | 876 | case "default": 877 | 878 | var newLocations = [Float]() 879 | var lastLocation : Float = 0.01 880 | let jump = 0.25 / Float(gradientNode.colors.count) 881 | for var i = 0; i < locations.count; i++ { 882 | lastLocation = randomFloat(min: lastLocation + jump, max: min(lastLocation + 6.0 * jump, 0.99)) 883 | newLocations.append(lastLocation) 884 | } 885 | gradientNode.locations = newLocations 886 | locationsButtonStatus = "random" 887 | 888 | case "random": 889 | 890 | gradientNode.locations = nil 891 | locationsButtonStatus = "default" 892 | 893 | default: return 894 | } 895 | } 896 | } 897 | 898 | 899 | 900 | // MARK: Options 901 | 902 | 903 | func discardOutsideGradientButtonPressed () { 904 | 905 | let button = (view?.viewWithTag(92) as! UIButton) 906 | 907 | switch button.titleLabel!.text! { 908 | case "Discard Outside: Yes": 909 | button.setTitle("Discard Outside: No", forState: .Normal) 910 | case "Discard Outside: No": 911 | button.setTitle("Discard Outside: Yes", forState: .Normal) 912 | default: return 913 | } 914 | 915 | gradientNode.discardOutsideGradient = !gradientNode.discardOutsideGradient 916 | } 917 | 918 | 919 | func keepTextureShapeButtonPressed () { 920 | 921 | let button = (view?.viewWithTag(94) as! UIButton) 922 | 923 | switch button.titleLabel!.text! { 924 | case "Keep Shape: Yes": 925 | button.setTitle("Keep Shape: No", forState: .Normal) 926 | case "Keep Shape: No": 927 | button.setTitle("Keep Shape: Yes", forState: .Normal) 928 | default: return 929 | } 930 | 931 | gradientNode.keepTextureShape = !gradientNode.keepTextureShape 932 | } 933 | 934 | 935 | 936 | // MARK: Sliders 937 | 938 | 939 | func blendingSliderChanged () { 940 | 941 | gradientNode.blending = (view?.viewWithTag(56) as! UISlider).value 942 | } 943 | 944 | func firstRadiusSliderChanged () { 945 | 946 | if gradientNode.gradientType == "gamut" || gradientNode.gradientType == "sweep" { gradientNode.radius = (view?.viewWithTag(52) as! UISlider).value } 947 | if gradientNode.gradientType == "radial" { gradientNode.firstRadius = (view?.viewWithTag(52) as! UISlider).value } 948 | } 949 | 950 | func secondRadiusSliderChanged () { 951 | 952 | gradientNode.secondRadius = (view?.viewWithTag(54) as! UISlider).value 953 | } 954 | 955 | func startAngleSliderChanged () { 956 | 957 | gradientNode.startAngle = (view?.viewWithTag(50) as! UISlider).value 958 | } 959 | 960 | 961 | 962 | // MARK: - Touch Handling 963 | 964 | 965 | override func touchesBegan(touches: Set, withEvent event: UIEvent?) { 966 | 967 | for touch in touches { 968 | 969 | if let touch = touch as UITouch? { 970 | let positionInScene = touch.locationInNode(self) 971 | let touchedNode = self.nodeAtPoint(positionInScene) 972 | 973 | if let gradientNode = touchedNode as? BDGradientNode { 974 | 975 | var point = self.convertPoint(positionInScene, toNode: gradientNode) 976 | point.x = point.x / gradientNode.size.width + 0.5 977 | point.y = point.y / gradientNode.size.height + 0.5 978 | 979 | switch gradientNode.gradientType { 980 | 981 | case "gamut": gradientNode.center = point 982 | 983 | case "linear": 984 | let closest = chooseCloserPoint(point, otherPoint1: gradientNode.startPoint, otherPoint2: gradientNode.endPoint) 985 | if closest == 1 { gradientNode.startPoint = point } 986 | else if closest == 2 || closest == 3 { gradientNode.endPoint = point } 987 | 988 | case "sweep": gradientNode.center = point 989 | 990 | case "radial": 991 | let closest = chooseCloserPoint(point, otherPoint1: gradientNode.firstCenter, otherPoint2: gradientNode.secondCenter) 992 | if closest == 1 { gradientNode.firstCenter = point } 993 | else if closest == 2 || closest == 3 { gradientNode.secondCenter = point } 994 | 995 | default: return 996 | } 997 | } 998 | } 999 | } 1000 | } 1001 | 1002 | 1003 | override func touchesMoved(touches: Set, withEvent event: UIEvent?) { 1004 | 1005 | for touch in touches { 1006 | 1007 | if let touch = touch as UITouch? { 1008 | let positionInScene = touch.locationInNode(self) 1009 | let touchedNode = self.nodeAtPoint(positionInScene) 1010 | 1011 | if let gradientNode = touchedNode as? BDGradientNode { 1012 | 1013 | var point = self.convertPoint(positionInScene, toNode: gradientNode) 1014 | point.x = point.x / gradientNode.size.width + 0.5 1015 | point.y = point.y / gradientNode.size.height + 0.5 1016 | 1017 | switch gradientNode.gradientType { 1018 | 1019 | case "gamut": gradientNode.center = point 1020 | 1021 | case "linear": 1022 | let closest = chooseCloserPoint(point, otherPoint1: gradientNode.startPoint, otherPoint2: gradientNode.endPoint) 1023 | if closest == 1 { gradientNode.startPoint = point } 1024 | else if closest == 2 || closest == 3 { gradientNode.endPoint = point } 1025 | 1026 | case "radial": 1027 | let closest = chooseCloserPoint(point, otherPoint1: gradientNode.firstCenter, otherPoint2: gradientNode.secondCenter) 1028 | if closest == 1 { gradientNode.firstCenter = point } 1029 | else if closest == 2 || closest == 3 { gradientNode.secondCenter = point } 1030 | 1031 | case "sweep": gradientNode.center = point 1032 | 1033 | default: return 1034 | } 1035 | } 1036 | } 1037 | } 1038 | } 1039 | 1040 | 1041 | 1042 | // MARK: - Helpers 1043 | 1044 | 1045 | /** 1046 | 1047 | Adjusts the UI elements for the given gradient. 1048 | 1049 | - parameter gradient: A gradientType of BDGradientNode. 1050 | 1051 | */ 1052 | func adjustUIForGradient (gradient: String) { 1053 | 1054 | switch gradientNode.gradientType { 1055 | 1056 | case "gamut": 1057 | if let label = view?.viewWithTag(20) as? UILabel { label.hidden = false } 1058 | if let label = view?.viewWithTag(21) as? UILabel { label.hidden = true } 1059 | if let label = view?.viewWithTag(22) as? UILabel { label.hidden = true } 1060 | if let button = view?.viewWithTag(30) as? UIButton { button.setTitle("Animate: No", forState: .Normal) } 1061 | enableSliderForTag(50) 1062 | enableSliderForTag(52) 1063 | if let slider = view?.viewWithTag(52) as? UISlider { slider.value = gradientNode.radius } 1064 | if let label = view?.viewWithTag(53) as? UILabel { label.text = "Radius" } 1065 | disableSliderForTag(54) 1066 | enableButtonForTag(92) 1067 | disableButtonForTag(93) 1068 | disableButtonForTag(96) 1069 | disableButtonForTag(97) 1070 | disableButtonForTag(98) 1071 | disableLabelForTag(99) 1072 | case "linear": 1073 | if let label = view?.viewWithTag(20) as? UILabel { label.hidden = true } 1074 | if let label = view?.viewWithTag(21) as? UILabel { label.hidden = false } 1075 | if let label = view?.viewWithTag(22) as? UILabel { label.hidden = true } 1076 | if let button = view?.viewWithTag(30) as? UIButton { button.setTitle("Animate: No", forState: .Normal) } 1077 | disableSliderForTag(50) 1078 | disableSliderForTag(52) 1079 | disableSliderForTag(54) 1080 | disableButtonForTag(92) 1081 | enableButtonForTag(93) 1082 | enableButtonForTag(96) 1083 | enableButtonForTag(97) 1084 | enableButtonForTag(98) 1085 | enableLabelForTag(99) 1086 | case "radial": 1087 | if let label = view?.viewWithTag(20) as? UILabel { label.hidden = true } 1088 | if let label = view?.viewWithTag(21) as? UILabel { label.hidden = true } 1089 | if let label = view?.viewWithTag(22) as? UILabel { label.hidden = false } 1090 | if let button = view?.viewWithTag(30) as? UIButton { button.setTitle("Animate: No", forState: .Normal) } 1091 | disableSliderForTag(50) 1092 | enableSliderForTag(52) 1093 | if let slider = view?.viewWithTag(52) as? UISlider { slider.value = gradientNode.firstRadius } 1094 | if let label = view?.viewWithTag(53) as? UILabel { label.text = "First Radius" } 1095 | enableSliderForTag(54) 1096 | enableButtonForTag(92) 1097 | enableButtonForTag(93) 1098 | enableButtonForTag(96) 1099 | enableButtonForTag(97) 1100 | enableButtonForTag(98) 1101 | enableLabelForTag(99) 1102 | case "sweep": 1103 | if let label = view?.viewWithTag(20) as? UILabel { label.hidden = false } 1104 | if let label = view?.viewWithTag(21) as? UILabel { label.hidden = true } 1105 | if let label = view?.viewWithTag(22) as? UILabel { label.hidden = true } 1106 | if let button = view?.viewWithTag(30) as? UIButton { button.setTitle("Animate: No", forState: .Normal) } 1107 | enableSliderForTag(50) 1108 | enableSliderForTag(52) 1109 | if let slider = view?.viewWithTag(52) as? UISlider { slider.value = gradientNode.radius } 1110 | if let label = view?.viewWithTag(53) as? UILabel { label.text = "Radius" } 1111 | disableSliderForTag(54) 1112 | enableButtonForTag(92) 1113 | enableButtonForTag(93) 1114 | enableButtonForTag(96) 1115 | enableButtonForTag(97) 1116 | enableButtonForTag(98) 1117 | enableLabelForTag(99) 1118 | default: return 1119 | } 1120 | } 1121 | 1122 | 1123 | /** 1124 | 1125 | The angle of a point around (0.5, 0.5). 1126 | 1127 | - parameter point: A point. 1128 | 1129 | - returns: The angle, in radians, of the vector of the given point around (0.5, 0.5). 1130 | 1131 | */ 1132 | func angleOfPoint (point: CGPoint) -> CGFloat { 1133 | 1134 | var newPoint = point 1135 | newPoint.x -= 0.5 1136 | newPoint.y -= 0.5 1137 | 1138 | return abs(atan2(newPoint.x, newPoint.y) - CGFloat(M_PI)) 1139 | } 1140 | 1141 | 1142 | /** 1143 | 1144 | Compares the point to check against the two other points to see which it's closer to. 1145 | 1146 | - parameter pointToCheck: The point to compare. 1147 | 1148 | - parameter otherPoint1: The first other point. 1149 | 1150 | - parameter otherPoint2: The second other point. 1151 | 1152 | - returns: If pointToCheck is closer to otherPoint1 than to otherPoint2: returns 1. If pointToCheck is closer to otherPoint2 than to otherPoint1: returns 2. If the distances are equal: returns 3. If somehow none of those conditions obtains: returns 0. 1153 | 1154 | */ 1155 | func chooseCloserPoint(pointToCheck: CGPoint, otherPoint1: CGPoint, otherPoint2: CGPoint) -> Int { 1156 | 1157 | let distance1 = distanceBetweenPoints(pointToCheck, secondPoint: otherPoint1) 1158 | let distance2 = distanceBetweenPoints(pointToCheck, secondPoint: otherPoint2) 1159 | 1160 | if distance1 < distance2 { return 1 } 1161 | if distance2 < distance1 { return 2 } 1162 | if distance1 == distance2 { return 3 } 1163 | 1164 | return 0 1165 | } 1166 | 1167 | 1168 | /** 1169 | 1170 | Disables the UIButton at the given tag. 1171 | 1172 | - parameter tag: The tag of the desired UIButton. 1173 | 1174 | */ 1175 | func disableButtonForTag (tag: Int) { 1176 | 1177 | if let button = (view?.viewWithTag(tag) as? UIButton) { 1178 | button.enabled = false 1179 | button.layer.borderColor = UIColor.lightGrayColor().CGColor 1180 | button.setTitleColor(UIColor.lightGrayColor(), forState: .Normal) 1181 | } 1182 | } 1183 | 1184 | 1185 | /** 1186 | 1187 | Disables the UILabel at the given tag. 1188 | 1189 | - parameter tag: The tag of the desired UILabel. 1190 | 1191 | */ 1192 | func disableLabelForTag (tag: Int) { 1193 | 1194 | if let label = (view?.viewWithTag(tag) as? UILabel) { 1195 | label.enabled = false 1196 | label.layer.borderColor = UIColor.lightGrayColor().CGColor 1197 | label.textColor = UIColor.lightGrayColor() 1198 | } 1199 | } 1200 | 1201 | 1202 | /** 1203 | 1204 | Disables the UISlider at the given tag. 1205 | 1206 | - parameter tag: The tag of the desired UISlider. 1207 | 1208 | */ 1209 | func disableSliderForTag (tag: Int) { 1210 | 1211 | if let slider = view?.viewWithTag(tag) as? UISlider { 1212 | if slider.userInteractionEnabled { 1213 | slider.userInteractionEnabled = false 1214 | slider.minimumTrackTintColor = UIColor.lightGrayColor() 1215 | slider.maximumTrackTintColor = UIColor.lightGrayColor() 1216 | 1217 | if let label = view?.viewWithTag(tag + 1) as? UILabel { 1218 | label.textColor = UIColor.lightGrayColor() 1219 | } 1220 | } 1221 | } 1222 | } 1223 | 1224 | 1225 | /** 1226 | 1227 | The distance between two points. 1228 | 1229 | - parameter firstPoint: The first point. 1230 | 1231 | - parameter secondPoint: The second Point. 1232 | 1233 | - returns: The distance between the two given points. 1234 | 1235 | */ 1236 | func distanceBetweenPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGFloat { 1237 | 1238 | let xDistance = secondPoint.x - firstPoint.x 1239 | let yDistance = secondPoint.y - firstPoint.y 1240 | 1241 | return sqrt(pow(xDistance, 2) + pow(yDistance, 2)) 1242 | } 1243 | 1244 | 1245 | /** 1246 | 1247 | Enables the UIButton at the given tag. 1248 | 1249 | - parameter tag: The tag of the desired UIButton. 1250 | 1251 | */ 1252 | func enableButtonForTag (tag: Int) { 1253 | 1254 | if let button = (view?.viewWithTag(tag) as? UIButton) { 1255 | button.enabled = true 1256 | button.layer.borderColor = blue.CGColor 1257 | button.setTitleColor(blue, forState: .Normal) 1258 | } 1259 | } 1260 | 1261 | 1262 | /** 1263 | 1264 | Enables the UILabel at the given tag. 1265 | 1266 | - parameter tag: The tag of the desired UILabel. 1267 | 1268 | */ 1269 | func enableLabelForTag (tag: Int) { 1270 | 1271 | if let label = (view?.viewWithTag(tag) as? UILabel) { 1272 | label.enabled = true 1273 | label.layer.borderColor = blue.CGColor 1274 | label.textColor = blue 1275 | } 1276 | } 1277 | 1278 | 1279 | /** 1280 | 1281 | Enables the UISlider at the given tag. 1282 | 1283 | - parameter tag: The tag of the desired UISlider. 1284 | 1285 | */ 1286 | func enableSliderForTag (tag: Int) { 1287 | 1288 | if let slider = view?.viewWithTag(tag) as? UISlider { 1289 | if slider.userInteractionEnabled == false { 1290 | slider.userInteractionEnabled = true 1291 | slider.minimumTrackTintColor = blue 1292 | slider.maximumTrackTintColor = UIColor.darkGrayColor() 1293 | 1294 | if let label = view?.viewWithTag(tag + 1) as? UILabel { 1295 | label.textColor = blue 1296 | } 1297 | } 1298 | } 1299 | } 1300 | 1301 | 1302 | /** 1303 | 1304 | Generates a random CGFloat between the given min and max. 1305 | 1306 | - parameter min: The lowest possible return value. 1307 | 1308 | - parameter max: The highest possible return value. 1309 | 1310 | - returns: A random CGFloat between the given min and max. 1311 | 1312 | */ 1313 | func randomCGFloat(min min: CGFloat, max: CGFloat) -> CGFloat { 1314 | 1315 | let random = CGFloat(Float(arc4random()) / 0xFFFFFFFF) 1316 | return random * (max - min) + min 1317 | } 1318 | 1319 | 1320 | /** 1321 | 1322 | Generates a random Float between the given min and max. 1323 | 1324 | - parameter min: The lowest possible return value. 1325 | 1326 | - parameter max: The highest possible return value. 1327 | 1328 | - returns: A random Float between the given min and max. 1329 | 1330 | */ 1331 | func randomFloat(min min: Float, max: Float) -> Float { 1332 | 1333 | let random = Float(arc4random()) / 0xFFFFFFFF 1334 | return random * (max - min) + min 1335 | } 1336 | 1337 | 1338 | /** 1339 | 1340 | Creates an array of the given number of random colors. 1341 | 1342 | - parameter numberOfColors: The number of colors that will be in the array. 1343 | 1344 | - returns: An array of the given number of random colors. 1345 | 1346 | */ 1347 | func randomColorArray(numberOfColors: Int) -> [UIColor] { 1348 | 1349 | var newColors = [UIColor]() 1350 | 1351 | for var i = 0; i < numberOfColors; i++ { 1352 | 1353 | let newColor = UIColor(hue: randomCGFloat(min: 0.0, max: 1.0), saturation: randomCGFloat(min: 0.33, max: 1.0), brightness: randomCGFloat(min: 0.75, max: 1.0), alpha: 1.0) 1354 | newColors.append(newColor) 1355 | } 1356 | 1357 | return newColors 1358 | } 1359 | 1360 | 1361 | /** 1362 | 1363 | Resets the currently selected BDGradientNode with the current settings. Note that only colors and locations are not updated in real-time. 1364 | 1365 | */ 1366 | func resetCurrentNode () { 1367 | 1368 | gradientNode.removeAllActions() 1369 | 1370 | locationsButtonStatus = "default" 1371 | 1372 | if let button = view?.viewWithTag(30) as? UIButton { button.setTitle("Animate: No", forState: .Normal) } 1373 | 1374 | switch gradientNode.gradientType { 1375 | 1376 | case "gamut": gamutGradientButtonPressed() 1377 | case "linear": linearGradientButtonPressed() 1378 | case "sweep": sweepGradientButtonPressed() 1379 | case "radial": radialGradientButtonPressed() 1380 | default: return 1381 | } 1382 | } 1383 | 1384 | 1385 | /** 1386 | 1387 | Rotates a point around (0.5, 0.5) by the given angle. 1388 | 1389 | - parameter point: The starting point. 1390 | 1391 | - parameter angle: The angle by which the point will be rotated in radians. 1392 | 1393 | - returns: A point rotated from the starting point, around (0.5, 0.5), by the given angle. 1394 | 1395 | */ 1396 | func rotatePoint (point: CGPoint, byAngle angle: CGFloat) -> CGPoint { 1397 | 1398 | let sinAngle = sin(angle) 1399 | let cosAngle = cos(angle) 1400 | 1401 | var newPoint = point 1402 | newPoint.x -= 0.5 1403 | newPoint.y -= 0.5 1404 | 1405 | newPoint.x = newPoint.x * cosAngle - newPoint.y * sinAngle 1406 | newPoint.y = newPoint.x * sinAngle + newPoint.y * cosAngle 1407 | 1408 | newPoint.x += 0.5 1409 | newPoint.y += 0.5 1410 | 1411 | return newPoint 1412 | } 1413 | 1414 | 1415 | /** 1416 | 1417 | A UIButton for a given tag. 1418 | 1419 | - parameter tag: The tag for the desired UIButton. 1420 | 1421 | - returns: The UIButton at the tag, if it exists; nil otherwise. 1422 | 1423 | */ 1424 | func buttonForTag (tag: Int) -> UIButton? { 1425 | 1426 | if let button = view?.viewWithTag(tag) as? UIButton { return button } 1427 | else { return nil } 1428 | } 1429 | } 1430 | -------------------------------------------------------------------------------- /BDGradientNodeDemo/GradientViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientViewController.swift 3 | // BDGradientNodeDemo 4 | // 5 | // Created by Braindrizzle Studio. 6 | // http://braindrizzlestudio.com 7 | // Copyright (c) 2015 Braindrizzle Studio. All rights reserved. 8 | // 9 | // This application is a demo of BDGradientNode. 10 | // 11 | 12 | import UIKit 13 | import SpriteKit 14 | 15 | 16 | class GradientViewController : UIViewController { 17 | 18 | override func viewDidLoad() { 19 | 20 | super.viewDidLoad() 21 | 22 | let skView = self.view as! SKView 23 | skView.ignoresSiblingOrder = true 24 | 25 | let scene = GradientScene() 26 | scene.size = self.view.bounds.size 27 | scene.scaleMode = .AspectFill 28 | 29 | skView.presentScene(scene) 30 | } 31 | 32 | override func prefersStatusBarHidden() -> Bool { 33 | 34 | return true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x", 7 | "filename" : "Icon-Small@2x.png" 8 | }, 9 | { 10 | "idiom" : "iphone", 11 | "size" : "29x29", 12 | "scale" : "3x", 13 | "filename" : "Icon-Small@3x.png" 14 | }, 15 | { 16 | "idiom" : "iphone", 17 | "size" : "40x40", 18 | "scale" : "2x", 19 | "filename" : "Icon-40@2x.png" 20 | }, 21 | { 22 | "idiom" : "iphone", 23 | "size" : "40x40", 24 | "scale" : "3x", 25 | "filename" : "Icon-40@3x.png" 26 | }, 27 | { 28 | "idiom" : "iphone", 29 | "size" : "60x60", 30 | "scale" : "2x", 31 | "filename" : "Icon-60@2x.png" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "3x", 37 | "filename" : "Icon-60@3x.png" 38 | }, 39 | { 40 | "idiom" : "ipad", 41 | "size" : "29x29", 42 | "scale" : "1x", 43 | "filename" : "Icon-Small.png" 44 | }, 45 | { 46 | "idiom" : "ipad", 47 | "size" : "29x29", 48 | "scale" : "2x", 49 | "filename" : "Icon-Small@2x.png" 50 | }, 51 | { 52 | "idiom" : "ipad", 53 | "size" : "40x40", 54 | "scale" : "1x", 55 | "filename" : "Icon-40.png" 56 | }, 57 | { 58 | "idiom" : "ipad", 59 | "size" : "40x40", 60 | "scale" : "2x", 61 | "filename" : "Icon-40@2x.png" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "76x76", 66 | "scale" : "1x", 67 | "filename" : "Icon-76.png" 68 | }, 69 | { 70 | "idiom" : "ipad", 71 | "size" : "76x76", 72 | "scale" : "2x", 73 | "filename" : "Icon-76@2x.png" 74 | } 75 | ], 76 | "info" : { 77 | "version" : 1, 78 | "author" : "makeappicon" 79 | } 80 | } -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braindrizzlestudio/BDGradientNode/99a45882dcf99e4f969e1441d8c713e5eca5a64c/BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-40.png -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braindrizzlestudio/BDGradientNode/99a45882dcf99e4f969e1441d8c713e5eca5a64c/BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braindrizzlestudio/BDGradientNode/99a45882dcf99e4f969e1441d8c713e5eca5a64c/BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-40@3x.png -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braindrizzlestudio/BDGradientNode/99a45882dcf99e4f969e1441d8c713e5eca5a64c/BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braindrizzlestudio/BDGradientNode/99a45882dcf99e4f969e1441d8c713e5eca5a64c/BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braindrizzlestudio/BDGradientNode/99a45882dcf99e4f969e1441d8c713e5eca5a64c/BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braindrizzlestudio/BDGradientNode/99a45882dcf99e4f969e1441d8c713e5eca5a64c/BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braindrizzlestudio/BDGradientNode/99a45882dcf99e4f969e1441d8c713e5eca5a64c/BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-Small.png -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braindrizzlestudio/BDGradientNode/99a45882dcf99e4f969e1441d8c713e5eca5a64c/BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braindrizzlestudio/BDGradientNode/99a45882dcf99e4f969e1441d8c713e5eca5a64c/BDGradientNodeDemo/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/Spaceship.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "Spaceship.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/Spaceship.imageset/Spaceship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braindrizzlestudio/BDGradientNode/99a45882dcf99e4f969e1441d8c713e5eca5a64c/BDGradientNodeDemo/Images.xcassets/Spaceship.imageset/Spaceship.png -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/gamutlogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "gamutlogo.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /BDGradientNodeDemo/Images.xcassets/gamutlogo.imageset/gamutlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braindrizzlestudio/BDGradientNode/99a45882dcf99e4f969e1441d8c713e5eca5a64c/BDGradientNodeDemo/Images.xcassets/gamutlogo.imageset/gamutlogo.png -------------------------------------------------------------------------------- /BDGradientNodeDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.braindrizzlestudio.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.3.1 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 6 23 | LSRequiresIPhoneOS 24 | 25 | PrefersOpenGL 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UIStatusBarHidden 36 | 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 braindrizzlestudio 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Braindrizzle Studio](http://braindrizzlestudio.com/images/logo/logo-overlay-orange-180.png "Braindrizzle Studio:tm:") 2 | 3 | # BDGradientNode 4 | #### Version: 1.3.1 5 | 6 | ### By: [Braindrizzle Studio:tm:](http://braindrizzlestudio.com) 7 | 8 | 9 | #### What is it? 10 | 11 | BDGradientNode is a subclass of SKSpriteNode for adding gradients to textures. This is normally a surprisingly convoluted process! (More on that in the Background section below.) With BDGradientNode you can easily add linear, radial, and sweep gradients--blended or not--to any texture. 12 | 13 | 14 | ## Documentation 15 | 16 | I have tried to provide thorough documentation in the BDGradientNode.swift file. The documentation for use is provided in this readme. 17 | 18 | 19 | ## Installation 20 | 21 | Simply copy BDGradientNode.swift into your project. Done! 22 | Note: the project requires Swift 2. 23 | 24 | 25 | ## Use 26 | 27 | 1. Instantiate a BDGradientNode. 28 | 29 | 2. Enjoy your BDGradientNode! 30 | 31 | NOTE: Due to a change in Swift 2 / iOS 9, you are now required to add the line PrefersOpenGL = YES to your info.plist 32 | 33 | 34 | ## Examples 35 | 36 | The demo app code will serve as the most useful example, but here are the basics. 37 | 38 | #### Instantiation 39 | 40 | This will produce the blue cone in the screenshot. 41 | ```swift 42 | let color1 = UIColor(hue: 230/360, saturation: 1.0, brightness: 1.0, alpha: 1.0) 43 | let color2 = UIColor(hue: 210/360, saturation: 1.0, brightness: 1.0, alpha: 1.0) 44 | let color3 = UIColor(hue: 190/360, saturation: 1.0, brightness: 1.0, alpha: 1.0) 45 | let colors = [color1, color2, color3] 46 | 47 | let blending : Float = 1.0 48 | 49 | let firstCenter = CGPoint(x: 0.2, y: 0.2) 50 | let firstRadius : Float = 0.1 51 | 52 | let secondCenter = CGPoint(x: 0.5, y: 0.5) 53 | let secondRadius : Float = 0.4 54 | 55 | let nodeSize = CGSize(width: self.size.width, height: self.size.width) 56 | 57 | let texture = SKTexture(imageNamed: "dummypixel") 58 | 59 | let myGradientNode = BDGradientNode(radialGradientWithTexture: texture, colors: colors, locations: nil, firstCenter: firstCenter, firstRadius: firstRadius, secondCenter: secondCenter, secondRadius: secondRadius, blending: blending, discardOutsideGradient: true, keepTextureShape: false, size: nodeSize) 60 | 61 | addChild(myGradientNode) 62 | ``` 63 | 64 | ![Radial Gradient](http://braindrizzlestudio.com/images/other/example-radial.jpg "Radial Gradient") 65 | 66 | This will instantiate the linear gradient of 3 colors, blended with the Spacehip, in the screenshot. 67 | ```swift 68 | let color1 = UIColor(hue: 0/360, saturation: 1.0, brightness: 1.0, alpha: 1.0) 69 | let color2 = UIColor(hue: 0/360, saturation: 0.0, brightness: 1.0, alpha: 1.0) 70 | let color3 = UIColor(hue: 220/360, saturation: 1.0, brightness: 1.0, alpha: 1.0) 71 | let colors = [color1, color2, color3] 72 | 73 | let blending = 0.5 74 | 75 | let location1 : CGFloat = 0.5 76 | let locations : [CGFloat] = [location1] 77 | 78 | let startPoint = CGPoint(x: 0.3, y: 0.0) 79 | let endPoint = CGPoint(x: 0.6, y: 0.8) 80 | 81 | let size = CGSize(width: self.size.width, height: self.size.width) 82 | 83 | let texture = SKTexture(imageNamed: "Spaceship") 84 | 85 | let myGradientNode = BDGradientNode(linearGradientWithTexture: texture, colors: colors, locations: nil, startPoint: startPoint, endPoint: endPoint, blending: blending, keepTextureShape: true, size: nodeSize) 86 | ``` 87 | 88 | ![Linear Gradient](http://braindrizzlestudio.com/images/other/example-linear.jpg "Linear Gradient") 89 | 90 | 91 | #### Animation 92 | 93 | After instantiating the BDGradientNode you can simply change the properties of that type of node while your program is running and the gradient will change in real-time. This makes animation very simple. The only limitation is changing the number of colors/locations; the colors and locations themselves can be swapped by passing new arrays during runtime, but the arrays have to be the same size as the BDGradientNode you instantiated. (More on this in the Limitations section below.) 94 | 95 | To use SKActions to animate you can use runBlock, as in the demo app code. 96 | 97 | ```swift 98 | func gamutAnimation () { 99 | 100 | self.gradientNode.center.x += 0.001 101 | self.gradientNode.center.y += 0.001 102 | 103 | let angleAction = SKAction.runBlock { 104 | 105 | self.gradientNode.startAngle = (self.gradientNode.startAngle + 0.1) % Float(2 * M_PI) 106 | if let slider = self.view?.viewWithTag(50) as? UISlider { 107 | slider.value = self.gradientNode.startAngle 108 | } 109 | } 110 | 111 | let centerAction = SKAction.runBlock { 112 | 113 | let angle : CGFloat = 0.03 114 | self.gradientNode.center = self.rotatePoint(self.gradientNode.center, byAngle: angle) 115 | 116 | let multiplier = CGFloat(sin(self.gradientNode.startAngle) / 100) 117 | var normalizedPoint = self.gradientNode.center 118 | normalizedPoint.x -= 0.5 119 | normalizedPoint.y -= 0.5 120 | let length = sqrt(pow(normalizedPoint.x, 2) + pow(normalizedPoint.y, 2)) 121 | normalizedPoint.x /= length 122 | normalizedPoint.y /= length 123 | 124 | self.gradientNode.center.x += multiplier * normalizedPoint.x 125 | self.gradientNode.center.y -= multiplier * normalizedPoint.y 126 | } 127 | 128 | let radiusAction = SKAction.runBlock { 129 | 130 | let change = Float(sin(self.angleOfPoint(self.gradientNode.center))) / 100 131 | self.gradientNode.radius += change 132 | if let slider = self.view?.viewWithTag(52) as? UISlider { 133 | slider.value = self.gradientNode.radius 134 | } 135 | } 136 | 137 | let delayAction = SKAction.waitForDuration(0.05) 138 | 139 | let actionGroup = SKAction.group([angleAction, centerAction, radiusAction, delayAction]) 140 | 141 | gradientNode.runAction(SKAction.repeatActionForever(actionGroup)) 142 | } 143 | ``` 144 | 145 | 146 | #### Tips 147 | 148 | - To have gradients with arbitrary shapes simply pass a texture with your shape and set keepTextureShape to true. 149 | 150 | - You can set one of the radial gradient radii to a negative number to get a double-cone effect. 151 | 152 | - The radial gradient is by far the most complex of the gradients; it's also the least performant during instantiation. Keep that in mind! 153 | 154 | - Linear and radial gradients look much better with colors that are similar. Once we switch the linear interpolation for something better, they'll look better with disparate colors. 155 | 156 | - If you're not going to blend with a texture: make a dummy pixel and stretch it to the size you want with the BDGradientNode 'size' parameter. 157 | 158 | 159 | #### Important Note 160 | 161 | - This is in the initializer documentation, but it's worth emphasizing: all points and radii are specified in Apple's normalized coordinate system, with (0, 0) at the bottom left and (1, 1) at the top right. 162 | 163 | 164 | ## Background 165 | 166 | We wrote BDGradientNode because gradients are a bit of a pain to set up on the fly in SpriteKit. UIViews are CALayer-backed and have access to CAGradientLayer, making them simple--fairly simple--to make. (Though sweep gradients can't be done.) SKNodes are NOT layer-backed. While you can jump through some hoops to make images, and so textures, from layers that can be assigned to an SKSpriteNode (which is what we first did when we just needed a simple gradient in SpriteKit) it's both cumbersome and inflexible. 167 | 168 | 169 | ## Limitations 170 | 171 | - The current version is limited only in not being able to adjust the number of colors/locations without re-initializing a BDGradientNode. 172 | 173 | - The shaders use high precision floats--not the most performant setting. But while on the iOS Simulator everything works with lower precisions, on hardware there can be artifacts in certain conditions. You can, of course, change the shaders for your own applications. Make sure you also change the shader-constructor's 'stringRange' instances to match, if required. 174 | 175 | 176 | #### A Note on the Gamut Gradient 177 | 178 | The gamut gradient uses the gamut of the HSB spectrum rather than requiring specified colors and locations. We made shaders for gamut versions of the linear and radial gradients too, but the linear interpolation of GLSL mix() ruined the smooth effect in those cases--they looked no better than using an array of red, purple, blue, etc.. Once we change out the GLSL mix() for something better we'll look at doing linear and radial gamuts again. 179 | 180 | 181 | ## The Demo 182 | 183 | The demo app was created solely to give an idea of what you can easily do with BDGradient Node. Make sure you play with all the settings, touch the image, and watch the animations! Try changing the settings, including with image-touches for the gradients that take them, while the animations are running! 184 | 185 | Note: the UI of the demo was not designed to fit on iPhone 4S. 186 | 187 | 188 | ## Coming Updates 189 | 190 | We have plans for version 2.0; it will be released in 2016. The biggest change will be switching from the linear interpolation of GLSL mix() to something a bit fancier, as long as it's reasonably performant. We hope to receive suggestions from you, too! Alternatively, we're looking into switching from OpenGL to Metal, as that's now the default setting for apps (which is why the new info.plist setting is required). 191 | 192 | 193 | ## Changelog 194 | 195 | ### 1.3.1 196 | - Due to changes in Swift 2 / iOS 9, a small change was made to the GLSL code 197 | - Added NOTE to users that they now must add PrefersOpenGL = YES to info.plist 198 | 199 | ### 1.3 200 | - Updated syntax for iOS 9 and Swift 2 201 | 202 | ### 1.2 203 | - Added the ability to vary blending percentage, rather than just having it off or on. 204 | - Added the option to keep or discard the texture colors outside of the gradient. For example: suppose you have a radial gradient; you'll have the option of whether or not to show the texture through the hole in the middle. 205 | 206 | ### 1.1 207 | - Switched colors and locations to uniforms, adding the ability to change the colors and locations of gradients on the fly (i.e. without initializing a new BDGradientNode). 208 | 209 | 210 | ## Contact Us! 211 | 212 | We'd really like to have feedback and suggestions. Please feel free to do so here on GitHub. 213 | 214 | Better yet, email support@braindrizzlestudio.com 215 | 216 | or visit us at http://www.braindrizzlestudio.com 217 | --------------------------------------------------------------------------------