├── .gitignore ├── ToneCurveEditor.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── ToneCurveEditor ├── AppDelegate.swift ├── Async.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── ImageWidget.swift ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── ToneCurveEditor.swift ├── ToneCurveEditorCurveLayer.swift ├── UIBezierPathExtension.swift ├── UIImageExtension.swift └── ViewController.swift └── ToneCurveEditorTests ├── Info.plist └── ToneCurveEditorTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | ToneCurveEditor.xcodeproj/project.xcworkspace/xcuserdata 2 | ToneCurveEditor.xcodeproj/xcuserdata 3 | -------------------------------------------------------------------------------- /ToneCurveEditor.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3E38F2BE19C2B29100B86916 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E38F2BD19C2B29100B86916 /* AppDelegate.swift */; }; 11 | 3E38F2C019C2B29100B86916 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E38F2BF19C2B29100B86916 /* ViewController.swift */; }; 12 | 3E38F2C319C2B29100B86916 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3E38F2C119C2B29100B86916 /* Main.storyboard */; }; 13 | 3E38F2C519C2B29100B86916 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3E38F2C419C2B29100B86916 /* Images.xcassets */; }; 14 | 3E38F2C819C2B29100B86916 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3E38F2C619C2B29100B86916 /* LaunchScreen.xib */; }; 15 | 3E38F2D419C2B29100B86916 /* ToneCurveEditorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E38F2D319C2B29100B86916 /* ToneCurveEditorTests.swift */; }; 16 | 3E38F2E219C2CCD300B86916 /* ImageWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E38F2E119C2CCD300B86916 /* ImageWidget.swift */; }; 17 | 3E38F2E419C2CD0A00B86916 /* ToneCurveEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E38F2E319C2CD0A00B86916 /* ToneCurveEditor.swift */; }; 18 | 3E38F2E819C776B700B86916 /* UIBezierPathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E38F2E719C776B700B86916 /* UIBezierPathExtension.swift */; }; 19 | BE12432819C58041001FFC85 /* UIImageExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE12432719C58041001FFC85 /* UIImageExtension.swift */; }; 20 | BE21CB6219C40907006BD6F4 /* ToneCurveEditorCurveLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE21CB6119C40907006BD6F4 /* ToneCurveEditorCurveLayer.swift */; }; 21 | BE21CB6519C46402006BD6F4 /* Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE21CB6419C46402006BD6F4 /* Async.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXContainerItemProxy section */ 25 | 3E38F2CE19C2B29100B86916 /* PBXContainerItemProxy */ = { 26 | isa = PBXContainerItemProxy; 27 | containerPortal = 3E38F2B019C2B29000B86916 /* Project object */; 28 | proxyType = 1; 29 | remoteGlobalIDString = 3E38F2B719C2B29100B86916; 30 | remoteInfo = ToneCurveEditor; 31 | }; 32 | /* End PBXContainerItemProxy section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 3E38F2B819C2B29100B86916 /* ToneCurveEditor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ToneCurveEditor.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 3E38F2BC19C2B29100B86916 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 37 | 3E38F2BD19C2B29100B86916 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 38 | 3E38F2BF19C2B29100B86916 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 39 | 3E38F2C219C2B29100B86916 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 40 | 3E38F2C419C2B29100B86916 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 41 | 3E38F2C719C2B29100B86916 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 42 | 3E38F2CD19C2B29100B86916 /* ToneCurveEditorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ToneCurveEditorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | 3E38F2D219C2B29100B86916 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 44 | 3E38F2D319C2B29100B86916 /* ToneCurveEditorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToneCurveEditorTests.swift; sourceTree = ""; }; 45 | 3E38F2E119C2CCD300B86916 /* ImageWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageWidget.swift; sourceTree = ""; }; 46 | 3E38F2E319C2CD0A00B86916 /* ToneCurveEditor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToneCurveEditor.swift; sourceTree = ""; }; 47 | 3E38F2E719C776B700B86916 /* UIBezierPathExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIBezierPathExtension.swift; sourceTree = ""; }; 48 | BE12432719C58041001FFC85 /* UIImageExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtension.swift; sourceTree = ""; }; 49 | BE21CB6119C40907006BD6F4 /* ToneCurveEditorCurveLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToneCurveEditorCurveLayer.swift; sourceTree = ""; }; 50 | BE21CB6419C46402006BD6F4 /* Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Async.swift; sourceTree = ""; }; 51 | /* End PBXFileReference section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | 3E38F2B519C2B29100B86916 /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | 3E38F2CA19C2B29100B86916 /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | /* End PBXFrameworksBuildPhase section */ 69 | 70 | /* Begin PBXGroup section */ 71 | 3E38F2AF19C2B29000B86916 = { 72 | isa = PBXGroup; 73 | children = ( 74 | 3E38F2BA19C2B29100B86916 /* ToneCurveEditor */, 75 | 3E38F2D019C2B29100B86916 /* ToneCurveEditorTests */, 76 | 3E38F2B919C2B29100B86916 /* Products */, 77 | ); 78 | sourceTree = ""; 79 | }; 80 | 3E38F2B919C2B29100B86916 /* Products */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 3E38F2B819C2B29100B86916 /* ToneCurveEditor.app */, 84 | 3E38F2CD19C2B29100B86916 /* ToneCurveEditorTests.xctest */, 85 | ); 86 | name = Products; 87 | sourceTree = ""; 88 | }; 89 | 3E38F2BA19C2B29100B86916 /* ToneCurveEditor */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | BE12432919C58116001FFC85 /* extensions */, 93 | BE21CB6319C463DD006BD6F4 /* async */, 94 | 3E38F2DD19C2CC8200B86916 /* widgets */, 95 | 3E38F2BD19C2B29100B86916 /* AppDelegate.swift */, 96 | 3E38F2BF19C2B29100B86916 /* ViewController.swift */, 97 | 3E38F2C119C2B29100B86916 /* Main.storyboard */, 98 | 3E38F2C419C2B29100B86916 /* Images.xcassets */, 99 | 3E38F2C619C2B29100B86916 /* LaunchScreen.xib */, 100 | 3E38F2BB19C2B29100B86916 /* Supporting Files */, 101 | ); 102 | path = ToneCurveEditor; 103 | sourceTree = ""; 104 | }; 105 | 3E38F2BB19C2B29100B86916 /* Supporting Files */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 3E38F2BC19C2B29100B86916 /* Info.plist */, 109 | ); 110 | name = "Supporting Files"; 111 | sourceTree = ""; 112 | }; 113 | 3E38F2D019C2B29100B86916 /* ToneCurveEditorTests */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 3E38F2D319C2B29100B86916 /* ToneCurveEditorTests.swift */, 117 | 3E38F2D119C2B29100B86916 /* Supporting Files */, 118 | ); 119 | path = ToneCurveEditorTests; 120 | sourceTree = ""; 121 | }; 122 | 3E38F2D119C2B29100B86916 /* Supporting Files */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 3E38F2D219C2B29100B86916 /* Info.plist */, 126 | ); 127 | name = "Supporting Files"; 128 | sourceTree = ""; 129 | }; 130 | 3E38F2DD19C2CC8200B86916 /* widgets */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 3E38F2E119C2CCD300B86916 /* ImageWidget.swift */, 134 | 3E38F2E319C2CD0A00B86916 /* ToneCurveEditor.swift */, 135 | BE21CB6119C40907006BD6F4 /* ToneCurveEditorCurveLayer.swift */, 136 | ); 137 | name = widgets; 138 | sourceTree = ""; 139 | }; 140 | BE12432919C58116001FFC85 /* extensions */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | BE12432719C58041001FFC85 /* UIImageExtension.swift */, 144 | 3E38F2E719C776B700B86916 /* UIBezierPathExtension.swift */, 145 | ); 146 | name = extensions; 147 | sourceTree = ""; 148 | }; 149 | BE21CB6319C463DD006BD6F4 /* async */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | BE21CB6419C46402006BD6F4 /* Async.swift */, 153 | ); 154 | name = async; 155 | sourceTree = ""; 156 | }; 157 | /* End PBXGroup section */ 158 | 159 | /* Begin PBXNativeTarget section */ 160 | 3E38F2B719C2B29100B86916 /* ToneCurveEditor */ = { 161 | isa = PBXNativeTarget; 162 | buildConfigurationList = 3E38F2D719C2B29100B86916 /* Build configuration list for PBXNativeTarget "ToneCurveEditor" */; 163 | buildPhases = ( 164 | 3E38F2B419C2B29100B86916 /* Sources */, 165 | 3E38F2B519C2B29100B86916 /* Frameworks */, 166 | 3E38F2B619C2B29100B86916 /* Resources */, 167 | ); 168 | buildRules = ( 169 | ); 170 | dependencies = ( 171 | ); 172 | name = ToneCurveEditor; 173 | productName = ToneCurveEditor; 174 | productReference = 3E38F2B819C2B29100B86916 /* ToneCurveEditor.app */; 175 | productType = "com.apple.product-type.application"; 176 | }; 177 | 3E38F2CC19C2B29100B86916 /* ToneCurveEditorTests */ = { 178 | isa = PBXNativeTarget; 179 | buildConfigurationList = 3E38F2DA19C2B29100B86916 /* Build configuration list for PBXNativeTarget "ToneCurveEditorTests" */; 180 | buildPhases = ( 181 | 3E38F2C919C2B29100B86916 /* Sources */, 182 | 3E38F2CA19C2B29100B86916 /* Frameworks */, 183 | 3E38F2CB19C2B29100B86916 /* Resources */, 184 | ); 185 | buildRules = ( 186 | ); 187 | dependencies = ( 188 | 3E38F2CF19C2B29100B86916 /* PBXTargetDependency */, 189 | ); 190 | name = ToneCurveEditorTests; 191 | productName = ToneCurveEditorTests; 192 | productReference = 3E38F2CD19C2B29100B86916 /* ToneCurveEditorTests.xctest */; 193 | productType = "com.apple.product-type.bundle.unit-test"; 194 | }; 195 | /* End PBXNativeTarget section */ 196 | 197 | /* Begin PBXProject section */ 198 | 3E38F2B019C2B29000B86916 /* Project object */ = { 199 | isa = PBXProject; 200 | attributes = { 201 | LastSwiftMigration = 0720; 202 | LastSwiftUpdateCheck = 0720; 203 | LastUpgradeCheck = 0600; 204 | ORGANIZATIONNAME = "Simon Gladman"; 205 | TargetAttributes = { 206 | 3E38F2B719C2B29100B86916 = { 207 | CreatedOnToolsVersion = 6.0; 208 | }; 209 | 3E38F2CC19C2B29100B86916 = { 210 | CreatedOnToolsVersion = 6.0; 211 | TestTargetID = 3E38F2B719C2B29100B86916; 212 | }; 213 | }; 214 | }; 215 | buildConfigurationList = 3E38F2B319C2B29000B86916 /* Build configuration list for PBXProject "ToneCurveEditor" */; 216 | compatibilityVersion = "Xcode 3.2"; 217 | developmentRegion = English; 218 | hasScannedForEncodings = 0; 219 | knownRegions = ( 220 | en, 221 | Base, 222 | ); 223 | mainGroup = 3E38F2AF19C2B29000B86916; 224 | productRefGroup = 3E38F2B919C2B29100B86916 /* Products */; 225 | projectDirPath = ""; 226 | projectRoot = ""; 227 | targets = ( 228 | 3E38F2B719C2B29100B86916 /* ToneCurveEditor */, 229 | 3E38F2CC19C2B29100B86916 /* ToneCurveEditorTests */, 230 | ); 231 | }; 232 | /* End PBXProject section */ 233 | 234 | /* Begin PBXResourcesBuildPhase section */ 235 | 3E38F2B619C2B29100B86916 /* Resources */ = { 236 | isa = PBXResourcesBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | 3E38F2C319C2B29100B86916 /* Main.storyboard in Resources */, 240 | 3E38F2C819C2B29100B86916 /* LaunchScreen.xib in Resources */, 241 | 3E38F2C519C2B29100B86916 /* Images.xcassets in Resources */, 242 | ); 243 | runOnlyForDeploymentPostprocessing = 0; 244 | }; 245 | 3E38F2CB19C2B29100B86916 /* Resources */ = { 246 | isa = PBXResourcesBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | ); 250 | runOnlyForDeploymentPostprocessing = 0; 251 | }; 252 | /* End PBXResourcesBuildPhase section */ 253 | 254 | /* Begin PBXSourcesBuildPhase section */ 255 | 3E38F2B419C2B29100B86916 /* Sources */ = { 256 | isa = PBXSourcesBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | 3E38F2C019C2B29100B86916 /* ViewController.swift in Sources */, 260 | 3E38F2E219C2CCD300B86916 /* ImageWidget.swift in Sources */, 261 | BE12432819C58041001FFC85 /* UIImageExtension.swift in Sources */, 262 | BE21CB6219C40907006BD6F4 /* ToneCurveEditorCurveLayer.swift in Sources */, 263 | 3E38F2E419C2CD0A00B86916 /* ToneCurveEditor.swift in Sources */, 264 | 3E38F2E819C776B700B86916 /* UIBezierPathExtension.swift in Sources */, 265 | BE21CB6519C46402006BD6F4 /* Async.swift in Sources */, 266 | 3E38F2BE19C2B29100B86916 /* AppDelegate.swift in Sources */, 267 | ); 268 | runOnlyForDeploymentPostprocessing = 0; 269 | }; 270 | 3E38F2C919C2B29100B86916 /* Sources */ = { 271 | isa = PBXSourcesBuildPhase; 272 | buildActionMask = 2147483647; 273 | files = ( 274 | 3E38F2D419C2B29100B86916 /* ToneCurveEditorTests.swift in Sources */, 275 | ); 276 | runOnlyForDeploymentPostprocessing = 0; 277 | }; 278 | /* End PBXSourcesBuildPhase section */ 279 | 280 | /* Begin PBXTargetDependency section */ 281 | 3E38F2CF19C2B29100B86916 /* PBXTargetDependency */ = { 282 | isa = PBXTargetDependency; 283 | target = 3E38F2B719C2B29100B86916 /* ToneCurveEditor */; 284 | targetProxy = 3E38F2CE19C2B29100B86916 /* PBXContainerItemProxy */; 285 | }; 286 | /* End PBXTargetDependency section */ 287 | 288 | /* Begin PBXVariantGroup section */ 289 | 3E38F2C119C2B29100B86916 /* Main.storyboard */ = { 290 | isa = PBXVariantGroup; 291 | children = ( 292 | 3E38F2C219C2B29100B86916 /* Base */, 293 | ); 294 | name = Main.storyboard; 295 | sourceTree = ""; 296 | }; 297 | 3E38F2C619C2B29100B86916 /* LaunchScreen.xib */ = { 298 | isa = PBXVariantGroup; 299 | children = ( 300 | 3E38F2C719C2B29100B86916 /* Base */, 301 | ); 302 | name = LaunchScreen.xib; 303 | sourceTree = ""; 304 | }; 305 | /* End PBXVariantGroup section */ 306 | 307 | /* Begin XCBuildConfiguration section */ 308 | 3E38F2D519C2B29100B86916 /* Debug */ = { 309 | isa = XCBuildConfiguration; 310 | buildSettings = { 311 | ALWAYS_SEARCH_USER_PATHS = NO; 312 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 313 | CLANG_CXX_LIBRARY = "libc++"; 314 | CLANG_ENABLE_MODULES = YES; 315 | CLANG_ENABLE_OBJC_ARC = YES; 316 | CLANG_WARN_BOOL_CONVERSION = YES; 317 | CLANG_WARN_CONSTANT_CONVERSION = YES; 318 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 319 | CLANG_WARN_EMPTY_BODY = YES; 320 | CLANG_WARN_ENUM_CONVERSION = YES; 321 | CLANG_WARN_INT_CONVERSION = YES; 322 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 323 | CLANG_WARN_UNREACHABLE_CODE = YES; 324 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 325 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 326 | COPY_PHASE_STRIP = NO; 327 | ENABLE_STRICT_OBJC_MSGSEND = YES; 328 | GCC_C_LANGUAGE_STANDARD = gnu99; 329 | GCC_DYNAMIC_NO_PIC = NO; 330 | GCC_OPTIMIZATION_LEVEL = 0; 331 | GCC_PREPROCESSOR_DEFINITIONS = ( 332 | "DEBUG=1", 333 | "$(inherited)", 334 | ); 335 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 336 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 337 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 338 | GCC_WARN_UNDECLARED_SELECTOR = YES; 339 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 340 | GCC_WARN_UNUSED_FUNCTION = YES; 341 | GCC_WARN_UNUSED_VARIABLE = YES; 342 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 343 | MTL_ENABLE_DEBUG_INFO = YES; 344 | ONLY_ACTIVE_ARCH = YES; 345 | SDKROOT = iphoneos; 346 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 347 | TARGETED_DEVICE_FAMILY = 2; 348 | }; 349 | name = Debug; 350 | }; 351 | 3E38F2D619C2B29100B86916 /* Release */ = { 352 | isa = XCBuildConfiguration; 353 | buildSettings = { 354 | ALWAYS_SEARCH_USER_PATHS = NO; 355 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 356 | CLANG_CXX_LIBRARY = "libc++"; 357 | CLANG_ENABLE_MODULES = YES; 358 | CLANG_ENABLE_OBJC_ARC = YES; 359 | CLANG_WARN_BOOL_CONVERSION = YES; 360 | CLANG_WARN_CONSTANT_CONVERSION = YES; 361 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 362 | CLANG_WARN_EMPTY_BODY = YES; 363 | CLANG_WARN_ENUM_CONVERSION = YES; 364 | CLANG_WARN_INT_CONVERSION = YES; 365 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 366 | CLANG_WARN_UNREACHABLE_CODE = YES; 367 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 368 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 369 | COPY_PHASE_STRIP = YES; 370 | ENABLE_NS_ASSERTIONS = NO; 371 | ENABLE_STRICT_OBJC_MSGSEND = YES; 372 | GCC_C_LANGUAGE_STANDARD = gnu99; 373 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 374 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 375 | GCC_WARN_UNDECLARED_SELECTOR = YES; 376 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 377 | GCC_WARN_UNUSED_FUNCTION = YES; 378 | GCC_WARN_UNUSED_VARIABLE = YES; 379 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 380 | MTL_ENABLE_DEBUG_INFO = NO; 381 | SDKROOT = iphoneos; 382 | TARGETED_DEVICE_FAMILY = 2; 383 | VALIDATE_PRODUCT = YES; 384 | }; 385 | name = Release; 386 | }; 387 | 3E38F2D819C2B29100B86916 /* Debug */ = { 388 | isa = XCBuildConfiguration; 389 | buildSettings = { 390 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 391 | CLANG_ENABLE_MODULES = YES; 392 | INFOPLIST_FILE = ToneCurveEditor/Info.plist; 393 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 394 | PRODUCT_NAME = "$(TARGET_NAME)"; 395 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 396 | }; 397 | name = Debug; 398 | }; 399 | 3E38F2D919C2B29100B86916 /* Release */ = { 400 | isa = XCBuildConfiguration; 401 | buildSettings = { 402 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 403 | CLANG_ENABLE_MODULES = YES; 404 | INFOPLIST_FILE = ToneCurveEditor/Info.plist; 405 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 406 | PRODUCT_NAME = "$(TARGET_NAME)"; 407 | }; 408 | name = Release; 409 | }; 410 | 3E38F2DB19C2B29100B86916 /* Debug */ = { 411 | isa = XCBuildConfiguration; 412 | buildSettings = { 413 | BUNDLE_LOADER = "$(TEST_HOST)"; 414 | FRAMEWORK_SEARCH_PATHS = ( 415 | "$(SDKROOT)/Developer/Library/Frameworks", 416 | "$(inherited)", 417 | ); 418 | GCC_PREPROCESSOR_DEFINITIONS = ( 419 | "DEBUG=1", 420 | "$(inherited)", 421 | ); 422 | INFOPLIST_FILE = ToneCurveEditorTests/Info.plist; 423 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 424 | PRODUCT_NAME = "$(TARGET_NAME)"; 425 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ToneCurveEditor.app/ToneCurveEditor"; 426 | }; 427 | name = Debug; 428 | }; 429 | 3E38F2DC19C2B29100B86916 /* Release */ = { 430 | isa = XCBuildConfiguration; 431 | buildSettings = { 432 | BUNDLE_LOADER = "$(TEST_HOST)"; 433 | FRAMEWORK_SEARCH_PATHS = ( 434 | "$(SDKROOT)/Developer/Library/Frameworks", 435 | "$(inherited)", 436 | ); 437 | INFOPLIST_FILE = ToneCurveEditorTests/Info.plist; 438 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 439 | PRODUCT_NAME = "$(TARGET_NAME)"; 440 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ToneCurveEditor.app/ToneCurveEditor"; 441 | }; 442 | name = Release; 443 | }; 444 | /* End XCBuildConfiguration section */ 445 | 446 | /* Begin XCConfigurationList section */ 447 | 3E38F2B319C2B29000B86916 /* Build configuration list for PBXProject "ToneCurveEditor" */ = { 448 | isa = XCConfigurationList; 449 | buildConfigurations = ( 450 | 3E38F2D519C2B29100B86916 /* Debug */, 451 | 3E38F2D619C2B29100B86916 /* Release */, 452 | ); 453 | defaultConfigurationIsVisible = 0; 454 | defaultConfigurationName = Release; 455 | }; 456 | 3E38F2D719C2B29100B86916 /* Build configuration list for PBXNativeTarget "ToneCurveEditor" */ = { 457 | isa = XCConfigurationList; 458 | buildConfigurations = ( 459 | 3E38F2D819C2B29100B86916 /* Debug */, 460 | 3E38F2D919C2B29100B86916 /* Release */, 461 | ); 462 | defaultConfigurationIsVisible = 0; 463 | defaultConfigurationName = Release; 464 | }; 465 | 3E38F2DA19C2B29100B86916 /* Build configuration list for PBXNativeTarget "ToneCurveEditorTests" */ = { 466 | isa = XCConfigurationList; 467 | buildConfigurations = ( 468 | 3E38F2DB19C2B29100B86916 /* Debug */, 469 | 3E38F2DC19C2B29100B86916 /* Release */, 470 | ); 471 | defaultConfigurationIsVisible = 0; 472 | defaultConfigurationName = Release; 473 | }; 474 | /* End XCConfigurationList section */ 475 | }; 476 | rootObject = 3E38F2B019C2B29000B86916 /* Project object */; 477 | } 478 | -------------------------------------------------------------------------------- /ToneCurveEditor.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ToneCurveEditor/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ToneCurveEditor 4 | // 5 | // Created by Simon Gladman on 12/09/2014. 6 | // Copyright (c) 2014 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 | -------------------------------------------------------------------------------- /ToneCurveEditor/Async.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Async.swift 3 | // 4 | // Created by Tobias DM on 15/07/14. 5 | // 6 | // OS X 10.10+ and iOS 8.0+ 7 | // Only use with ARC 8 | // 9 | // The MIT License (MIT) 10 | // Copyright (c) 2014 Tobias Due Munk 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 13 | // this software and associated documentation files (the "Software"), to deal in 14 | // the Software without restriction, including without limitation the rights to 15 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 16 | // the Software, and to permit persons to whom the Software is furnished to do so, 17 | // subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in all 20 | // copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 24 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 25 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 26 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 27 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | 29 | 30 | import Foundation 31 | 32 | // HACK: For Beta 5, 6 33 | prefix func +(v: qos_class_t) -> Int { 34 | return Int(v.rawValue) 35 | } 36 | 37 | private class GCD { 38 | 39 | /* dispatch_get_queue() */ 40 | class func mainQueue() -> dispatch_queue_t { 41 | return dispatch_get_main_queue() 42 | // Could use return dispatch_get_global_queue(+qos_class_main(), 0) 43 | } 44 | class func userInteractiveQueue() -> dispatch_queue_t { 45 | return dispatch_get_global_queue(+QOS_CLASS_USER_INTERACTIVE, 0) 46 | } 47 | class func userInitiatedQueue() -> dispatch_queue_t { 48 | return dispatch_get_global_queue(+QOS_CLASS_USER_INITIATED, 0) 49 | } 50 | class func defaultQueue() -> dispatch_queue_t { 51 | return dispatch_get_global_queue(+QOS_CLASS_DEFAULT, 0) 52 | } 53 | class func utilityQueue() -> dispatch_queue_t { 54 | return dispatch_get_global_queue(+QOS_CLASS_UTILITY, 0) 55 | } 56 | class func backgroundQueue() -> dispatch_queue_t { 57 | return dispatch_get_global_queue(+QOS_CLASS_BACKGROUND, 0) 58 | } 59 | } 60 | 61 | 62 | public struct Async { 63 | 64 | private let block: dispatch_block_t 65 | 66 | private init(_ block: dispatch_block_t) { 67 | self.block = block 68 | } 69 | } 70 | 71 | 72 | extension Async { // Static methods 73 | 74 | 75 | /* dispatch_async() */ 76 | 77 | private static func async(block: dispatch_block_t, inQueue queue: dispatch_queue_t) -> Async { 78 | // Create a new block (Qos Class) from block to allow adding a notification to it later (see matching regular Async methods) 79 | // Create block with the "inherit" type 80 | let _block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, block) 81 | // Add block to queue 82 | dispatch_async(queue, _block) 83 | // Wrap block in a struct since dispatch_block_t can't be extended 84 | return Async(_block) 85 | } 86 | static func main(block: dispatch_block_t) -> Async { 87 | return Async.async(block, inQueue: GCD.mainQueue()) 88 | } 89 | static func userInteractive(block: dispatch_block_t) -> Async { 90 | return Async.async(block, inQueue: GCD.userInteractiveQueue()) 91 | } 92 | static func userInitiated(block: dispatch_block_t) -> Async { 93 | return Async.async(block, inQueue: GCD.userInitiatedQueue()) 94 | } 95 | static func default_(block: dispatch_block_t) -> Async { 96 | return Async.async(block, inQueue: GCD.defaultQueue()) 97 | } 98 | static func utility(block: dispatch_block_t) -> Async { 99 | return Async.async(block, inQueue: GCD.utilityQueue()) 100 | } 101 | static func background(block: dispatch_block_t) -> Async { 102 | return Async.async(block, inQueue: GCD.backgroundQueue()) 103 | } 104 | static func customQueue(queue: dispatch_queue_t, block: dispatch_block_t) -> Async { 105 | return Async.async(block, inQueue: queue) 106 | } 107 | 108 | 109 | /* dispatch_after() */ 110 | 111 | private static func after(seconds: Double, block: dispatch_block_t, inQueue queue: dispatch_queue_t) -> Async { 112 | let nanoSeconds = Int64(seconds * Double(NSEC_PER_SEC)) 113 | let time = dispatch_time(DISPATCH_TIME_NOW, nanoSeconds) 114 | return at(time, block: block, inQueue: queue) 115 | } 116 | private static func at(time: dispatch_time_t, block: dispatch_block_t, inQueue queue: dispatch_queue_t) -> Async { 117 | // See Async.async() for comments 118 | let _block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, block) 119 | dispatch_after(time, queue, _block) 120 | return Async(_block) 121 | } 122 | static func main(after after: Double, block: dispatch_block_t) -> Async { 123 | return Async.after(after, block: block, inQueue: GCD.mainQueue()) 124 | } 125 | static func userInteractive(after after: Double, block: dispatch_block_t) -> Async { 126 | return Async.after(after, block: block, inQueue: GCD.userInteractiveQueue()) 127 | } 128 | static func userInitiated(after after: Double, block: dispatch_block_t) -> Async { 129 | return Async.after(after, block: block, inQueue: GCD.userInitiatedQueue()) 130 | } 131 | static func default_(after after: Double, block: dispatch_block_t) -> Async { 132 | return Async.after(after, block: block, inQueue: GCD.defaultQueue()) 133 | } 134 | static func utility(after after: Double, block: dispatch_block_t) -> Async { 135 | return Async.after(after, block: block, inQueue: GCD.utilityQueue()) 136 | } 137 | static func background(after after: Double, block: dispatch_block_t) -> Async { 138 | return Async.after(after, block: block, inQueue: GCD.backgroundQueue()) 139 | } 140 | static func customQueue(after after: Double, queue: dispatch_queue_t, block: dispatch_block_t) -> Async { 141 | return Async.after(after, block: block, inQueue: queue) 142 | } 143 | } 144 | 145 | 146 | extension Async { // Regualar methods matching static once 147 | 148 | 149 | /* dispatch_async() */ 150 | 151 | private func chain(block chainingBlock: dispatch_block_t, runInQueue queue: dispatch_queue_t) -> Async { 152 | // See Async.async() for comments 153 | let _chainingBlock = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, chainingBlock) 154 | dispatch_block_notify(self.block, queue, _chainingBlock) 155 | return Async(_chainingBlock) 156 | } 157 | 158 | func main(chainingBlock: dispatch_block_t) -> Async { 159 | return chain(block: chainingBlock, runInQueue: GCD.mainQueue()) 160 | } 161 | func userInteractive(chainingBlock: dispatch_block_t) -> Async { 162 | return chain(block: chainingBlock, runInQueue: GCD.userInteractiveQueue()) 163 | } 164 | func userInitiated(chainingBlock: dispatch_block_t) -> Async { 165 | return chain(block: chainingBlock, runInQueue: GCD.userInitiatedQueue()) 166 | } 167 | func default_(chainingBlock: dispatch_block_t) -> Async { 168 | return chain(block: chainingBlock, runInQueue: GCD.defaultQueue()) 169 | } 170 | func utility(chainingBlock: dispatch_block_t) -> Async { 171 | return chain(block: chainingBlock, runInQueue: GCD.utilityQueue()) 172 | } 173 | func background(chainingBlock: dispatch_block_t) -> Async { 174 | return chain(block: chainingBlock, runInQueue: GCD.backgroundQueue()) 175 | } 176 | func customQueue(queue: dispatch_queue_t, chainingBlock: dispatch_block_t) -> Async { 177 | return chain(block: chainingBlock, runInQueue: queue) 178 | } 179 | 180 | 181 | /* dispatch_after() */ 182 | 183 | private func after(seconds: Double, block chainingBlock: dispatch_block_t, runInQueue queue: dispatch_queue_t) -> Async { 184 | 185 | // Create a new block (Qos Class) from block to allow adding a notification to it later (see Async) 186 | // Create block with the "inherit" type 187 | let _chainingBlock = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, chainingBlock) 188 | 189 | // Wrap block to be called when previous block is finished 190 | let chainingWrapperBlock: dispatch_block_t = { 191 | // Calculate time from now 192 | let nanoSeconds = Int64(seconds * Double(NSEC_PER_SEC)) 193 | let time = dispatch_time(DISPATCH_TIME_NOW, nanoSeconds) 194 | dispatch_after(time, queue, _chainingBlock) 195 | } 196 | // Create a new block (Qos Class) from block to allow adding a notification to it later (see Async) 197 | // Create block with the "inherit" type 198 | let _chainingWrapperBlock = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, chainingWrapperBlock) 199 | // Add block to queue *after* previous block is finished 200 | dispatch_block_notify(self.block, queue, _chainingWrapperBlock) 201 | // Wrap block in a struct since dispatch_block_t can't be extended 202 | return Async(_chainingBlock) 203 | } 204 | func main(after after: Double, block: dispatch_block_t) -> Async { 205 | return self.after(after, block: block, runInQueue: GCD.mainQueue()) 206 | } 207 | func userInteractive(after after: Double, block: dispatch_block_t) -> Async { 208 | return self.after(after, block: block, runInQueue: GCD.userInteractiveQueue()) 209 | } 210 | func userInitiated(after after: Double, block: dispatch_block_t) -> Async { 211 | return self.after(after, block: block, runInQueue: GCD.userInitiatedQueue()) 212 | } 213 | func default_(after after: Double, block: dispatch_block_t) -> Async { 214 | return self.after(after, block: block, runInQueue: GCD.defaultQueue()) 215 | } 216 | func utility(after after: Double, block: dispatch_block_t) -> Async { 217 | return self.after(after, block: block, runInQueue: GCD.utilityQueue()) 218 | } 219 | func background(after after: Double, block: dispatch_block_t) -> Async { 220 | return self.after(after, block: block, runInQueue: GCD.backgroundQueue()) 221 | } 222 | func customQueue(after after: Double, queue: dispatch_queue_t, block: dispatch_block_t) -> Async { 223 | return self.after(after, block: block, runInQueue: queue) 224 | } 225 | 226 | 227 | /* cancel */ 228 | 229 | func cancel() { 230 | dispatch_block_cancel(block) 231 | } 232 | 233 | 234 | /* wait */ 235 | 236 | /// If optional parameter forSeconds is not provided, use DISPATCH_TIME_FOREVER 237 | func wait(seconds: Double = 0.0) { 238 | if seconds != 0.0 { 239 | let nanoSeconds = Int64(seconds * Double(NSEC_PER_SEC)) 240 | let time = dispatch_time(DISPATCH_TIME_NOW, nanoSeconds) 241 | dispatch_block_wait(block, time) 242 | } else { 243 | dispatch_block_wait(block, DISPATCH_TIME_FOREVER) 244 | } 245 | } 246 | } 247 | 248 | 249 | // Convenience 250 | extension qos_class_t { 251 | 252 | // Calculated property 253 | var description: String { 254 | get { 255 | switch +self { 256 | case +qos_class_main(): return "Main" 257 | case +QOS_CLASS_USER_INTERACTIVE: return "User Interactive" 258 | case +QOS_CLASS_USER_INITIATED: return "User Initiated" 259 | case +QOS_CLASS_DEFAULT: return "Default" 260 | case +QOS_CLASS_UTILITY: return "Utility" 261 | case +QOS_CLASS_BACKGROUND: return "Background" 262 | case +QOS_CLASS_UNSPECIFIED: return "Unspecified" 263 | default: return "Unknown" 264 | } 265 | } 266 | } 267 | } -------------------------------------------------------------------------------- /ToneCurveEditor/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ToneCurveEditor/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 | -------------------------------------------------------------------------------- /ToneCurveEditor/ImageWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageWidget.swift 3 | // ToneCurveEditor 4 | // 5 | // Created by Simon Gladman on 12/09/2014. 6 | // Copyright (c) 2014 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ImageWidget: UIControl , UINavigationControllerDelegate, UIImagePickerControllerDelegate 12 | { 13 | let blurOveray = UIVisualEffectView(effect: UIBlurEffect()) 14 | let loadImageButton = UIButton(frame: CGRectZero) 15 | let imageView = UIImageView(frame: CGRectZero) 16 | let activityIndicator = UIActivityIndicatorView(frame: CGRectZero) 17 | 18 | let ciContext = CIContext(options: nil) 19 | let filter = CIFilter(name: "CIToneCurve") 20 | 21 | var backgroundBlock : Async? 22 | var loadedImage : UIImage? 23 | var filteredImage : UIImage? 24 | 25 | weak var viewController : UIViewController? 26 | 27 | override init(frame: CGRect) 28 | { 29 | super.init(frame: frame) 30 | 31 | backgroundColor = UIColor.blackColor() 32 | 33 | imageView.contentMode = UIViewContentMode.ScaleAspectFit 34 | 35 | addSubview(imageView) 36 | addSubview(blurOveray) 37 | 38 | loadImageButton.setTitle("Load Image", forState: .Normal) 39 | loadImageButton.setTitleColor(UIColor.blackColor(), forState: .Normal) 40 | 41 | loadImageButton.addTarget(self, action: #selector(ImageWidget.loadImageButtonClickHandler(_:)), forControlEvents: .TouchUpInside) 42 | 43 | addSubview(loadImageButton) 44 | 45 | activityIndicator.hidesWhenStopped = true 46 | 47 | activityIndicator.color = UIColor.blackColor() 48 | addSubview(activityIndicator) 49 | } 50 | 51 | required init?(coder aDecoder: NSCoder) 52 | { 53 | super.init(coder: aDecoder) 54 | } 55 | 56 | func loadImageButtonClickHandler(button : UIButton) 57 | { 58 | let imagePicker = UIImagePickerController() 59 | 60 | imagePicker.delegate = self 61 | imagePicker.allowsEditing = false 62 | imagePicker.modalInPopover = false 63 | imagePicker.sourceType = UIImagePickerControllerSourceType.SavedPhotosAlbum 64 | 65 | viewController!.presentViewController(imagePicker, animated: true, completion: nil) 66 | } 67 | 68 | var filterIsRunning : Bool = false 69 | { 70 | didSet 71 | { 72 | if filterIsRunning 73 | { 74 | activityIndicator.startAnimating() 75 | } 76 | else 77 | { 78 | activityIndicator.stopAnimating() 79 | } 80 | } 81 | } 82 | 83 | var curveValues: [Double] = [0.0, 0.25, 0.5, 0.75, 1.0] 84 | { 85 | didSet 86 | { 87 | applyFilterAsync() 88 | } 89 | } 90 | 91 | func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) 92 | { 93 | backgroundBlock?.cancel() 94 | backgroundBlock = nil 95 | 96 | filterIsRunning = false 97 | 98 | if let rawImage = info[UIImagePickerControllerOriginalImage] as? UIImage 99 | { 100 | loadedImage = rawImage.resizeToBoundingSquare(boundingSquareSideLength: 1024) 101 | 102 | applyFilterAsync() 103 | } 104 | 105 | viewController!.dismissViewControllerAnimated(true, completion: nil) 106 | } 107 | 108 | 109 | 110 | func applyFilterAsync() 111 | { 112 | backgroundBlock = Async.background 113 | { 114 | guard !self.filterIsRunning, let 115 | filter = self.filter, 116 | loadedImage = self.loadedImage else { 117 | return 118 | } 119 | 120 | self.filterIsRunning = true 121 | self.filteredImage = ImageWidget.applyFilter(loadedImage: loadedImage, curveValues: self.curveValues, ciContext: self.ciContext, filter: filter) 122 | } 123 | .main 124 | { 125 | self.imageView.image = self.filteredImage 126 | self.filterIsRunning = false 127 | } 128 | } 129 | 130 | 131 | class func applyFilter(loadedImage loadedImage: UIImage, curveValues: [Double], ciContext: CIContext, filter: CIFilter) -> UIImage 132 | { 133 | let coreImage = CIImage(image: loadedImage) 134 | 135 | filter.setValue(coreImage, forKey: kCIInputImageKey) 136 | 137 | filter.setValue(CIVector(x: 0.0, y: CGFloat(curveValues[0])), forKey: "inputPoint0") 138 | filter.setValue(CIVector(x: 0.25, y: CGFloat(curveValues[1])), forKey: "inputPoint1") 139 | filter.setValue(CIVector(x: 0.5, y: CGFloat(curveValues[2])), forKey: "inputPoint2") 140 | filter.setValue(CIVector(x: 0.75, y: CGFloat(curveValues[3])), forKey: "inputPoint3") 141 | filter.setValue(CIVector(x: 1.0, y: CGFloat(curveValues[4])), forKey: "inputPoint4") 142 | 143 | let filteredImageData = filter.valueForKey(kCIOutputImageKey) as! CIImage 144 | let filteredImageRef = ciContext.createCGImage(filteredImageData, fromRect: filteredImageData.extent) 145 | let filteredImage = UIImage(CGImage: filteredImageRef) 146 | 147 | return filteredImage 148 | } 149 | 150 | override func layoutSubviews() 151 | { 152 | blurOveray.frame = CGRect(x: 0, y: frame.height - 50, width: frame.width, height: 50) 153 | 154 | loadImageButton.frame = CGRect(x: 20, y: frame.height - 50, width: 100, height: 50) 155 | 156 | imageView.frame = CGRect(x: 5, y: 5, width: frame.width - 10, height: frame.height - 10) 157 | 158 | activityIndicator.frame = CGRect(x: frame.width - 20, y: frame.height - 25, width: 0, height: 0) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /ToneCurveEditor/Images.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 | } -------------------------------------------------------------------------------- /ToneCurveEditor/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 | -------------------------------------------------------------------------------- /ToneCurveEditor/ToneCurveEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToneCurveEditor.swift 3 | // ToneCurveEditor 4 | // 5 | // Created by Simon Gladman on 12/09/2014. 6 | // Copyright (c) 2014 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ToneCurveEditor: UIControl 12 | { 13 | let formatString = "%.03f" 14 | 15 | var sliders = [UISlider]() 16 | var labels = [UILabel]() 17 | let curveLayer = ToneCurveEditorCurveLayer() 18 | 19 | override init(frame: CGRect) 20 | { 21 | super.init(frame: frame) 22 | 23 | curveLayer.toneCurveEditor = self 24 | curveLayer.contentsScale = UIScreen.mainScreen().scale 25 | layer.addSublayer(curveLayer) 26 | 27 | createSliders() 28 | createLabels() 29 | } 30 | 31 | required init?(coder aDecoder: NSCoder) 32 | { 33 | super.init(coder: aDecoder) 34 | } 35 | 36 | var curveValues : [Double] = [Double](count: 5, repeatedValue: 0.0) 37 | { 38 | didSet 39 | { 40 | for (i, value): (Int, Double) in curveValues.enumerate() 41 | { 42 | sliders[i].value = Float(value) 43 | labels[i].text = String(format: formatString, value) 44 | } 45 | 46 | drawCurve() 47 | } 48 | } 49 | 50 | func createSliders() 51 | { 52 | let rotateTransform = CGAffineTransformIdentity 53 | 54 | for _ in 0..<5 55 | { 56 | let slider = UISlider(frame: CGRectZero) 57 | 58 | slider.transform = CGAffineTransformRotate(rotateTransform, CGFloat(-90.0 * M_PI / 180.0)); 59 | slider.addTarget(self, action: #selector(ToneCurveEditor.sliderChangeHandler(_:)), forControlEvents: .ValueChanged) 60 | 61 | sliders.append(slider) 62 | 63 | addSubview(slider) 64 | } 65 | } 66 | 67 | func createLabels() 68 | { 69 | for _ in 0..<5 70 | { 71 | let label = UILabel(frame: CGRectZero) 72 | 73 | //label.textAlignment = NSTextAlignment(rawValue: 2)! 74 | 75 | labels.append(label) 76 | 77 | addSubview(label) 78 | } 79 | } 80 | 81 | func drawCurve() 82 | { 83 | curveLayer.frame = bounds.insetBy(dx: 0, dy: 0) 84 | curveLayer.setNeedsDisplay() 85 | } 86 | 87 | func sliderChangeHandler(slider : UISlider) 88 | { 89 | let index = sliders.indexOf(slider) 90 | curveValues[index!] = Double(slider.value) 91 | let label = labels[index!] 92 | label.text = String(format: formatString, slider.value) 93 | 94 | sendActionsForControlEvents(.ValueChanged) 95 | } 96 | 97 | override func layoutSubviews() 98 | { 99 | let margin = 20 100 | let targetHeight = Int(frame.height) - margin - margin 101 | let targetWidth = Int(frame.width) / sliders.count 102 | 103 | for (i, slider): (Int, UISlider) in sliders.enumerate() 104 | { 105 | let targetX = i * Int(frame.width) / sliders.count 106 | 107 | slider.frame = CGRect(x: targetX, y: margin, width: targetWidth, height: targetHeight) 108 | } 109 | 110 | for (i, label): (Int, UILabel) in labels.enumerate() 111 | { 112 | let targetX = i * Int(frame.width) / labels.count 113 | 114 | label.frame = CGRect(x: targetX + (targetWidth / 2) + (margin / 5), y: (targetHeight - (margin / 5)), width: targetWidth - (margin * 2), height: margin) 115 | } 116 | 117 | drawCurve() 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /ToneCurveEditor/ToneCurveEditorCurveLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToneCurveEditorCurveLayer.swift 3 | // ToneCurveEditor 4 | // 5 | // Created by Simon Gladman on 13/09/2014. 6 | // Copyright (c) 2014 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreGraphics 11 | 12 | class ToneCurveEditorCurveLayer: CALayer 13 | { 14 | weak var toneCurveEditor : ToneCurveEditor? 15 | 16 | override func drawInContext(ctx: CGContext) 17 | { 18 | if let curveValues = toneCurveEditor?.curveValues 19 | { 20 | let path = UIBezierPath() 21 | 22 | let margin = 20 23 | let thumbRadius = 15 24 | let widgetWidth = Int(frame.width) 25 | let widgetHeight = Int(frame.height) - margin - margin - thumbRadius - thumbRadius 26 | 27 | var interpolationPoints : [CGPoint] = [CGPoint]() 28 | 29 | for (i, value): (Int, Double) in curveValues.enumerate() 30 | { 31 | let pathPointX = i * (widgetWidth / curveValues.count) + (widgetWidth / curveValues.count / 2) 32 | let pathPointY = thumbRadius + margin + widgetHeight - Int(Double(widgetHeight) * value) 33 | 34 | interpolationPoints.append(CGPoint(x: pathPointX,y: pathPointY)) 35 | } 36 | 37 | path.interpolatePointsWithHermite(interpolationPoints) 38 | 39 | CGContextSetLineJoin(ctx, CGLineJoin.Round) 40 | CGContextAddPath(ctx, path.CGPath) 41 | CGContextSetStrokeColorWithColor(ctx, UIColor.blueColor().CGColor) 42 | CGContextSetLineWidth(ctx, 6) 43 | CGContextStrokePath(ctx) 44 | } 45 | } 46 | 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /ToneCurveEditor/UIBezierPathExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // ToneCurveEditor 4 | // 5 | // Created by Simon Gladman on 15/09/2014. 6 | // Copyright (c) 2014 Simon Gladman. All rights reserved. 7 | // 8 | // Based on Objective C code from: https://github.com/jnfisher/ios-curve-interpolation/blob/master/Curve%20Interpolation/UIBezierPath%2BInterpolation.m 9 | // From this article: http://spin.atomicobject.com/2014/05/28/ios-interpolating-points/ 10 | 11 | import Foundation 12 | import UIKit 13 | 14 | 15 | extension UIBezierPath 16 | { 17 | func interpolatePointsWithHermite(interpolationPoints : [CGPoint], alpha: CGFloat = 1.0/3.0) 18 | { 19 | guard !interpolationPoints.isEmpty else { return } 20 | self.moveToPoint(interpolationPoints[0]) 21 | 22 | let n = interpolationPoints.count - 1 23 | 24 | for index in 0.. 0 36 | { 37 | mx = (nextPoint.x - previousPoint.x) / 2.0 38 | my = (nextPoint.y - previousPoint.y) / 2.0 39 | } 40 | else 41 | { 42 | mx = (nextPoint.x - currentPoint.x) / 2.0 43 | my = (nextPoint.y - currentPoint.y) / 2.0 44 | } 45 | 46 | let controlPoint1 = CGPoint(x: currentPoint.x + mx * alpha, y: currentPoint.y + my * alpha) 47 | currentPoint = interpolationPoints[nextIndex] 48 | nextIndex = (nextIndex + 1) % interpolationPoints.count 49 | prevIndex = index 50 | previousPoint = interpolationPoints[prevIndex] 51 | nextPoint = interpolationPoints[nextIndex] 52 | 53 | if index < n - 1 54 | { 55 | mx = (nextPoint.x - previousPoint.x) / 2.0 56 | my = (nextPoint.y - previousPoint.y) / 2.0 57 | } 58 | else 59 | { 60 | mx = (currentPoint.x - previousPoint.x) / 2.0 61 | my = (currentPoint.y - previousPoint.y) / 2.0 62 | } 63 | 64 | let controlPoint2 = CGPoint(x: currentPoint.x - mx * alpha, y: currentPoint.y - my * alpha) 65 | 66 | self.addCurveToPoint(endPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2) 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /ToneCurveEditor/UIImageExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageExtension.swift 3 | // ToneCurveEditor 4 | // 5 | // Created by Simon Gladman on 14/09/2014. 6 | // Copyright (c) 2014 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Foundation 11 | 12 | extension UIImage 13 | { 14 | func resizeToBoundingSquare(boundingSquareSideLength boundingSquareSideLength : CGFloat) -> UIImage 15 | { 16 | let imgScale = self.size.width > self.size.height ? boundingSquareSideLength / self.size.width : boundingSquareSideLength / self.size.height 17 | let newWidth = self.size.width * imgScale 18 | let newHeight = self.size.height * imgScale 19 | let newSize = CGSize(width: newWidth, height: newHeight) 20 | 21 | UIGraphicsBeginImageContext(newSize) 22 | 23 | self.drawInRect(CGRect(x: 0, y: 0, width: newWidth, height: newHeight)) 24 | 25 | let resizedImage = UIGraphicsGetImageFromCurrentImageContext() 26 | 27 | UIGraphicsEndImageContext(); 28 | 29 | return resizedImage 30 | } 31 | } -------------------------------------------------------------------------------- /ToneCurveEditor/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ToneCurveEditor 4 | // 5 | // Created by Simon Gladman on 12/09/2014. 6 | // Copyright (c) 2014 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController 12 | { 13 | let imageWidget = ImageWidget(frame: CGRectZero) 14 | let toneCurveEditor = ToneCurveEditor(frame: CGRectZero) 15 | let resetButton = UIButton(frame: CGRectZero) 16 | 17 | override func viewDidLoad() 18 | { 19 | super.viewDidLoad() 20 | 21 | imageWidget.viewController = self 22 | 23 | resetButton.setTitle("Reset Curve", forState: .Normal) 24 | resetButton.setTitleColor(UIColor.blackColor(), forState: .Normal) 25 | resetButton.addTarget(self, action: #selector(ViewController.resetButtonClickedSelector(_:)), forControlEvents: .TouchUpInside) 26 | 27 | view.addSubview(imageWidget) 28 | view.addSubview(toneCurveEditor) 29 | view.addSubview(resetButton) 30 | 31 | toneCurveEditor.curveValues = curveValues 32 | toneCurveEditor.addTarget(self, action: #selector(ViewController.toneCurveEditorChangedSelector(_:)), forControlEvents: .ValueChanged) 33 | } 34 | 35 | func resetCurveValues() 36 | { 37 | curveValues = [0.0, 0.25, 0.5, 0.75, 1.0] 38 | } 39 | 40 | func resetButtonClickedSelector(value: UIButton) 41 | { 42 | self.resetCurveValues() 43 | } 44 | 45 | func toneCurveEditorChangedSelector(value : ToneCurveEditor) 46 | { 47 | curveValues = value.curveValues 48 | } 49 | 50 | var curveValues: [Double] = [0.0, 0.25, 0.5, 0.75, 1.0] 51 | { 52 | didSet 53 | { 54 | imageWidget.curveValues = curveValues 55 | toneCurveEditor.curveValues = curveValues 56 | } 57 | } 58 | 59 | override func viewDidLayoutSubviews() 60 | { 61 | let topMargin = Int(topLayoutGuide.length) 62 | 63 | if view.frame.size.width < view.frame.size.height 64 | { 65 | // portrait mode 66 | let widgetWidth = Int(view.frame.size.width) 67 | let widgetHeight = Int(view.frame.size.height) / 2 68 | 69 | imageWidget.frame = CGRect(x: 5, y: topMargin, width: widgetWidth - 10, height: widgetHeight - topMargin - topMargin) 70 | toneCurveEditor.frame = CGRect(x: 0, y: widgetHeight, width: widgetWidth, height: widgetHeight - 50) 71 | 72 | resetButton.frame = CGRect(x: 20, y: view.frame.height - 70, width: 100, height: 50) 73 | } 74 | else 75 | { 76 | // landscape mode 77 | let widgetWidth = Int(view.frame.size.width) / 2 78 | let widgetHeight = Int(view.frame.size.height) 79 | 80 | imageWidget.frame = CGRect(x: widgetWidth, y: topMargin, width: widgetWidth - 5, height: widgetHeight - topMargin - topMargin) 81 | toneCurveEditor.frame = CGRect(x: 0, y: 0, width: widgetWidth, height: widgetHeight - 50) 82 | 83 | resetButton.frame = CGRect(x: 20, y: view.frame.height - 70, width: 100, height: 50) 84 | } 85 | } 86 | 87 | 88 | 89 | 90 | 91 | } 92 | 93 | -------------------------------------------------------------------------------- /ToneCurveEditorTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ToneCurveEditorTests/ToneCurveEditorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToneCurveEditorTests.swift 3 | // ToneCurveEditorTests 4 | // 5 | // Created by Simon Gladman on 12/09/2014. 6 | // Copyright (c) 2014 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class ToneCurveEditorTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | --------------------------------------------------------------------------------