├── .gitignore ├── MetalPaint.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist └── MetalPaint ├── AppDelegate.swift ├── Assets.xcassets └── AppIcon.appiconset │ └── Contents.json ├── Base.lproj ├── LaunchScreen.storyboard └── Main.storyboard ├── ColorPicker.swift ├── Info.plist ├── Metal.swift ├── Shaders.metal └── ViewController.swift /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | *~.nib 3 | *.swp 4 | .DS_Store 5 | *.mode1 6 | *.mode1v3 7 | *.mode2v3 8 | *.perspective 9 | *.perspectivev3 10 | *.pbxuser 11 | xcuserdata 12 | *.xccheckout 13 | *.xcuserstate 14 | lto-llvm.o* 15 | *.xcscmblueprint 16 | -------------------------------------------------------------------------------- /MetalPaint.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0D3B7A661C10F6E800074758 /* ColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3B7A651C10F6E800074758 /* ColorPicker.swift */; }; 11 | 0D40B0421C0F799600FBB00A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D40B0411C0F799600FBB00A /* AppDelegate.swift */; }; 12 | 0D40B0441C0F799600FBB00A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D40B0431C0F799600FBB00A /* ViewController.swift */; }; 13 | 0D40B0471C0F799600FBB00A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D40B0451C0F799600FBB00A /* Main.storyboard */; }; 14 | 0D40B0491C0F799600FBB00A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0D40B0481C0F799600FBB00A /* Assets.xcassets */; }; 15 | 0D40B04C1C0F799600FBB00A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D40B04A1C0F799600FBB00A /* LaunchScreen.storyboard */; }; 16 | 0D40B0541C0FA16600FBB00A /* Metal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D40B0531C0FA16600FBB00A /* Metal.swift */; }; 17 | 0D40B0561C0FA22300FBB00A /* Shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 0D40B0551C0FA22300FBB00A /* Shaders.metal */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 0D3B7A651C10F6E800074758 /* ColorPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPicker.swift; sourceTree = ""; }; 22 | 0D40B03E1C0F799600FBB00A /* MetalPaint.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MetalPaint.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 0D40B0411C0F799600FBB00A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | 0D40B0431C0F799600FBB00A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | 0D40B0461C0F799600FBB00A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | 0D40B0481C0F799600FBB00A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 27 | 0D40B04B1C0F799600FBB00A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 28 | 0D40B04D1C0F799600FBB00A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 0D40B0531C0FA16600FBB00A /* Metal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Metal.swift; sourceTree = ""; }; 30 | 0D40B0551C0FA22300FBB00A /* Shaders.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = Shaders.metal; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | 0D40B03B1C0F799600FBB00A /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 0D40B0351C0F799600FBB00A = { 45 | isa = PBXGroup; 46 | children = ( 47 | 0D40B0401C0F799600FBB00A /* MetalPaint */, 48 | 0D40B03F1C0F799600FBB00A /* Products */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | 0D40B03F1C0F799600FBB00A /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 0D40B03E1C0F799600FBB00A /* MetalPaint.app */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | 0D40B0401C0F799600FBB00A /* MetalPaint */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 0D40B0411C0F799600FBB00A /* AppDelegate.swift */, 64 | 0D40B0431C0F799600FBB00A /* ViewController.swift */, 65 | 0D3B7A651C10F6E800074758 /* ColorPicker.swift */, 66 | 0D40B0531C0FA16600FBB00A /* Metal.swift */, 67 | 0D40B0551C0FA22300FBB00A /* Shaders.metal */, 68 | 0D40B0451C0F799600FBB00A /* Main.storyboard */, 69 | 0D40B0481C0F799600FBB00A /* Assets.xcassets */, 70 | 0D40B04A1C0F799600FBB00A /* LaunchScreen.storyboard */, 71 | 0D40B04D1C0F799600FBB00A /* Info.plist */, 72 | ); 73 | path = MetalPaint; 74 | sourceTree = ""; 75 | }; 76 | /* End PBXGroup section */ 77 | 78 | /* Begin PBXNativeTarget section */ 79 | 0D40B03D1C0F799600FBB00A /* MetalPaint */ = { 80 | isa = PBXNativeTarget; 81 | buildConfigurationList = 0D40B0501C0F799600FBB00A /* Build configuration list for PBXNativeTarget "MetalPaint" */; 82 | buildPhases = ( 83 | 0D40B03A1C0F799600FBB00A /* Sources */, 84 | 0D40B03B1C0F799600FBB00A /* Frameworks */, 85 | 0D40B03C1C0F799600FBB00A /* Resources */, 86 | ); 87 | buildRules = ( 88 | ); 89 | dependencies = ( 90 | ); 91 | name = MetalPaint; 92 | productName = MetalPaint; 93 | productReference = 0D40B03E1C0F799600FBB00A /* MetalPaint.app */; 94 | productType = "com.apple.product-type.application"; 95 | }; 96 | /* End PBXNativeTarget section */ 97 | 98 | /* Begin PBXProject section */ 99 | 0D40B0361C0F799600FBB00A /* Project object */ = { 100 | isa = PBXProject; 101 | attributes = { 102 | LastSwiftUpdateCheck = 0710; 103 | LastUpgradeCheck = 1020; 104 | ORGANIZATIONNAME = "Ryder Mackay"; 105 | TargetAttributes = { 106 | 0D40B03D1C0F799600FBB00A = { 107 | CreatedOnToolsVersion = 7.1.1; 108 | DevelopmentTeam = J67PW9QQ97; 109 | }; 110 | }; 111 | }; 112 | buildConfigurationList = 0D40B0391C0F799600FBB00A /* Build configuration list for PBXProject "MetalPaint" */; 113 | compatibilityVersion = "Xcode 3.2"; 114 | developmentRegion = en; 115 | hasScannedForEncodings = 0; 116 | knownRegions = ( 117 | en, 118 | Base, 119 | ); 120 | mainGroup = 0D40B0351C0F799600FBB00A; 121 | productRefGroup = 0D40B03F1C0F799600FBB00A /* Products */; 122 | projectDirPath = ""; 123 | projectRoot = ""; 124 | targets = ( 125 | 0D40B03D1C0F799600FBB00A /* MetalPaint */, 126 | ); 127 | }; 128 | /* End PBXProject section */ 129 | 130 | /* Begin PBXResourcesBuildPhase section */ 131 | 0D40B03C1C0F799600FBB00A /* Resources */ = { 132 | isa = PBXResourcesBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | 0D40B04C1C0F799600FBB00A /* LaunchScreen.storyboard in Resources */, 136 | 0D40B0491C0F799600FBB00A /* Assets.xcassets in Resources */, 137 | 0D40B0471C0F799600FBB00A /* Main.storyboard in Resources */, 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | /* End PBXResourcesBuildPhase section */ 142 | 143 | /* Begin PBXSourcesBuildPhase section */ 144 | 0D40B03A1C0F799600FBB00A /* Sources */ = { 145 | isa = PBXSourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | 0D40B0561C0FA22300FBB00A /* Shaders.metal in Sources */, 149 | 0D40B0441C0F799600FBB00A /* ViewController.swift in Sources */, 150 | 0D3B7A661C10F6E800074758 /* ColorPicker.swift in Sources */, 151 | 0D40B0541C0FA16600FBB00A /* Metal.swift in Sources */, 152 | 0D40B0421C0F799600FBB00A /* AppDelegate.swift in Sources */, 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXSourcesBuildPhase section */ 157 | 158 | /* Begin PBXVariantGroup section */ 159 | 0D40B0451C0F799600FBB00A /* Main.storyboard */ = { 160 | isa = PBXVariantGroup; 161 | children = ( 162 | 0D40B0461C0F799600FBB00A /* Base */, 163 | ); 164 | name = Main.storyboard; 165 | sourceTree = ""; 166 | }; 167 | 0D40B04A1C0F799600FBB00A /* LaunchScreen.storyboard */ = { 168 | isa = PBXVariantGroup; 169 | children = ( 170 | 0D40B04B1C0F799600FBB00A /* Base */, 171 | ); 172 | name = LaunchScreen.storyboard; 173 | sourceTree = ""; 174 | }; 175 | /* End PBXVariantGroup section */ 176 | 177 | /* Begin XCBuildConfiguration section */ 178 | 0D40B04E1C0F799600FBB00A /* Debug */ = { 179 | isa = XCBuildConfiguration; 180 | buildSettings = { 181 | ALWAYS_SEARCH_USER_PATHS = NO; 182 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 183 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 184 | CLANG_CXX_LIBRARY = "libc++"; 185 | CLANG_ENABLE_MODULES = YES; 186 | CLANG_ENABLE_OBJC_ARC = YES; 187 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 188 | CLANG_WARN_BOOL_CONVERSION = YES; 189 | CLANG_WARN_COMMA = YES; 190 | CLANG_WARN_CONSTANT_CONVERSION = YES; 191 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 192 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 193 | CLANG_WARN_EMPTY_BODY = YES; 194 | CLANG_WARN_ENUM_CONVERSION = YES; 195 | CLANG_WARN_INFINITE_RECURSION = YES; 196 | CLANG_WARN_INT_CONVERSION = YES; 197 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 198 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 199 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 200 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 201 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 202 | CLANG_WARN_STRICT_PROTOTYPES = YES; 203 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 204 | CLANG_WARN_UNREACHABLE_CODE = YES; 205 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 206 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 207 | COPY_PHASE_STRIP = NO; 208 | DEBUG_INFORMATION_FORMAT = dwarf; 209 | ENABLE_STRICT_OBJC_MSGSEND = YES; 210 | ENABLE_TESTABILITY = YES; 211 | GCC_C_LANGUAGE_STANDARD = gnu99; 212 | GCC_DYNAMIC_NO_PIC = NO; 213 | GCC_NO_COMMON_BLOCKS = YES; 214 | GCC_OPTIMIZATION_LEVEL = 0; 215 | GCC_PREPROCESSOR_DEFINITIONS = ( 216 | "DEBUG=1", 217 | "$(inherited)", 218 | ); 219 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 220 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 221 | GCC_WARN_UNDECLARED_SELECTOR = YES; 222 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 223 | GCC_WARN_UNUSED_FUNCTION = YES; 224 | GCC_WARN_UNUSED_VARIABLE = YES; 225 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 226 | MTL_ENABLE_DEBUG_INFO = YES; 227 | ONLY_ACTIVE_ARCH = YES; 228 | SDKROOT = iphoneos; 229 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 230 | SWIFT_VERSION = 5.0; 231 | TARGETED_DEVICE_FAMILY = "1,2"; 232 | }; 233 | name = Debug; 234 | }; 235 | 0D40B04F1C0F799600FBB00A /* Release */ = { 236 | isa = XCBuildConfiguration; 237 | buildSettings = { 238 | ALWAYS_SEARCH_USER_PATHS = NO; 239 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 240 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 241 | CLANG_CXX_LIBRARY = "libc++"; 242 | CLANG_ENABLE_MODULES = YES; 243 | CLANG_ENABLE_OBJC_ARC = YES; 244 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 245 | CLANG_WARN_BOOL_CONVERSION = YES; 246 | CLANG_WARN_COMMA = YES; 247 | CLANG_WARN_CONSTANT_CONVERSION = YES; 248 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 249 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 250 | CLANG_WARN_EMPTY_BODY = YES; 251 | CLANG_WARN_ENUM_CONVERSION = YES; 252 | CLANG_WARN_INFINITE_RECURSION = YES; 253 | CLANG_WARN_INT_CONVERSION = YES; 254 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 256 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 257 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 258 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 259 | CLANG_WARN_STRICT_PROTOTYPES = YES; 260 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 261 | CLANG_WARN_UNREACHABLE_CODE = YES; 262 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 263 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 264 | COPY_PHASE_STRIP = NO; 265 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 266 | ENABLE_NS_ASSERTIONS = NO; 267 | ENABLE_STRICT_OBJC_MSGSEND = YES; 268 | GCC_C_LANGUAGE_STANDARD = gnu99; 269 | GCC_NO_COMMON_BLOCKS = YES; 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 277 | MTL_ENABLE_DEBUG_INFO = NO; 278 | SDKROOT = iphoneos; 279 | SWIFT_COMPILATION_MODE = wholemodule; 280 | SWIFT_VERSION = 5.0; 281 | TARGETED_DEVICE_FAMILY = "1,2"; 282 | VALIDATE_PRODUCT = YES; 283 | }; 284 | name = Release; 285 | }; 286 | 0D40B0511C0F799600FBB00A /* Debug */ = { 287 | isa = XCBuildConfiguration; 288 | buildSettings = { 289 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 290 | DEVELOPMENT_TEAM = J67PW9QQ97; 291 | INFOPLIST_FILE = MetalPaint/Info.plist; 292 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 293 | PRODUCT_BUNDLE_IDENTIFIER = com.rydermackay.MetalPaint; 294 | PRODUCT_NAME = "$(TARGET_NAME)"; 295 | }; 296 | name = Debug; 297 | }; 298 | 0D40B0521C0F799600FBB00A /* Release */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 302 | DEVELOPMENT_TEAM = J67PW9QQ97; 303 | INFOPLIST_FILE = MetalPaint/Info.plist; 304 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 305 | PRODUCT_BUNDLE_IDENTIFIER = com.rydermackay.MetalPaint; 306 | PRODUCT_NAME = "$(TARGET_NAME)"; 307 | }; 308 | name = Release; 309 | }; 310 | /* End XCBuildConfiguration section */ 311 | 312 | /* Begin XCConfigurationList section */ 313 | 0D40B0391C0F799600FBB00A /* Build configuration list for PBXProject "MetalPaint" */ = { 314 | isa = XCConfigurationList; 315 | buildConfigurations = ( 316 | 0D40B04E1C0F799600FBB00A /* Debug */, 317 | 0D40B04F1C0F799600FBB00A /* Release */, 318 | ); 319 | defaultConfigurationIsVisible = 0; 320 | defaultConfigurationName = Release; 321 | }; 322 | 0D40B0501C0F799600FBB00A /* Build configuration list for PBXNativeTarget "MetalPaint" */ = { 323 | isa = XCConfigurationList; 324 | buildConfigurations = ( 325 | 0D40B0511C0F799600FBB00A /* Debug */, 326 | 0D40B0521C0F799600FBB00A /* Release */, 327 | ); 328 | defaultConfigurationIsVisible = 0; 329 | defaultConfigurationName = Release; 330 | }; 331 | /* End XCConfigurationList section */ 332 | }; 333 | rootObject = 0D40B0361C0F799600FBB00A /* Project object */; 334 | } 335 | -------------------------------------------------------------------------------- /MetalPaint.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MetalPaint.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MetalPaint/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MetalPaint 4 | // 5 | // Created by Ryder Mackay on 2015-12-02. 6 | // Copyright © 2015 Ryder Mackay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | } 15 | 16 | -------------------------------------------------------------------------------- /MetalPaint/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /MetalPaint/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 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /MetalPaint/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 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /MetalPaint/ColorPicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorPicker.swift 3 | // MetalPaint 4 | // 5 | // Created by Ryder Mackay on 2015-12-03. 6 | // Copyright © 2015 Ryder Mackay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | final class ColorPicker : UIControl { 13 | 14 | private var buttons: [UIButton] = [] 15 | 16 | var stackView: UIStackView! 17 | lazy var visualEffectView: UIVisualEffectView = { 18 | let effect = UIBlurEffect(style: .light) 19 | let v = UIVisualEffectView(effect: effect) 20 | v.frame = self.bounds 21 | v.autoresizingMask = [.flexibleWidth, .flexibleHeight] 22 | self.addSubview(v) 23 | return v 24 | }() 25 | 26 | var colors: [UIColor] = [] { 27 | didSet { 28 | if stackView != nil { 29 | stackView.removeFromSuperview() 30 | } 31 | 32 | buttons = colors.map { 33 | let b = UIButton(type: .custom) 34 | b.setImage(swatchImageForColor(color: $0, forState: .normal), for: .normal) 35 | b.addTarget(self, action: #selector(selectColor(_:)), for: .touchUpInside) 36 | return b 37 | } 38 | stackView = UIStackView(arrangedSubviews: buttons) 39 | stackView.axis = .horizontal 40 | stackView.frame = visualEffectView.contentView.bounds 41 | stackView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 42 | stackView.distribution = .equalSpacing 43 | stackView.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) 44 | stackView.isLayoutMarginsRelativeArrangement = true 45 | visualEffectView.contentView.addSubview(stackView) 46 | } 47 | } 48 | 49 | override func layoutSubviews() { 50 | super.layoutSubviews() 51 | 52 | let maskLayer = CAShapeLayer() 53 | maskLayer.frame = layer.bounds 54 | maskLayer.path = UIBezierPath(roundedRect: layer.bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: 10, height: 10)).cgPath 55 | layer.mask = maskLayer 56 | } 57 | 58 | override var intrinsicContentSize: CGSize { 59 | return CGSize(width: 20 + colors.count * (44 + 20) + 20, height: 44 + 40) 60 | } 61 | 62 | @IBAction func selectColor(_ sender: UIButton) { 63 | selectedIndex = buttons.firstIndex(of: sender)! 64 | sendActions(for: .valueChanged) 65 | } 66 | 67 | var selectedIndex: Int = 0 { 68 | didSet { 69 | for (index, button) in buttons.enumerated() { 70 | button.isSelected = index == selectedIndex 71 | } 72 | } 73 | } 74 | 75 | // TODO: use UIGraphicsImgaeRenderer 76 | func swatchImageForColor(color: UIColor, forState state: UIControl.State) -> UIImage { 77 | let length = 44 78 | let rect = CGRect(origin: .zero, size: CGSize(width: length, height: length)) 79 | UIGraphicsBeginImageContextWithOptions(rect.size, false, 0) 80 | color.setFill() 81 | let oval = UIBezierPath(ovalIn: rect) 82 | oval.addClip() 83 | oval.fill() 84 | let image = UIGraphicsGetImageFromCurrentImageContext()! 85 | UIGraphicsEndImageContext() 86 | return image 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /MetalPaint/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /MetalPaint/Metal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Metal.swift 3 | // MetalPaint 4 | // 5 | // Created by Ryder Mackay on 2015-12-02. 6 | // Copyright © 2015 Ryder Mackay. All rights reserved. 7 | // 8 | 9 | import Metal 10 | import simd 11 | 12 | final class QuadRenderer { 13 | 14 | var texturedQuad: TexturedQuad 15 | var blendRGBAndAlphaPipelineState: MTLRenderPipelineState! 16 | var blendRGBOnlyPipelineState: MTLRenderPipelineState! 17 | 18 | init(device: MTLDevice) { 19 | texturedQuad = TexturedQuad(device: device) 20 | 21 | let library = device.makeDefaultLibrary()! 22 | 23 | let renderPipelineDescriptor = MTLRenderPipelineDescriptor() 24 | renderPipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm 25 | renderPipelineDescriptor.vertexFunction = library.makeFunction(name: "passThroughVertex") 26 | renderPipelineDescriptor.fragmentFunction = library.makeFunction(name: "passThroughFragment") 27 | 28 | // enable source over blending, e.g. r = (s * s.a) + d * (1 - s.a) 29 | renderPipelineDescriptor.colorAttachments[0].isBlendingEnabled = true 30 | renderPipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add 31 | renderPipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add 32 | 33 | renderPipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one 34 | renderPipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one 35 | renderPipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha 36 | renderPipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha 37 | 38 | blendRGBOnlyPipelineState = try! device.makeRenderPipelineState(descriptor: renderPipelineDescriptor) 39 | 40 | renderPipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha 41 | renderPipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha 42 | renderPipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha 43 | renderPipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha 44 | 45 | blendRGBAndAlphaPipelineState = try! device.makeRenderPipelineState(descriptor: renderPipelineDescriptor) 46 | } 47 | 48 | // set transforms etc. 49 | func updateUniforms() { 50 | 51 | } 52 | 53 | func renderTexture(texture: MTLTexture, inTexture colorAttachmentTexture: MTLTexture, commandBuffer: MTLCommandBuffer, shouldClear: Bool, textureIsPremultipled: Bool) { 54 | let descriptor = MTLRenderPassDescriptor() 55 | descriptor.colorAttachments[0].loadAction = shouldClear ? .clear : .load 56 | descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 1) 57 | descriptor.colorAttachments[0].storeAction = .store 58 | descriptor.colorAttachments[0].texture = colorAttachmentTexture 59 | 60 | let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)! 61 | renderCommandEncoder.setRenderPipelineState(textureIsPremultipled ? blendRGBOnlyPipelineState : blendRGBAndAlphaPipelineState) 62 | texturedQuad.encodeDrawCommands(encoder: renderCommandEncoder, texture: texture) 63 | renderCommandEncoder.endEncoding() 64 | } 65 | } 66 | 67 | struct Vertex { 68 | let position: float4 69 | let textureCoords: float2 70 | 71 | init(x: Float, y: Float, u: Float, v: Float) { 72 | position = float4(x, y, 1, 1) 73 | textureCoords = float2(u, v) 74 | } 75 | } 76 | 77 | final class TexturedQuad { 78 | 79 | static var vertices: [Vertex] { 80 | return [ 81 | Vertex(x: -1, y: -1, u: 0, v: 1), 82 | Vertex(x: 1, y: -1, u: 1, v: 1), 83 | Vertex(x: 1, y: 1, u: 1, v: 0), 84 | Vertex(x: 1, y: 1, u: 1, v: 0), 85 | Vertex(x: -1, y: 1, u: 0, v: 0), 86 | Vertex(x: -1, y: -1, u: 0, v: 1), 87 | ] 88 | } 89 | 90 | private let device: MTLDevice 91 | private let vertexBuffer: MTLBuffer 92 | private let samplerState: MTLSamplerState 93 | 94 | init(device: MTLDevice) { 95 | self.device = device 96 | var vertices = TexturedQuad.vertices 97 | vertexBuffer = device.makeBuffer(bytes: &vertices, length: vertices.count * MemoryLayout.stride, options: [])! 98 | 99 | let samplerDescriptor = MTLSamplerDescriptor() 100 | samplerDescriptor.sAddressMode = .clampToEdge 101 | samplerDescriptor.tAddressMode = .clampToEdge 102 | samplerDescriptor.minFilter = .linear 103 | samplerDescriptor.magFilter = .linear 104 | 105 | samplerState = device.makeSamplerState(descriptor: samplerDescriptor)! 106 | } 107 | 108 | var primitiveType: MTLPrimitiveType { return .triangle } 109 | var vertexCount: Int { return vertexBuffer.length / MemoryLayout.stride } 110 | 111 | func encodeDrawCommands(encoder: MTLRenderCommandEncoder, texture: MTLTexture) { 112 | encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) 113 | encoder.setFragmentTexture(texture, index: 0) 114 | encoder.setFragmentSamplerState(samplerState, index: 0) 115 | encoder.drawPrimitives(type: primitiveType, vertexStart: 0, vertexCount: vertexCount) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /MetalPaint/Shaders.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Shaders.metal 3 | // MetalPaint 4 | // 5 | // Created by Ryder Mackay on 2015-12-02. 6 | // Copyright © 2015 Ryder Mackay. All rights reserved. 7 | // 8 | 9 | #include 10 | using namespace metal; 11 | 12 | struct VertexInOut 13 | { 14 | float4 position [[position]]; 15 | float2 texCoords; 16 | }; 17 | 18 | vertex VertexInOut passThroughVertex(uint vid [[ vertex_id ]], 19 | constant VertexInOut *vertices [[ buffer(0) ]]) 20 | { 21 | VertexInOut outVertex; 22 | 23 | outVertex.position = vertices[vid].position; 24 | outVertex.texCoords = vertices[vid].texCoords; 25 | 26 | return outVertex; 27 | }; 28 | 29 | fragment float4 passThroughFragment(VertexInOut vert [[stage_in]], 30 | sampler samplr [[sampler(0)]], 31 | texture2d texture [[texture(0)]]) 32 | { 33 | return texture.sample(samplr, vert.texCoords); 34 | }; 35 | -------------------------------------------------------------------------------- /MetalPaint/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // MetalPaint 4 | // 5 | // Created by Ryder Mackay on 2015-12-02. 6 | // Copyright © 2015 Ryder Mackay. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MetalKit 11 | import Metal 12 | import simd 13 | 14 | struct Size { 15 | let width, height: Int 16 | } 17 | 18 | final class Line { 19 | func drawInLayer(layer: Layer) { 20 | 21 | } 22 | } 23 | 24 | final class Layer { 25 | let texture: MTLTexture 26 | let width: Int 27 | let height: Int 28 | var size: CGSize { return CGSize(width: width, height: height) } 29 | 30 | // creates a texture in device to render into 31 | init(size: CGSize, device: MTLDevice) { 32 | let width = Int(floor(size.width)) 33 | let height = Int(floor(size.height)) 34 | self.width = width 35 | self.height = height 36 | let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: width, height: height, mipmapped: false) 37 | descriptor.usage.insert(.renderTarget) 38 | texture = device.makeTexture(descriptor: descriptor)! 39 | } 40 | 41 | func drawInLayer(layer: Layer) { 42 | 43 | } 44 | 45 | func drawInView(view: MTKView) { 46 | 47 | } 48 | 49 | func drawInTexture(texture: MTLTexture) { 50 | 51 | } 52 | } 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | extension ViewController: MTKViewDelegate { 68 | 69 | func draw(in view: MTKView) { 70 | guard let drawable = view.currentDrawable, 71 | let commandBuffer = commandQueue.makeCommandBuffer() else { return } 72 | 73 | renderer.renderTexture(texture: frozenLayer.texture, inTexture: drawable.texture, commandBuffer: commandBuffer, shouldClear: true, textureIsPremultipled: true) 74 | 75 | if let texture = activeLayer?.texture { 76 | renderer.renderTexture(texture: texture, inTexture: drawable.texture, commandBuffer: commandBuffer, shouldClear: false, textureIsPremultipled: true) 77 | renderer.renderTexture(texture: predictiveLayer!.texture, inTexture: drawable.texture, commandBuffer: commandBuffer, shouldClear: false, textureIsPremultipled: true) 78 | } 79 | 80 | commandBuffer.present(drawable) 81 | commandBuffer.commit() 82 | } 83 | 84 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 85 | // update uniforms, temporary buffers etc 86 | } 87 | } 88 | 89 | 90 | class ViewController: UIViewController { 91 | 92 | lazy var renderer: QuadRenderer = { return QuadRenderer(device: self.device) }() 93 | lazy var commandQueue = device.makeCommandQueue()! 94 | 95 | @IBOutlet var metalView: MTKView! 96 | 97 | var device: MTLDevice! 98 | 99 | var brushTexture: MTLTexture! 100 | 101 | override func viewDidLoad() { 102 | super.viewDidLoad() 103 | // Do any additional setup after loading the view, typically from a nib. 104 | 105 | if let device = MTLCreateSystemDefaultDevice() { 106 | 107 | self.device = device 108 | 109 | metalView.device = device 110 | metalView.enableSetNeedsDisplay = true 111 | metalView.delegate = self 112 | 113 | let length = 22 114 | let rect = CGRect(origin: .zero, size: CGSize(width: length, height: length)) 115 | UIGraphicsBeginImageContextWithOptions(rect.size, true, 0) 116 | UIColor.black.setFill() 117 | UIBezierPath(ovalIn: rect).fill() 118 | let image = UIGraphicsGetImageFromCurrentImageContext()! 119 | UIGraphicsEndImageContext() 120 | 121 | brushTexture = try! MTKTextureLoader(device: device).newTexture(cgImage: image.cgImage!, options: nil) 122 | } 123 | 124 | let doubleTap = UITapGestureRecognizer(target: self, action: #selector(clear(_:))) 125 | doubleTap.numberOfTapsRequired = 1 126 | doubleTap.numberOfTouchesRequired = 2 127 | doubleTap.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.direct.rawValue)] 128 | view.addGestureRecognizer(doubleTap) 129 | 130 | 131 | colorPicker = ColorPicker() 132 | colorPicker.colors = [.black, .red, .orange, .yellow, .green, .blue, .purple] 133 | colorPicker.translatesAutoresizingMaskIntoConstraints = false 134 | view.addSubview(colorPicker) 135 | view.leftAnchor.constraint(lessThanOrEqualTo: colorPicker.leftAnchor).isActive = true 136 | view.rightAnchor.constraint(greaterThanOrEqualTo: colorPicker.rightAnchor).isActive = true 137 | view.centerXAnchor.constraint(equalTo: colorPicker.centerXAnchor).isActive = true 138 | view.bottomAnchor.constraint(equalTo: colorPicker.bottomAnchor).isActive = true 139 | colorPicker.addTarget(self, action: #selector(pickedColor(_:)), for: .valueChanged) 140 | } 141 | 142 | var colorPicker: ColorPicker! 143 | var selectedColor = UIColor.black 144 | 145 | @IBAction func pickedColor(_ sender: ColorPicker) { 146 | selectedColor = sender.colors[sender.selectedIndex] 147 | } 148 | 149 | @IBAction func clear(_ sender: AnyObject?) { 150 | for layer in ([frozenLayer, activeLayer!, predictiveLayer!] as! [Layer]) { 151 | clearLayer(layer: layer) 152 | } 153 | metalView.setNeedsDisplay() 154 | } 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | var activeLayer: Layer? 171 | var predictiveLayer: Layer? 172 | lazy var frozenLayer: Layer! = { return Layer(size: self.metalView.drawableSize, device: self.device) }() 173 | 174 | // need event b/c it has coalesced + predicted touches 175 | func drawTouches(touches: Set, withEvent event: UIEvent?) { 176 | if activeLayer == nil { 177 | activeLayer = Layer(size: frozenLayer.size, device: device) 178 | } 179 | 180 | if predictiveLayer == nil { 181 | predictiveLayer = Layer(size: activeLayer!.size, device: device) 182 | } 183 | 184 | for touch in touches { 185 | 186 | // get or create active line for touch via map table 187 | 188 | // remove predicted touches and mark area as needing redraw 189 | clearLayer(layer: predictiveLayer!) 190 | 191 | // add touch methods should return dirty rect 192 | 193 | if let coalescedTouches = event?.coalescedTouches(for: touch) { 194 | addPointsToLineForTouches(touches: coalescedTouches, type: .Coalesced) 195 | } else { 196 | addPointsToLineForTouches(touches: [touch], type: .Main) 197 | } 198 | if let predictedTouches = event?.predictedTouches(for: touch) { 199 | addPointsToLineForTouches(touches: predictedTouches, type: .Predicted) 200 | } 201 | } 202 | 203 | metalView.setNeedsDisplay() 204 | } 205 | 206 | func clearLayer(layer: Layer) { 207 | let commandBuffer = commandQueue.makeCommandBuffer()! 208 | let descriptor = MTLRenderPassDescriptor() 209 | descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0) 210 | descriptor.colorAttachments[0].loadAction = .clear 211 | descriptor.colorAttachments[0].texture = layer.texture 212 | descriptor.colorAttachments[0].storeAction = .store 213 | let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)! 214 | renderCommandEncoder.endEncoding() 215 | 216 | commandBuffer.commit() 217 | } 218 | 219 | enum TouchType { 220 | case Main 221 | case Coalesced 222 | case Predicted 223 | } 224 | 225 | let colorSpace = CGColorSpaceCreateDeviceRGB() 226 | 227 | lazy var ciContext: CIContext = { 228 | return CIContext(mtlDevice: self.device, options: [ 229 | CIContextOption.outputColorSpace: self.colorSpace, 230 | CIContextOption.workingColorSpace: self.colorSpace 231 | ]) 232 | }() 233 | 234 | var coreImageRenderDestinationTexture: MTLTexture! 235 | 236 | var lastCommandBuffer: MTLCommandBuffer! 237 | 238 | /// CIColorControls inputBiasVector value. Note alpha is zero. 239 | var selectedColorVector: CIVector { 240 | let color = CIColor(color: selectedColor) 241 | return CIVector(x: color.red, y: color.green, z: color.blue, w: 0) 242 | } 243 | 244 | func addPointsToLineForTouches(touches: [UITouch], type: TouchType) { 245 | 246 | let texture = type == .Predicted ? predictiveLayer!.texture : activeLayer!.texture 247 | let sourceImage = CIImage(mtlTexture: texture, options: [CIImageOption.colorSpace: colorSpace]) 248 | 249 | let brushSize = MTLSize(width: brushTexture.width, height: brushTexture.height, depth: 1) 250 | 251 | if coreImageRenderDestinationTexture == nil { 252 | let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: brushSize.width, height: brushSize.height, mipmapped: false) 253 | descriptor.usage = [.renderTarget, .shaderRead, .shaderWrite] 254 | coreImageRenderDestinationTexture = device.makeTexture(descriptor: descriptor) 255 | } 256 | 257 | let commandBuffer = commandQueue.makeCommandBuffer()! 258 | 259 | for touch in touches { 260 | 261 | var point = touch.preciseLocation(in: view) 262 | point.x = metalView.drawableSize.width * point.x / metalView.bounds.width 263 | point.y = metalView.drawableSize.height * point.y / metalView.bounds.height 264 | let size = CGSize(width: brushTexture.width, height: brushTexture.height) 265 | let translation = CGAffineTransform(translationX: floor(point.x - size.width / 2), y: floor(point.y - size.height / 2)) 266 | var brushImage = CIImage(mtlTexture: brushTexture, options: [CIImageOption.colorSpace: colorSpace])!.transformed(by: translation) 267 | brushImage = brushImage.applyingFilter("CIColorMatrix", parameters: ["inputBiasVector": selectedColorVector]).cropped(to: brushImage.extent) 268 | 269 | if touch.type == .stylus || touch.force > 0 { 270 | let alpha = max(touch.force / touch.maximumPossibleForce, 0.025) 271 | brushImage = brushImage.image(withAlpha: alpha) 272 | } 273 | 274 | let renderImage = brushImage.composited(over: sourceImage!).cropped(to: brushImage.extent).cropped(to: sourceImage!.extent) // should slice off edges 275 | if renderImage.extent.isNull { 276 | continue 277 | } 278 | 279 | let extent = renderImage.extent 280 | let region = MTLRegionMake2D(Int(extent.minX), Int(extent.minY), Int(extent.width), Int(extent.height)) 281 | 282 | // CoreImage will give you premultiplied color values so if you blend RGB w/ sourceAlpha you'll just darken the image to nothing 283 | // treat the content of this buffer (and all active line buffers) as premultiplied 284 | ciContext.render(renderImage, to: coreImageRenderDestinationTexture, commandBuffer: commandBuffer, bounds: renderImage.extent, colorSpace: colorSpace) 285 | 286 | let blit = commandBuffer.makeBlitCommandEncoder()! 287 | blit.copy(from: coreImageRenderDestinationTexture, sourceSlice: 0, sourceLevel: 0, sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), sourceSize: MTLSize(width: Int(extent.width), height: Int(extent.height), depth: 1), to: texture, destinationSlice: 0, destinationLevel: 0, destinationOrigin: region.origin) 288 | blit.endEncoding() 289 | } 290 | 291 | commandBuffer.commit() 292 | } 293 | 294 | var shouldUseCoalescedTouches = true 295 | 296 | func endTouches(touches: Set, cancel: Bool) { 297 | if !cancel { 298 | // snapshot + put on undo stack 299 | activeLayer?.drawInLayer(layer: frozenLayer) 300 | 301 | 302 | let commandBuffer = commandQueue.makeCommandBuffer()! 303 | renderer.renderTexture(texture: activeLayer!.texture, inTexture: frozenLayer.texture, commandBuffer: commandBuffer, shouldClear: false, textureIsPremultipled: true) 304 | commandBuffer.commit() 305 | } 306 | 307 | clearLayer(layer: activeLayer!) 308 | clearLayer(layer: predictiveLayer!) 309 | 310 | // cancel? discard layer 311 | // otherwise snapshot frozen layer under layer's rect, then source-over composite it into frozen layer 312 | 313 | metalView.setNeedsDisplay() 314 | } 315 | 316 | 317 | func hitTestColorPicker(with touches: Set, event: UIEvent?, shouldHide: Bool) { 318 | for touch in touches { 319 | if colorPicker.hitTest(touch.location(in: colorPicker), with: event) != nil { 320 | // hide color picker 321 | UIView.animate(withDuration: 0.2) { 322 | self.colorPicker.alpha = 0 323 | } 324 | } 325 | } 326 | } 327 | 328 | func showColorPicker() { 329 | UIView.animate(withDuration: 0.2) { 330 | self.colorPicker.alpha = 1 331 | } 332 | } 333 | } 334 | 335 | extension CIImage { 336 | func image(withAlpha alpha: CGFloat) -> CIImage { 337 | return applyingFilter("CIColorMatrix", parameters: ["inputAVector": CIVector(x: 0, y: 0, z: 0, w: alpha)]) 338 | } 339 | } 340 | 341 | // MARK: Touch handling 342 | 343 | extension ViewController { 344 | override func touchesBegan(_ touches: Set, with event: UIEvent?) { 345 | drawTouches(touches: touches, withEvent: event) 346 | hitTestColorPicker(with: touches, event: event, shouldHide: true) 347 | } 348 | 349 | override func touchesMoved(_ touches: Set, with event: UIEvent?) { 350 | drawTouches(touches: touches, withEvent: event) 351 | hitTestColorPicker(with: touches, event: event, shouldHide: true) 352 | } 353 | 354 | override func touchesCancelled(_ touches: Set, with event: UIEvent?) { 355 | endTouches(touches: touches, cancel: true) 356 | showColorPicker() 357 | } 358 | 359 | override func touchesEnded(_ touches: Set, with event: UIEvent?) { 360 | drawTouches(touches: touches, withEvent: event) 361 | endTouches(touches: touches, cancel: false) 362 | showColorPicker() 363 | } 364 | 365 | override func touchesEstimatedPropertiesUpdated(_ touches: Set) { 366 | // update force or pencil tilt 367 | } 368 | } 369 | 370 | --------------------------------------------------------------------------------