├── FurrySketch.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── simongladman.xcuserdatad │ └── xcschemes │ ├── FurrySketch.xcscheme │ └── xcschememanagement.plist ├── FurrySketch ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── ViewController.swift └── assets │ └── furry.jpg └── README.md /FurrySketch.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3EEE20931C06743600765BAC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EEE20921C06743600765BAC /* AppDelegate.swift */; }; 11 | 3EEE20951C06743600765BAC /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EEE20941C06743600765BAC /* ViewController.swift */; }; 12 | 3EEE20981C06743600765BAC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3EEE20961C06743600765BAC /* Main.storyboard */; }; 13 | 3EEE209A1C06743600765BAC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3EEE20991C06743600765BAC /* Assets.xcassets */; }; 14 | 3EEE209D1C06743600765BAC /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3EEE209B1C06743600765BAC /* LaunchScreen.storyboard */; }; 15 | 3EEE20A61C06DA9300765BAC /* furry.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 3EEE20A51C06DA9300765BAC /* furry.jpg */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 3EEE208F1C06743600765BAC /* FurrySketch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FurrySketch.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 3EEE20921C06743600765BAC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 21 | 3EEE20941C06743600765BAC /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 22 | 3EEE20971C06743600765BAC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 23 | 3EEE20991C06743600765BAC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | 3EEE209C1C06743600765BAC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 25 | 3EEE209E1C06743600765BAC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 26 | 3EEE20A51C06DA9300765BAC /* furry.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = furry.jpg; path = assets/furry.jpg; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | 3EEE208C1C06743600765BAC /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 3EEE20861C06743600765BAC = { 41 | isa = PBXGroup; 42 | children = ( 43 | 3EEE20911C06743600765BAC /* FurrySketch */, 44 | 3EEE20901C06743600765BAC /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | 3EEE20901C06743600765BAC /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 3EEE208F1C06743600765BAC /* FurrySketch.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | 3EEE20911C06743600765BAC /* FurrySketch */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 3EEE20A41C06DA7D00765BAC /* assets */, 60 | 3EEE20921C06743600765BAC /* AppDelegate.swift */, 61 | 3EEE20941C06743600765BAC /* ViewController.swift */, 62 | 3EEE20961C06743600765BAC /* Main.storyboard */, 63 | 3EEE20991C06743600765BAC /* Assets.xcassets */, 64 | 3EEE209B1C06743600765BAC /* LaunchScreen.storyboard */, 65 | 3EEE209E1C06743600765BAC /* Info.plist */, 66 | ); 67 | path = FurrySketch; 68 | sourceTree = ""; 69 | }; 70 | 3EEE20A41C06DA7D00765BAC /* assets */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 3EEE20A51C06DA9300765BAC /* furry.jpg */, 74 | ); 75 | name = assets; 76 | sourceTree = ""; 77 | }; 78 | /* End PBXGroup section */ 79 | 80 | /* Begin PBXNativeTarget section */ 81 | 3EEE208E1C06743600765BAC /* FurrySketch */ = { 82 | isa = PBXNativeTarget; 83 | buildConfigurationList = 3EEE20A11C06743600765BAC /* Build configuration list for PBXNativeTarget "FurrySketch" */; 84 | buildPhases = ( 85 | 3EEE208B1C06743600765BAC /* Sources */, 86 | 3EEE208C1C06743600765BAC /* Frameworks */, 87 | 3EEE208D1C06743600765BAC /* Resources */, 88 | ); 89 | buildRules = ( 90 | ); 91 | dependencies = ( 92 | ); 93 | name = FurrySketch; 94 | productName = FurrySketch; 95 | productReference = 3EEE208F1C06743600765BAC /* FurrySketch.app */; 96 | productType = "com.apple.product-type.application"; 97 | }; 98 | /* End PBXNativeTarget section */ 99 | 100 | /* Begin PBXProject section */ 101 | 3EEE20871C06743600765BAC /* Project object */ = { 102 | isa = PBXProject; 103 | attributes = { 104 | LastSwiftUpdateCheck = 0710; 105 | LastUpgradeCheck = 0710; 106 | ORGANIZATIONNAME = "Simon Gladman"; 107 | TargetAttributes = { 108 | 3EEE208E1C06743600765BAC = { 109 | CreatedOnToolsVersion = 7.1.1; 110 | }; 111 | }; 112 | }; 113 | buildConfigurationList = 3EEE208A1C06743600765BAC /* Build configuration list for PBXProject "FurrySketch" */; 114 | compatibilityVersion = "Xcode 3.2"; 115 | developmentRegion = English; 116 | hasScannedForEncodings = 0; 117 | knownRegions = ( 118 | en, 119 | Base, 120 | ); 121 | mainGroup = 3EEE20861C06743600765BAC; 122 | productRefGroup = 3EEE20901C06743600765BAC /* Products */; 123 | projectDirPath = ""; 124 | projectRoot = ""; 125 | targets = ( 126 | 3EEE208E1C06743600765BAC /* FurrySketch */, 127 | ); 128 | }; 129 | /* End PBXProject section */ 130 | 131 | /* Begin PBXResourcesBuildPhase section */ 132 | 3EEE208D1C06743600765BAC /* Resources */ = { 133 | isa = PBXResourcesBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | 3EEE20A61C06DA9300765BAC /* furry.jpg in Resources */, 137 | 3EEE209D1C06743600765BAC /* LaunchScreen.storyboard in Resources */, 138 | 3EEE209A1C06743600765BAC /* Assets.xcassets in Resources */, 139 | 3EEE20981C06743600765BAC /* Main.storyboard in Resources */, 140 | ); 141 | runOnlyForDeploymentPostprocessing = 0; 142 | }; 143 | /* End PBXResourcesBuildPhase section */ 144 | 145 | /* Begin PBXSourcesBuildPhase section */ 146 | 3EEE208B1C06743600765BAC /* Sources */ = { 147 | isa = PBXSourcesBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | 3EEE20951C06743600765BAC /* ViewController.swift in Sources */, 151 | 3EEE20931C06743600765BAC /* AppDelegate.swift in Sources */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | /* End PBXSourcesBuildPhase section */ 156 | 157 | /* Begin PBXVariantGroup section */ 158 | 3EEE20961C06743600765BAC /* Main.storyboard */ = { 159 | isa = PBXVariantGroup; 160 | children = ( 161 | 3EEE20971C06743600765BAC /* Base */, 162 | ); 163 | name = Main.storyboard; 164 | sourceTree = ""; 165 | }; 166 | 3EEE209B1C06743600765BAC /* LaunchScreen.storyboard */ = { 167 | isa = PBXVariantGroup; 168 | children = ( 169 | 3EEE209C1C06743600765BAC /* Base */, 170 | ); 171 | name = LaunchScreen.storyboard; 172 | sourceTree = ""; 173 | }; 174 | /* End PBXVariantGroup section */ 175 | 176 | /* Begin XCBuildConfiguration section */ 177 | 3EEE209F1C06743600765BAC /* Debug */ = { 178 | isa = XCBuildConfiguration; 179 | buildSettings = { 180 | ALWAYS_SEARCH_USER_PATHS = NO; 181 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 182 | CLANG_CXX_LIBRARY = "libc++"; 183 | CLANG_ENABLE_MODULES = YES; 184 | CLANG_ENABLE_OBJC_ARC = YES; 185 | CLANG_WARN_BOOL_CONVERSION = YES; 186 | CLANG_WARN_CONSTANT_CONVERSION = YES; 187 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 188 | CLANG_WARN_EMPTY_BODY = YES; 189 | CLANG_WARN_ENUM_CONVERSION = YES; 190 | CLANG_WARN_INT_CONVERSION = YES; 191 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 192 | CLANG_WARN_UNREACHABLE_CODE = YES; 193 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 194 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 195 | COPY_PHASE_STRIP = NO; 196 | DEBUG_INFORMATION_FORMAT = dwarf; 197 | ENABLE_STRICT_OBJC_MSGSEND = YES; 198 | ENABLE_TESTABILITY = YES; 199 | GCC_C_LANGUAGE_STANDARD = gnu99; 200 | GCC_DYNAMIC_NO_PIC = NO; 201 | GCC_NO_COMMON_BLOCKS = YES; 202 | GCC_OPTIMIZATION_LEVEL = 0; 203 | GCC_PREPROCESSOR_DEFINITIONS = ( 204 | "DEBUG=1", 205 | "$(inherited)", 206 | ); 207 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 208 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 209 | GCC_WARN_UNDECLARED_SELECTOR = YES; 210 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 211 | GCC_WARN_UNUSED_FUNCTION = YES; 212 | GCC_WARN_UNUSED_VARIABLE = YES; 213 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 214 | MTL_ENABLE_DEBUG_INFO = YES; 215 | ONLY_ACTIVE_ARCH = YES; 216 | SDKROOT = iphoneos; 217 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 218 | TARGETED_DEVICE_FAMILY = 2; 219 | }; 220 | name = Debug; 221 | }; 222 | 3EEE20A01C06743600765BAC /* Release */ = { 223 | isa = XCBuildConfiguration; 224 | buildSettings = { 225 | ALWAYS_SEARCH_USER_PATHS = NO; 226 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 227 | CLANG_CXX_LIBRARY = "libc++"; 228 | CLANG_ENABLE_MODULES = YES; 229 | CLANG_ENABLE_OBJC_ARC = YES; 230 | CLANG_WARN_BOOL_CONVERSION = YES; 231 | CLANG_WARN_CONSTANT_CONVERSION = YES; 232 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 233 | CLANG_WARN_EMPTY_BODY = YES; 234 | CLANG_WARN_ENUM_CONVERSION = YES; 235 | CLANG_WARN_INT_CONVERSION = YES; 236 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 237 | CLANG_WARN_UNREACHABLE_CODE = YES; 238 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 239 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 240 | COPY_PHASE_STRIP = NO; 241 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 242 | ENABLE_NS_ASSERTIONS = NO; 243 | ENABLE_STRICT_OBJC_MSGSEND = YES; 244 | GCC_C_LANGUAGE_STANDARD = gnu99; 245 | GCC_NO_COMMON_BLOCKS = YES; 246 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 247 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 248 | GCC_WARN_UNDECLARED_SELECTOR = YES; 249 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 250 | GCC_WARN_UNUSED_FUNCTION = YES; 251 | GCC_WARN_UNUSED_VARIABLE = YES; 252 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 253 | MTL_ENABLE_DEBUG_INFO = NO; 254 | SDKROOT = iphoneos; 255 | TARGETED_DEVICE_FAMILY = 2; 256 | VALIDATE_PRODUCT = YES; 257 | }; 258 | name = Release; 259 | }; 260 | 3EEE20A21C06743600765BAC /* Debug */ = { 261 | isa = XCBuildConfiguration; 262 | buildSettings = { 263 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 264 | INFOPLIST_FILE = FurrySketch/Info.plist; 265 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 266 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.flexmonkey.FurrySketch; 267 | PRODUCT_NAME = "$(TARGET_NAME)"; 268 | }; 269 | name = Debug; 270 | }; 271 | 3EEE20A31C06743600765BAC /* Release */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 275 | INFOPLIST_FILE = FurrySketch/Info.plist; 276 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 277 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.flexmonkey.FurrySketch; 278 | PRODUCT_NAME = "$(TARGET_NAME)"; 279 | }; 280 | name = Release; 281 | }; 282 | /* End XCBuildConfiguration section */ 283 | 284 | /* Begin XCConfigurationList section */ 285 | 3EEE208A1C06743600765BAC /* Build configuration list for PBXProject "FurrySketch" */ = { 286 | isa = XCConfigurationList; 287 | buildConfigurations = ( 288 | 3EEE209F1C06743600765BAC /* Debug */, 289 | 3EEE20A01C06743600765BAC /* Release */, 290 | ); 291 | defaultConfigurationIsVisible = 0; 292 | defaultConfigurationName = Release; 293 | }; 294 | 3EEE20A11C06743600765BAC /* Build configuration list for PBXNativeTarget "FurrySketch" */ = { 295 | isa = XCConfigurationList; 296 | buildConfigurations = ( 297 | 3EEE20A21C06743600765BAC /* Debug */, 298 | 3EEE20A31C06743600765BAC /* Release */, 299 | ); 300 | defaultConfigurationIsVisible = 0; 301 | }; 302 | /* End XCConfigurationList section */ 303 | }; 304 | rootObject = 3EEE20871C06743600765BAC /* Project object */; 305 | } 306 | -------------------------------------------------------------------------------- /FurrySketch.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FurrySketch.xcodeproj/xcuserdata/simongladman.xcuserdatad/xcschemes/FurrySketch.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /FurrySketch.xcodeproj/xcuserdata/simongladman.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | FurrySketch.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 3EEE208E1C06743600765BAC 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /FurrySketch/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // FurrySketch 4 | // 5 | // Created by Simon Gladman on 25/11/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // 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. 24 | // 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. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // 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. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // 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. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // 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. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /FurrySketch/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "ipad", 5 | "size" : "29x29", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "ipad", 10 | "size" : "29x29", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "ipad", 15 | "size" : "40x40", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "40x40", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "76x76", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "76x76", 31 | "scale" : "2x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /FurrySketch/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /FurrySketch/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /FurrySketch/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 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations~ipad 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationPortraitUpsideDown 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /FurrySketch/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // FurrySketch 4 | // 5 | // Created by Simon Gladman on 25/11/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | 12 | class ViewController: UIViewController 13 | { 14 | 15 | let halfPi = CGFloat(M_PI_2) 16 | let imageView = UIImageView() 17 | let compositeFilter = CIFilter(name: "CISourceOverCompositing")! 18 | 19 | let slider = UISlider() 20 | var hue = CGFloat(0) 21 | 22 | lazy var imageAccumulator: CIImageAccumulator = 23 | { 24 | [unowned self] in 25 | return CIImageAccumulator(extent: self.view.frame, format: kCIFormatARGB8) 26 | }() 27 | 28 | override func viewDidLoad() 29 | { 30 | super.viewDidLoad() 31 | 32 | slider.maximumValue = 1 33 | slider.addTarget(self, action: "sliderChangeHandler", forControlEvents: .ValueChanged) 34 | 35 | view.addSubview(imageView) 36 | view.addSubview(slider) 37 | 38 | view.backgroundColor = UIColor.blackColor() 39 | 40 | sliderChangeHandler() 41 | } 42 | 43 | func sliderChangeHandler() 44 | { 45 | hue = CGFloat(slider.value) 46 | 47 | slider.minimumTrackTintColor = color 48 | slider.maximumTrackTintColor = color 49 | slider.thumbTintColor = color 50 | } 51 | 52 | var color: UIColor 53 | { 54 | return UIColor(hue: hue, saturation: 1, brightness: 1, alpha: 1) 55 | } 56 | 57 | override func motionBegan(motion: UIEventSubtype, withEvent event: UIEvent?) 58 | { 59 | if motion == UIEventSubtype.MotionShake 60 | { 61 | imageAccumulator.clear() 62 | imageAccumulator.setImage(CIImage(color: CIColor(string: "00000000"))) 63 | 64 | imageView.image = nil 65 | } 66 | } 67 | 68 | override func touchesMoved(touches: Set, withEvent event: UIEvent?) 69 | { 70 | guard let 71 | touch = touches.first, 72 | coalescedTouces = event?.coalescedTouchesForTouch(touch) where 73 | touch.type == UITouchType.Stylus else 74 | { 75 | return 76 | } 77 | 78 | UIGraphicsBeginImageContext(view.frame.size) 79 | 80 | let cgContext = UIGraphicsGetCurrentContext() 81 | 82 | CGContextSetLineWidth(cgContext, 1) 83 | 84 | CGContextSetStrokeColorWithColor(cgContext, color.colorWithAlphaComponent(0.025).CGColor) 85 | 86 | for coalescedTouch in coalescedTouces 87 | { 88 | let touchLocation = coalescedTouch.locationInView(view) 89 | 90 | let normalisedAlititudeAngle = (halfPi - touch.altitudeAngle) / halfPi 91 | let dx = coalescedTouch.azimuthUnitVectorInView(view).dx * 20 * normalisedAlititudeAngle 92 | let dy = coalescedTouch.azimuthUnitVectorInView(view).dy * 20 * normalisedAlititudeAngle 93 | 94 | let count = 10 + Int((coalescedTouch.force / coalescedTouch.maximumPossibleForce) * 100) 95 | 96 | for _ in 0 ... count 97 | { 98 | let randomAngle = drand48() * (M_PI * 2) 99 | 100 | let innerRandomRadius = drand48() * 20 101 | let innerRandomX = CGFloat(sin(randomAngle) * innerRandomRadius) 102 | let innerRandomY = CGFloat(cos(randomAngle) * innerRandomRadius) 103 | 104 | let outerRandomRadius = innerRandomRadius + drand48() * 40 * Double(normalisedAlititudeAngle) 105 | let outerRandomX = CGFloat(sin(randomAngle) * outerRandomRadius) - dx 106 | let outerRandomY = CGFloat(cos(randomAngle) * outerRandomRadius) - dy 107 | 108 | CGContextMoveToPoint(cgContext, 109 | touchLocation.x + innerRandomX, 110 | touchLocation.y + innerRandomY) 111 | 112 | CGContextAddLineToPoint(cgContext, 113 | touchLocation.x + outerRandomX, 114 | touchLocation.y + outerRandomY) 115 | 116 | CGContextStrokePath(cgContext) 117 | } 118 | } 119 | 120 | let drawnImage = UIGraphicsGetImageFromCurrentImageContext() 121 | 122 | UIGraphicsEndImageContext() 123 | 124 | compositeFilter.setValue(CIImage(image: drawnImage), 125 | forKey: kCIInputImageKey) 126 | compositeFilter.setValue(imageAccumulator.image(), 127 | forKey: kCIInputBackgroundImageKey) 128 | 129 | imageAccumulator.setImage(compositeFilter.valueForKey(kCIOutputImageKey) as! CIImage) 130 | 131 | imageView.image = UIImage(CIImage: imageAccumulator.image()) 132 | } 133 | 134 | override func viewDidLayoutSubviews() 135 | { 136 | imageView.frame = view.bounds 137 | 138 | slider.frame = CGRect(x: 0, 139 | y: view.frame.height - slider.intrinsicContentSize().height - 20, 140 | width: view.frame.width, 141 | height: slider.intrinsicContentSize().height).insetBy(dx: 20, dy: 0) 142 | } 143 | } -------------------------------------------------------------------------------- /FurrySketch/assets/furry.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlexMonkey/FurrySketch/2cfcfdd4e495179428f942e217f04628e075ee21/FurrySketch/assets/furry.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FurrySketch 2 | ##### Using Apple Pencil's Azimuth & Altitude Data for Creating Furry Brush Strokes 3 | 4 | ##### _Companion project to this blog post: http://flexmonkey.blogspot.co.uk/2015/11/furrysketch-hirsute-drawing-with-apple.html_ 5 | 6 | ![image](/FurrySketch/assets/furry.jpg) 7 | 8 | 9 | After my recent experiments (mis)using my Apple Pencil for nefarious and alternative uses, I thought it was about time to play with the Pencil for its intended purpose, sketching, and see how I could use its orientation data to affect brush strokes. 10 | 11 | FurrySketch is a Pencil powered drawing application that draws a sort of multicoloured hair and, most excitingly, the hair's direction matches the angle of the Pencil. It was super simple to write and, at least in my opinion, gives really nice results. 12 | 13 | The basic mechanics are lifted from my ForceSketch project: I use a `CIImageAccumulator` with each new bitmap (created inside `touchesMoved`) composited over the previously accumulated images with a Core Image `CISourceOverCompositing` filter. 14 | 15 | The interesting part for Pencil fans is creating the hairy brush strokes: 16 | 17 | ## Hirsute Brush Mechanics 18 | 19 | Inside `touchesMoved`, I loop over the coalesced touches (you can read about coalesced touches in my Advanced Touch Handling blog post). For each of the intermediate touches, I want to find its location in the view, its altitude angle and its azimuth vector. This vector points in the direction of the Pencil's azimuth angle and by multiplying it with the normalised altitude angle, I get an offset I can use when drawing my little hairs: 20 | 21 | ```swift 22 | for coalescedTouch in coalescedTouces 23 | { 24 | let touchLocation = coalescedTouch.locationInView(view) 25 | 26 | let normalisedAlititudeAngle = (halfPi - touch.altitudeAngle) / halfPi 27 | let dx = coalescedTouch.azimuthUnitVectorInView(view).dx * 20 * normalisedAlititudeAngle 28 | let dy = coalescedTouch.azimuthUnitVectorInView(view).dy * 20 * normalisedAlititudeAngle 29 | ``` 30 | 31 | I then use the touch's force to decide how many hairs to draw... 32 | 33 | ```swift 34 | let count = 10 + Int((coalescedTouch.force / coalescedTouch.maximumPossibleForce) * 100) 35 | ``` 36 | 37 | Now I iterate `count` times. With each iteration, I create a random angle and create constants for the inner radius and start point for each hair: 38 | 39 | ```swift 40 | let innerRandomRadius = drand48() * 20 41 | let innerRandomX = CGFloat(sin(randomAngle) * innerRandomRadius) 42 | 43 | let innerRandomY = CGFloat(cos(randomAngle) * innerRandomRadius) 44 | ``` 45 | 46 | Although the start point of the hair isn't affected by the Pencil's orientation, the end point is. Here, I create another, larger, random radius, use the same angle and offset the end point by dx and dy I created above: 47 | 48 | ```swift 49 | randomRadius = innerRandomRadius + drand48() * 40 * Double(normalisedAlititudeAngle) 50 | 51 | let outerRandomX = CGFloat(sin(randomAngle) * outerRandomRadius) - dx 52 | let outerRandomY = CGFloat(cos(randomAngle) * outerRandomRadius) - dy 53 | ``` 54 | 55 | With those values, I can draw the hairs to my context and repeat over: 56 | 57 | ```swift 58 | CGContextMoveToPoint(cgContext, 59 | touchLocation.x + innerRandomX, 60 | touchLocation.y + innerRandomY) 61 | 62 | CGContextAddLineToPoint(cgContext, 63 | touchLocation.x + outerRandomX, 64 | touchLocation.y + outerRandomY) 65 | 66 | CGContextStrokePath(cgContext) 67 | ``` 68 | 69 | A quick reminder on `CIImageAccumulator`: once the big, hairy loop is finished, I can get the newly generated image from the context and use the `CISourceOverCompositing` filter to composite that image for that touch move over the previous and finally display it in an `UIImageView`: 70 | 71 | ```swift 72 | let drawnImage = UIGraphicsGetImageFromCurrentImageContext() 73 | 74 | UIGraphicsEndImageContext() 75 | 76 | compositeFilter.setValue(CIImage(image: drawnImage), 77 | forKey: kCIInputImageKey) 78 | compositeFilter.setValue(imageAccumulator.image(), 79 | forKey: kCIInputBackgroundImageKey) 80 | 81 | imageAccumulator.setImage(compositeFilter.valueForKey(kCIOutputImageKey) as! CIImage) 82 | 83 | imageView.image = UIImage(CIImage: imageAccumulator.image()) 84 | ``` 85 | 86 | ## In Conclusion 87 | 88 | If you are writing drawing apps, adding Pencil support is not only super easy, it adds real value for your users. The technique I've used here to draw hair is only a few lines of code way from spray cans and air brushes and I genuinely believe the iPad Pro will prove to be an amazing device for creatives. 89 | --------------------------------------------------------------------------------