├── .gitignore ├── .swift-version ├── .travis.yml ├── Example ├── AppDelegate.swift ├── Compatibility.swift ├── Example.xcodeproj │ └── project.pbxproj ├── Images.xcassets │ ├── Contents.json │ └── randomButton.imageset │ │ ├── Contents.json │ │ └── imageButton.png ├── Info.plist ├── Main.storyboard ├── OMTextLayer │ └── OMTextLayer.swift ├── OMView.swift ├── OShadingGradientLayerViewController.swift └── UIBezierPath+Polygon.swift ├── LICENSE ├── OShadingGradientLayer.podspec ├── OShadingGradientLayer.xcodeproj ├── OMShadingGradientLayerTests_Info.plist ├── OMShadingGradientLayer_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ └── OMShadingGradientLayer-Package.xcscheme ├── Package.swift ├── README.md ├── ScreenShot ├── ScreenShot_1.png ├── ScreenShot_2.png └── ScreenShot_3.png ├── Sources └── OShadingGradientLayer │ ├── Categories │ ├── CALayer+AnimationKeyPath.swift │ ├── CGAffineTransform+Extensions.swift │ ├── CGColorSpace+Extension.swift │ ├── CGFloat+Extensions.swift │ ├── CGFloat+Math.swift │ ├── CGPoint+Extension.swift │ ├── CGSize+Math.swift │ ├── Double+Math.swift │ ├── Float+Extensions.swift │ ├── Float+Math.swift │ ├── UIColor+Extensions.swift │ └── UIColor+Interpolation.swift │ ├── GradientBaseLayer.swift │ ├── GradientLayerProtocols.swift │ ├── Log │ └── Log.swift │ ├── Math │ ├── Easing.swift │ ├── Interpolation.swift │ └── Math.swift │ ├── OShadingGradientLayer.swift │ └── ShadingGradient.swift ├── Tests ├── LinuxMain.swift └── OMShadingGradientLayerTests │ ├── OMShadingGradientLayerTests.swift │ └── XCTestManifests.swift └── gif └── gif.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/screenshots 64 | Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata 65 | Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist 66 | ExampleText/ExampleText.xcodeproj/project.xcworkspace/contents.xcworkspacedata 67 | ExampleText/ExampleText.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist 68 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode8 3 | branches: 4 | only: 5 | - master 6 | env: 7 | - LC_CTYPE=en_US.UTF-8 LANG=en_US.UTF-8 8 | script: 9 | - set -o pipefail 10 | before_script: 11 | - gem install xcpretty 12 | 13 | script: 14 | - xcodebuild -project Example/Example.xcodeproj -scheme Example -sdk iphonesimulator test | xcpretty -c 15 | - pod lib lint --quick -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Copyright 2015 - Jorge Ouahbi 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import UIKit 19 | 20 | @UIApplicationMain 21 | class AppDelegate: UIResponder, UIApplicationDelegate { 22 | 23 | var window: UIWindow? 24 | 25 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 26 | return true 27 | } 28 | 29 | func applicationWillResignActive(_ application: UIApplication) { 30 | // 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. 31 | // 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. 32 | } 33 | 34 | func applicationDidEnterBackground(_ application: UIApplication) { 35 | // 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. 36 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 37 | } 38 | 39 | func applicationWillEnterForeground(_ application: UIApplication) { 40 | // 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. 41 | } 42 | 43 | func applicationDidBecomeActive(_ application: UIApplication) { 44 | // 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. 45 | } 46 | 47 | func applicationWillTerminate(_ application: UIApplication) { 48 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 49 | } 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Example/Compatibility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Compatibility.swift 3 | // 4 | // Created by Jorge Ouahbi on 12/9/16. 5 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 6 | // 7 | 8 | // Based on Erica Sadun code 9 | // https://gist.github.com/erica/c54826fd3411d6db053bfdfe1f64ab54 10 | 11 | import Foundation 12 | 13 | 14 | #if os(OSX) 15 | import Cocoa 16 | public typealias BezierPath = NSBezierPath 17 | public typealias ViewController = NSViewController 18 | public typealias View = NSView 19 | public typealias Image = NSImage 20 | public typealias Font = NSFont 21 | public typealias GestureRecognizer = NSGestureRecognizer 22 | public typealias TapRecognizer = NSClickGestureRecognizer 23 | public typealias PanRecognizer = NSPanGestureRecognizer 24 | public typealias Button = NSButton 25 | #else 26 | import UIKit 27 | public typealias BezierPath = UIBezierPath 28 | public typealias ViewController = UIViewController 29 | public typealias View = UIView 30 | public typealias Image = UIImage 31 | public typealias Font = UIFont 32 | public typealias GestureRecognizer = UIGestureRecognizer 33 | public typealias TapRecognizer = UITapGestureRecognizer 34 | public typealias PanRecognizer = UIPanGestureRecognizer 35 | public typealias Button = UIButton 36 | #endif 37 | 38 | 39 | #if os(OSX) 40 | // UIKit Compatibility 41 | extension NSBezierPath { 42 | open func addLine(to point: CGPoint) { 43 | self.line(to: point) 44 | } 45 | 46 | open func addCurve(to point: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint) { 47 | self.curve(to: point, controlPoint1: controlPoint1, controlPoint2: controlPoint2) 48 | } 49 | 50 | open func addQuadCurve(to point: CGPoint, controlPoint: CGPoint) { 51 | self.curve(to: point, controlPoint1: controlPoint, controlPoint2: controlPoint) 52 | } 53 | } 54 | #endif 55 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3506A6881D9BD61F001C97BF /* UIBezierPath+Polygon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3506A6871D9BD61F001C97BF /* UIBezierPath+Polygon.swift */; }; 11 | 3506A68A1D9BD625001C97BF /* Compatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3506A6891D9BD625001C97BF /* Compatibility.swift */; }; 12 | 35F0C59D1CC6D53B0076B308 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F0C5951CC6D53B0076B308 /* AppDelegate.swift */; }; 13 | 35F0C59E1CC6D53B0076B308 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 35F0C5991CC6D53B0076B308 /* Images.xcassets */; }; 14 | 35F0C5A01CC6D53B0076B308 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 35F0C59B1CC6D53B0076B308 /* Main.storyboard */; }; 15 | 35F0C5A11CC6D53B0076B308 /* OShadingGradientLayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F0C59C1CC6D53B0076B308 /* OShadingGradientLayerViewController.swift */; }; 16 | D1073FFD2521741100953986 /* OMTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1073FFC2521741100953986 /* OMTextLayer.swift */; }; 17 | D1074001252175EC00953986 /* OMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1074000252175EC00953986 /* OMView.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 3506A6871D9BD61F001C97BF /* UIBezierPath+Polygon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIBezierPath+Polygon.swift"; sourceTree = ""; }; 22 | 3506A6891D9BD625001C97BF /* Compatibility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Compatibility.swift; sourceTree = ""; }; 23 | 35F0C5951CC6D53B0076B308 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | 35F0C5991CC6D53B0076B308 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 25 | 35F0C59A1CC6D53B0076B308 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 26 | 35F0C59B1CC6D53B0076B308 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; 27 | 35F0C59C1CC6D53B0076B308 /* OShadingGradientLayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OShadingGradientLayerViewController.swift; sourceTree = ""; }; 28 | D1073FFC2521741100953986 /* OMTextLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OMTextLayer.swift; sourceTree = ""; }; 29 | D1074000252175EC00953986 /* OMView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OMView.swift; sourceTree = ""; }; 30 | D1AD4B2B25EDA26500D1A95C /* OShadingGradientLayer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OShadingGradientLayer.xcodeproj; path = ../OShadingGradientLayer.xcodeproj; sourceTree = ""; }; 31 | D247910F1A36BB95007AC991 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | /* End PBXFileReference section */ 33 | 34 | /* Begin PBXFrameworksBuildPhase section */ 35 | D247910C1A36BB95007AC991 /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 2147483647; 38 | files = ( 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | 35F0C5941CC6D53B0076B308 /* Example */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | D1074000252175EC00953986 /* OMView.swift */, 49 | D1073FFB2521741100953986 /* OMTextLayer */, 50 | 3506A6891D9BD625001C97BF /* Compatibility.swift */, 51 | 3506A6871D9BD61F001C97BF /* UIBezierPath+Polygon.swift */, 52 | 35F0C59C1CC6D53B0076B308 /* OShadingGradientLayerViewController.swift */, 53 | 35F0C5951CC6D53B0076B308 /* AppDelegate.swift */, 54 | 35F0C5991CC6D53B0076B308 /* Images.xcassets */, 55 | 35F0C59A1CC6D53B0076B308 /* Info.plist */, 56 | 35F0C59B1CC6D53B0076B308 /* Main.storyboard */, 57 | ); 58 | name = Example; 59 | sourceTree = ""; 60 | }; 61 | D1073FFB2521741100953986 /* OMTextLayer */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | D1073FFC2521741100953986 /* OMTextLayer.swift */, 65 | ); 66 | path = OMTextLayer; 67 | sourceTree = ""; 68 | }; 69 | D1AD4B3E25EDA27A00D1A95C /* Frameworks */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | ); 73 | name = Frameworks; 74 | sourceTree = ""; 75 | }; 76 | D24791061A36BB95007AC991 = { 77 | isa = PBXGroup; 78 | children = ( 79 | D1AD4B2B25EDA26500D1A95C /* OShadingGradientLayer.xcodeproj */, 80 | 35F0C5941CC6D53B0076B308 /* Example */, 81 | D24791101A36BB95007AC991 /* Products */, 82 | D1AD4B3E25EDA27A00D1A95C /* Frameworks */, 83 | ); 84 | sourceTree = ""; 85 | }; 86 | D24791101A36BB95007AC991 /* Products */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | D247910F1A36BB95007AC991 /* Example.app */, 90 | ); 91 | name = Products; 92 | sourceTree = ""; 93 | }; 94 | /* End PBXGroup section */ 95 | 96 | /* Begin PBXNativeTarget section */ 97 | D247910E1A36BB95007AC991 /* Example */ = { 98 | isa = PBXNativeTarget; 99 | buildConfigurationList = D247912E1A36BB95007AC991 /* Build configuration list for PBXNativeTarget "Example" */; 100 | buildPhases = ( 101 | D247910B1A36BB95007AC991 /* Sources */, 102 | D247910C1A36BB95007AC991 /* Frameworks */, 103 | D247910D1A36BB95007AC991 /* Resources */, 104 | ); 105 | buildRules = ( 106 | ); 107 | dependencies = ( 108 | ); 109 | name = Example; 110 | productName = ExampleSwift; 111 | productReference = D247910F1A36BB95007AC991 /* Example.app */; 112 | productType = "com.apple.product-type.application"; 113 | }; 114 | /* End PBXNativeTarget section */ 115 | 116 | /* Begin PBXProject section */ 117 | D24791071A36BB95007AC991 /* Project object */ = { 118 | isa = PBXProject; 119 | attributes = { 120 | LastSwiftUpdateCheck = 0720; 121 | LastUpgradeCheck = 1200; 122 | ORGANIZATIONNAME = "Jorge Ouahbi"; 123 | TargetAttributes = { 124 | D247910E1A36BB95007AC991 = { 125 | CreatedOnToolsVersion = 6.1.1; 126 | DevelopmentTeam = 9LCWBZHZFL; 127 | LastSwiftMigration = 1100; 128 | ProvisioningStyle = Automatic; 129 | }; 130 | }; 131 | }; 132 | buildConfigurationList = D247910A1A36BB95007AC991 /* Build configuration list for PBXProject "Example" */; 133 | compatibilityVersion = "Xcode 3.2"; 134 | developmentRegion = en; 135 | hasScannedForEncodings = 0; 136 | knownRegions = ( 137 | en, 138 | Base, 139 | ); 140 | mainGroup = D24791061A36BB95007AC991; 141 | productRefGroup = D24791101A36BB95007AC991 /* Products */; 142 | projectDirPath = ""; 143 | projectRoot = ""; 144 | targets = ( 145 | D247910E1A36BB95007AC991 /* Example */, 146 | ); 147 | }; 148 | /* End PBXProject section */ 149 | 150 | /* Begin PBXResourcesBuildPhase section */ 151 | D247910D1A36BB95007AC991 /* Resources */ = { 152 | isa = PBXResourcesBuildPhase; 153 | buildActionMask = 2147483647; 154 | files = ( 155 | 35F0C5A01CC6D53B0076B308 /* Main.storyboard in Resources */, 156 | 35F0C59E1CC6D53B0076B308 /* Images.xcassets in Resources */, 157 | ); 158 | runOnlyForDeploymentPostprocessing = 0; 159 | }; 160 | /* End PBXResourcesBuildPhase section */ 161 | 162 | /* Begin PBXSourcesBuildPhase section */ 163 | D247910B1A36BB95007AC991 /* Sources */ = { 164 | isa = PBXSourcesBuildPhase; 165 | buildActionMask = 2147483647; 166 | files = ( 167 | 3506A68A1D9BD625001C97BF /* Compatibility.swift in Sources */, 168 | 35F0C59D1CC6D53B0076B308 /* AppDelegate.swift in Sources */, 169 | D1074001252175EC00953986 /* OMView.swift in Sources */, 170 | 35F0C5A11CC6D53B0076B308 /* OShadingGradientLayerViewController.swift in Sources */, 171 | D1073FFD2521741100953986 /* OMTextLayer.swift in Sources */, 172 | 3506A6881D9BD61F001C97BF /* UIBezierPath+Polygon.swift in Sources */, 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | /* End PBXSourcesBuildPhase section */ 177 | 178 | /* Begin XCBuildConfiguration section */ 179 | D247912C1A36BB95007AC991 /* Debug */ = { 180 | isa = XCBuildConfiguration; 181 | buildSettings = { 182 | ALWAYS_SEARCH_USER_PATHS = NO; 183 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 184 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 185 | CLANG_CXX_LIBRARY = "libc++"; 186 | CLANG_ENABLE_MODULES = YES; 187 | CLANG_ENABLE_OBJC_ARC = YES; 188 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 189 | CLANG_WARN_BOOL_CONVERSION = YES; 190 | CLANG_WARN_COMMA = YES; 191 | CLANG_WARN_CONSTANT_CONVERSION = YES; 192 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 193 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 194 | CLANG_WARN_EMPTY_BODY = YES; 195 | CLANG_WARN_ENUM_CONVERSION = YES; 196 | CLANG_WARN_INFINITE_RECURSION = YES; 197 | CLANG_WARN_INT_CONVERSION = YES; 198 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 199 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 200 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 201 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 202 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 203 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 204 | CLANG_WARN_STRICT_PROTOTYPES = YES; 205 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 206 | CLANG_WARN_UNREACHABLE_CODE = YES; 207 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 208 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 209 | COPY_PHASE_STRIP = NO; 210 | ENABLE_STRICT_OBJC_MSGSEND = YES; 211 | ENABLE_TESTABILITY = YES; 212 | FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/**"; 213 | GCC_C_LANGUAGE_STANDARD = gnu99; 214 | GCC_DYNAMIC_NO_PIC = NO; 215 | GCC_NO_COMMON_BLOCKS = YES; 216 | GCC_OPTIMIZATION_LEVEL = 0; 217 | GCC_PREPROCESSOR_DEFINITIONS = ( 218 | "DEBUG=1", 219 | "$(inherited)", 220 | ); 221 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 222 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 223 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 224 | GCC_WARN_UNDECLARED_SELECTOR = YES; 225 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 226 | GCC_WARN_UNUSED_FUNCTION = YES; 227 | GCC_WARN_UNUSED_VARIABLE = YES; 228 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 229 | MTL_ENABLE_DEBUG_INFO = YES; 230 | ONLY_ACTIVE_ARCH = YES; 231 | SDKROOT = iphoneos; 232 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 233 | SWIFT_VERSION = 5.0; 234 | TARGETED_DEVICE_FAMILY = "1,2"; 235 | }; 236 | name = Debug; 237 | }; 238 | D247912D1A36BB95007AC991 /* Release */ = { 239 | isa = XCBuildConfiguration; 240 | buildSettings = { 241 | ALWAYS_SEARCH_USER_PATHS = NO; 242 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 244 | CLANG_CXX_LIBRARY = "libc++"; 245 | CLANG_ENABLE_MODULES = YES; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 248 | CLANG_WARN_BOOL_CONVERSION = YES; 249 | CLANG_WARN_COMMA = YES; 250 | CLANG_WARN_CONSTANT_CONVERSION = YES; 251 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 252 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 253 | CLANG_WARN_EMPTY_BODY = YES; 254 | CLANG_WARN_ENUM_CONVERSION = YES; 255 | CLANG_WARN_INFINITE_RECURSION = YES; 256 | CLANG_WARN_INT_CONVERSION = YES; 257 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 258 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 259 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 261 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 262 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 263 | CLANG_WARN_STRICT_PROTOTYPES = YES; 264 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 265 | CLANG_WARN_UNREACHABLE_CODE = YES; 266 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 267 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 268 | COPY_PHASE_STRIP = YES; 269 | ENABLE_NS_ASSERTIONS = NO; 270 | ENABLE_STRICT_OBJC_MSGSEND = YES; 271 | FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/**"; 272 | GCC_C_LANGUAGE_STANDARD = gnu99; 273 | GCC_NO_COMMON_BLOCKS = YES; 274 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 275 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 276 | GCC_WARN_UNDECLARED_SELECTOR = YES; 277 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 278 | GCC_WARN_UNUSED_FUNCTION = YES; 279 | GCC_WARN_UNUSED_VARIABLE = YES; 280 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 281 | MTL_ENABLE_DEBUG_INFO = NO; 282 | SDKROOT = iphoneos; 283 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 284 | SWIFT_VERSION = 5.0; 285 | TARGETED_DEVICE_FAMILY = "1,2"; 286 | VALIDATE_PRODUCT = YES; 287 | }; 288 | name = Release; 289 | }; 290 | D247912F1A36BB95007AC991 /* Debug */ = { 291 | isa = XCBuildConfiguration; 292 | buildSettings = { 293 | CLANG_ENABLE_MODULES = YES; 294 | CODE_SIGN_IDENTITY = "Apple Development"; 295 | CODE_SIGN_STYLE = Automatic; 296 | DEVELOPMENT_TEAM = 9LCWBZHZFL; 297 | FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/**"; 298 | INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; 299 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 300 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 301 | PRODUCT_BUNDLE_IDENTIFIER = jom.Example; 302 | PRODUCT_NAME = Example; 303 | PROVISIONING_PROFILE_SPECIFIER = ""; 304 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 305 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 306 | SWIFT_VERSION = 5.0; 307 | }; 308 | name = Debug; 309 | }; 310 | D24791301A36BB95007AC991 /* Release */ = { 311 | isa = XCBuildConfiguration; 312 | buildSettings = { 313 | CLANG_ENABLE_MODULES = YES; 314 | CODE_SIGN_IDENTITY = "Apple Development"; 315 | CODE_SIGN_STYLE = Automatic; 316 | DEVELOPMENT_TEAM = 9LCWBZHZFL; 317 | FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/**"; 318 | INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; 319 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 320 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 321 | PRODUCT_BUNDLE_IDENTIFIER = jom.Example; 322 | PRODUCT_NAME = Example; 323 | PROVISIONING_PROFILE_SPECIFIER = ""; 324 | SWIFT_SWIFT3_OBJC_INFERENCE = On; 325 | SWIFT_VERSION = 5.0; 326 | }; 327 | name = Release; 328 | }; 329 | /* End XCBuildConfiguration section */ 330 | 331 | /* Begin XCConfigurationList section */ 332 | D247910A1A36BB95007AC991 /* Build configuration list for PBXProject "Example" */ = { 333 | isa = XCConfigurationList; 334 | buildConfigurations = ( 335 | D247912C1A36BB95007AC991 /* Debug */, 336 | D247912D1A36BB95007AC991 /* Release */, 337 | ); 338 | defaultConfigurationIsVisible = 0; 339 | defaultConfigurationName = Release; 340 | }; 341 | D247912E1A36BB95007AC991 /* Build configuration list for PBXNativeTarget "Example" */ = { 342 | isa = XCConfigurationList; 343 | buildConfigurations = ( 344 | D247912F1A36BB95007AC991 /* Debug */, 345 | D24791301A36BB95007AC991 /* Release */, 346 | ); 347 | defaultConfigurationIsVisible = 0; 348 | defaultConfigurationName = Release; 349 | }; 350 | /* End XCConfigurationList section */ 351 | }; 352 | rootObject = D24791071A36BB95007AC991 /* Project object */; 353 | } 354 | -------------------------------------------------------------------------------- /Example/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Images.xcassets/randomButton.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "imageButton.png", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Images.xcassets/randomButton.imageset/imageButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaouahbi/OShadingGradientLayer/dbe3723a7e9a80d0dc4f8518d49dcc6e41773f2c/Example/Images.xcassets/randomButton.imageset/imageButton.png -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 0 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Example/OMTextLayer/OMTextLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015 - Jorge Ouahbi 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | 18 | // 19 | // OMTextLayer.swift 20 | // 21 | // Created by Jorge Ouahbi on 23/3/15. 22 | // 23 | // Description: 24 | // CALayer derived class that uses CoreText for draw a text. 25 | // 26 | // Versión 0.1 (29-3-2015) 27 | // Creation. 28 | // Versión 0.11 (29-3-2015) 29 | // Replaced paragraphStyle by a array of CTParagraphStyleSetting. 30 | // Versión 0.11 (15-5-2015) 31 | // Added font ligature 32 | // Versión 0.12 (1-9-2016) 33 | // Added font underline 34 | // Versión 0.12 (6-9-2016) 35 | // Updated to swift 3.0 36 | // Versión 0.13 (22-9-2016) 37 | // Added code so that the text can follow an angle with a certain radius (Based on ArcTextView example by Apple) 38 | // Versión 0.14 (25-9-2016) 39 | // Added text to path helper function 40 | // Versión 0.15 (30-11-2016) 41 | // Added vertical alignment 42 | 43 | 44 | #if os(iOS) 45 | import UIKit 46 | #elseif os(OSX) 47 | import AppKit 48 | #endif 49 | 50 | import CoreGraphics 51 | import CoreText 52 | import CoreFoundation 53 | import LibControl 54 | 55 | @objc public class OMTextLayer : CALayer 56 | { 57 | // MARK: properties 58 | 59 | fileprivate(set) var fontRef:CTFont = CTFontCreateWithName("Helvetica" as CFString, 12.0, nil); 60 | 61 | 62 | var verticalCeter : Bool = true { 63 | didSet { 64 | setNeedsDisplay() 65 | } 66 | } 67 | var angleLenght : Double? { 68 | didSet { 69 | setNeedsDisplay() 70 | } 71 | } 72 | var radiusRatio : CGFloat = 0.0 { 73 | didSet { 74 | radiusRatio = clamp(radiusRatio, lowerValue: 0, upperValue: 1.0) 75 | setNeedsDisplay() 76 | } 77 | } 78 | 79 | var underlineColor : UIColor? { 80 | didSet { 81 | setNeedsDisplay() 82 | } 83 | } 84 | 85 | var underlineStyle : CTUnderlineStyle = CTUnderlineStyle() { 86 | didSet { 87 | setNeedsDisplay() 88 | } 89 | } 90 | 91 | /// default 1: default ligatures, 0: no ligatures, 2: all ligatures 92 | 93 | var fontLigature:NSNumber = NSNumber(value: 1 as Int32) { 94 | didSet { 95 | setNeedsDisplay() 96 | } 97 | } 98 | 99 | var fontStrokeColor:UIColor = UIColor.lightGray { 100 | didSet { 101 | setNeedsDisplay() 102 | } 103 | } 104 | 105 | var fontStrokeWidth:Float = -3 { 106 | didSet { 107 | setNeedsDisplay() 108 | } 109 | } 110 | 111 | var string : String? = nil { 112 | didSet { 113 | setNeedsDisplay() 114 | } 115 | } 116 | 117 | var foregroundColor:UIColor = UIColor.black { 118 | didSet { 119 | setNeedsDisplay() 120 | } 121 | } 122 | 123 | var fontBackgroundColor:UIColor = UIColor.clear { 124 | didSet{ 125 | setNeedsDisplay() 126 | } 127 | } 128 | 129 | var lineBreakMode:CTLineBreakMode = .byCharWrapping { 130 | didSet{ 131 | setNeedsDisplay() 132 | } 133 | } 134 | 135 | var alignment: CTTextAlignment = CTTextAlignment.center { 136 | didSet{ 137 | setNeedsDisplay() 138 | } 139 | } 140 | 141 | var font: UIFont? = nil { 142 | didSet{ 143 | if let font = font { 144 | setFont(font, matrix: nil) 145 | } 146 | setNeedsDisplay() 147 | } 148 | } 149 | 150 | // MARK: constructors 151 | 152 | override init() { 153 | super.init() 154 | 155 | self.contentsScale = UIScreen.main.scale 156 | self.needsDisplayOnBoundsChange = true; 157 | 158 | // https://github.com/danielamitay/iOS-App-Performance-Cheatsheet/blob/master/QuartzCore.md 159 | 160 | //self.shouldRasterize = true 161 | //self.rasterizationScale = UIScreen.main.scale 162 | self.drawsAsynchronously = true 163 | self.allowsGroupOpacity = false 164 | 165 | } 166 | 167 | convenience init(string : String, alignmentMode:String = "center") { 168 | self.init() 169 | setAlignmentMode(alignmentMode) 170 | self.string = string 171 | } 172 | 173 | 174 | convenience init(string : String, font:UIFont ,alignmentMode:String = "center") { 175 | self.init() 176 | setAlignmentMode(alignmentMode) 177 | self.string = string 178 | setFont(font, matrix: nil) 179 | } 180 | 181 | override init(layer: Any) { 182 | super.init(layer: layer) 183 | if let other = layer as? OMTextLayer { 184 | self.string = other.string 185 | self.fontRef = other.fontRef 186 | self.fontStrokeColor = other.fontStrokeColor 187 | self.fontStrokeWidth = other.fontStrokeWidth 188 | self.foregroundColor = other.foregroundColor 189 | self.fontBackgroundColor = other.fontBackgroundColor 190 | self.lineBreakMode = other.lineBreakMode 191 | self.alignment = other.alignment 192 | self.underlineColor = other.underlineColor 193 | self.underlineStyle = other.underlineStyle 194 | } 195 | } 196 | 197 | required init?(coder aDecoder: NSCoder) { 198 | super.init(coder:aDecoder) 199 | } 200 | 201 | // MARK: private helpers 202 | 203 | // 204 | // Add attributes to the String 205 | // 206 | 207 | func stringWithAttributes(_ string : String) -> CFAttributedString { 208 | 209 | return attributedStringWithAttributes(NSAttributedString(string : string)) 210 | } 211 | 212 | // 213 | // Add the attributes to the CFAttributedString 214 | // 215 | 216 | func attributedStringWithAttributes(_ attrString : CFAttributedString) -> CFAttributedString { 217 | 218 | let stringLength = CFAttributedStringGetLength(attrString) 219 | 220 | let range = CFRangeMake(0, stringLength) 221 | 222 | // 223 | // Create a mutable attributed string with a max length of 0. 224 | // The max length is a hint as to how much internal storage to reserve. 225 | // 0 means no hint. 226 | // 227 | 228 | let newString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0); 229 | 230 | // Copy the textString into the newly created attrString 231 | 232 | CFAttributedStringReplaceString (newString, CFRangeMake(0,0), CFAttributedStringGetString(attrString)); 233 | 234 | CFAttributedStringSetAttribute(newString, 235 | range, 236 | kCTForegroundColorAttributeName, 237 | foregroundColor.cgColor); 238 | 239 | if #available(iOS 10.0, *) { 240 | CFAttributedStringSetAttribute(newString, 241 | range, 242 | kCTBackgroundColorAttributeName, 243 | fontBackgroundColor.cgColor) 244 | } else { 245 | // Fallback on earlier versions 246 | }; 247 | 248 | 249 | CFAttributedStringSetAttribute(newString,range,kCTFontAttributeName,fontRef) 250 | 251 | // TODO: add more CTParagraphStyleSetting 252 | // CTParagraph 253 | 254 | let setting = [CTParagraphStyleSetting(spec: .alignment, valueSize: MemoryLayout.size(ofValue: alignment), value: &alignment), 255 | CTParagraphStyleSetting(spec: .lineBreakMode, valueSize: MemoryLayout.size(ofValue: lineBreakMode), value: &lineBreakMode)] 256 | 257 | CFAttributedStringSetAttribute(newString, 258 | range, 259 | kCTParagraphStyleAttributeName, 260 | CTParagraphStyleCreate(setting, setting.count)) 261 | 262 | CFAttributedStringSetAttribute(newString, 263 | range, 264 | kCTStrokeWidthAttributeName, 265 | NSNumber(value: fontStrokeWidth as Float)) 266 | 267 | CFAttributedStringSetAttribute(newString, 268 | range, 269 | kCTStrokeColorAttributeName, 270 | fontStrokeColor.cgColor) 271 | 272 | CFAttributedStringSetAttribute(newString, 273 | range, 274 | kCTLigatureAttributeName, 275 | fontLigature) 276 | 277 | CFAttributedStringSetAttribute(newString, 278 | range, 279 | kCTUnderlineStyleAttributeName, 280 | NSNumber(value: underlineStyle.rawValue as Int32)); 281 | 282 | if let underlineColor = underlineColor { 283 | CFAttributedStringSetAttribute(newString, 284 | range, 285 | kCTUnderlineColorAttributeName, 286 | underlineColor.cgColor); 287 | } 288 | 289 | // TODO: Add more attributes 290 | 291 | return newString! 292 | } 293 | 294 | 295 | // 296 | // Calculate the frame size of a String 297 | // 298 | 299 | func frameSize() -> CGSize { 300 | 301 | return frameSizeLengthFromAttributedString(NSAttributedString(string : self.string!)) 302 | } 303 | 304 | func frameSizeLengthFromAttributedString(_ attrString : NSAttributedString) -> CGSize { 305 | 306 | let attrStringWithAttributes = attributedStringWithAttributes(attrString) 307 | 308 | let stringLength = CFAttributedStringGetLength(attrStringWithAttributes) 309 | 310 | // Create the framesetter with the attributed string. 311 | 312 | let framesetter = CTFramesetterCreateWithAttributedString(attrStringWithAttributes); 313 | 314 | let targetSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) 315 | 316 | let frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter,CFRangeMake(0, stringLength), nil, targetSize, nil) 317 | 318 | return frameSize; 319 | } 320 | 321 | func setAlignmentMode(_ alignmentMode : String) 322 | { 323 | switch (alignmentMode) 324 | { 325 | case "center": 326 | alignment = CTTextAlignment.center 327 | case "left": 328 | alignment = CTTextAlignment.left 329 | case "right": 330 | alignment = CTTextAlignment.right 331 | case "justified": 332 | alignment = CTTextAlignment.justified 333 | case "natural": 334 | alignment = CTTextAlignment.natural 335 | default: 336 | alignment = CTTextAlignment.left 337 | } 338 | } 339 | 340 | fileprivate func setFont(_ fontName:String!, fontSize:CGFloat, matrix: UnsafePointer? = nil) { 341 | 342 | assert(fontSize > 0,"Invalid font size (fontSize ≤ 0)") 343 | assert(fontName.isEmpty == false ,"Invalid font name (empty)") 344 | 345 | if(fontSize > 0 && fontName != nil) { 346 | fontRef = CTFontCreateWithName(fontName as CFString, fontSize, matrix) 347 | } 348 | } 349 | 350 | fileprivate func setFont(_ font:UIFont, matrix: UnsafePointer? = nil) { 351 | setFont(font.fontName, fontSize: font.pointSize, matrix: matrix) 352 | } 353 | 354 | // MARK: overrides 355 | 356 | public override func draw(in context: CGContext) { 357 | 358 | if let string = self.string { 359 | 360 | context.saveGState(); 361 | 362 | // Set the text matrix. 363 | context.textMatrix = CGAffineTransform.identity; 364 | 365 | // Core Text Coordinate System and Core Graphics are OSX style 366 | #if os(iOS) 367 | context.translateBy(x: 0, y: self.bounds.size.height); 368 | context.scaleBy(x: 1.0, y: -1.0); 369 | #endif 370 | 371 | var rect:CGRect = bounds 372 | 373 | if (radiusRatio == 0 && angleLenght == nil) { 374 | 375 | // Create a path which bounds the area where you will be drawing text. 376 | // The path need not be rectangular. 377 | 378 | let path = CGMutablePath(); 379 | 380 | // Add the atributtes to the String 381 | let attrStringWithAttributes = stringWithAttributes(string) 382 | 383 | // Create the framesetter with the attributed string. 384 | let framesetter = CTFramesetterCreateWithAttributedString(attrStringWithAttributes); 385 | 386 | if verticalCeter { 387 | let boundingBox = CTFontGetBoundingBox(fontRef); 388 | // Get the position on the y axis (middle) 389 | let midHeight = (rect.size.height * 0.5) - boundingBox.size.height * 0.5 390 | rect = CGRect(x:0, y:midHeight, width:rect.size.width, height:boundingBox.size.height) 391 | } 392 | 393 | // add the rect for the frame 394 | path.addRect(rect); 395 | 396 | // Create a frame. 397 | let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil); 398 | 399 | // Draw the specified frame in the given context. 400 | CTFrameDraw(frame, context); 401 | 402 | // context.flush() 403 | 404 | } else { 405 | drawWithArc(context: context, rect:rect) 406 | } 407 | 408 | context.restoreGState() 409 | 410 | } 411 | 412 | super.draw(in: context) 413 | } 414 | } 415 | 416 | struct GlyphArcInfo { 417 | var width:CGFloat; 418 | var angle:CGFloat; // in radians 419 | }; 420 | 421 | 422 | extension OMTextLayer 423 | { 424 | func createLine() -> CTLine? 425 | { 426 | if let string = string { 427 | return CTLineCreateWithAttributedString(self.stringWithAttributes(string)) 428 | } 429 | return nil; 430 | } 431 | 432 | func getRunFont(_ run:CTRun) -> CTFont 433 | { 434 | let dict = CTRunGetAttributes(run) as NSDictionary 435 | let runFont: CTFont = dict.object(forKey: String(kCTFontAttributeName)) as! CTFont 436 | return runFont; 437 | } 438 | 439 | func createPathFromStringWithAttributes() -> UIBezierPath? { 440 | 441 | Log.print("\(self.name ?? ""): createPathFromStringWithAttributes()") 442 | 443 | if let line = createLine() { 444 | 445 | let letters = CGMutablePath() 446 | 447 | let runArray = CTLineGetGlyphRuns(line) as NSArray 448 | 449 | let run: CTRun = runArray[0] as! CTRun 450 | 451 | let runFont: CTFont = getRunFont(run) 452 | 453 | let glyphCount = CTRunGetGlyphCount(run) 454 | 455 | for runGlyphIndex in 0 ..< glyphCount { 456 | 457 | let thisGlyphRange = CFRangeMake(runGlyphIndex, 1) 458 | var glyph = CGGlyph() 459 | var position = CGPoint.zero 460 | 461 | CTRunGetGlyphs(run, thisGlyphRange, &glyph) 462 | CTRunGetPositions(run, thisGlyphRange, &position) 463 | 464 | var affine = CGAffineTransform.identity 465 | let letter = CTFontCreatePathForGlyph(runFont, glyph, &affine) 466 | if let letter = letter { 467 | let lettersAffine = CGAffineTransform(translationX: position.x, y: position.y) 468 | letters.addPath(letter,transform:lettersAffine); 469 | } 470 | } 471 | 472 | let path = UIBezierPath() 473 | path.move(to: CGPoint.zero) 474 | path.append(UIBezierPath(cgPath: letters)) 475 | return path 476 | } 477 | return nil; 478 | } 479 | } 480 | 481 | extension OMTextLayer { 482 | 483 | func prepareGlyphArcInfo(line:CTLine, glyphCount:CFIndex, angle:Double) -> [GlyphArcInfo] { 484 | assert(glyphCount > 0); 485 | 486 | let runArray = CTLineGetGlyphRuns(line) as Array 487 | 488 | var glyphArcInfo : [GlyphArcInfo] = [] 489 | glyphArcInfo.reserveCapacity(glyphCount) 490 | 491 | // Examine each run in the line, updating glyphOffset to track how far along the run is in terms of glyphCount. 492 | var glyphOffset:CFIndex = 0; 493 | for run in runArray { 494 | let runGlyphCount = CTRunGetGlyphCount(run as! CTRun); 495 | 496 | // Ask for the width of each glyph in turn. 497 | for runGlyphIndex in 0 ..< runGlyphCount { 498 | let i = runGlyphIndex + glyphOffset 499 | let runGlyphRange = CFRangeMake(runGlyphIndex, 1) 500 | let runTypographicWidth = CGFloat(CTRunGetTypographicBounds(run as! CTRun, runGlyphRange, nil, nil, nil)) 501 | let newGlyphArcInfo = GlyphArcInfo(width:runTypographicWidth,angle:0) 502 | 503 | glyphArcInfo.insert(newGlyphArcInfo, at:i) 504 | } 505 | 506 | glyphOffset += runGlyphCount; 507 | } 508 | 509 | let lineLength = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil)) 510 | 511 | var info = glyphArcInfo.first! 512 | var prevHalfWidth:CGFloat = info.width / 2.0; 513 | 514 | info.angle = (prevHalfWidth / lineLength) * CGFloat(angleLenght!) 515 | 516 | var angleArc = info.angle 517 | 518 | // Divide the arc into slices such that each one covers the distance from one glyph's center to the next. 519 | 520 | for lineGlyphIndex:CFIndex in 1 ..< glyphCount { 521 | 522 | let halfWidth = glyphArcInfo[lineGlyphIndex].width / 2.0; 523 | 524 | let prevCenterToCenter:CGFloat = prevHalfWidth + halfWidth; 525 | 526 | let glyphAngle = (prevCenterToCenter / lineLength) * CGFloat(angleLenght!) 527 | 528 | angleArc += glyphAngle 529 | 530 | //Log.printd("\(self.name ?? ""): #\(lineGlyphIndex) angle : \(CPCAngle.format(Double(glyphAngle))) arc length :\(CPCAngle.format(Double(angleArc)))") 531 | 532 | glyphArcInfo[lineGlyphIndex].angle = glyphAngle 533 | 534 | prevHalfWidth = halfWidth 535 | } 536 | 537 | return glyphArcInfo; 538 | } 539 | 540 | 541 | func drawWithArc(context:CGContext, rect:CGRect) 542 | { 543 | Log.print("\(self.name ?? ""): drawWithArc(\(rect))") 544 | 545 | if let string = string, let angle = self.angleLenght { 546 | 547 | let attributeString = self.stringWithAttributes(string) 548 | let line = CTLineCreateWithAttributedString(attributeString) 549 | let glyphCount:CFIndex = CTLineGetGlyphCount(line); 550 | if glyphCount == 0 { 551 | Log.print("\(self.name ?? ""): 0 glyphs \(attributeString))") 552 | return; 553 | } 554 | 555 | let glyphArcInfo = prepareGlyphArcInfo(line: line,glyphCount: glyphCount,angle: angle) 556 | if glyphArcInfo.count > 0 { 557 | 558 | // Move the origin from the lower left of the view nearer to its center. 559 | context.saveGState(); 560 | context.translateBy(x: rect.midX, y: rect.midY) 561 | 562 | // Rotate the context 90 degrees counterclockwise. 563 | context.rotate(by: CGFloat(.pi / 2.0)); 564 | 565 | // Now for the actual drawing. The angle offset for each glyph relative to the previous glyph has already been 566 | // calculated; with that information in hand, draw those glyphs overstruck and centered over one another, making sure 567 | // to rotate the context after each glyph so the glyphs are spread along a semicircular path. 568 | var textPosition = CGPoint(x:0.0,y: self.radiusRatio * minRadius(rect.size)); 569 | 570 | context.textPosition = textPosition 571 | 572 | let runArray = CTLineGetGlyphRuns(line); 573 | let runCount = CFArrayGetCount(runArray); 574 | 575 | var glyphOffset:CFIndex = 0; 576 | 577 | for runIndex:CFIndex in 0 ..< runCount { 578 | let run = (runArray as NSArray)[runIndex] 579 | let runGlyphCount:CFIndex = CTRunGetGlyphCount(run as! CTRun); 580 | 581 | for runGlyphIndex:CFIndex in 0 ..< runGlyphCount { 582 | 583 | let glyphRange:CFRange = CFRangeMake(runGlyphIndex, 1); 584 | 585 | let angleRotation:CGFloat = -(glyphArcInfo[runGlyphIndex + glyphOffset].angle); 586 | 587 | //Log.printd("\(self.name ?? ""): run glyph#\(runGlyphIndex) angle rotation : \(CPCAngle.format(Double(angleRotation)))"); 588 | 589 | context.rotate(by: angleRotation); 590 | 591 | // Center this glyph by moving left by half its width. 592 | 593 | let glyphWidth:CGFloat = glyphArcInfo[runGlyphIndex + glyphOffset].width; 594 | let halfGlyphWidth:CGFloat = glyphWidth / 2.0; 595 | let positionForThisGlyph:CGPoint = CGPoint(x:textPosition.x - halfGlyphWidth, y:textPosition.y); 596 | 597 | // Glyphs are positioned relative to the text position for the line, 598 | // so offset text position leftwards by this glyph's width in preparation for the next glyph. 599 | 600 | textPosition.x -= glyphWidth; 601 | 602 | var textMatrix = CTRunGetTextMatrix(run as! CTRun) 603 | 604 | textMatrix.tx = positionForThisGlyph.x; 605 | textMatrix.ty = positionForThisGlyph.y; 606 | 607 | context.textMatrix = textMatrix; 608 | 609 | CTRunDraw(run as! CTRun, context, glyphRange); 610 | } 611 | glyphOffset += runGlyphCount; 612 | } 613 | context.restoreGState(); 614 | } 615 | } 616 | } 617 | } 618 | 619 | -------------------------------------------------------------------------------- /Example/OMView.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | // 4 | // Copyright 2016 - Jorge Ouahbi 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | 20 | // 21 | // OMView.swift 22 | // 23 | // Created by Jorge Ouahbi on 21/4/16. 24 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 25 | // 26 | 27 | import UIKit 28 | 29 | 30 | // MARK: - Layer's View 31 | public class OMView: UIView { 32 | 33 | // MARK: - Properties 34 | /// The view’s conical layer used for rendering. (read-only) 35 | var gradientLayer: T { 36 | return super.layer as! T 37 | } 38 | 39 | override open class var layerClass: AnyClass { 40 | return T.self as AnyClass 41 | } 42 | 43 | override init(frame: CGRect) { 44 | super.init(frame: frame) 45 | setup() 46 | } 47 | 48 | required public init?(coder aDecoder: NSCoder) { 49 | super.init(coder: aDecoder) 50 | setup() 51 | } 52 | 53 | fileprivate func setup() { 54 | let scale = UIScreen.main.scale 55 | layer.contentsScale = scale 56 | layer.needsDisplayOnBoundsChange = true 57 | layer.drawsAsynchronously = true 58 | layer.allowsGroupOpacity = true 59 | layer.shouldRasterize = true 60 | layer.rasterizationScale = scale 61 | self.backgroundColor = UIColor.white 62 | layer.setNeedsDisplay() 63 | } 64 | } 65 | 66 | 67 | // MARK: Interface Builder Additions 68 | 69 | // 70 | //@IBDesignable @available(*, unavailable, message = "This is reserved for Interface Builder") 71 | //extension OMGradientView { 72 | // 73 | // @IBInspectable public var gradientType: Int { 74 | // set { 75 | // if let type = OMGradientType(rawValue: newValue) { 76 | // self.type = type 77 | // } 78 | // } 79 | // get { 80 | // return self.type.rawValue 81 | // } 82 | // } 83 | // 84 | // @IBInspectable public var startColor: UIColor { 85 | // set { 86 | // if colors.isEmpty { 87 | // colors.append(newValue) 88 | // colors.append(UIColor.clearColor()) 89 | // } else { 90 | // colors[0] = newValue 91 | // } 92 | // } 93 | // get { 94 | // return (colors.count >= 1) ? colors[0] : UIColor.clearColor() 95 | // } 96 | // } 97 | // 98 | // @IBInspectable public var endColor: UIColor { 99 | // set { 100 | // if colors.isEmpty { 101 | // colors.append(UIColor.clearColor()) 102 | // colors.append(newValue) 103 | // } else { 104 | // colors[1] = newValue 105 | // } 106 | // } 107 | // get { 108 | // return (colors.count >= 2) ? colors[1] : UIColor.clearColor() 109 | // } 110 | // } 111 | // 112 | // public override func prepareForInterfaceBuilder() { 113 | // // To improve IB performance, reduce generated image size 114 | // layer.contentsScale = 0.25 115 | // } 116 | // 117 | //} 118 | -------------------------------------------------------------------------------- /Example/OShadingGradientLayerViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Copyright 2015 - Jorge Ouahbi 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | 19 | import UIKit 20 | import OMShadingGradientLayer 21 | import LibControl 22 | 23 | let kDefaultAnimationDuration: TimeInterval = 5.0 24 | 25 | class OShadingGradientLayerViewController : UIViewController, UITableViewDataSource, UITableViewDelegate { 26 | 27 | @IBOutlet weak var tableView: UITableView! 28 | 29 | @IBOutlet weak var pointStartX : UISlider! 30 | @IBOutlet weak var pointEndX : UISlider! 31 | 32 | @IBOutlet weak var pointStartY : UISlider! 33 | @IBOutlet weak var pointEndY : UISlider! 34 | 35 | @IBOutlet weak var endPointSliderValueLabel : UILabel! 36 | @IBOutlet weak var startPointSliderValueLabel : UILabel! 37 | 38 | @IBOutlet weak var viewForGradientLayer : UIView! 39 | 40 | @IBOutlet weak var startRadiusSlider : UISlider! 41 | @IBOutlet weak var endRadiusSlider : UISlider! 42 | 43 | @IBOutlet weak var startRadiusSliderValueLabel : UILabel! 44 | @IBOutlet weak var endRadiusSliderValueLabel : UILabel! 45 | 46 | @IBOutlet weak var typeGardientSwitch: UISwitch! 47 | 48 | @IBOutlet weak var extendsPastEnd : UISwitch! 49 | @IBOutlet weak var extendsPastStart : UISwitch! 50 | @IBOutlet weak var segmenFunction : UISegmentedControl! 51 | 52 | @IBOutlet weak var strokePath : UISwitch! 53 | @IBOutlet weak var randomPath : UISwitch! 54 | 55 | var colors : [UIColor] = [] 56 | var locations : [CGFloat] = [0.0,1.0] 57 | var subviewForGradientLayer : OMView! 58 | var gradientLayer: OMShadingGradientLayer = OMShadingGradientLayer(type:.radial) 59 | var animate = true 60 | 61 | lazy var slopeFunction: [(Double) -> Double] = { 62 | return [ 63 | Linear, 64 | QuadraticEaseIn, 65 | QuadraticEaseOut, 66 | QuadraticEaseInOut, 67 | CubicEaseIn, 68 | CubicEaseOut, 69 | CubicEaseInOut, 70 | QuarticEaseIn, 71 | QuarticEaseOut, 72 | QuarticEaseInOut, 73 | QuinticEaseIn, 74 | QuinticEaseOut, 75 | QuinticEaseInOut, 76 | SineEaseIn, 77 | SineEaseOut, 78 | SineEaseInOut, 79 | CircularEaseIn, 80 | CircularEaseOut, 81 | CircularEaseInOut, 82 | ExponentialEaseIn, 83 | ExponentialEaseOut, 84 | ExponentialEaseInOut, 85 | ElasticEaseIn, 86 | ElasticEaseOut, 87 | ElasticEaseInOut, 88 | BackEaseIn, 89 | BackEaseOut, 90 | BackEaseInOut, 91 | BounceEaseIn, 92 | BounceEaseOut, 93 | BounceEaseInOut, 94 | ] 95 | }() 96 | 97 | lazy var slopeFunctionString:[String] = { 98 | return [ 99 | "Linear", 100 | "QuadraticEaseIn", 101 | "QuadraticEaseOut", 102 | "QuadraticEaseInOut", 103 | "CubicEaseIn", 104 | "CubicEaseOut", 105 | "CubicEaseInOut", 106 | "QuarticEaseIn", 107 | "QuarticEaseOut", 108 | "QuarticEaseInOut", 109 | "QuinticEaseIn", 110 | "QuinticEaseOut", 111 | "QuinticEaseInOut", 112 | "SineEaseIn", 113 | "SineEaseOut", 114 | "SineEaseInOut", 115 | "CircularEaseIn", 116 | "CircularEaseOut", 117 | "CircularEaseInOut", 118 | "ExponentialEaseIn", 119 | "ExponentialEaseOut", 120 | "ExponentialEaseInOut", 121 | "ElasticEaseIn", 122 | "ElasticEaseOut", 123 | "ElasticEaseInOut", 124 | "BackEaseIn", 125 | "BackEaseOut", 126 | "BackEaseInOut", 127 | "BounceEaseIn", 128 | "BounceEaseOut", 129 | "BounceEaseInOut", 130 | ] 131 | }() 132 | 133 | // MARK: - UITableView Helpers 134 | 135 | func selectIndexPath(_ row:Int, section:Int = 0) { 136 | let indexPath = IndexPath(item: row, section: section) 137 | self.tableView.selectRow(at: indexPath,animated: true,scrollPosition: .bottom) 138 | self.gradientLayer.slopeFunction = self.slopeFunction[(indexPath as NSIndexPath).row]; 139 | } 140 | 141 | // MARK: - UITableView Datasource 142 | 143 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 144 | return slopeFunction.count 145 | } 146 | 147 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 148 | let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) 149 | 150 | assert(self.slopeFunctionString.count == self.slopeFunction.count) 151 | 152 | cell.textLabel?.textAlignment = .center 153 | cell.textLabel?.font = UIFont(name: "Helvetica", size: 9) 154 | cell.textLabel?.text = "\(self.slopeFunctionString[(indexPath as NSIndexPath).row])" 155 | 156 | cell.layer.cornerRadius = 8 157 | cell.layer.masksToBounds = true 158 | 159 | return cell 160 | } 161 | 162 | // MARK: - UITableView Delegate 163 | 164 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 165 | self.gradientLayer.slopeFunction = self.slopeFunction[(indexPath as NSIndexPath).row]; 166 | } 167 | 168 | @IBAction func maskTextSwitchChanged(_ sender: UISwitch) { 169 | 170 | if (sender.isOn) { 171 | textLayer.removeFromSuperlayer() 172 | gradientLayer.mask = textLayer 173 | } else { 174 | gradientLayer.mask = nil 175 | gradientLayer.addSublayer(textLayer) 176 | } 177 | updateGradientLayer() 178 | } 179 | 180 | 181 | // MARK: - View life cycle 182 | var textLayer: OMTextLayer = OMTextLayer(string: "Hello text shading", font: UIFont(name: "Helvetica",size: 50)!) 183 | 184 | override func viewDidLoad() { 185 | super.viewDidLoad() 186 | 187 | subviewForGradientLayer = OMView(frame: viewForGradientLayer.frame) 188 | 189 | viewForGradientLayer.addSubview(subviewForGradientLayer) 190 | 191 | gradientLayer = subviewForGradientLayer!.gradientLayer 192 | 193 | gradientLayer.addSublayer(textLayer) 194 | 195 | randomizeColors() 196 | } 197 | 198 | 199 | override func viewDidAppear(_ animated: Bool) { 200 | super.viewDidAppear(animated) 201 | 202 | let viewBounds = viewForGradientLayer.bounds 203 | 204 | pointStartX.maximumValue = 1.0 205 | pointStartY.minimumValue = 0.0 206 | 207 | pointEndX.maximumValue = 1.0 208 | pointEndY.minimumValue = 0.0 209 | 210 | let startPoint = CGPoint(x:viewBounds.minX / viewBounds.size.width,y: viewBounds.minY / viewBounds.size.height) 211 | let endPoint = CGPoint(x:viewBounds.minX / viewBounds.size.width,y: viewBounds.maxY / viewBounds.size.height) 212 | 213 | pointStartX.value = Float(startPoint.x) 214 | pointStartY.value = Float(startPoint.y) 215 | 216 | pointEndX.value = Float(endPoint.x) 217 | pointEndY.value = Float(endPoint.y) 218 | 219 | extendsPastEnd.isOn = true 220 | extendsPastStart.isOn = true 221 | 222 | // radius 223 | 224 | endRadiusSlider.maximumValue = 1.0 225 | endRadiusSlider.minimumValue = 0 226 | 227 | startRadiusSlider.maximumValue = 1.0 228 | startRadiusSlider.minimumValue = 0 229 | 230 | startRadiusSlider.value = 1.0 231 | endRadiusSlider.value = 0 232 | 233 | 234 | // select the first element 235 | selectIndexPath(0) 236 | 237 | gradientLayer.frame = viewBounds 238 | gradientLayer.locations = locations 239 | 240 | 241 | // update the gradient layer frame 242 | self.gradientLayer.frame = self.viewForGradientLayer.bounds 243 | 244 | // text layers 245 | self.textLayer.frame = self.viewForGradientLayer.bounds 246 | 247 | // 2% 248 | // textLayer.borderWidth = 100 249 | 250 | // self.textLayer.frame = self.viewForGradientLayer.bounds 251 | // 252 | // viewForGradientLayer.layer.addSublayer(gradientLayer) 253 | // 254 | viewForGradientLayer.backgroundColor = UIColor.clear 255 | 256 | #if DEBUG 257 | viewForGradientLayer.layer.borderWidth = 1.0 258 | viewForGradientLayer.layer.borderColor = UIColor.blackColor().CGColor 259 | #endif 260 | 261 | updateTextPointsUI() 262 | updateGradientLayer() 263 | } 264 | 265 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) 266 | { 267 | coordinator.animate(alongsideTransition: {(UIViewControllerTransitionCoordinatorContext) in 268 | 269 | }) {(UIViewControllerTransitionCoordinatorContext) in 270 | // update the gradient layer frame 271 | self.gradientLayer.frame = self.viewForGradientLayer.bounds 272 | } 273 | } 274 | 275 | // MARK: - Triggered actions 276 | 277 | @IBAction func extendsPastStartChanged(_ sender: UISwitch) { 278 | 279 | gradientLayer.extendsBeforeStart = sender.isOn 280 | } 281 | 282 | @IBAction func extendsPastEndChanged(_ sender: UISwitch) { 283 | 284 | gradientLayer.extendsPastEnd = sender.isOn 285 | } 286 | 287 | @IBAction func gradientSliderChanged(_ sender: UISlider) { 288 | 289 | updateTextPointsUI() 290 | updateGradientLayer() 291 | } 292 | 293 | @IBAction func strokeSwitchChanged(_ sender: UISwitch) { 294 | 295 | if ((gradientLayer.path) != nil) { 296 | gradientLayer.stroke = sender.isOn 297 | } 298 | 299 | sender.isOn = gradientLayer.stroke 300 | } 301 | 302 | @IBAction func maskSwitchChanged(_ sender: UISwitch) { 303 | if sender.isOn { 304 | let style = PolygonStyle(rawValue:Int(arc4random_uniform(6)))! 305 | let radius = CGFloat(drand48()) * viewForGradientLayer.bounds.size.min 306 | let sides = Int(arc4random_uniform(32)) + 4 307 | let path = UIBezierPath.polygon(frame: viewForGradientLayer.bounds, 308 | sides: sides, 309 | radius: radius, 310 | startAngle: 0, 311 | style: style, 312 | percentInflection: CGFloat(drand48())) 313 | gradientLayer.path = path.cgPath 314 | } else{ 315 | gradientLayer.path = nil 316 | strokePath.isOn = false 317 | } 318 | updateGradientLayer() 319 | } 320 | 321 | 322 | @IBAction func typeSwitchChanged(_ sender: UISwitch) { 323 | self.gradientLayer.gradientType = sender.isOn ? .radial : .axial; 324 | updateGradientLayer() 325 | } 326 | 327 | fileprivate func updateSlopeFunction(_ index: Int) { 328 | switch(index) 329 | { 330 | case 0: 331 | self.gradientLayer.function = .linear 332 | break 333 | case 1: 334 | self.gradientLayer.function = .exponential 335 | break 336 | case 2: 337 | self.gradientLayer.function = .cosine 338 | break 339 | default: 340 | self.gradientLayer.function = .linear 341 | assertionFailure(); 342 | } 343 | } 344 | 345 | @IBAction func functionSwitchChanged(_ sender: UISegmentedControl) { 346 | 347 | updateSlopeFunction(sender.selectedSegmentIndex) 348 | 349 | updateGradientLayer() 350 | } 351 | 352 | @IBAction func animateSwitchChanged(_ sender: UISwitch) { 353 | self.animate = sender.isOn 354 | updateGradientLayer() 355 | } 356 | 357 | @IBAction func randomButtonTouchUpInside(_ sender: UIButton) 358 | { 359 | // random points 360 | pointStartX.value = Float(CGFloat(drand48())) 361 | pointStartY.value = Float(CGFloat(drand48())) 362 | pointEndX.value = Float(CGFloat(drand48())) 363 | pointEndY.value = Float(CGFloat(drand48())) 364 | 365 | // select random slope function 366 | selectIndexPath(Int(arc4random()) % tableView.numberOfRows(inSection: 0)) 367 | let segmentIndex = Int(arc4random()) % segmenFunction.numberOfSegments 368 | updateSlopeFunction(segmentIndex) 369 | segmenFunction.selectedSegmentIndex = segmentIndex 370 | typeGardientSwitch.isOn = Float(drand48()) > 0.5 ? true : false 371 | extendsPastEnd.isOn = Float(drand48()) > 0.5 ? true : false 372 | extendsPastStart.isOn = Float(drand48()) > 0.5 ? true : false 373 | 374 | if (typeGardientSwitch.isOn) { 375 | // random radius 376 | endRadiusSlider.value = Float(drand48()); 377 | startRadiusSlider.value = Float(drand48()); 378 | // random scale CGAffineTransform 379 | gradientLayer.radialTransform = CGAffineTransform.randomScale() 380 | } 381 | // random colors 382 | randomizeColors() 383 | // update the UI 384 | updateTextPointsUI(); 385 | // update the gradient layer 386 | updateGradientLayer() 387 | } 388 | 389 | // MARK: - Helpers 390 | 391 | func randomizeColors() { 392 | self.locations = [] 393 | self.colors.removeAll() 394 | var numberOfColor = round(Float(drand48()) * 16) 395 | while numberOfColor > 0 { 396 | let color = UIColor.random 397 | self.colors.append(color) 398 | numberOfColor = numberOfColor - 1 399 | } 400 | self.gradientLayer.colors = colors 401 | } 402 | 403 | 404 | func updateGradientLayer() { 405 | 406 | viewForGradientLayer.layoutIfNeeded() 407 | 408 | let endRadius = Double(endRadiusSlider.value) 409 | let startRadius = Double(startRadiusSlider.value) 410 | 411 | let startPoint = CGPoint(x:CGFloat(pointStartX.value),y:CGFloat(pointStartY.value)) 412 | let endPoint = CGPoint(x:CGFloat(pointEndX.value),y:CGFloat(pointEndY.value)) 413 | 414 | gradientLayer.extendsPastEnd = extendsPastEnd.isOn 415 | gradientLayer.extendsBeforeStart = extendsPastStart.isOn 416 | gradientLayer.gradientType = typeGardientSwitch.isOn ? .radial: .axial 417 | 418 | if (self.animate) { 419 | 420 | // allways remove all animations 421 | 422 | gradientLayer.removeAllAnimations() 423 | 424 | let mediaTime = CACurrentMediaTime() 425 | CATransaction.begin() 426 | 427 | gradientLayer.animateKeyPath("colors", 428 | fromValue:nil, 429 | toValue: colors as AnyObject?, 430 | beginTime: mediaTime, 431 | duration: kDefaultAnimationDuration, 432 | delegate: nil) 433 | 434 | gradientLayer.animateKeyPath("locations", 435 | fromValue:nil, 436 | toValue: self.locations as AnyObject?, 437 | beginTime: mediaTime, 438 | duration: kDefaultAnimationDuration, 439 | delegate: nil) 440 | 441 | gradientLayer.animateKeyPath("startPoint", 442 | fromValue: NSValue(cgPoint: gradientLayer.startPoint), 443 | toValue: NSValue(cgPoint:startPoint), 444 | beginTime: mediaTime , 445 | duration: kDefaultAnimationDuration, 446 | delegate: nil) 447 | 448 | gradientLayer.animateKeyPath("endPoint", 449 | fromValue: NSValue(cgPoint:gradientLayer.endPoint), 450 | toValue: NSValue(cgPoint:endPoint), 451 | beginTime: mediaTime, 452 | duration: kDefaultAnimationDuration, 453 | delegate: nil) 454 | 455 | if gradientLayer.gradientType == .radial { 456 | gradientLayer.animateKeyPath("startRadius", 457 | fromValue: Double(gradientLayer.startRadius) as AnyObject?, 458 | toValue: startRadius as AnyObject?, 459 | beginTime: mediaTime , 460 | duration: kDefaultAnimationDuration, 461 | delegate: nil) 462 | 463 | gradientLayer.animateKeyPath("endRadius", 464 | fromValue: Double(gradientLayer.endRadius) as AnyObject?, 465 | toValue: endRadius as AnyObject?, 466 | beginTime: mediaTime, 467 | duration: kDefaultAnimationDuration, 468 | delegate: nil) 469 | } 470 | 471 | CATransaction.commit() 472 | 473 | } else { 474 | 475 | gradientLayer.startPoint = startPoint 476 | gradientLayer.endPoint = endPoint 477 | gradientLayer.colors = self.colors 478 | 479 | // gradientLayer.locations = self.locations 480 | 481 | gradientLayer.startRadius = CGFloat(startRadius) 482 | gradientLayer.endRadius = CGFloat(endRadius) 483 | 484 | self.gradientLayer.setNeedsDisplay() 485 | } 486 | } 487 | 488 | func updateTextPointsUI() { 489 | 490 | // points text 491 | startPointSliderValueLabel.text = String(format: "%.1f,%.1f", pointStartX.value,pointStartY.value) 492 | endPointSliderValueLabel.text = String(format: "%.1f,%.1f", pointEndX.value,pointEndY.value) 493 | 494 | //radius text 495 | startRadiusSliderValueLabel.text = String(format: "%.1f", Double(startRadiusSlider.value)) 496 | endRadiusSliderValueLabel.text = String(format: "%.1f", Double(endRadiusSlider.value)) 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /Example/UIBezierPath+Polygon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIBezierPath+Polygon.swift 3 | // 4 | // Created by Jorge Ouahbi on 12/9/16. 5 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 6 | // 7 | 8 | // Based on Erica Sadun code 9 | // https://gist.github.com/erica/c54826fd3411d6db053bfdfe1f64ab54 10 | 11 | import UIKit 12 | import LibControl 13 | 14 | 15 | public enum PolygonStyle : Int { case flatsingle, flatdouble, curvesingle, curvedouble, flattruple, curvetruple } 16 | 17 | public struct Bezier { 18 | 19 | static func polygon( 20 | sides sideCount: Int = 5, 21 | radius: CGFloat = 50.0, 22 | startAngle offset: CGFloat = 0.0, 23 | style: PolygonStyle = .curvesingle, 24 | percentInflection: CGFloat = 0.0) -> BezierPath 25 | { 26 | guard sideCount >= 3 else { 27 | fatalError("Bezier polygon construction requires 3+ sides") 28 | } 29 | 30 | func pointAt(_ theta: CGFloat, inflected: Bool = false, centered: Bool = false) -> CGPoint { 31 | let inflection = inflected ? percentInflection : 0.0 32 | let r = centered ? 0.0 : radius * (1.0 + inflection) 33 | return CGPoint( 34 | x: r * CGFloat(cos(theta)), 35 | y: r * CGFloat(sin(theta))) 36 | } 37 | 38 | let π = CGFloat(Double.pi); let 𝜏 = 2.0 * π 39 | let path = BezierPath() 40 | let dθ = 𝜏 / CGFloat(sideCount) 41 | 42 | path.move(to: pointAt(0.0 + offset)) 43 | switch (percentInflection == 0.0, style) { 44 | case (true, _): 45 | for θ in stride(from: 0.0, through: 𝜏, by: dθ) { 46 | path.addLine(to: pointAt(θ + offset)) 47 | } 48 | case (false, .curvesingle): 49 | let cpθ = dθ / 2.0 50 | for θ in stride(from: 0.0, to: 𝜏, by: dθ) { 51 | path.addQuadCurve( 52 | to: pointAt(θ + dθ + offset), 53 | controlPoint: pointAt(θ + cpθ + offset, inflected: true)) 54 | } 55 | case (false, .flatsingle): 56 | let cpθ = dθ / 2.0 57 | for θ in stride(from: 0.0, to: 𝜏, by: dθ) { 58 | path.addLine(to: pointAt(θ + cpθ + offset, inflected: true)) 59 | path.addLine(to: pointAt(θ + dθ + offset)) 60 | } 61 | case (false, .curvedouble): 62 | let (cp1θ, cp2θ) = (dθ / 3.0, 2.0 * dθ / 3.0) 63 | for θ in stride(from: 0.0, to: 𝜏, by: dθ) { 64 | path.addCurve( 65 | to: pointAt(θ + dθ + offset), 66 | controlPoint1: pointAt(θ + cp1θ + offset, inflected: true), 67 | controlPoint2: pointAt(θ + cp2θ + offset, inflected: true) 68 | ) 69 | } 70 | case (false, .flatdouble): 71 | let (cp1θ, cp2θ) = (dθ / 3.0, 2.0 * dθ / 3.0) 72 | for θ in stride(from: 0.0, to: 𝜏, by: dθ) { 73 | path.addLine(to: pointAt(θ + cp1θ + offset, inflected: true)) 74 | path.addLine(to: pointAt(θ + cp2θ + offset, inflected: true)) 75 | path.addLine(to: pointAt(θ + dθ + offset)) 76 | } 77 | 78 | case (false, .flattruple): 79 | let (cp1θ, cp2θ) = (dθ / 3.0, 2.0 * dθ / 3.0) 80 | for θ in stride(from: 0.0, to: 𝜏, by: dθ) { 81 | path.addLine(to: pointAt(θ + cp1θ + offset, inflected: true)) 82 | path.addLine(to: pointAt(θ + dθ / 2.0 + offset, centered: true)) 83 | path.addLine(to: pointAt(θ + cp2θ + offset, inflected: true)) 84 | path.addLine(to: pointAt(θ + dθ + offset)) 85 | } 86 | case (false, .curvetruple): 87 | let (cp1θ, cp2θ) = (dθ / 3.0, 2.0 * dθ / 3.0) 88 | for θ in stride(from: 0.0, to: 𝜏, by: dθ) { 89 | path.addQuadCurve( 90 | to: pointAt(θ + dθ / 2.0 + offset, centered:true), 91 | controlPoint: pointAt(θ + cp1θ + offset, inflected: true)) 92 | path.addQuadCurve( 93 | to: pointAt(θ + dθ + offset), 94 | controlPoint: pointAt(θ + cp2θ + offset, inflected: true)) 95 | } 96 | } 97 | 98 | path.close() 99 | return path 100 | } 101 | } 102 | 103 | extension UIBezierPath { 104 | 105 | public class func polygon(frame : CGRect, 106 | sides: Int = 5, 107 | radius: CGFloat = 50.0, 108 | startAngle : CGFloat = 0.0, 109 | style: PolygonStyle = .curvesingle, 110 | percentInflection: CGFloat = 0.0) -> UIBezierPath 111 | { 112 | let bezier = Bezier.polygon( 113 | sides:sides, 114 | radius:radius, 115 | startAngle:startAngle, 116 | style: style, 117 | percentInflection:percentInflection) 118 | 119 | bezier.FitPathToRect(frame) 120 | bezier.MovePathCenterToPoint(CGPoint(x:frame.midX,y:frame.midY)); 121 | 122 | return bezier; 123 | } 124 | } 125 | 126 | 127 | func RectGetCenter(_ rect : CGRect)-> CGPoint 128 | { 129 | return CGPoint(x:rect.midX, y:rect.midY); 130 | } 131 | 132 | func SizeScaleByFactor(_ aSize:CGSize, factor:CGFloat) -> CGSize 133 | { 134 | return CGSize(width:aSize.width * factor, height: aSize.height * factor); 135 | } 136 | 137 | func AspectScaleFit(_ sourceSize:CGSize, destRect:CGRect) -> CGFloat 138 | { 139 | let destSize = destRect.size; 140 | let scaleW = destSize.width / sourceSize.width; 141 | let scaleH = destSize.height / sourceSize.height; 142 | return min(scaleW, scaleH); 143 | } 144 | 145 | 146 | func RectAroundCenter(_ center:CGPoint, size:CGSize) -> CGRect 147 | { 148 | let halfWidth = size.width / 2.0; 149 | let halfHeight = size.height / 2.0; 150 | 151 | return CGRect(x:center.x - halfWidth, y:center.y - halfHeight, width:size.width, height:size.height); 152 | } 153 | 154 | func RectByFittingRect(sourceRect:CGRect, destinationRect:CGRect) -> CGRect 155 | { 156 | let aspect = AspectScaleFit(sourceRect.size, destRect: destinationRect); 157 | let targetSize = SizeScaleByFactor(sourceRect.size, factor: aspect); 158 | return RectAroundCenter(RectGetCenter(destinationRect), size: targetSize); 159 | } 160 | 161 | // MARK: - UIBezierPath+polygon 162 | 163 | extension UIBezierPath 164 | { 165 | func FitPathToRect( _ destRect:CGRect) { 166 | let bounds = self.boundingBox(); 167 | let fitRect = RectByFittingRect(sourceRect: bounds, destinationRect: destRect); 168 | let scale = AspectScaleFit(bounds.size, destRect: destRect); 169 | 170 | let newCenter = RectGetCenter(fitRect); 171 | self.MovePathCenterToPoint(newCenter); 172 | self.ScalePath(sx: scale, sy: scale); 173 | } 174 | 175 | func AdjustPathToRect( _ destRect:CGRect) { 176 | let bounds = self.boundingBox(); 177 | let scaleX = destRect.size.width / bounds.size.width; 178 | let scaleY = destRect.size.height / bounds.size.height; 179 | 180 | let newCenter = CGPoint(x:destRect.midX,y:destRect.midY) 181 | self.MovePathCenterToPoint(newCenter); 182 | self.ScalePath(sx: scaleX, sy: scaleY); 183 | } 184 | 185 | func ApplyCenteredPathTransform(_ transform:CGAffineTransform) { 186 | let center = self.boundingBox().size.center 187 | var t = CGAffineTransform.identity; 188 | t = t.translatedBy(x: center.x, y: center.y); 189 | t = transform.concatenating(t); 190 | t = t.translatedBy(x: -center.x, y: -center.y); 191 | self.apply(t); 192 | } 193 | 194 | class func PathByApplyingTransform( _ transform:CGAffineTransform) -> UIBezierPath { 195 | let copy = self.copy(); 196 | (copy as! UIBezierPath).ApplyCenteredPathTransform(transform); 197 | return copy as! UIBezierPath; 198 | } 199 | 200 | func RotatePath(_ theta:CGFloat) { 201 | let t = CGAffineTransform(rotationAngle: theta); 202 | self.ApplyCenteredPathTransform(t); 203 | } 204 | 205 | func ScalePath( sx:CGFloat, sy:CGFloat) { 206 | let t = CGAffineTransform(scaleX: sx, y: sy); 207 | self.ApplyCenteredPathTransform( t); 208 | } 209 | 210 | func OffsetPath(_ offset:CGSize) { 211 | let t = CGAffineTransform(translationX: offset.width, y: offset.height); 212 | self.ApplyCenteredPathTransform( t); 213 | } 214 | 215 | func MovePathToPoint( _ destPoint:CGPoint) { 216 | let bounds = self.boundingBox() 217 | let p1 = bounds.origin; 218 | let p2 = destPoint; 219 | let vector = CGSize(width:p2.x - p1.x, 220 | height:p2.y - p1.y); 221 | self.OffsetPath(vector); 222 | } 223 | 224 | func MovePathCenterToPoint(_ destPoint:CGPoint) { 225 | let bounds = self.boundingBox() 226 | let p1 = bounds.origin; 227 | let p2 = destPoint; 228 | var vector = CGSize(width:p2.x - p1.x, height:p2.y - p1.y); 229 | vector.width -= bounds.size.width / 2.0; 230 | vector.height -= bounds.size.height / 2.0; 231 | self.OffsetPath( vector); 232 | } 233 | 234 | func boundingBox() -> CGRect { 235 | return self.cgPath.boundingBox 236 | } 237 | 238 | } 239 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /OShadingGradientLayer.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint OMShadingGradientLayer.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'OMShadingGradientLayer' 11 | s.version = '0.1.0' 12 | s.summary = 'Shading gradient layer with animatable properties.' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = 'Shading gradient CALayer with animatable properties in Swift.' 21 | 22 | s.homepage = 'https://github.com/jaouahbi/OMShadingGradientLayer' 23 | s.screenshots = ['https://s3.amazonaws.com/cocoacontrols_production/uploads/control_image/image/8908/ScreenShot-1.png', 24 | 'https://s3.amazonaws.com/cocoacontrols_production/uploads/control_image/image/8909/ScreenShot-2.png'] 25 | s.license = { :type => 'APACHE 2.0', :file => 'LICENSE' } 26 | s.author = { 'Jorge Ouahbi' => 'jorgeouahbi@gmail.com' } 27 | s.source = { :git => 'https://github.com/jaouahbi/OMShadingGradientLayer.git', :tag => s.version.to_s } 28 | s.social_media_url = 'https://twitter.com/a_c_r_a_t_a' 29 | s.ios.deployment_target = '8.0' 30 | s.source_files = 'OMShadingGradientLayer/Classes/**/*' 31 | end -------------------------------------------------------------------------------- /OShadingGradientLayer.xcodeproj/OMShadingGradientLayerTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /OShadingGradientLayer.xcodeproj/OMShadingGradientLayer_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /OShadingGradientLayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /OShadingGradientLayer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OShadingGradientLayer.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /OShadingGradientLayer.xcodeproj/xcshareddata/xcschemes/OMShadingGradientLayer-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "OMShadingGradientLayer", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "OMShadingGradientLayer", 12 | targets: ["OMShadingGradientLayer"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "OMShadingGradientLayer", 23 | dependencies: []), 24 | .testTarget( 25 | name: "OMShadingGradientLayerTests", 26 | dependencies: ["OMShadingGradientLayer"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OShadingGradientLayer 2 | 3 | CoreAnimation Shading gradient layer with animatable properties in Swift 4 | 5 | ![](https://github.com/jaouahbi/OShadingGradientLayer/blob/master/gif/gif.gif) 6 | 7 | ## Features 8 | 9 | - [x] CoreAnimation Gradient layer with animatable properties 10 | - [x] Axial and radial shading gradient styles 11 | - [x] Fill and stroke shading gradient styles 12 | - [x] Linear, cosine and exponential interpolation 13 | - [x] Different slope functions 14 | - [x] Radial affine transformation 15 | - [x] CGShading with colors and locations 16 | 17 | ## Requirements 18 | 19 | - iOS 10.0+ 20 | - Xcode 8 21 | 22 | ## Communication 23 | 24 | Open a issue. 25 | 26 | ## Manual Installation 27 | 28 | > To use OShadingGradientLayer with a project , you must include all Swift files located inside the `OShadingGradientLayer` directory directly in your project. 29 | 30 | ## CocoaPods Installation 31 | 32 | > NO 33 | 34 | ## Carthage Installation 35 | 36 | > NO 37 | 38 | * * * 39 | 40 | ## Credits 41 | 42 | OShadingGradientLayer is owned and maintained by the [Jorge Ouahbi]. 43 | 44 | ## License 45 | 46 | OShadingGradientLayer is released under the APACHE 2.0 license. See LICENSE for details. 47 | -------------------------------------------------------------------------------- /ScreenShot/ScreenShot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaouahbi/OShadingGradientLayer/dbe3723a7e9a80d0dc4f8518d49dcc6e41773f2c/ScreenShot/ScreenShot_1.png -------------------------------------------------------------------------------- /ScreenShot/ScreenShot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaouahbi/OShadingGradientLayer/dbe3723a7e9a80d0dc4f8518d49dcc6e41773f2c/ScreenShot/ScreenShot_2.png -------------------------------------------------------------------------------- /ScreenShot/ScreenShot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaouahbi/OShadingGradientLayer/dbe3723a7e9a80d0dc4f8518d49dcc6e41773f2c/ScreenShot/ScreenShot_3.png -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Categories/CALayer+AnimationKeyPath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015 - Jorge Ouahbi 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | 18 | // 19 | // CALayer+AnimationKeyPath.swift 20 | // 21 | // Created by Jorge Ouahbi on 26/3/15. 22 | // Copyright © 2015 Jorge Ouahbi. All rights reserved. 23 | // 24 | // v1.0 25 | 26 | import UIKit 27 | public extension CALayer { 28 | 29 | // MARK: - CALayer Animation Helpers 30 | 31 | func animationActionForKey(_ event:String!) -> CABasicAnimation! { 32 | let animation = CABasicAnimation(keyPath: event) 33 | //animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunction.linear) 34 | animation.fromValue = self.presentation()!.value(forKey: event); 35 | return animation 36 | } 37 | 38 | func animateKeyPath(_ keyPath : String, 39 | fromValue : AnyObject?, 40 | toValue:AnyObject?, 41 | beginTime:TimeInterval, 42 | duration:TimeInterval, 43 | delegate:AnyObject?) 44 | { 45 | let animation = CABasicAnimation(keyPath:keyPath); 46 | 47 | var currentValue: AnyObject? = self.presentation()?.value(forKey: keyPath) as AnyObject? 48 | 49 | if (currentValue == nil) { 50 | currentValue = fromValue 51 | } 52 | 53 | animation.fromValue = currentValue 54 | animation.toValue = toValue 55 | animation.delegate = delegate as! CAAnimationDelegate? 56 | 57 | if(duration > 0.0){ 58 | animation.duration = duration 59 | } 60 | if(beginTime > 0.0){ 61 | animation.beginTime = beginTime 62 | } 63 | 64 | //animation.timingFunction = CAMediaTimingFunction(name:CAMediaTimingFunctionName.linear) 65 | animation.setValue(self,forKey:keyPath) 66 | self.add(animation, forKey:keyPath) 67 | self.setValue(toValue,forKey:keyPath) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Categories/CGAffineTransform+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGAffineTransform+Extensions.swift 3 | // ExampleSwift 4 | // 5 | // Created by Jorge Ouahbi on 8/10/16. 6 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension CGAffineTransform { 12 | static func randomRotate() -> CGAffineTransform { 13 | return CGAffineTransform(rotationAngle: CGFloat((drand48() * 360.0).degreesToRadians())) 14 | } 15 | static func randomScale(scaleXMax:CGFloat = 16,scaleYMax:CGFloat = 16) -> CGAffineTransform { 16 | let scaleX = (CGFloat(drand48()) * scaleXMax) + 1.0 17 | let scaleY = (CGFloat(drand48()) * scaleYMax) + 1.0 18 | 19 | let flip:CGFloat = drand48() < 0.5 ? -1 : 1 20 | return CGAffineTransform(scaleX: scaleX, y:scaleY * flip) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Categories/CGColorSpace+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015 - Jorge Ouahbi 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | 18 | // 19 | // CGColorSpace+Extensions.swift 20 | // 21 | // Created by Jorge Ouahbi on 13/5/16. 22 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 23 | // 24 | // v 1.0 25 | 26 | import UIKit 27 | 28 | extension CGColorSpaceModel { 29 | var name : String { 30 | switch self { 31 | case .unknown:return "Unknown" 32 | case .monochrome:return "Monochrome" 33 | case .rgb:return "RGB" 34 | case .cmyk:return "CMYK" 35 | case .lab:return "Lab" 36 | case .deviceN:return "DeviceN" 37 | case .indexed:return "Indexed" 38 | case .pattern:return "Pattern" 39 | case .XYZ:return "XYZ" 40 | @unknown default: 41 | return "Unknown" 42 | } 43 | } 44 | } 45 | extension CGColorSpace { 46 | var isUnknown: Bool { 47 | return model == .unknown 48 | } 49 | var isRGB : Bool { 50 | return model == .rgb 51 | } 52 | var isCMYK : Bool { 53 | return model == .cmyk 54 | } 55 | var isLab : Bool { 56 | return model == .lab 57 | } 58 | var isMonochrome : Bool { 59 | return model == .monochrome 60 | } 61 | var isDeviceN : Bool { 62 | return model == .deviceN 63 | } 64 | var isIndexed : Bool { 65 | return model == .indexed 66 | } 67 | var isPattern : Bool { 68 | return model == .pattern 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Categories/CGFloat+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGFloat+Extensions.swift 3 | // 4 | // Created by Jorge Ouahbi on 19/8/16. 5 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 6 | // 7 | // v1.0 8 | 9 | import UIKit 10 | 11 | 12 | extension CGFloat { 13 | func format(_ short:Bool)->String { 14 | if(short){ 15 | return String(format: "%.1f", self) 16 | }else{ 17 | return String(format: "%.6f", self) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Categories/CGFloat+Math.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015 - Jorge Ouahbi 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | 18 | // CGFloat+Math.swift 19 | // 20 | // Created by Jorge Ouahbi on 25/11/15. 21 | // Copyright d© 2015 Jorge Ouahbi. All rights reserved. 22 | // 23 | 24 | import UIKit 25 | 26 | /// CGFloat Extension for conversion from/to degrees/radians and clamp 27 | 28 | public func clamp(_ value:CGFloat,lowerValue: CGFloat, upperValue: CGFloat) -> CGFloat{ 29 | return Swift.min(Swift.max(value, lowerValue), upperValue) 30 | } 31 | 32 | public func map(input:CGFloat,input_start:CGFloat,input_end:CGFloat,output_start:CGFloat,output_end:CGFloat)-> CGFloat { 33 | let slope = 1.0 * (output_end - output_start) / (input_end - input_start) 34 | return output_start + round(slope * (input - input_start)) 35 | } 36 | 37 | 38 | public extension CGFloat { 39 | 40 | func degreesToRadians () -> CGFloat { 41 | return self * CGFloat(0.01745329252) 42 | } 43 | func radiansToDegrees () -> CGFloat { 44 | return self * CGFloat(57.29577951) 45 | } 46 | mutating func clamp(toLowerValue lowerValue: CGFloat, upperValue: CGFloat){ 47 | self = Swift.min(Swift.max(self, lowerValue), upperValue) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Categories/CGPoint+Extension.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Copyright 2015 - Jorge Ouahbi 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | // 19 | // CGPoint+Extension.swift 20 | // 21 | // Created by Jorge Ouahbi on 26/4/16. 22 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 23 | // 24 | 25 | // v1.0 26 | 27 | import UIKit 28 | 29 | 30 | public func ==(lhs: CGPoint, rhs: CGPoint) -> Bool { 31 | return lhs.equalTo(rhs) 32 | } 33 | 34 | public func *(lhs: CGPoint, rhs: CGSize) -> CGPoint { 35 | return CGPoint(x:lhs.x*rhs.width,y: lhs.y*rhs.height) 36 | } 37 | 38 | public func *(lhs: CGPoint, scalar: CGFloat) -> CGPoint { 39 | return CGPoint(x:lhs.x*scalar,y: lhs.y*scalar) 40 | } 41 | public func /(lhs: CGPoint, rhs: CGSize) -> CGPoint { 42 | return CGPoint(x:lhs.x/rhs.width,y: lhs.y/rhs.height) 43 | } 44 | 45 | 46 | extension CGPoint: Hashable { 47 | 48 | // public var hashValue: Int { 49 | // return self.x.hashValue << MemoryLayout.size ^ self.y.hashValue 50 | // } 51 | 52 | public func hash(into hasher: inout Hasher) { 53 | hasher.combine(self.x) 54 | hasher.combine(self.y) 55 | } 56 | 57 | var isZero : Bool { 58 | return self.equalTo(CGPoint.zero) 59 | } 60 | 61 | func distance(_ point:CGPoint) -> CGFloat { 62 | let diff = CGPoint(x: self.x - point.x, y: self.y - point.y); 63 | return CGFloat(sqrtf(Float(diff.x*diff.x + diff.y*diff.y))); 64 | } 65 | 66 | 67 | func projectLine( _ point:CGPoint, length:CGFloat) -> CGPoint { 68 | 69 | var newPoint = CGPoint(x: point.x, y: point.y) 70 | let x = (point.x - self.x); 71 | let y = (point.y - self.y); 72 | if (x.floatingPointClass == .negativeZero) { 73 | newPoint.y += length; 74 | } else if (y.floatingPointClass == .negativeZero) { 75 | newPoint.x += length; 76 | } else { 77 | #if CGFLOAT_IS_DOUBLE 78 | let angle = atan(y / x); 79 | newPoint.x += sin(angle) * length; 80 | newPoint.y += cos(angle) * length; 81 | #else 82 | let angle = atanf(Float(y) / Float(x)); 83 | newPoint.x += CGFloat(sinf(angle) * Float(length)); 84 | newPoint.y += CGFloat(cosf(angle) * Float(length)); 85 | #endif 86 | } 87 | return newPoint; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Categories/CGSize+Math.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015 - Jorge Ouahbi 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | // 18 | // CGSizeExtension.swift 19 | // 20 | // Created by Jorge Ouahbi on 25/11/15. 21 | // Copyright © 2015 Jorge Ouahbi. All rights reserved. 22 | // 23 | 24 | import UIKit 25 | 26 | /** 27 | * @brief CGSize Extension 28 | */ 29 | extension CGSize 30 | { 31 | func min() -> CGFloat { 32 | return Swift.min(height,width); 33 | } 34 | 35 | func max() -> CGFloat { 36 | return Swift.max(height,width); 37 | } 38 | 39 | func max(_ other : CGSize) -> CGSize { 40 | return self.max() >= other.max() ? self : other; 41 | } 42 | 43 | func hypot() -> CGFloat { 44 | return CoreGraphics.hypot(height,width) 45 | } 46 | 47 | func center() -> CGPoint { 48 | return CGPoint(x:width * 0.5,y:height * 0.5) 49 | } 50 | 51 | func integral() -> CGSize { 52 | return CGSize(width:round(width),height:round(height)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Categories/Double+Math.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015 - Jorge Ouahbi 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | 18 | // Double+Math.swift 19 | // 20 | // Created by Jorge Ouahbi on 25/11/15. 21 | // Copyright © 2015 Jorge Ouahbi. All rights reserved. 22 | // 23 | 24 | import UIKit 25 | 26 | /** 27 | * Double Extension for conversion from/to degrees/radians and clamp 28 | */ 29 | 30 | public extension Double { 31 | 32 | func degreesToRadians () -> Double { 33 | return self * 0.01745329252 34 | } 35 | func radiansToDegrees () -> Double { 36 | return self * 57.29577951 37 | } 38 | 39 | mutating func clamp(toLowerValue lowerValue: Double, upperValue: Double){ 40 | self = min(max(self, lowerValue), upperValue) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Categories/Float+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Float+Extensions.swift 3 | // 4 | // Created by Jorge Ouahbi on 19/8/16. 5 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 6 | // 7 | // v1.0 8 | 9 | import UIKit 10 | 11 | 12 | extension Float { 13 | func format(_ short:Bool)->String { 14 | return CGFloat(self).format(short) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Categories/Float+Math.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015 - Jorge Ouahbi 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | 18 | // Float+Math.swift 19 | // 20 | // Created by Jorge Ouahbi on 25/11/15. 21 | // Copyright © 2015 Jorge Ouahbi. All rights reserved. 22 | // 23 | 24 | import UIKit 25 | 26 | /** 27 | * Float Extension for conversion from/to degrees/radians and clamp 28 | */ 29 | 30 | public extension Float { 31 | 32 | func degreesToRadians () -> Float { 33 | return self * 0.01745329252 34 | } 35 | func radiansToDegrees () -> Float { 36 | return self * 57.29577951 37 | } 38 | 39 | mutating func clamp(toLowerValue lowerValue: Float, upperValue: Float){ 40 | self = min(max(self, lowerValue), upperValue) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Categories/UIColor+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015 - Jorge Ouahbi 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | // 17 | 18 | // 19 | // UIColor+Extensions.swift 20 | // 21 | // Created by Jorge Ouahbi on 27/4/16. 22 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 23 | // 24 | // v 1.0 Merged files 25 | // v 1.1 Some clean and better component access 26 | 27 | 28 | import UIKit 29 | 30 | /// Attributes 31 | 32 | let kLuminanceDarkCutoff:CGFloat = 0.6; 33 | 34 | extension UIColor 35 | { 36 | /// chroma RGB 37 | var croma : CGFloat { 38 | 39 | let comp = components 40 | let min1 = min(comp[1], comp[2]) 41 | let max1 = max(comp[1], comp[2]) 42 | 43 | return max(comp[0], max1) - min(comp[0], min1); 44 | 45 | } 46 | // luma RGB 47 | // WebKit 48 | // luma = (r * 0.2125 + g * 0.7154 + b * 0.0721) * ((double)a / 255.0); 49 | 50 | var luma : CGFloat { 51 | let comp = components 52 | let lumaRed = 0.2126 * Float(comp[0]) 53 | let lumaGreen = 0.7152 * Float(comp[1]) 54 | let lumaBlue = 0.0722 * Float(comp[2]) 55 | let luma = Float(lumaRed + lumaGreen + lumaBlue) 56 | 57 | return CGFloat(luma * Float(components[3])) 58 | } 59 | 60 | var luminance : CGFloat { 61 | let comp = components 62 | let fmin = min(min(comp[0],comp[1]),comp[2]) 63 | let fmax = max(max(comp[0],comp[1]),comp[2]) 64 | return (fmax + fmin) / 2.0; 65 | } 66 | 67 | 68 | var isLight : Bool { 69 | return self.luma >= kLuminanceDarkCutoff 70 | } 71 | 72 | var isDark : Bool { 73 | return self.luma < kLuminanceDarkCutoff; 74 | } 75 | } 76 | 77 | 78 | extension UIColor { 79 | 80 | public convenience init(hex: String) { 81 | var characterSet = NSCharacterSet.whitespacesAndNewlines 82 | characterSet = characterSet.union(NSCharacterSet(charactersIn: "#") as CharacterSet) 83 | let cString = hex.trimmingCharacters(in: characterSet).uppercased() 84 | if (cString.count != 6) { 85 | self.init(white: 1.0, alpha: 1.0) 86 | } else { 87 | var rgbValue: UInt32 = 0 88 | Scanner(string: cString).scanHexInt32(&rgbValue) 89 | 90 | self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, 91 | green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, 92 | blue: CGFloat(rgbValue & 0x0000FF) / 255.0, 93 | alpha: CGFloat(1.0)) 94 | } 95 | } 96 | 97 | // MARK: RGBA components 98 | 99 | /// Returns an array of `CGFloat`s containing four elements with `self`'s: 100 | /// - red (index `0`) 101 | /// - green (index `1`) 102 | /// - blue (index `2`) 103 | /// - alpha (index `3`) 104 | /// or 105 | /// - white (index `0`) 106 | /// - alpha (index `1`) 107 | var components : [CGFloat] { 108 | // Constructs the array in which to store the RGBA-components. 109 | if let components = self.cgColor.components { 110 | if numberOfComponents == 4 { 111 | return [components[0],components[1],components[2],components[3]] 112 | } else { 113 | return [components[0], components[1]] 114 | } 115 | } 116 | 117 | return [] 118 | } 119 | 120 | /// red component 121 | var r : CGFloat { 122 | return components[0]; 123 | } 124 | /// green component 125 | var g: CGFloat { 126 | return components[1]; 127 | } 128 | /// blue component 129 | var b: CGFloat { 130 | return components[2]; 131 | } 132 | 133 | /// number of color components 134 | var numberOfComponents : size_t { 135 | return self.cgColor.numberOfComponents 136 | } 137 | /// color space 138 | var colorSpace : CGColorSpace? { 139 | return self.cgColor.colorSpace 140 | } 141 | 142 | // MARK: HSBA components 143 | 144 | /// Returns an array of `CGFloat`s containing four elements with `self`'s: 145 | /// - hue (index `0`) 146 | /// - saturation (index `1`) 147 | /// - brightness (index `2`) 148 | /// - alpha (index `3`) 149 | var hsbaComponents: [CGFloat] { 150 | // Constructs the array in which to store the HSBA-components. 151 | 152 | var components0:CGFloat = 0 153 | var components1:CGFloat = 0 154 | var components2:CGFloat = 0 155 | var components3:CGFloat = 0 156 | 157 | getHue( &components0, 158 | saturation: &components1, 159 | brightness: &components2, 160 | alpha: &components3) 161 | 162 | return [components0,components1,components2,components3]; 163 | } 164 | /// alpha component 165 | var alpha : CGFloat { 166 | return self.cgColor.alpha 167 | } 168 | /// hue component 169 | var hue: CGFloat { 170 | return hsbaComponents[0]; 171 | } 172 | /// saturation component 173 | var saturation: CGFloat { 174 | return hsbaComponents[1]; 175 | } 176 | 177 | /// Returns a lighter color by the provided percentage 178 | /// 179 | /// - param: lighting percent percentage 180 | /// - returns: lighter UIColor 181 | 182 | func lighterColor(percent : Double) -> UIColor { 183 | return colorWithBrightnessFactor(factor: CGFloat(1 + percent)); 184 | } 185 | 186 | /// Returns a darker color by the provided percentage 187 | /// 188 | /// - param: darking percent percentage 189 | /// - returns: darker UIColor 190 | 191 | func darkerColor(percent : Double) -> UIColor { 192 | return colorWithBrightnessFactor(factor: CGFloat(1 - percent)); 193 | } 194 | 195 | /// Return a modified color using the brightness factor provided 196 | /// 197 | /// - param: factor brightness factor 198 | /// - returns: modified color 199 | func colorWithBrightnessFactor(factor: CGFloat) -> UIColor { 200 | return UIColor(hue: hsbaComponents[0], 201 | saturation: hsbaComponents[1], 202 | brightness: hsbaComponents[2] * factor, 203 | alpha: hsbaComponents[3]) 204 | 205 | } 206 | 207 | /// Color difference 208 | /// 209 | /// - Parameter fromColor: color 210 | /// - Returns: Color difference 211 | func difference(fromColor: UIColor) -> Int { 212 | // get the current color's red, green, blue and alpha values 213 | let red:CGFloat = self.components[0] 214 | let green:CGFloat = self.components[1] 215 | let blue:CGFloat = self.components[2] 216 | //var alpha:CGFloat = self.components[3] 217 | 218 | // get the fromColor's red, green, blue and alpha values 219 | let fromRed:CGFloat = fromColor.components[0] 220 | let fromGreen:CGFloat = fromColor.components[1] 221 | let fromBlue:CGFloat = fromColor.components[2] 222 | //var fromAlpha:CGFloat = fromColor.components[3] 223 | 224 | let redValue = (max(red, fromRed) - min(red, fromRed)) * 255 225 | let greenValue = (max(green, fromGreen) - min(green, fromGreen)) * 255 226 | let blueValue = (max(blue, fromBlue) - min(blue, fromBlue)) * 255 227 | 228 | return Int(redValue + greenValue + blueValue) 229 | } 230 | 231 | /// Brightness difference 232 | /// 233 | /// - Parameter fromColor: color 234 | /// - Returns: Brightness difference 235 | func brightnessDifference(fromColor: UIColor) -> Int { 236 | // get the current color's red, green, blue and alpha values 237 | let red:CGFloat = self.components[0] 238 | let green:CGFloat = self.components[1] 239 | let blue:CGFloat = self.components[2] 240 | //var alpha:CGFloat = self.components[3] 241 | let brightness = Int((((red * 299) + (green * 587) + (blue * 114)) * 255) / 1000) 242 | 243 | // get the fromColor's red, green, blue and alpha values 244 | let fromRed:CGFloat = fromColor.components[0] 245 | let fromGreen:CGFloat = fromColor.components[1] 246 | let fromBlue:CGFloat = fromColor.components[2] 247 | //var fromAlpha:CGFloat = fromColor.components[3] 248 | 249 | let fromBrightness = Int((((fromRed * 299) + (fromGreen * 587) + (fromBlue * 114)) * 255) / 1000) 250 | 251 | return max(brightness, fromBrightness) - min(brightness, fromBrightness) 252 | } 253 | 254 | /// Color delta 255 | /// 256 | /// - Parameter color: color 257 | /// - Returns: Color delta 258 | func colorDelta (color: UIColor) -> Double { 259 | var total = CGFloat(0) 260 | total += pow(self.components[0] - color.components[0], 2) 261 | total += pow(self.components[1] - color.components[1], 2) 262 | total += pow(self.components[2] - color.components[2], 2) 263 | total += pow(self.components[3] - color.components[3], 2) 264 | return sqrt(Double(total) * 255.0) 265 | } 266 | 267 | /// Short UIColor description 268 | var shortDescription:String { 269 | let components = self.components 270 | if (numberOfComponents == 2) { 271 | let c = String(format: "%.1f %.1f", components[0], components[1]) 272 | if let colorSpace = self.colorSpace { 273 | return "\(colorSpace.model.name):\(c)"; 274 | } 275 | return "\(c)"; 276 | } else { 277 | assert(numberOfComponents == 4) 278 | let c = String(format: "%.1f %.1f %.1f %.1f", components[0],components[1],components[2],components[3]) 279 | if let colorSpace = self.colorSpace { 280 | return "\(colorSpace.model.name):\(c)"; 281 | } 282 | return "\(c)"; 283 | } 284 | } 285 | } 286 | 287 | /// Random RGBA 288 | 289 | extension UIColor { 290 | 291 | class public func random() -> UIColor? { 292 | let r = CGFloat(drand48()) 293 | let g = CGFloat(drand48()) 294 | let b = CGFloat(drand48()) 295 | return UIColor(red: r, green: g, blue: b, alpha: 1.0) 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Categories/UIColor+Interpolation.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Copyright 2015 - Jorge Ouahbi 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | // 18 | 19 | // 20 | // UIColor+Interpolation.swift 21 | // 22 | // Created by Jorge Ouahbi on 27/4/16. 23 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 24 | // 25 | 26 | 27 | import UIKit 28 | 29 | extension UIColor 30 | { 31 | // RGBA 32 | 33 | /// Linear interpolation 34 | /// 35 | /// - Parameters: 36 | /// - start: start UIColor 37 | /// - end: start UIColor 38 | /// - t: alpha 39 | /// - Returns: return UIColor 40 | 41 | public class func lerp(_ start:UIColor, end:UIColor, t:CGFloat) -> UIColor { 42 | 43 | let srgba = start.components 44 | let ergba = end.components 45 | 46 | return UIColor(red: Interpolation.lerp(srgba[0],y1: ergba[0],t: t), 47 | green: Interpolation.lerp(srgba[1],y1: ergba[1],t: t), 48 | blue: Interpolation.lerp(srgba[2],y1: ergba[2],t: t), 49 | alpha: Interpolation.lerp(srgba[3],y1: ergba[3],t: t)) 50 | } 51 | 52 | /// Cosine interpolate 53 | /// 54 | /// - Parameters: 55 | /// - start: start UIColor 56 | /// - end: start UIColor 57 | /// - t: alpha 58 | /// - Returns: return UIColor 59 | public class func coserp(_ start:UIColor, end:UIColor, t:CGFloat) -> UIColor { 60 | let srgba = start.components 61 | let ergba = end.components 62 | return UIColor(red: Interpolation.coserp(srgba[0],y1: ergba[0],t: t), 63 | green: Interpolation.coserp(srgba[1],y1: ergba[1],t: t), 64 | blue: Interpolation.coserp(srgba[2],y1: ergba[2],t: t), 65 | alpha: Interpolation.coserp(srgba[3],y1: ergba[3],t: t)) 66 | } 67 | 68 | /// Exponential interpolation 69 | /// 70 | /// - Parameters: 71 | /// - start: start UIColor 72 | /// - end: start UIColor 73 | /// - t: alpha 74 | /// - Returns: return UIColor 75 | 76 | public class func eerp(_ start:UIColor, end:UIColor, t:CGFloat) -> UIColor { 77 | let srgba = start.components 78 | let ergba = end.components 79 | 80 | let r = clamp(Interpolation.eerp(srgba[0],y1: ergba[0],t: t), lowerValue: 0,upperValue: 1) 81 | let g = clamp(Interpolation.eerp(srgba[1],y1: ergba[1],t: t),lowerValue: 0, upperValue: 1) 82 | let b = clamp(Interpolation.eerp(srgba[2],y1: ergba[2],t: t), lowerValue: 0, upperValue: 1) 83 | let a = clamp(Interpolation.eerp(srgba[3],y1: ergba[3],t: t), lowerValue: 0,upperValue: 1) 84 | 85 | assert(r <= 1.0 && g <= 1.0 && b <= 1.0 && a <= 1.0); 86 | 87 | return UIColor(red: r, 88 | green: g, 89 | blue: b, 90 | alpha: a) 91 | 92 | } 93 | 94 | 95 | /// Bilinear interpolation 96 | /// 97 | /// - Parameters: 98 | /// - start: start UIColor 99 | /// - end: start UIColor 100 | /// - t: alpha 101 | /// - Returns: return UIColor 102 | 103 | public class func bilerp(_ start:[UIColor], end:[UIColor], t:[CGFloat]) -> UIColor { 104 | let srgba0 = start[0].components 105 | let ergba0 = end[0].components 106 | 107 | let srgba1 = start[1].components 108 | let ergba1 = end[1].components 109 | 110 | return UIColor(red: Interpolation.bilerp(srgba0[0], y1: ergba0[0], t1: t[0], y2: srgba1[0], y3: ergba1[0], t2: t[1]), 111 | green: Interpolation.bilerp(srgba0[1], y1: ergba0[1], t1: t[0], y2: srgba1[1], y3: ergba1[1], t2: t[1]), 112 | blue: Interpolation.bilerp(srgba0[2], y1: ergba0[2], t1: t[0], y2: srgba1[2], y3: ergba1[2], t2: t[1]), 113 | alpha: Interpolation.bilerp(srgba0[3], y1: ergba0[3], t1: t[0], y2: srgba1[3], y3: ergba1[3], t2: t[1])) 114 | 115 | } 116 | } 117 | 118 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/GradientBaseLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OMGradientLayer.swift 3 | // 4 | // Created by Jorge Ouahbi on 19/8/16. 5 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 6 | // 7 | 8 | // 9 | // Copyright 2015 - Jorge Ouahbi 10 | // 11 | // Licensed under the Apache License, Version 2.0 (the "License"); 12 | // you may not use this file except in compliance with the License. 13 | // You may obtain a copy of the License at 14 | // 15 | // http://www.apache.org/licenses/LICENSE-2.0 16 | // 17 | // Unless required by applicable law or agreed to in writing, software 18 | // distributed under the License is distributed on an "AS IS" BASIS, 19 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | // See the License for the specific language governing permissions and 21 | // limitations under the License. 22 | // 23 | 24 | 25 | import UIKit 26 | import LibControl 27 | 28 | public typealias GradientColors = (UIColor,UIColor) 29 | typealias TransformContextClosure = (_ ctx:CGContext, _ startPoint:CGPoint, _ endPoint:CGPoint, _ startRadius:CGFloat, _ endRadius:CGFloat) -> (Void) 30 | 31 | 32 | public class GradientBaseLayer: CALayer, GradientLayerProtocol { 33 | 34 | // MARK: - OMColorsAndLocationsProtocol 35 | 36 | @objc open var colors: [UIColor] = [] { 37 | didSet { 38 | // if only exist one color, duplicate it. 39 | if (colors.count == 1) { 40 | let color = colors.first! 41 | colors = [color, color]; 42 | } 43 | 44 | // map monochrome colors to rgba colors 45 | colors = colors.map({ 46 | return ($0.colorSpace?.model == .monochrome) ? 47 | UIColor(red: $0.components[0], 48 | green : $0.components[0], 49 | blue : $0.components[0], 50 | alpha : $0.components[1]) : $0 51 | }) 52 | 53 | self.setNeedsDisplay() 54 | } 55 | } 56 | @objc open var locations : [CGFloat]? = nil { 57 | didSet { 58 | if locations != nil{ 59 | locations!.sort { $0 < $1 } 60 | } 61 | self.setNeedsDisplay() 62 | } 63 | } 64 | 65 | open var isAxial : Bool { 66 | return (gradientType == .axial) 67 | } 68 | open var isRadial : Bool { 69 | return (gradientType == .radial) 70 | } 71 | 72 | // MARK: - OMAxialGradientLayerProtocol 73 | 74 | open var gradientType: GradientType = .axial { 75 | didSet { 76 | self.setNeedsDisplay(); 77 | } 78 | } 79 | 80 | @objc open var startPoint: CGPoint = CGPoint(x: 0.0, y: 0.5) { 81 | didSet { 82 | self.setNeedsDisplay(); 83 | } 84 | } 85 | @objc open var endPoint: CGPoint = CGPoint(x: 1.0, y: 0.5) { 86 | didSet{ 87 | self.setNeedsDisplay(); 88 | } 89 | } 90 | 91 | open var extendsBeforeStart : Bool = false { 92 | didSet { 93 | self.setNeedsDisplay() 94 | } 95 | } 96 | open var extendsPastEnd : Bool = false { 97 | didSet { 98 | self.setNeedsDisplay() 99 | } 100 | } 101 | 102 | // MARK: - OMRadialGradientLayerProtocol 103 | @objc open var startRadius: CGFloat = 0 { 104 | didSet { 105 | startRadius = clamp(startRadius, lowerValue: 0, upperValue: 1.0) 106 | self.setNeedsDisplay(); 107 | } 108 | } 109 | @objc open var endRadius: CGFloat = 0 { 110 | didSet { 111 | endRadius = clamp(endRadius, lowerValue: 0, upperValue: 1.0) 112 | self.setNeedsDisplay(); 113 | } 114 | } 115 | 116 | // MARK: OMMaskeableLayerProtocol 117 | open var lineWidth : CGFloat = 1.0 { 118 | didSet { 119 | self.setNeedsDisplay() 120 | } 121 | } 122 | open var stroke : Bool = false { 123 | didSet { 124 | self.setNeedsDisplay() 125 | } 126 | } 127 | open var path: CGPath? { 128 | didSet { 129 | self.setNeedsDisplay() 130 | } 131 | } 132 | 133 | /// Transform the radial gradient 134 | /// example: oval gradient = CGAffineTransform(scaleX: 2, y: 1.0); 135 | 136 | open var radialTransform: CGAffineTransform = CGAffineTransform.identity { 137 | didSet { 138 | self.setNeedsDisplay() 139 | } 140 | } 141 | 142 | 143 | // Some predefined Gradients (from WebKit) 144 | 145 | public lazy var insetGradient:GradientColors = { 146 | return (UIColor(red:0 / 255.0, green:0 / 255.0,blue: 0 / 255.0,alpha: 0 ), 147 | UIColor(red: 0 / 255.0, green:0 / 255.0,blue: 0 / 255.0,alpha: 0.2 )) 148 | 149 | }() 150 | 151 | public lazy var shineGradient:GradientColors = { 152 | return (UIColor(red:1, green:1,blue: 1,alpha: 0 ), 153 | UIColor(red: 1, green:1,blue:1,alpha: 0.8 )) 154 | 155 | }() 156 | 157 | 158 | public lazy var shadeGradient:GradientColors = { 159 | return (UIColor(red: 252 / 255.0, green: 252 / 255.0,blue: 252 / 255.0,alpha: 0.65 ), 160 | UIColor(red: 178 / 255.0, green:178 / 255.0,blue: 178 / 255.0,alpha: 0.65 )) 161 | 162 | }() 163 | 164 | 165 | public lazy var convexGradient:GradientColors = { 166 | return (UIColor(red:1,green:1,blue:1,alpha: 0.43 ), 167 | UIColor(red:1,green:1,blue:1,alpha: 0.5 )) 168 | 169 | }() 170 | 171 | 172 | public lazy var concaveGradient:GradientColors = { 173 | return (UIColor(red:1,green:1,blue:1,alpha: 0.0 ), 174 | UIColor(red:1,green:1,blue:1,alpha: 0.46 )) 175 | 176 | }() 177 | 178 | 179 | // Here's a method that creates a view that allows 360 degree rotation of its two-colour 180 | // gradient based upon input from a slider (or anything). The incoming slider value 181 | // ("x" variable below) is between 0.0 and 1.0. 182 | // 183 | // At 0.0 the gradient is horizontal (with colour A on top, and colour B below), rotating 184 | // through 360 degrees to value 1.0 (identical to value 0.0 - or a full rotation). 185 | // 186 | // E.g. when x = 0.25, colour A is left and colour B is right. At 0.5, colour A is below 187 | // and colour B is above, 0.75 colour A is right and colour B is left. It rotates anti-clockwise 188 | // from right to left. 189 | // 190 | // It takes four arguments: frame, colourA, colourB and the input value (0-1). 191 | // 192 | // from: http://stackoverflow.com/a/29168654/6387073 193 | 194 | 195 | public class func pointsFromNormalizedAngle(_ normalizedAngle:Double) -> (CGPoint,CGPoint) { 196 | 197 | //x is between 0 and 1, eg. from a slider, representing 0 - 360 degrees 198 | //colour A starts on top, with colour B below 199 | //rotations move anti-clockwise 200 | 201 | //create coordinates 202 | let r = 2.0 * .pi; 203 | let a = pow(sin((r*((normalizedAngle+0.75)/2))),2); 204 | let b = pow(sin((r*((normalizedAngle+0.0)/2))),2); 205 | let c = pow(sin((r*((normalizedAngle+0.25)/2))),2); 206 | let d = pow(sin((r*((normalizedAngle+0.5)/2))),2); 207 | 208 | //set the gradient direction 209 | return (CGPoint(x: a, y: b),CGPoint(x: c, y: d)) 210 | } 211 | 212 | // MARK: - Object constructors 213 | required public init?(coder aDecoder: NSCoder) { 214 | super.init(coder:aDecoder) 215 | } 216 | 217 | convenience public init(type:GradientType) { 218 | self.init() 219 | self.gradientType = type 220 | } 221 | 222 | // MARK: - Object Overrides 223 | override public init() { 224 | super.init() 225 | self.allowsEdgeAntialiasing = true 226 | self.contentsScale = UIScreen.main.scale 227 | self.needsDisplayOnBoundsChange = true; 228 | self.drawsAsynchronously = true; 229 | } 230 | 231 | override public init(layer: Any) { 232 | super.init(layer: layer) 233 | 234 | if let other = layer as? GradientBaseLayer { 235 | 236 | // common 237 | self.colors = other.colors 238 | self.locations = other.locations 239 | self.gradientType = other.gradientType 240 | 241 | // axial gradient properties 242 | self.startPoint = other.startPoint 243 | self.endPoint = other.endPoint 244 | self.extendsBeforeStart = other.extendsBeforeStart 245 | self.extendsPastEnd = other.extendsPastEnd 246 | 247 | // radial gradient properties 248 | self.startRadius = other.startRadius 249 | self.endRadius = other.endRadius 250 | 251 | // OMMaskeableLayerProtocol 252 | self.path = other.path 253 | self.stroke = other.stroke 254 | self.lineWidth = other.lineWidth 255 | 256 | self.radialTransform = other.radialTransform 257 | } 258 | } 259 | 260 | // MARK: - Functions 261 | override open class func needsDisplay(forKey event: String) -> Bool { 262 | if (event == GradientLayerProperties.startPoint || 263 | event == GradientLayerProperties.locations || 264 | event == GradientLayerProperties.colors || 265 | event == GradientLayerProperties.endPoint || 266 | event == GradientLayerProperties.startRadius || 267 | event == GradientLayerProperties.endRadius) { 268 | return true 269 | } 270 | return super.needsDisplay(forKey: event) 271 | } 272 | 273 | override open func action(forKey event: String) -> CAAction? { 274 | if (event == GradientLayerProperties.startPoint || 275 | event == GradientLayerProperties.locations || 276 | event == GradientLayerProperties.colors || 277 | event == GradientLayerProperties.endPoint || 278 | event == GradientLayerProperties.startRadius || 279 | event == GradientLayerProperties.endRadius) { 280 | return animationActionForKey(event); 281 | } 282 | return super.action(forKey: event) 283 | } 284 | 285 | override open func draw(in ctx: CGContext) { 286 | // super.drawInContext(ctx) do nothing 287 | let clipBoundingBox = ctx.boundingBoxOfClipPath 288 | ctx.clear(clipBoundingBox); 289 | ctx.clip(to: clipBoundingBox) 290 | } 291 | 292 | func prepareContextIfNeeds(_ ctx:CGContext, scale:CGAffineTransform, closure:TransformContextClosure) { 293 | 294 | let sp = CGPoint( x: self.startPoint.x * self.bounds.size.width, 295 | y: self.startPoint.y * self.bounds.size.height) 296 | let ep = CGPoint( x: self.endPoint.x * self.bounds.size.width, 297 | y: self.endPoint.y * self.bounds.size.height) 298 | 299 | // let ep = self.endPoint * self.bounds.size 300 | let mr = minRadius(self.bounds.size) 301 | // Scaling transformation and keeping track of the inverse 302 | let invScaleT = scale.inverted(); 303 | // Extract the Sx and Sy elements from the inverse matrix (See the Quartz documentation for the math behind the matrices) 304 | let invS = CGPoint(x:invScaleT.a, y:invScaleT.d); 305 | // Transform center and radius of gradient with the inverse 306 | let startPointAffined = CGPoint(x:sp.x * invS.x, y:sp.y * invS.y); 307 | let endPointAffined = CGPoint(x:ep.x * invS.x, y:ep.y * invS.y); 308 | let startRadiusAffined = mr * startRadius * invS.x; 309 | let endRadiusAffined = mr * endRadius * invS.x; 310 | // Draw the gradient with the scale transform on the context 311 | ctx.scaleBy(x: scale.a, y: scale.d); 312 | closure(ctx, startPointAffined, endPointAffined, startRadiusAffined, endRadiusAffined) 313 | // Reset the context 314 | ctx.scaleBy(x: invS.x, y: invS.y); 315 | } 316 | 317 | 318 | func addPathAndClipIfNeeded(_ ctx:CGContext) { 319 | if (self.path != nil) { 320 | ctx.addPath(self.path!); 321 | if (self.stroke) { 322 | ctx.setLineWidth(self.lineWidth); 323 | ctx.replacePathWithStrokedPath(); 324 | } 325 | ctx.clip(); 326 | } 327 | } 328 | 329 | func isDrawable() -> Bool { 330 | if (colors.count == 0) { 331 | // nothing to do 332 | Log.print("\(self.name ?? "") Unable to do the shading without colors.") 333 | return false 334 | } 335 | if (startPoint.isZero && endPoint.isZero) { 336 | // nothing to do 337 | Log.print("\(self.name ?? "") Start point and end point are {x:0, y:0}.") 338 | return false 339 | } 340 | if (startRadius == endRadius && self.isRadial) { 341 | // nothing to do 342 | Log.print("\(self.name ?? "") Start radius and end radius are equal. \(startRadius) \(endRadius)") 343 | return false 344 | } 345 | return true; 346 | } 347 | 348 | 349 | override open var description:String { 350 | get { 351 | var currentDescription:String = "type: \((self.isAxial ? "Axial" : "Radial")) " 352 | if let locations = locations { 353 | if(locations.count == colors.count) { 354 | _ = zip(colors,locations).compactMap { currentDescription += "color: \($0.0.shortDescription) location: \($0.1) " } 355 | } else { 356 | if (locations.count > 0) { 357 | _ = locations.map({currentDescription += "\($0) "}) 358 | } 359 | if (colors.count > 0) { 360 | _ = colors.map({currentDescription += "\($0.shortDescription) "}) 361 | } 362 | } 363 | } 364 | if (self.isRadial) { 365 | currentDescription += "center from : \(startPoint) to \(endPoint), radius from : \(startRadius) to \(endRadius)" 366 | } else if (self.isAxial) { 367 | currentDescription += "from : \(startPoint) to \(endPoint)" 368 | } 369 | if (self.extendsPastEnd) { 370 | currentDescription += " draws after end location " 371 | } 372 | if (self.extendsBeforeStart) { 373 | currentDescription += " draws before start location " 374 | } 375 | return currentDescription 376 | } 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/GradientLayerProtocols.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // OMGradientLayerProtocol.swift 4 | // 5 | // Created by Jorge Ouahbi on 19/8/16. 6 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 7 | // 8 | 9 | // 10 | // Copyright 2015 - Jorge Ouahbi 11 | // 12 | // Licensed under the Apache License, Version 2.0 (the "License"); 13 | // you may not use this file except in compliance with the License. 14 | // You may obtain a copy of the License at 15 | // 16 | // http://www.apache.org/licenses/LICENSE-2.0 17 | // 18 | // Unless required by applicable law or agreed to in writing, software 19 | // distributed under the License is distributed on an "AS IS" BASIS, 20 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | // See the License for the specific language governing permissions and 22 | // limitations under the License. 23 | // 24 | 25 | 26 | import UIKit 27 | 28 | 29 | public enum GradientType : Int { 30 | case axial 31 | case radial 32 | } 33 | 34 | // Animatable Properties 35 | public struct GradientLayerProperties { 36 | 37 | // OMGradientLayerProtocol 38 | static var startPoint = "startPoint" 39 | static var startRadius = "startRadius" 40 | static var endPoint = "endPoint" 41 | static var endRadius = "endRadius" 42 | static var colors = "colors" 43 | static var locations = "locations" 44 | 45 | // OMShapeableLayerProtocol 46 | static var lineWidth = "endRadius" 47 | static var stroke = "colors" 48 | static var path = "path" 49 | 50 | }; 51 | 52 | public protocol OMShapeableLayerProtocol { 53 | // The path stroke line width. 54 | // Defaults to 1.0. Animatable. 55 | var lineWidth : CGFloat {get set} 56 | // The strokeable flag. 57 | // Defaults to false. Animatable. 58 | var stroke : Bool {get set} 59 | // The path. 60 | // Defaults to nil. Animatable. 61 | var path : CGPath? {get set} 62 | } 63 | 64 | public protocol ColorsAndLocationsProtocol { 65 | // The array of UIColor objects defining the color of each gradient 66 | // stop. Defaults to nil. Animatable. 67 | var colors: [UIColor] {get set} 68 | // An optional array of CGFloat objects defining the location of each 69 | // gradient stop as a value in the range [0,1]. The values must be 70 | // monotonically increasing. If a nil array is given, the stops are 71 | // assumed to spread uniformly across the [0,1] range. When rendered, 72 | // the colors are mapped to the output colorspace before being 73 | // interpolated. Defaults to nil. Animatable. 74 | var locations : [CGFloat]? {get set} 75 | } 76 | 77 | // Axial Gradient layer Protocol 78 | public protocol GradientLayerProtocol : OMShapeableLayerProtocol, ColorsAndLocationsProtocol { 79 | 80 | //Defaults to CGPoint(x: 0.5,y: 0.0). Animatable. 81 | var startPoint: CGPoint {get set} 82 | //Defaults to CGPoint(x: 0.5,y: 1.0). Animatable. 83 | var endPoint: CGPoint {get set} 84 | var extendsBeforeStart : Bool {get set} 85 | var extendsPastEnd:Bool {get set} 86 | var gradientType : GradientType {get set} 87 | // Radial 88 | var startRadius:CGFloat {get set} 89 | var endRadius: CGFloat {get set} 90 | } 91 | 92 | 93 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Log/Log.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2017 - Jorge Ouahbi 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | // 18 | // Log.swift 19 | // 20 | // Created by Jorge Ouahbi on 25/9/16. 21 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 22 | // 23 | import Foundation 24 | import os.log 25 | 26 | struct LogConfiguration { 27 | public static let logFilename: String = "trace.log" 28 | public static var details: LogDetails = [.crash] 29 | // Set the minimum log level. By default set to .info which is the minimum. Everything will be loged. 30 | public static var minimumLogLevel: LogLevel = .warn 31 | } 32 | 33 | /** 34 | Enumeration of the log levels 35 | */ 36 | public enum LogLevel: Int { 37 | // Make sure all log events goes to the device log 38 | case productionLogAll = -1 39 | // Trace loging, lowest level 40 | case trace = 0 41 | // Informational loging 42 | case info = 1 43 | // Debug loging, default level 44 | case debug = 2 45 | // Warning loging, You should take notice 46 | case warn = 3 47 | // Error loging, Something went wrong, take action 48 | case error = 4 49 | // Fatal loging, Something went seriously wrong, can't recover from it. 50 | case fatal = 5 51 | // Set the minimumLogLevel to .none to stop everything from loging 52 | case none = 6 53 | //static var minimumLogLevel: LogLevel = .none 54 | /** 55 | Get the emoticon for the log level. 56 | */ 57 | public func description() -> String { 58 | switch self { 59 | case .info: 60 | return "✔️" 61 | case .trace: 62 | return "❕" 63 | case .debug: 64 | return "✳️" 65 | case .warn: 66 | return "⚠️" 67 | case .error: 68 | return "📛" 69 | case .fatal: 70 | return "🆘" 71 | case .productionLogAll: 72 | return "🚧" 73 | case .none: 74 | return "" 75 | } 76 | } 77 | } 78 | //TODO: (jom) complete this shit 79 | // LogProtocol 80 | public protocol LogProtocol { 81 | // associatedtype DumpType 82 | // associatedtype PrintType 83 | static func redirect(_ fileName: String) 84 | static func readLogFile(_ fileName: String) -> String 85 | // static func dump(_ value: DumpType) 86 | // static func print(_ object: PrintType, level: LogLevel, filename: String, 87 | // line: Int, funcname: String, trace: [String]) 88 | static var dateFormatter: DateFormatter {get} 89 | } 90 | // TODO: (jom) debugPrint 91 | struct LogDetails: OptionSet { 92 | let rawValue: Int 93 | static let date = LogDetails(rawValue: 1 << 0) 94 | static let process = LogDetails(rawValue: 1 << 1) 95 | static let file = LogDetails(rawValue: 1 << 2) 96 | static let crash = LogDetails(rawValue: 1 << 3) 97 | static let all: LogDetails = [.date, .process, .file, .crash] 98 | } 99 | 100 | public protocol LogCrashProtocol { 101 | func recordError(error: NSError) 102 | } 103 | /** 104 | Extending the Stuff object with print functionality 105 | */ 106 | public class Log: LogProtocol { 107 | static var crash: LogCrashProtocol? 108 | /// Redirect Stderr to document folder 109 | /// - Parameter fileName: Filename 110 | public static func redirect(_ fileName: String) { 111 | let allPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) 112 | let documentsDirectory = allPaths.first! 113 | let pathStringComponent = "/" + fileName 114 | let pathForLog = (documentsDirectory as NSString).appending(pathStringComponent) 115 | freopen(pathForLog.cString(using: String.Encoding.ascii)!, "a+", stdout) 116 | Log.print("Redicted logs to \(pathForLog)", .info) 117 | 118 | } 119 | public static func readLogFile(_ fileName: String) -> String { 120 | do { 121 | // get the documents folder url 122 | if let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { 123 | // create the destination url for the text file to be saved 124 | let fileURL = documentDirectory.appendingPathComponent(fileName) 125 | // any posterior code goes here 126 | // reading from disk 127 | return try String(contentsOf: fileURL) 128 | } 129 | } catch let error { 130 | Log.print("readLogFile \(error)", .error) 131 | } 132 | return "" 133 | } 134 | /// Date formater 135 | public static var dateFormatter: DateFormatter = { 136 | let dateFormatter = DateFormatter() 137 | dateFormatter.dateFormat = "MM/dd/yyyy HH:mm:ss:SSS" 138 | return dateFormatter 139 | }() 140 | /// Dumps the given object’s contents using its mirror to standard output. 141 | /// - Parameter value: Value to dump 142 | static func dump(_ value: T) { 143 | Swift.dump(value) 144 | } 145 | // 146 | // Build a log text for writing to the output 147 | // - Parameters: 148 | // - object: T 149 | // - level: LogLevel 150 | // - filename: #file 151 | // - line: #line 152 | // - funcname: #function 153 | #if !TEST_COVERAGE 154 | static func buildMessage(_ object: T, 155 | _ level: LogLevel = (T.self is Error.Type ? .error : .debug), 156 | filename: String = #file, line: Int = #line, funcname: String = #function) -> String { 157 | let process = ProcessInfo.processInfo 158 | let threadId = Thread.current.name ?? "" 159 | let file = URL(string: filename)?.lastPathComponent ?? "" 160 | var output = "\(object)" // Output trace 161 | if let object = object as? Error { // Get the symbols only when exist a error. 162 | output = "\(object.localizedDescription)" 163 | //"\(GCDebugManager.formatStackSymbols(GCDebugManager.callStackAppSymbols))" 164 | } 165 | var logText: String 166 | if LogConfiguration.details.contains([.date, .process, .file]) { // Check the detail level 167 | logText = """ 168 | \n\(level.description()) .\(level) 169 | ⏱ \(dateFormatter.string(from: Foundation.Date())) 170 | 📱 \(process.processName) [\(process.processIdentifier):\(threadId)] 171 | 📂 \(file)(\(line)) 172 | ⚙️ \(funcname) 173 | ➡️\r\t\(output) 174 | """ 175 | } else if LogConfiguration.details.contains([.date, .process]) { 176 | logText = """ 177 | \n\(level.description()) .\(level) 178 | ⏱ \(dateFormatter.string(from: Foundation.Date())) 179 | 📱 \(process.processName) [\(process.processIdentifier):\(threadId)] 180 | ⚙️ \(funcname) 181 | ➡️\r\t\(output) 182 | """ 183 | } else if LogConfiguration.details.contains([.date]) { 184 | logText = """ 185 | \n\(level.description()) .\(level) 186 | ⏱ \(dateFormatter.string(from: Foundation.Date())) 187 | ⚙️ \(funcname) 188 | ➡️\r\t\(output) 189 | """ 190 | } else if LogConfiguration.details.contains([.process, .file]) { 191 | logText = """ 192 | \n\(level.description()) .\(level) 193 | 📱 \(process.processName) [\(process.processIdentifier):\(threadId)] 194 | 📂 \(file)(\(line)) 195 | ⚙️ \(funcname) 196 | ➡️\r\t\(output) 197 | """ 198 | } else { 199 | logText = """ 200 | \n\(level.description()) .\(level) 201 | ⚙️ \(funcname) 202 | ➡️\r\t\(output) 203 | """ 204 | } 205 | return logText 206 | } 207 | #endif 208 | /// 209 | /// The print command for writing to the output window 210 | /// - Parameters: 211 | /// - object: T 212 | /// - level: LogLevel 213 | /// - filename: #file 214 | /// - line: #line 215 | /// - funcname: #function 216 | /// 217 | // TODO: check if we are in test. 218 | #if TEST_COVERAGE 219 | static func print(_ object: T, _ level: LogLevel = .debug, filename: String = #file, line: Int = #line, funcname: String = #function) { 220 | NSLog(object) 221 | } 222 | #else 223 | static func print(_ object: T, 224 | _ level: LogLevel = (T.self is Error.Type ? .error : .debug), 225 | filename: String = #file, 226 | line: Int = #line, 227 | funcname: String = #function) { 228 | if level.rawValue >= LogConfiguration.minimumLogLevel.rawValue { 229 | let logText = buildMessage(object, level, filename: filename, line: line, funcname: funcname) 230 | // Report to Crashlytics the error 231 | if LogConfiguration.details.contains(.crash) && object is Error { 232 | if let err = object as? NSError { 233 | //Crashlytics.sharedInstance().recordError(err) 234 | Log.crash?.recordError(error: err) 235 | } 236 | } 237 | if LogConfiguration.minimumLogLevel == .productionLogAll || level == .productionLogAll { 238 | if #available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3, *) { 239 | let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "GCLog") 240 | // Parameters can be logged in two ways depending on the privacy level of the log. 241 | // Private data can be logged using %{PRIVATE}@ and public data with %{PUBLIC}@. 242 | os_log("%{PUBLIC}@", log: log, logText) 243 | } else { 244 | NSLog("{GCLog} \(logText)") 245 | } 246 | } else { 247 | Swift.print(logText) 248 | } 249 | } 250 | } 251 | #endif 252 | } 253 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Math/Easing.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Copyright 2015 - Jorge Ouahbi 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | 19 | // 20 | // Easing.swift 21 | // 22 | // Created by Jorge Ouahbi on 21/4/16. 23 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 24 | // 25 | 26 | import Foundation 27 | 28 | public typealias EasingFunction = (Double) -> Double 29 | public typealias EasingFunctionsTuple = (function: EasingFunction, name: String) 30 | 31 | 32 | /* 33 | 34 | public var kEasingFunctions : Array = [ 35 | (Linear,"Linear"), 36 | (QuadraticEaseIn,"QuadraticEaseIn"), 37 | (QuadraticEaseOut,"QuadraticEaseOut"), 38 | (QuadraticEaseInOut,"QuadraticEaseInOut"), 39 | (CubicEaseIn,"CubicEaseIn"), 40 | (CubicEaseOut,"CubicEaseOut"), 41 | (CubicEaseInOut,"CubicEaseInOut"), 42 | (QuarticEaseIn,"QuarticEaseIn"), 43 | (QuarticEaseOut,"QuarticEaseOut"), 44 | (QuarticEaseInOut,"QuarticEaseInOut"), 45 | (QuinticEaseIn,"QuinticEaseIn"), 46 | (QuinticEaseOut,"QuinticEaseOut"), 47 | (QuinticEaseInOut,"QuinticEaseInOut"), 48 | (SineEaseIn,"SineEaseIn"), 49 | (SineEaseOut,"SineEaseOut"), 50 | (SineEaseInOut,"SineEaseInOut"), 51 | (CircularEaseIn,"CircularEaseIn"), 52 | (CircularEaseOut,"CircularEaseOut"), 53 | (CircularEaseInOut,"CircularEaseInOut"), 54 | (ExponentialEaseIn,"ExponentialEaseIn"), 55 | (ExponentialEaseOut,"ExponentialEaseOut"), 56 | (ExponentialEaseInOut,"ExponentialEaseInOut"), 57 | (ElasticEaseIn,"ElasticEaseIn"), 58 | (ElasticEaseOut,"ElasticEaseOut"), 59 | (ElasticEaseInOut,"ElasticEaseInOut"), 60 | (BackEaseIn,"BackEaseIn"), 61 | (BackEaseOut,"BackEaseOut"), 62 | (BackEaseInOut,"BackEaseInOut"), 63 | (BounceEaseIn,"BounceEaseIn"), 64 | (BounceEaseOut,"BounceEaseOut"), 65 | (BounceEaseInOut,"BounceEaseInOut") 66 | ] 67 | 68 | */ 69 | // 70 | // easing.c 71 | // 72 | // Copyright (c) 2011, Auerhaus Development, LLC 73 | // 74 | // This program is free software. It comes without any warranty, to 75 | // the extent permitted by applicable law. You can redistribute it 76 | // and/or modify it under the terms of the Do What The Fuck You Want 77 | // To Public License, Version 2, as published by Sam Hocevar. See 78 | // http://sam.zoy.org/wtfpl/COPYING for more details. 79 | // 80 | 81 | // Modeled after the line y = x 82 | func Linear(_ p: Double) -> Double 83 | { 84 | return p; 85 | } 86 | 87 | // Modeled after the parabola y = x^2 88 | func QuadraticEaseIn(_ p: Double) -> Double 89 | { 90 | return p * p; 91 | } 92 | 93 | // Modeled after the parabola y = -x^2 + 2x 94 | func QuadraticEaseOut(_ p: Double) -> Double 95 | { 96 | return -(p * (p - 2)); 97 | } 98 | 99 | // Modeled after the piecewise quadratic 100 | // y = (1/2)((2x)^2) ; [0, 0.5) 101 | // y = -(1/2)((2x-1)*(2x-3) - 1) ; [0.5, 1] 102 | func QuadraticEaseInOut(_ p: Double) -> Double 103 | { 104 | if(p < 0.5) 105 | { 106 | return 2 * p * p; 107 | } 108 | else 109 | { 110 | return (-2 * p * p) + (4 * p) - 1; 111 | } 112 | } 113 | 114 | // Modeled after the cubic y = x^3 115 | func CubicEaseIn(_ p: Double) -> Double 116 | { 117 | return p * p * p; 118 | } 119 | 120 | // Modeled after the cubic y = (x - 1)^3 + 1 121 | func CubicEaseOut(_ p: Double) -> Double 122 | { 123 | let f = (p - 1); 124 | return f * f * f + 1; 125 | } 126 | 127 | // Modeled after the piecewise cubic 128 | // y = (1/2)((2x)^3) ; [0, 0.5) 129 | // y = (1/2)((2x-2)^3 + 2) ; [0.5, 1] 130 | func CubicEaseInOut(_ p: Double) -> Double 131 | { 132 | if(p < 0.5) 133 | { 134 | return 4 * p * p * p; 135 | } 136 | else 137 | { 138 | let f = ((2 * p) - 2); 139 | return 0.5 * f * f * f + 1; 140 | } 141 | } 142 | 143 | // Modeled after the quartic x^4 144 | func QuarticEaseIn(_ p: Double) -> Double 145 | { 146 | return p * p * p * p; 147 | } 148 | 149 | // Modeled after the quartic y = 1 - (x - 1)^4 150 | func QuarticEaseOut(_ p: Double) -> Double 151 | { 152 | let f = (p - 1); 153 | return f * f * f * (1 - p) + 1; 154 | } 155 | 156 | // Modeled after the piecewise quartic 157 | // y = (1/2)((2x)^4) ; [0, 0.5) 158 | // y = -(1/2)((2x-2)^4 - 2) ; [0.5, 1] 159 | func QuarticEaseInOut(_ p: Double) -> Double 160 | { 161 | if(p < 0.5) 162 | { 163 | return 8 * p * p * p * p; 164 | } 165 | else 166 | { 167 | let f = (p - 1); 168 | return -8 * f * f * f * f + 1; 169 | } 170 | } 171 | 172 | // Modeled after the quintic y = x^5 173 | func QuinticEaseIn(_ p: Double) -> Double 174 | { 175 | return p * p * p * p * p; 176 | } 177 | 178 | // Modeled after the quintic y = (x - 1)^5 + 1 179 | func QuinticEaseOut(_ p: Double) -> Double 180 | { 181 | let f = (p - 1); 182 | return f * f * f * f * f + 1; 183 | } 184 | 185 | // Modeled after the piecewise quintic 186 | // y = (1/2)((2x)^5) ; [0, 0.5) 187 | // y = (1/2)((2x-2)^5 + 2) ; [0.5, 1] 188 | func QuinticEaseInOut(_ p: Double) -> Double 189 | { 190 | if(p < 0.5) 191 | { 192 | return 16 * p * p * p * p * p; 193 | } 194 | else 195 | { 196 | let f = ((2 * p) - 2); 197 | return 0.5 * f * f * f * f * f + 1; 198 | } 199 | } 200 | 201 | // Modeled after quarter-cycle of sine wave 202 | func SineEaseIn(_ p: Double) -> Double 203 | { 204 | return sin((p - 1) * .pi / 2.0) + 1; 205 | } 206 | 207 | // Modeled after quarter-cycle of sine wave (different phase) 208 | func SineEaseOut(_ p: Double) -> Double 209 | { 210 | return sin(p * .pi / 2.0); 211 | } 212 | 213 | // Modeled after half sine wave 214 | func SineEaseInOut(_ p: Double) -> Double 215 | { 216 | return 0.5 * (1 - cos(p * .pi)); 217 | } 218 | 219 | // Modeled after shifted quadrant IV of unit circle 220 | func CircularEaseIn(_ p: Double) -> Double 221 | { 222 | return 1 - sqrt(1 - (p * p)); 223 | } 224 | 225 | // Modeled after shifted quadrant II of unit circle 226 | func CircularEaseOut(_ p: Double) -> Double 227 | { 228 | return sqrt((2 - p) * p); 229 | } 230 | 231 | // Modeled after the piecewise circular function 232 | // y = (1/2)(1 - sqrt(1 - 4x^2)) ; [0, 0.5) 233 | // y = (1/2)(sqrt(-(2x - 3)*(2x - 1)) + 1) ; [0.5, 1] 234 | func CircularEaseInOut(_ p: Double) -> Double 235 | { 236 | if(p < 0.5) 237 | { 238 | return 0.5 * (1 - sqrt(1 - 4 * (p * p))); 239 | } 240 | else 241 | { 242 | return 0.5 * (sqrt(-((2 * p) - 3) * ((2 * p) - 1)) + 1); 243 | } 244 | } 245 | 246 | // Modeled after the exponential function y = 2^(10(x - 1)) 247 | func ExponentialEaseIn(_ p: Double) -> Double 248 | { 249 | return (p == 0.0) ? p : pow(2, 10 * (p - 1)); 250 | } 251 | 252 | // Modeled after the exponential function y = -2^(-10x) + 1 253 | func ExponentialEaseOut(_ p: Double) -> Double 254 | { 255 | return (p == 1.0) ? p : 1 - pow(2, -10 * p); 256 | } 257 | 258 | // Modeled after the piecewise exponential 259 | // y = (1/2)2^(10(2x - 1)) ; [0,0.5) 260 | // y = -(1/2)*2^(-10(2x - 1))) + 1 ; [0.5,1] 261 | func ExponentialEaseInOut(_ p: Double) -> Double 262 | { 263 | if(p == 0.0 || p == 1.0) {return p;} 264 | 265 | if(p < 0.5) 266 | { 267 | return 0.5 * pow(2, (20 * p) - 10); 268 | } 269 | else 270 | { 271 | return -0.5 * pow(2, (-20 * p) + 10) + 1; 272 | } 273 | } 274 | 275 | // Modeled after the damped sine wave y = sin(13pi/2*x)*pow(2, 10 * (x - 1)) 276 | func ElasticEaseIn(_ p: Double) -> Double 277 | { 278 | return sin(13 * .pi / 2.0 * p) * pow(2, 10 * (p - 1)); 279 | } 280 | 281 | // Modeled after the damped sine wave y = sin(-13pi/2*(x + 1))*pow(2, -10x) + 1 282 | func ElasticEaseOut(_ p: Double) -> Double 283 | { 284 | return sin(-13 * .pi / 2.0 * (p + 1)) * pow(2, -10 * p) + 1; 285 | } 286 | 287 | // Modeled after the piecewise exponentially-damped sine wave: 288 | // y = (1/2)*sin(13pi/2*(2*x))*pow(2, 10 * ((2*x) - 1)) ; [0,0.5) 289 | // y = (1/2)*(sin(-13pi/2*((2x-1)+1))*pow(2,-10(2*x-1)) + 2) ; [0.5, 1] 290 | func ElasticEaseInOut(_ p: Double) -> Double 291 | { 292 | if(p < 0.5) 293 | { 294 | return 0.5 * sin(13 * .pi / 2.0 * (2 * p)) * pow(2, 10 * ((2 * p) - 1)); 295 | } 296 | else 297 | { 298 | return 0.5 * (sin(-13 * .pi / 2.0 * ((2 * p - 1) + 1)) * pow(2, -10 * (2 * p - 1)) + 2); 299 | } 300 | } 301 | 302 | // Modeled after the overshooting cubic y = x^3-x*sin(x*pi) 303 | func BackEaseIn(_ p: Double) -> Double 304 | { 305 | return p * p * p - p * sin(p * .pi); 306 | } 307 | 308 | // Modeled after overshooting cubic y = 1-((1-x)^3-(1-x)*sin((1-x)*pi)) 309 | func BackEaseOut(_ p: Double) -> Double 310 | { 311 | let f = (1 - p); 312 | return 1 - (f * f * f - f * sin(f * .pi)); 313 | } 314 | 315 | // Modeled after the piecewise overshooting cubic function: 316 | // y = (1/2)*((2x)^3-(2x)*sin(2*x*pi)) ; [0, 0.5) 317 | // y = (1/2)*(1-((1-x)^3-(1-x)*sin((1-x)*pi))+1) ; [0.5, 1] 318 | func BackEaseInOut(_ p: Double) -> Double 319 | { 320 | if(p < 0.5) 321 | { 322 | let f = 2 * p; 323 | return 0.5 * (f * f * f - f * sin(f * .pi)); 324 | } 325 | else 326 | { 327 | let f = (1 - (2*p - 1)); 328 | let fsin = sin(f * .pi) 329 | return 0.5 * (1 - (f * f * f - f * fsin)) + 0.5; 330 | } 331 | } 332 | 333 | func BounceEaseIn(_ p: Double) -> Double 334 | { 335 | return 1 - BounceEaseOut(1 - p); 336 | } 337 | 338 | func BounceEaseOut(_ p: Double) -> Double 339 | { 340 | if(p < 4/11.0) 341 | { 342 | return (121 * p * p)/16.0; 343 | } 344 | else if(p < 8/11.0) 345 | { 346 | return (363/40.0 * p * p) - (99/10.0 * p) + 17/5.0; 347 | } 348 | else if(p < 9/10.0) 349 | { 350 | return (4356/361.0 * p * p) - (35442/1805.0 * p) + 16061/1805.0; 351 | } 352 | else 353 | { 354 | return (54/5.0 * p * p) - (513/25.0 * p) + 268/25.0; 355 | } 356 | } 357 | 358 | func BounceEaseInOut(_ p: Double) -> Double 359 | { 360 | if(p < 0.5) 361 | { 362 | return 0.5 * BounceEaseIn(p*2); 363 | } 364 | else 365 | { 366 | return 0.5 * BounceEaseOut(p * 2 - 1) + 0.5; 367 | } 368 | } 369 | 370 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Math/Interpolation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015 - Jorge Ouahbi 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | 18 | // 19 | // Interpolation.swift 20 | // 21 | // Created by Jorge Ouahbi on 13/5/16. 22 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 23 | // 24 | 25 | import UIKit 26 | 27 | /// Interpolation type: http://paulbourke.net/miscellaneous/interpolation/ 28 | /// 29 | /// - linear: lineal interpolation 30 | /// - exponential: exponential interpolation 31 | /// - cosine: cosine interpolation 32 | /// - cubic: cubic interpolation 33 | /// - bilinear: bilinear interpolation 34 | 35 | enum InterpolationType { 36 | case linear 37 | case exponential 38 | case cosine 39 | case cubic 40 | case bilinear 41 | } 42 | 43 | class Interpolation 44 | { 45 | /// Cubic Interpolation 46 | /// 47 | /// - Parameters: 48 | /// - y0: element 0 49 | /// - y1: element 1 50 | /// - y2: element 2 51 | /// - y3: element 3 52 | /// - t: alpha 53 | /// - Returns: the interpolate value 54 | /// - Note: 55 | /// Paul Breeuwsma proposes the following coefficients for a smoother interpolated curve, 56 | /// which uses the slope between the previous point and the next as the derivative at the current point. 57 | /// This results in what are generally referred to as Catmull-Rom splines. 58 | /// a0 = -0.5*y0 + 1.5*y1 - 1.5*y2 + 0.5*y3; 59 | /// a1 = y0 - 2.5*y1 + 2*y2 - 0.5*y3; 60 | /// a2 = -0.5*y0 + 0.5*y2; 61 | /// a3 = y1; 62 | 63 | class func cubicerp(_ y0:CGFloat,y1:CGFloat,y2:CGFloat,y3:CGFloat,t:CGFloat) -> CGFloat { 64 | var a0:CGFloat 65 | var a1:CGFloat 66 | var a2:CGFloat 67 | var a3:CGFloat 68 | var t2:CGFloat 69 | 70 | assert(t >= 0.0 && t <= 1.0); 71 | 72 | t2 = t*t; 73 | a0 = y3 - y2 - y0 + y1; 74 | a1 = y0 - y1 - a0; 75 | a2 = y2 - y0; 76 | a3 = y1; 77 | 78 | return(a0*t*t2+a1*t2+a2*t+a3); 79 | } 80 | 81 | /// Exponential Interpolation 82 | /// 83 | /// - Parameters: 84 | /// - y0: element 0 85 | /// - y1: element 1 86 | /// - t: alpha 87 | /// - Returns: the interpolate value 88 | 89 | class func eerp(_ y0:CGFloat,y1:CGFloat,t:CGFloat) -> CGFloat { 90 | assert(t >= 0.0 && t <= 1.0); 91 | let end = log(max(Double(y0), 0.01)) 92 | let start = log(max(Double(y1), 0.01)) 93 | return CGFloat(exp(start - (end + start) * Double(t))) 94 | } 95 | 96 | 97 | 98 | /// Linear Interpolation 99 | /// 100 | /// - Parameters: 101 | /// - y0: element 0 102 | /// - y1: element 1 103 | /// - t: alpha 104 | /// - Returns: the interpolate value 105 | /// - Note: 106 | /// Imprecise method which does not guarantee v = v1 when t = 1, due to floating-point arithmetic error. 107 | /// This form may be used when the hardware has a native Fused Multiply-Add instruction. 108 | /// return v0 + t*(v1-v0); 109 | /// 110 | /// Precise method which guarantees v = v1 when t = 1. 111 | /// (1-t)*v0 + t*v1; 112 | 113 | class func lerp(_ y0:CGFloat,y1:CGFloat,t:CGFloat) -> CGFloat { 114 | assert(t >= 0.0 && t <= 1.0); 115 | let inverse = 1.0 - t; 116 | return inverse * y0 + t * y1 117 | } 118 | 119 | /// Bilinear Interpolation 120 | /// 121 | /// - Parameters: 122 | /// - y0: element 0 123 | /// - y1: element 1 124 | /// - t1: alpha 125 | /// - y2: element 2 126 | /// - y3: element 3 127 | /// - t2: alpha 128 | /// - Returns: the interpolate value 129 | 130 | class func bilerp(_ y0:CGFloat,y1:CGFloat,t1:CGFloat,y2:CGFloat,y3:CGFloat,t2:CGFloat) -> CGFloat { 131 | assert(t1 >= 0.0 && t1 <= 1.0); 132 | assert(t2 >= 0.0 && t2 <= 1.0); 133 | 134 | let x = lerp(y0, y1: y1, t: t1) 135 | let y = lerp(y2, y1: y3, t: t2) 136 | 137 | return lerp(x, y1: y, t: 0.5) 138 | } 139 | 140 | /// Cosine Interpolation 141 | /// 142 | /// - Parameters: 143 | /// - y0: element 0 144 | /// - y1: element 1 145 | /// - t: alpha 146 | /// - Returns: the interpolate value 147 | 148 | class func coserp(_ y0:CGFloat,y1:CGFloat,t:CGFloat) -> CGFloat { 149 | assert(t >= 0.0 && t <= 1.0); 150 | let mu2 = CGFloat(1.0-cos(Double(t) * .pi))/2; 151 | return (y0*(1.0-mu2)+y1*mu2); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/Math/Math.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015 - Jorge Ouahbi 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | // 18 | // Math.swift 19 | // 20 | // Created by Jorge Ouahbi on 9/5/16. 21 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 22 | // 23 | 24 | import UIKit 25 | 26 | 27 | // clamp a number between lower and upper. 28 | public func clamp(_ value: T, lower: T, upper: T) -> T { 29 | return min(max(value, lower), upper) 30 | } 31 | 32 | // is the number between lower and upper. 33 | public func between(_ value: T, lower: T, upper: T , include: Bool = true) -> Bool { 34 | let left = min(lower, upper) 35 | let right = max(lower, upper) 36 | return include ? (value >= left && value <= right) : (value > left && value < right) 37 | } 38 | 39 | // min radius from rectangle 40 | public func minRadius(_ size: CGSize) -> CGFloat { 41 | assert(size != CGSize.zero) 42 | return size.min() * 0.5; 43 | } 44 | 45 | // max radius from a rectangle (pythagoras) 46 | public func maxRadius(_ size: CGSize) -> CGFloat { 47 | assert(size != CGSize.zero) 48 | return 0.5 * sqrt(size.width * size.width + size.height * size.height) 49 | } 50 | 51 | // monotonically increasing function 52 | public func monotonic(_ numberOfElements:Int) -> [CGFloat] { 53 | assert(numberOfElements > 0) 54 | var monotonicFunction:[CGFloat] = [] 55 | let numberOfLocations:CGFloat = CGFloat(numberOfElements - 1) 56 | for locationIndex in 0 ..< numberOfElements { 57 | monotonicFunction.append(CGFloat(locationIndex) / numberOfLocations) 58 | } 59 | return monotonicFunction 60 | } 61 | 62 | // redistributes values on a slope (ease-in ease-out) 63 | public func slope( x:Float, A:Float) -> Float { 64 | let p = powf(x,A); 65 | return p/(p + powf(1.0-x, A)); 66 | } 67 | 68 | public func linlin( val:Double, inMin:Double, inMax:Double, outMin:Double, outMax:Double) -> Double { 69 | return ((val - inMin) / (inMax - inMin) * (outMax - outMin)) + outMin; 70 | } 71 | 72 | public func linexp( val:Double, inMin:Double, inMax:Double, outMin:Double, outMax:Double) -> Double { 73 | //clipping 74 | let valclamp = max(min(val, inMax), inMin); 75 | return pow((outMax / outMin), (valclamp - inMin) / (inMax - inMin)) * outMin; 76 | } 77 | public func explin(val:Double, inMin:Double, inMax:Double, outMin:Double, outMax:Double) -> Double { 78 | //clipping 79 | let valclamp = max(min(val, inMax), inMin); 80 | return (log(valclamp/inMin) / log(inMax/inMin) * (outMax - outMin)) + outMin; 81 | } 82 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/OShadingGradientLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015 - Jorge Ouahbi 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | 18 | import UIKit 19 | import LibControl 20 | 21 | //public func * (left: CGPoint, right: CGSize) -> CGSize { 22 | // return CGSize(width: left.x * right.width, height: left.y * right.height) 23 | //} 24 | //public func / (left: CGPoint, right: CGSize) -> CGSize { 25 | // return CGSize(width: left.x * right.width, height: left.y * right.height) 26 | //} 27 | // 28 | ////public func * (left: CGPoint, right: CGSize) -> CGPoint { 29 | //// return CGPoint(x: left.x * right.width, y: left.y * right.height) 30 | ////} 31 | ////public func / (left: CGPoint, right: CGSize) -> CGPoint { 32 | //// return CGPoint(x: left.x * right.width, y: left.y * right.height) 33 | ////} 34 | 35 | public class OMShadingGradientLayer: GradientBaseLayer { 36 | 37 | var shading: [ShadingGradient] = [] 38 | /// Contruct gradient object with a type 39 | convenience public init(type: GradientType) { 40 | self.init() 41 | self.gradientType = type; 42 | 43 | if type == .radial { 44 | self.startPoint = CGPoint(x: 0.5,y: 0.5) 45 | self.endPoint = CGPoint(x: 0.5,y: 0.5) 46 | } 47 | } 48 | 49 | // MARK: - Object Overrides 50 | override public init() { 51 | super.init() 52 | } 53 | 54 | /// Slope function 55 | open var slopeFunction: EasingFunction = Linear { 56 | didSet { 57 | setNeedsDisplay(); 58 | } 59 | } 60 | /// Interpolation gardient function 61 | open var function: GradientFunction = .linear { 62 | didSet { 63 | setNeedsDisplay(); 64 | } 65 | } 66 | /// Contruct gradient object with a layer 67 | override public init(layer: Any) { 68 | super.init(layer: layer as AnyObject) 69 | if let other = layer as? OMShadingGradientLayer { 70 | self.slopeFunction = other.slopeFunction; 71 | } 72 | } 73 | /// Contruct gradient object from NSCoder 74 | required public init?(coder aDecoder: NSCoder) { 75 | super.init(coder: aDecoder) 76 | } 77 | 78 | override open func draw(in ctx: CGContext) { 79 | super.draw(in: ctx) 80 | 81 | var locations :[CGFloat]? = self.locations 82 | var colors :[UIColor] = self.colors 83 | var startPoint : CGPoint = self.startPoint 84 | var endPoint : CGPoint = self.endPoint 85 | var startRadius : CGFloat = self.startRadius 86 | var endRadius : CGFloat = self.endRadius 87 | 88 | let player = presentation() 89 | 90 | if let player = player { 91 | 92 | Log.print("\(self.name ?? "") (presentation) \(player)") 93 | 94 | colors = player.colors 95 | locations = player.locations 96 | startPoint = player.startPoint 97 | endPoint = player.endPoint 98 | startRadius = player.startRadius 99 | endRadius = player.endRadius 100 | 101 | } else { 102 | Log.print("\(self.name ?? "") (model) \(self)") 103 | } 104 | 105 | if isDrawable() { 106 | ctx.saveGState() 107 | // The starting point of the axis, in the shading's target coordinate space. 108 | var start:CGPoint = CGPoint(x: startPoint.x * self.bounds.size.width, 109 | y: startPoint.y * self.bounds.size.height) 110 | // The ending point 111 | // The ending point of the axis, in the shading's target coordinate space. 112 | var end:CGPoint = CGPoint(x: endPoint.x * self.bounds.size.width, 113 | y: endPoint.y * self.bounds.size.height) 114 | // The context must be clipped before scale the matrix. 115 | addPathAndClipIfNeeded(ctx) 116 | 117 | if (self.isAxial) { 118 | if(self.stroke) { 119 | if(self.path != nil) { 120 | // if we are using the stroke, we offset the from and to points 121 | // by half the stroke width away from the center of the stroke. 122 | // Otherwise we tend to end up with fills that only cover half of the 123 | // because users set the start and end points based on the center 124 | // of the stroke. 125 | let hw = self.lineWidth * 0.5; 126 | start = end.projectLine(start,length: hw) 127 | end = start.projectLine(end,length: -hw) 128 | } 129 | } 130 | 131 | ctx.scaleBy(x: self.bounds.size.width, 132 | y: self.bounds.size.height ); 133 | 134 | start = CGPoint(x: start.x / self.bounds.size.width, 135 | y: start.y / self.bounds.size.height) 136 | end = CGPoint(x: end.x / self.bounds.size.width, 137 | y: end.y / self.bounds.size.height) 138 | } 139 | else 140 | { 141 | // The starting circle has radius `startRadius' and is centered at 142 | // `start', specified in the shading's target coordinate space. The ending 143 | // circle has radius `endRadius' and is centered at `end', specified in the 144 | // shading's target coordinate space. 145 | } 146 | 147 | if !self.radialTransform.isIdentity && !self.isAxial { 148 | // transform the radial context 149 | self.prepareContextIfNeeds(ctx, scale: self.radialTransform, 150 | closure:{(ctx, startPoint, endPoint, startRadius, endRadius) -> (Void) in 151 | var shading = ShadingGradient(colors: colors, 152 | locations: locations, 153 | startPoint: startPoint , 154 | startRadius: startRadius, 155 | endPoint:endPoint , 156 | endRadius: endRadius , 157 | extendStart: self.extendsBeforeStart, 158 | extendEnd: self.extendsPastEnd, 159 | gradientType: self.gradientType, 160 | functionType: self.function, 161 | slopeFunction: self.slopeFunction) 162 | 163 | 164 | self.shading.append(shading) 165 | if let handle = shading.shadingHandle { 166 | ctx.drawShading(handle) 167 | } 168 | 169 | }) 170 | } else { 171 | let minimumRadius = minRadius(self.bounds.size) 172 | 173 | var shading = ShadingGradient(colors: colors, 174 | locations: locations, 175 | startPoint: start , 176 | startRadius: startRadius * minimumRadius, 177 | endPoint:end , 178 | endRadius: endRadius * minimumRadius, 179 | extendStart: self.extendsBeforeStart, 180 | extendEnd: self.extendsPastEnd, 181 | gradientType: self.gradientType, 182 | functionType: self.function, 183 | slopeFunction: self.slopeFunction) 184 | self.shading.append(shading) 185 | if let handle = shading.shadingHandle { 186 | ctx.drawShading(handle) 187 | } 188 | } 189 | ctx.restoreGState(); 190 | } 191 | } 192 | 193 | override open var description:String { 194 | get { 195 | var currentDescription:String = super.description 196 | if (self.function == .linear) { 197 | currentDescription += " linear interpolation" 198 | } else if(self.function == .exponential) { 199 | currentDescription += " exponential interpolation" 200 | } else if(self.function == .cosine) { 201 | currentDescription += " cosine interpolation" 202 | } 203 | return currentDescription 204 | } 205 | } 206 | } 207 | 208 | 209 | -------------------------------------------------------------------------------- /Sources/OShadingGradientLayer/ShadingGradient.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Copyright 2015 - Jorge Ouahbi 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | 19 | // 20 | // OMShadingGradient.swift 21 | // 22 | // Created by Jorge Ouahbi on 20/4/16. 23 | // Copyright © 2016 Jorge Ouahbi. All rights reserved. 24 | // 25 | 26 | 27 | import Foundation 28 | import UIKit 29 | import LibControl 30 | 31 | // function slope 32 | typealias GradientSlopeFunction = EasingFunction 33 | 34 | // interpolate two UIColors 35 | typealias GradientInterpolationFunction = (UIColor,UIColor,CGFloat) -> UIColor 36 | 37 | public enum GradientFunction { 38 | case linear 39 | case exponential 40 | case cosine 41 | } 42 | // TODO(jom): add a black and with gradients 43 | func ShadingFunctionCreate(_ colors : [UIColor], 44 | locations : [CGFloat], 45 | slopeFunction: @escaping GradientSlopeFunction, 46 | interpolationFunction: @escaping GradientInterpolationFunction) -> (UnsafePointer, UnsafeMutablePointer) -> Void 47 | { 48 | return { inData, outData in 49 | 50 | let interp = Double(inData[0]) 51 | let alpha = CGFloat(slopeFunction(interp)) 52 | 53 | var positionIndex = 0; 54 | let colorCount = colors.count 55 | var stop1Position = locations.first! 56 | var stop1Color = colors[0] 57 | 58 | positionIndex += 1; 59 | 60 | var stop2Position:CGFloat = 0.0 61 | var stop2Color:UIColor; 62 | 63 | if (colorCount > 1) { 64 | 65 | // First stop color 66 | stop2Color = colors[1] 67 | 68 | // When originally are 1 location and 1 color. 69 | // Add the stop2Position to 1.0 70 | 71 | if locations.count == 1 { 72 | stop2Position = 1.0 73 | } else { 74 | // First stop location 75 | stop2Position = locations[1]; 76 | } 77 | // Next positon index 78 | positionIndex += 1; 79 | 80 | 81 | } else { 82 | // if we only have one value, that's what we return 83 | stop2Position = stop1Position; 84 | stop2Color = stop1Color; 85 | } 86 | 87 | while (positionIndex < colorCount && stop2Position < alpha) { 88 | stop1Color = stop2Color; 89 | stop1Position = stop2Position; 90 | stop2Color = colors[positionIndex] 91 | stop2Position = locations[positionIndex] 92 | positionIndex += 1; 93 | } 94 | 95 | if (alpha <= stop1Position) { 96 | // if we are less than our lowest position, return our first color 97 | Log.print("alpha:\(String(format:"%.1f",alpha)) <= position \(String(format:"%.1f",stop1Position)) color \(stop1Color.shortDescription)") 98 | outData[0] = (stop1Color.components[0]) 99 | outData[1] = (stop1Color.components[1]) 100 | outData[2] = (stop1Color.components[2]) 101 | outData[3] = (stop1Color.components[3]) 102 | 103 | } else if (alpha >= stop2Position) { 104 | // likewise if we are greater than our highest position, return the last color 105 | Log.print("alpha:\(String(format:"%.1f",alpha)) >= position \(String(format:"%.1f",stop2Position)) color \(stop1Color.shortDescription)") 106 | outData[0] = (stop2Color.components[0]) 107 | outData[1] = (stop2Color.components[1]) 108 | outData[2] = (stop2Color.components[2]) 109 | outData[3] = (stop2Color.components[3]) 110 | 111 | } else { 112 | 113 | // otherwise interpolate between the two 114 | let newPosition = (alpha - stop1Position) / (stop2Position - stop1Position); 115 | 116 | let newColor : UIColor = interpolationFunction(stop1Color, stop2Color, newPosition) 117 | 118 | Log.print("alpha:\(String(format:"%.1f",alpha)) position \(String(format:"%.1f",newPosition)) color \(newColor.shortDescription)") 119 | 120 | for componentIndex in 0 ..< 3 { 121 | outData[componentIndex] = (newColor.components[componentIndex]) 122 | } 123 | // The alpha component is always 1, the shading is always opaque. 124 | outData[3] = 1.0 125 | } 126 | } 127 | } 128 | 129 | 130 | func ShadingCallback(_ infoPointer: UnsafeMutableRawPointer?, 131 | inData: UnsafePointer, 132 | outData: UnsafeMutablePointer) -> Swift.Void { 133 | // Load the UnsafeMutableRawPointer, and call the shadingFunction 134 | var shadingPtr = infoPointer?.load(as: ShadingGradient.self) 135 | // print(shadingPtr!) 136 | shadingPtr?.shadingFunction(inData, outData) 137 | } 138 | 139 | 140 | public struct ShadingGradient { 141 | var monotonicLocations: [CGFloat] = [] 142 | var locations: [CGFloat]? 143 | 144 | let colors : [UIColor] 145 | let startPoint : CGPoint 146 | let endPoint : CGPoint 147 | let startRadius : CGFloat 148 | let endRadius : CGFloat 149 | let extendsPastStart:Bool 150 | let extendsPastEnd:Bool 151 | let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB() 152 | let slopeFunction: EasingFunction 153 | let functionType : GradientFunction 154 | let gradientType : GradientType 155 | 156 | init(colors: [UIColor], 157 | locations: [CGFloat]?, 158 | startPoint: CGPoint, 159 | endPoint: CGPoint, 160 | extendStart: Bool = false, 161 | extendEnd: Bool = false, 162 | functionType: GradientFunction = .linear, 163 | slopeFunction: @escaping EasingFunction = Linear) { 164 | 165 | self.init(colors:colors, 166 | locations: locations, 167 | startPoint: startPoint, 168 | startRadius: 0, 169 | endPoint: endPoint, 170 | endRadius: 0, 171 | extendStart: extendStart, 172 | extendEnd: extendEnd, 173 | gradientType: .axial, 174 | functionType: functionType, 175 | slopeFunction: slopeFunction) 176 | } 177 | 178 | init(colors: [UIColor], 179 | locations: [CGFloat]?, 180 | startPoint: CGPoint, 181 | startRadius: CGFloat, 182 | endPoint: CGPoint, 183 | endRadius: CGFloat, 184 | extendStart: Bool = false, 185 | extendEnd: Bool = false, 186 | functionType: GradientFunction = .linear, 187 | slopeFunction: @escaping EasingFunction = Linear) { 188 | 189 | self.init(colors:colors, 190 | locations: locations, 191 | startPoint: startPoint, 192 | startRadius: startRadius, 193 | endPoint: endPoint, 194 | endRadius: endRadius, 195 | extendStart: extendStart, 196 | extendEnd: extendEnd, 197 | gradientType: .radial, 198 | functionType: functionType, 199 | slopeFunction: slopeFunction) 200 | } 201 | 202 | init(colors: [UIColor], 203 | locations: [CGFloat]?, 204 | startPoint: CGPoint, 205 | startRadius: CGFloat, 206 | endPoint: CGPoint, 207 | endRadius: CGFloat, 208 | extendStart: Bool, 209 | extendEnd: Bool, 210 | gradientType : GradientType = .axial, 211 | functionType : GradientFunction = .linear, 212 | slopeFunction: @escaping EasingFunction = Linear) 213 | { 214 | self.locations = locations 215 | self.startPoint = startPoint 216 | self.endPoint = endPoint 217 | self.startRadius = startRadius 218 | self.endRadius = endRadius 219 | 220 | // already checked in OMShadingGradientLayer 221 | assert(colors.count >= 2); 222 | 223 | // if only exist one color, duplicate it. 224 | if (colors.count == 1) { 225 | let color = colors.first! 226 | self.colors = [color,color]; 227 | } else { 228 | self.colors = colors 229 | } 230 | 231 | // check the color space of all colors. 232 | if let lastColor = colors.last { 233 | for color in colors { 234 | // must be the same colorspace 235 | assert(lastColor.colorSpace?.model == color.colorSpace?.model, 236 | "unexpected color model \(String(describing: color.colorSpace?.model.name)) != \(String(describing: lastColor.colorSpace?.model.name))") 237 | // and correct model 238 | assert(color.colorSpace?.model == .rgb,"unexpected color space model \(String(describing: color.colorSpace?.model.name))") 239 | if(color.colorSpace?.model != .rgb) { 240 | //TODO: handle different color spaces 241 | Log.print("Unsupported color space. model: \(String(describing: color.colorSpace?.model.name))") 242 | } 243 | } 244 | } 245 | 246 | self.slopeFunction = slopeFunction 247 | self.functionType = functionType 248 | self.gradientType = gradientType 249 | self.extendsPastStart = extendStart 250 | self.extendsPastEnd = extendEnd 251 | 252 | // handle nil locations 253 | if let locations = self.locations { 254 | if locations.count > 0 { 255 | monotonicLocations = locations 256 | } 257 | } 258 | 259 | // TODO(jom): handle different number colors and locations 260 | 261 | if monotonicLocations.isEmpty { 262 | self.monotonicLocations = monotonicIncremental(colors.count) 263 | self.locations = self.monotonicLocations 264 | } 265 | 266 | Log.print("\(monotonicLocations.count) monotonic locations") 267 | Log.print("\(monotonicLocations)") 268 | } 269 | 270 | lazy var shadingFunction : (UnsafePointer, UnsafeMutablePointer) -> Void = { 271 | 272 | // @default: linear interpolation 273 | var interpolationFunction: GradientInterpolationFunction = UIColor.lerp 274 | switch(self.functionType){ 275 | case .linear : 276 | interpolationFunction = UIColor.lerp 277 | break 278 | case .exponential : 279 | interpolationFunction = UIColor.eerp 280 | break 281 | case .cosine : 282 | interpolationFunction = UIColor.coserp 283 | break 284 | } 285 | let colors = self.colors 286 | let locations = self.locations 287 | return ShadingFunctionCreate(colors, 288 | locations: locations!, 289 | slopeFunction: self.slopeFunction, 290 | interpolationFunction: interpolationFunction ) 291 | }() 292 | 293 | lazy var handleFunction : CGFunction! = { 294 | var callbacks = CGFunctionCallbacks(version: 0, evaluate: ShadingCallback, releaseInfo: nil) 295 | let infoPointer = UnsafeMutableRawPointer.allocate(byteCount: MemoryLayout.size, alignment: MemoryLayout.alignment) 296 | infoPointer.storeBytes(of: self, as: ShadingGradient.self) 297 | 298 | return CGFunction(info: infoPointer, // info 299 | domainDimension: 1, // domainDimension 300 | domain: [0, 1], // domain 301 | rangeDimension: 4, // rangeDimension 302 | range: [0, 1, 0, 1, 0, 1, 0, 1], // range 303 | callbacks: &callbacks) // callbacks 304 | }() 305 | 306 | lazy var shadingHandle: CGShading? = { 307 | var callbacks = CGFunctionCallbacks(version: 0, evaluate: ShadingCallback, releaseInfo: nil) 308 | if let handleFunction = self.handleFunction { 309 | if (self.gradientType == .axial) { 310 | return CGShading(axialSpace: self.colorSpace, 311 | start: self.startPoint, 312 | end: self.endPoint, 313 | function: handleFunction, 314 | extendStart: self.extendsPastStart, 315 | extendEnd: self.extendsPastEnd) 316 | } else { 317 | assert(self.gradientType == .radial) 318 | return CGShading(radialSpace: self.colorSpace, 319 | start: self.startPoint, 320 | startRadius: self.startRadius, 321 | end: self.endPoint, 322 | endRadius: self.endRadius, 323 | function: handleFunction, 324 | extendStart: self.extendsPastStart, 325 | extendEnd: self.extendsPastEnd) 326 | } 327 | } 328 | 329 | return nil 330 | }() 331 | } 332 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import OMShadingGradientLayerTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += OMShadingGradientLayerTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/OMShadingGradientLayerTests/OMShadingGradientLayerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import OMShadingGradientLayer 3 | 4 | final class OMShadingGradientLayerTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(OMShadingGradientLayer().text, "Hello, World!") 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/OMShadingGradientLayerTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(OMShadingGradientLayerTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /gif/gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaouahbi/OShadingGradientLayer/dbe3723a7e9a80d0dc4f8518d49dcc6e41773f2c/gif/gif.gif --------------------------------------------------------------------------------