├── CoreImageToy.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── jacob.xcuserdatad │ │ ├── IDEFindNavigatorScopes.plist │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── CoreImageToy.xcscheme └── xcuserdata │ └── jacob.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── CoreImageToy ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── CIToy.png │ │ └── Contents.json │ └── Contents.json ├── CoreImageToyApp.swift ├── Filters │ ├── CustomFilters.metal │ └── CustomFilters.swift ├── Model │ ├── Constants.swift │ ├── ImageFilterCategory.swift │ └── ImageFilterSelection.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── UI │ ├── FilterSelectionControls.swift │ ├── ImageViewModel.swift │ └── PhotosList.swift └── LICENSE /CoreImageToy.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 60; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 16488E162BAE2D66003E1D7F /* CoreImageToyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16488E152BAE2D66003E1D7F /* CoreImageToyApp.swift */; }; 11 | 16488E1A2BAE2D67003E1D7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 16488E192BAE2D67003E1D7F /* Assets.xcassets */; }; 12 | 16488E1D2BAE2D67003E1D7F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 16488E1C2BAE2D67003E1D7F /* Preview Assets.xcassets */; }; 13 | 16488E292BAE2DA8003E1D7F /* ImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16488E262BAE2DA8003E1D7F /* ImageViewModel.swift */; }; 14 | 16488E2A2BAE2DA8003E1D7F /* PhotosList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16488E272BAE2DA8003E1D7F /* PhotosList.swift */; }; 15 | 16488E2B2BAE2DA8003E1D7F /* FilterSelectionControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16488E282BAE2DA8003E1D7F /* FilterSelectionControls.swift */; }; 16 | 16488E2E2BAE2DAF003E1D7F /* CustomFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16488E2C2BAE2DAF003E1D7F /* CustomFilters.swift */; }; 17 | 16488E2F2BAE2DAF003E1D7F /* CustomFilters.metal in Sources */ = {isa = PBXBuildFile; fileRef = 16488E2D2BAE2DAF003E1D7F /* CustomFilters.metal */; }; 18 | 16488E312BAE2DB8003E1D7F /* ImageFilterCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16488E302BAE2DB8003E1D7F /* ImageFilterCategory.swift */; }; 19 | 16488E332BAE2DC7003E1D7F /* ImageFilterSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16488E322BAE2DC7003E1D7F /* ImageFilterSelection.swift */; }; 20 | 16488E352BAE2F98003E1D7F /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16488E342BAE2F98003E1D7F /* Constants.swift */; }; 21 | 16CF72362BB1928100E385AF /* CoreImageUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 16CF72352BB1928100E385AF /* CoreImageUtils */; }; 22 | 16CF72392BB1933200E385AF /* CoreImageUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 16CF72382BB1933200E385AF /* CoreImageUtils */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 16488E122BAE2D66003E1D7F /* CoreImageToy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CoreImageToy.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 16488E152BAE2D66003E1D7F /* CoreImageToyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreImageToyApp.swift; sourceTree = ""; }; 28 | 16488E192BAE2D67003E1D7F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 29 | 16488E1C2BAE2D67003E1D7F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 30 | 16488E262BAE2DA8003E1D7F /* ImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageViewModel.swift; sourceTree = ""; }; 31 | 16488E272BAE2DA8003E1D7F /* PhotosList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosList.swift; sourceTree = ""; }; 32 | 16488E282BAE2DA8003E1D7F /* FilterSelectionControls.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterSelectionControls.swift; sourceTree = ""; }; 33 | 16488E2C2BAE2DAF003E1D7F /* CustomFilters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFilters.swift; sourceTree = ""; }; 34 | 16488E2D2BAE2DAF003E1D7F /* CustomFilters.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = CustomFilters.metal; sourceTree = ""; }; 35 | 16488E302BAE2DB8003E1D7F /* ImageFilterCategory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageFilterCategory.swift; sourceTree = ""; }; 36 | 16488E322BAE2DC7003E1D7F /* ImageFilterSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFilterSelection.swift; sourceTree = ""; }; 37 | 16488E342BAE2F98003E1D7F /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | 16488E0F2BAE2D66003E1D7F /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | 16CF72362BB1928100E385AF /* CoreImageUtils in Frameworks */, 46 | 16CF72392BB1933200E385AF /* CoreImageUtils in Frameworks */, 47 | ); 48 | runOnlyForDeploymentPostprocessing = 0; 49 | }; 50 | /* End PBXFrameworksBuildPhase section */ 51 | 52 | /* Begin PBXGroup section */ 53 | 16488E092BAE2D66003E1D7F = { 54 | isa = PBXGroup; 55 | children = ( 56 | 16488E142BAE2D66003E1D7F /* CoreImageToy */, 57 | 16488E132BAE2D66003E1D7F /* Products */, 58 | ); 59 | sourceTree = ""; 60 | }; 61 | 16488E132BAE2D66003E1D7F /* Products */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 16488E122BAE2D66003E1D7F /* CoreImageToy.app */, 65 | ); 66 | name = Products; 67 | sourceTree = ""; 68 | }; 69 | 16488E142BAE2D66003E1D7F /* CoreImageToy */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 16488E152BAE2D66003E1D7F /* CoreImageToyApp.swift */, 73 | 16488E252BAE2D93003E1D7F /* UI */, 74 | 16488E242BAE2D8F003E1D7F /* Model */, 75 | 16488E232BAE2D8A003E1D7F /* Filters */, 76 | 16488E192BAE2D67003E1D7F /* Assets.xcassets */, 77 | 16488E1B2BAE2D67003E1D7F /* Preview Content */, 78 | ); 79 | path = CoreImageToy; 80 | sourceTree = ""; 81 | }; 82 | 16488E1B2BAE2D67003E1D7F /* Preview Content */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 16488E1C2BAE2D67003E1D7F /* Preview Assets.xcassets */, 86 | ); 87 | path = "Preview Content"; 88 | sourceTree = ""; 89 | }; 90 | 16488E232BAE2D8A003E1D7F /* Filters */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 16488E2C2BAE2DAF003E1D7F /* CustomFilters.swift */, 94 | 16488E2D2BAE2DAF003E1D7F /* CustomFilters.metal */, 95 | ); 96 | path = Filters; 97 | sourceTree = ""; 98 | }; 99 | 16488E242BAE2D8F003E1D7F /* Model */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 16488E302BAE2DB8003E1D7F /* ImageFilterCategory.swift */, 103 | 16488E342BAE2F98003E1D7F /* Constants.swift */, 104 | 16488E322BAE2DC7003E1D7F /* ImageFilterSelection.swift */, 105 | ); 106 | path = Model; 107 | sourceTree = ""; 108 | }; 109 | 16488E252BAE2D93003E1D7F /* UI */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 16488E282BAE2DA8003E1D7F /* FilterSelectionControls.swift */, 113 | 16488E272BAE2DA8003E1D7F /* PhotosList.swift */, 114 | 16488E262BAE2DA8003E1D7F /* ImageViewModel.swift */, 115 | ); 116 | path = UI; 117 | sourceTree = ""; 118 | }; 119 | /* End PBXGroup section */ 120 | 121 | /* Begin PBXNativeTarget section */ 122 | 16488E112BAE2D66003E1D7F /* CoreImageToy */ = { 123 | isa = PBXNativeTarget; 124 | buildConfigurationList = 16488E202BAE2D67003E1D7F /* Build configuration list for PBXNativeTarget "CoreImageToy" */; 125 | buildPhases = ( 126 | 16488E0E2BAE2D66003E1D7F /* Sources */, 127 | 16488E0F2BAE2D66003E1D7F /* Frameworks */, 128 | 16488E102BAE2D66003E1D7F /* Resources */, 129 | ); 130 | buildRules = ( 131 | ); 132 | dependencies = ( 133 | ); 134 | name = CoreImageToy; 135 | packageProductDependencies = ( 136 | 16CF72352BB1928100E385AF /* CoreImageUtils */, 137 | 16CF72382BB1933200E385AF /* CoreImageUtils */, 138 | ); 139 | productName = CoreImageToy; 140 | productReference = 16488E122BAE2D66003E1D7F /* CoreImageToy.app */; 141 | productType = "com.apple.product-type.application"; 142 | }; 143 | /* End PBXNativeTarget section */ 144 | 145 | /* Begin PBXProject section */ 146 | 16488E0A2BAE2D66003E1D7F /* Project object */ = { 147 | isa = PBXProject; 148 | attributes = { 149 | BuildIndependentTargetsInParallel = 1; 150 | LastSwiftUpdateCheck = 1520; 151 | LastUpgradeCheck = 1520; 152 | TargetAttributes = { 153 | 16488E112BAE2D66003E1D7F = { 154 | CreatedOnToolsVersion = 15.2; 155 | }; 156 | }; 157 | }; 158 | buildConfigurationList = 16488E0D2BAE2D66003E1D7F /* Build configuration list for PBXProject "CoreImageToy" */; 159 | compatibilityVersion = "Xcode 14.0"; 160 | developmentRegion = en; 161 | hasScannedForEncodings = 0; 162 | knownRegions = ( 163 | en, 164 | Base, 165 | ); 166 | mainGroup = 16488E092BAE2D66003E1D7F; 167 | packageReferences = ( 168 | 16CF72372BB1933200E385AF /* XCLocalSwiftPackageReference "../CoreImageUtils" */, 169 | ); 170 | productRefGroup = 16488E132BAE2D66003E1D7F /* Products */; 171 | projectDirPath = ""; 172 | projectRoot = ""; 173 | targets = ( 174 | 16488E112BAE2D66003E1D7F /* CoreImageToy */, 175 | ); 176 | }; 177 | /* End PBXProject section */ 178 | 179 | /* Begin PBXResourcesBuildPhase section */ 180 | 16488E102BAE2D66003E1D7F /* Resources */ = { 181 | isa = PBXResourcesBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | 16488E1D2BAE2D67003E1D7F /* Preview Assets.xcassets in Resources */, 185 | 16488E1A2BAE2D67003E1D7F /* Assets.xcassets in Resources */, 186 | ); 187 | runOnlyForDeploymentPostprocessing = 0; 188 | }; 189 | /* End PBXResourcesBuildPhase section */ 190 | 191 | /* Begin PBXSourcesBuildPhase section */ 192 | 16488E0E2BAE2D66003E1D7F /* Sources */ = { 193 | isa = PBXSourcesBuildPhase; 194 | buildActionMask = 2147483647; 195 | files = ( 196 | 16488E2A2BAE2DA8003E1D7F /* PhotosList.swift in Sources */, 197 | 16488E292BAE2DA8003E1D7F /* ImageViewModel.swift in Sources */, 198 | 16488E312BAE2DB8003E1D7F /* ImageFilterCategory.swift in Sources */, 199 | 16488E2B2BAE2DA8003E1D7F /* FilterSelectionControls.swift in Sources */, 200 | 16488E2E2BAE2DAF003E1D7F /* CustomFilters.swift in Sources */, 201 | 16488E2F2BAE2DAF003E1D7F /* CustomFilters.metal in Sources */, 202 | 16488E332BAE2DC7003E1D7F /* ImageFilterSelection.swift in Sources */, 203 | 16488E162BAE2D66003E1D7F /* CoreImageToyApp.swift in Sources */, 204 | 16488E352BAE2F98003E1D7F /* Constants.swift in Sources */, 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | }; 208 | /* End PBXSourcesBuildPhase section */ 209 | 210 | /* Begin XCBuildConfiguration section */ 211 | 16488E1E2BAE2D67003E1D7F /* Debug */ = { 212 | isa = XCBuildConfiguration; 213 | buildSettings = { 214 | ALWAYS_SEARCH_USER_PATHS = NO; 215 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 216 | CLANG_ANALYZER_NONNULL = YES; 217 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 218 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 219 | CLANG_ENABLE_MODULES = YES; 220 | CLANG_ENABLE_OBJC_ARC = YES; 221 | CLANG_ENABLE_OBJC_WEAK = YES; 222 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 223 | CLANG_WARN_BOOL_CONVERSION = YES; 224 | CLANG_WARN_COMMA = YES; 225 | CLANG_WARN_CONSTANT_CONVERSION = YES; 226 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 227 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 228 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 229 | CLANG_WARN_EMPTY_BODY = YES; 230 | CLANG_WARN_ENUM_CONVERSION = YES; 231 | CLANG_WARN_INFINITE_RECURSION = YES; 232 | CLANG_WARN_INT_CONVERSION = YES; 233 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 234 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 235 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 236 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 237 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 238 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 239 | CLANG_WARN_STRICT_PROTOTYPES = YES; 240 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 241 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 242 | CLANG_WARN_UNREACHABLE_CODE = YES; 243 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 244 | COPY_PHASE_STRIP = NO; 245 | DEBUG_INFORMATION_FORMAT = dwarf; 246 | ENABLE_STRICT_OBJC_MSGSEND = YES; 247 | ENABLE_TESTABILITY = YES; 248 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 249 | GCC_C_LANGUAGE_STANDARD = gnu17; 250 | GCC_DYNAMIC_NO_PIC = NO; 251 | GCC_NO_COMMON_BLOCKS = YES; 252 | GCC_OPTIMIZATION_LEVEL = 0; 253 | GCC_PREPROCESSOR_DEFINITIONS = ( 254 | "DEBUG=1", 255 | "$(inherited)", 256 | ); 257 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 258 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 259 | GCC_WARN_UNDECLARED_SELECTOR = YES; 260 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 261 | GCC_WARN_UNUSED_FUNCTION = YES; 262 | GCC_WARN_UNUSED_VARIABLE = YES; 263 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 264 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 265 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 266 | MTL_FAST_MATH = YES; 267 | ONLY_ACTIVE_ARCH = YES; 268 | SDKROOT = iphoneos; 269 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 270 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 271 | }; 272 | name = Debug; 273 | }; 274 | 16488E1F2BAE2D67003E1D7F /* Release */ = { 275 | isa = XCBuildConfiguration; 276 | buildSettings = { 277 | ALWAYS_SEARCH_USER_PATHS = NO; 278 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 279 | CLANG_ANALYZER_NONNULL = YES; 280 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 281 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 282 | CLANG_ENABLE_MODULES = YES; 283 | CLANG_ENABLE_OBJC_ARC = YES; 284 | CLANG_ENABLE_OBJC_WEAK = YES; 285 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 286 | CLANG_WARN_BOOL_CONVERSION = YES; 287 | CLANG_WARN_COMMA = YES; 288 | CLANG_WARN_CONSTANT_CONVERSION = YES; 289 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 290 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 291 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 292 | CLANG_WARN_EMPTY_BODY = YES; 293 | CLANG_WARN_ENUM_CONVERSION = YES; 294 | CLANG_WARN_INFINITE_RECURSION = YES; 295 | CLANG_WARN_INT_CONVERSION = YES; 296 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 297 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 298 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 299 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 300 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 301 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 302 | CLANG_WARN_STRICT_PROTOTYPES = YES; 303 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 304 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 305 | CLANG_WARN_UNREACHABLE_CODE = YES; 306 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 307 | COPY_PHASE_STRIP = NO; 308 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 309 | ENABLE_NS_ASSERTIONS = NO; 310 | ENABLE_STRICT_OBJC_MSGSEND = YES; 311 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 312 | GCC_C_LANGUAGE_STANDARD = gnu17; 313 | GCC_NO_COMMON_BLOCKS = YES; 314 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 315 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 316 | GCC_WARN_UNDECLARED_SELECTOR = YES; 317 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 318 | GCC_WARN_UNUSED_FUNCTION = YES; 319 | GCC_WARN_UNUSED_VARIABLE = YES; 320 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 321 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 322 | MTL_ENABLE_DEBUG_INFO = NO; 323 | MTL_FAST_MATH = YES; 324 | SDKROOT = iphoneos; 325 | SWIFT_COMPILATION_MODE = wholemodule; 326 | VALIDATE_PRODUCT = YES; 327 | }; 328 | name = Release; 329 | }; 330 | 16488E212BAE2D67003E1D7F /* Debug */ = { 331 | isa = XCBuildConfiguration; 332 | buildSettings = { 333 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 334 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 335 | CODE_SIGN_STYLE = Automatic; 336 | CURRENT_PROJECT_VERSION = 3; 337 | DEVELOPMENT_ASSET_PATHS = "\"CoreImageToy/Preview Content\""; 338 | DEVELOPMENT_TEAM = 9H7GAWSLA8; 339 | ENABLE_PREVIEWS = YES; 340 | GENERATE_INFOPLIST_FILE = YES; 341 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 342 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 343 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 344 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 345 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 346 | LD_RUNPATH_SEARCH_PATHS = ( 347 | "$(inherited)", 348 | "@executable_path/Frameworks", 349 | ); 350 | MARKETING_VERSION = 1.2; 351 | MTLLINKER_FLAGS = "-framework CoreImage"; 352 | PRODUCT_BUNDLE_IDENTIFIER = com.jacob.CoreImageToy; 353 | PRODUCT_NAME = "$(TARGET_NAME)"; 354 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 355 | SUPPORTS_MACCATALYST = NO; 356 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 357 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 358 | SWIFT_EMIT_LOC_STRINGS = YES; 359 | SWIFT_VERSION = 5.0; 360 | TARGETED_DEVICE_FAMILY = 1; 361 | }; 362 | name = Debug; 363 | }; 364 | 16488E222BAE2D67003E1D7F /* Release */ = { 365 | isa = XCBuildConfiguration; 366 | buildSettings = { 367 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 368 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 369 | CODE_SIGN_STYLE = Automatic; 370 | CURRENT_PROJECT_VERSION = 3; 371 | DEVELOPMENT_ASSET_PATHS = "\"CoreImageToy/Preview Content\""; 372 | DEVELOPMENT_TEAM = 9H7GAWSLA8; 373 | ENABLE_PREVIEWS = YES; 374 | GENERATE_INFOPLIST_FILE = YES; 375 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 376 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 377 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 378 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 379 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 380 | LD_RUNPATH_SEARCH_PATHS = ( 381 | "$(inherited)", 382 | "@executable_path/Frameworks", 383 | ); 384 | MARKETING_VERSION = 1.2; 385 | MTLLINKER_FLAGS = "-framework CoreImage"; 386 | PRODUCT_BUNDLE_IDENTIFIER = com.jacob.CoreImageToy; 387 | PRODUCT_NAME = "$(TARGET_NAME)"; 388 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 389 | SUPPORTS_MACCATALYST = NO; 390 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 391 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 392 | SWIFT_EMIT_LOC_STRINGS = YES; 393 | SWIFT_VERSION = 5.0; 394 | TARGETED_DEVICE_FAMILY = 1; 395 | }; 396 | name = Release; 397 | }; 398 | /* End XCBuildConfiguration section */ 399 | 400 | /* Begin XCConfigurationList section */ 401 | 16488E0D2BAE2D66003E1D7F /* Build configuration list for PBXProject "CoreImageToy" */ = { 402 | isa = XCConfigurationList; 403 | buildConfigurations = ( 404 | 16488E1E2BAE2D67003E1D7F /* Debug */, 405 | 16488E1F2BAE2D67003E1D7F /* Release */, 406 | ); 407 | defaultConfigurationIsVisible = 0; 408 | defaultConfigurationName = Release; 409 | }; 410 | 16488E202BAE2D67003E1D7F /* Build configuration list for PBXNativeTarget "CoreImageToy" */ = { 411 | isa = XCConfigurationList; 412 | buildConfigurations = ( 413 | 16488E212BAE2D67003E1D7F /* Debug */, 414 | 16488E222BAE2D67003E1D7F /* Release */, 415 | ); 416 | defaultConfigurationIsVisible = 0; 417 | defaultConfigurationName = Release; 418 | }; 419 | /* End XCConfigurationList section */ 420 | 421 | /* Begin XCLocalSwiftPackageReference section */ 422 | 16CF72372BB1933200E385AF /* XCLocalSwiftPackageReference "../CoreImageUtils" */ = { 423 | isa = XCLocalSwiftPackageReference; 424 | relativePath = ../CoreImageUtils; 425 | }; 426 | /* End XCLocalSwiftPackageReference section */ 427 | 428 | /* Begin XCSwiftPackageProductDependency section */ 429 | 16CF72352BB1928100E385AF /* CoreImageUtils */ = { 430 | isa = XCSwiftPackageProductDependency; 431 | productName = CoreImageUtils; 432 | }; 433 | 16CF72382BB1933200E385AF /* CoreImageUtils */ = { 434 | isa = XCSwiftPackageProductDependency; 435 | productName = CoreImageUtils; 436 | }; 437 | /* End XCSwiftPackageProductDependency section */ 438 | }; 439 | rootObject = 16488E0A2BAE2D66003E1D7F /* Project object */; 440 | } 441 | -------------------------------------------------------------------------------- /CoreImageToy.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CoreImageToy.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CoreImageToy.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-syntax", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-syntax.git", 7 | "state" : { 8 | "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", 9 | "version" : "509.1.1" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /CoreImageToy.xcodeproj/project.xcworkspace/xcuserdata/jacob.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CoreImageToy.xcodeproj/project.xcworkspace/xcuserdata/jacob.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacobsapps/CoreImageToy/4ad1c0c906406141021324227c27e8833415f27a/CoreImageToy.xcodeproj/project.xcworkspace/xcuserdata/jacob.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /CoreImageToy.xcodeproj/xcshareddata/xcschemes/CoreImageToy.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /CoreImageToy.xcodeproj/xcuserdata/jacob.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /CoreImageToy.xcodeproj/xcuserdata/jacob.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | CoreImageToy.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 16488E112BAE2D66003E1D7F 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /CoreImageToy/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CoreImageToy/Assets.xcassets/AppIcon.appiconset/CIToy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacobsapps/CoreImageToy/4ad1c0c906406141021324227c27e8833415f27a/CoreImageToy/Assets.xcassets/AppIcon.appiconset/CIToy.png -------------------------------------------------------------------------------- /CoreImageToy/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "CIToy.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CoreImageToy/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CoreImageToy/CoreImageToyApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreImageToyApp.swift 3 | // CoreImageToy 4 | // 5 | // Created by Jacob Bartlett on 22/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct CoreImageToyApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | PhotosList() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CoreImageToy/Filters/CustomFilters.metal: -------------------------------------------------------------------------------- 1 | // 2 | // CustomFilters.metal 3 | // CoreImageToy 4 | // 5 | // Created by Jacob Bartlett on 22/03/2024. 6 | // 7 | 8 | #include 9 | #include 10 | using namespace metal; 11 | 12 | [[ stitchable ]] 13 | float4 grainyFilter( 14 | coreimage::sample_t s, 15 | coreimage::destination dest 16 | ) { 17 | float value = fract(sin(dot(dest.coord() / 1000, float2(12.9898, 78.233))) * 43758.5453); 18 | float3 noise = float3(value, value, value) * 0.1; 19 | float3 dampingFactor = float3(0, 0, 0) * -0.1; 20 | return float4(s.rgb + noise + dampingFactor, s.a); 21 | } 22 | 23 | [[ stitchable ]] 24 | float4 diagonalFilter( 25 | coreimage::sample_t s, 26 | coreimage::destination dest 27 | ) { 28 | float diagLine = dest.coord().x - dest.coord().y; 29 | float onDiagonal = fract(diagLine / 10.0); 30 | if (onDiagonal > 0.5) { 31 | float4 lightLines = float4(1, 1, 1, 1) * 0.1; 32 | float4 dampingFactor = float4(0, 0, 0, 1) * -0.1; 33 | return s + lightLines + dampingFactor; 34 | } 35 | return s; 36 | } 37 | 38 | [[ stitchable ]] 39 | float4 warmInversionFilter( 40 | coreimage::sample_t s 41 | ) { 42 | return float4(1 - pow(s.r, 0.35), 1 - pow(s.g, 0.35), 1 - pow(s.b, 0.35), 1.0); 43 | } 44 | 45 | [[ stitchable ]] 46 | float4 normalizeFilter( 47 | coreimage::sample_t s 48 | ) { 49 | return float4(pow(s.r, 2), pow(s.g, 2), pow(s.b, 2), 1.0); 50 | } 51 | 52 | [[ stitchable ]] 53 | float4 waveFilter( 54 | coreimage::sample_t s, 55 | coreimage::destination dest 56 | ) { 57 | float baseOscillation = (sin(dest.coord().x / 15) + 1) * 0.5; 58 | float minOscillation = 0.8; 59 | float maxOscillation = 1.0; 60 | float oscillationRange = maxOscillation - minOscillation; 61 | float adjustedOscillationFactor = minOscillation + (baseOscillation * oscillationRange); 62 | return s * adjustedOscillationFactor; 63 | } 64 | 65 | [[ stitchable ]] 66 | float4 gallifreyFilter( 67 | coreimage::sample_t s 68 | ) { 69 | return float4(s.g, s.b, s.r, s.a); 70 | } 71 | 72 | [[ stitchable ]] 73 | float4 alienFilter( 74 | coreimage::sample_t s 75 | ) { 76 | return float4(s.b, s.r, s.g, s.a); 77 | } 78 | 79 | [[ stitchable ]] 80 | float4 grayscaleFilter( 81 | coreimage::sample_t s 82 | ) { 83 | float3 grayscaleWeights = float3(0.2125, 0.7154, 0.0721); 84 | float avgLuminescence = dot(s.rgb, grayscaleWeights); 85 | return float4(avgLuminescence, avgLuminescence, avgLuminescence, s.a); 86 | } 87 | 88 | [[ stitchable ]] 89 | float4 spectralFilter( 90 | coreimage::sample_t s 91 | ) { 92 | float3 grayscaleWeights = float3(0.2125, 0.7154, 0.0721); 93 | float avgLuminescence = dot(s.rgb, grayscaleWeights); 94 | float invertedLuminescence = 1 - avgLuminescence; 95 | float scaledLumin = pow(invertedLuminescence, 3); 96 | return float4(scaledLumin, scaledLumin, scaledLumin, s.a); 97 | } 98 | 99 | [[ stitchable ]] 100 | float4 shiftFilter( 101 | coreimage::sampler src 102 | ) { 103 | // I actually read the docs! 104 | // https://developer.apple.com/metal/MetalCIKLReference6.pdf 105 | float4 shiftedSample = sample(src, src.coord() - float2(0.15, 0.15)); 106 | return shiftedSample; 107 | } 108 | 109 | [[ stitchable ]] 110 | float4 threeDGlassesFilter( 111 | coreimage::sampler src 112 | ) { 113 | float4 color = sample(src, src.coord()); 114 | float2 redCoord = src.coord() - float2(0.04, 0.04); 115 | color.r = sample(src, redCoord).r; 116 | float2 blueCoord = src.coord() + float2(0.02, 0.02); 117 | color.b = sample(src, blueCoord).b; 118 | return color * color.a; 119 | } 120 | 121 | [[ stitchable ]] 122 | float2 thickGlassSquares( 123 | float intensity, 124 | coreimage::destination dest 125 | ) { 126 | return float2(dest.coord().x + (intensity * (sin(dest.coord().x / 40))), 127 | dest.coord().y + (intensity * (sin(dest.coord().y / 40)))); 128 | } 129 | 130 | [[ stitchable ]] 131 | float2 lensFilter( 132 | float width, 133 | float height, 134 | float centerX, 135 | float centerY, 136 | float radius, 137 | float intensity, 138 | coreimage::destination dest 139 | ) { 140 | float2 size = float2(width, height); 141 | float2 normalizedCoord = dest.coord() / size; 142 | float2 center = float2(centerX, centerY); 143 | float distanceFromCenter = distance(normalizedCoord, center); 144 | 145 | if (distanceFromCenter < radius) { 146 | float2 vectorFromCenter = normalizedCoord - center; 147 | float normalizedDistance = pow(distanceFromCenter / radius, 4); 148 | float distortion = tan(M_PI_2_F * normalizedDistance) * intensity; 149 | float2 distortedPosition = center + (vectorFromCenter * (1 + distortion)); 150 | return distortedPosition * size; 151 | 152 | } else { 153 | return dest.coord(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /CoreImageToy/Filters/CustomFilters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomFilters.swift 3 | // CoreImageToy 4 | // 5 | // Created by Jacob Bartlett on 22/03/2024. 6 | // 7 | 8 | import CoreImage 9 | import CoreImageUtils 10 | 11 | @ColorKernel 12 | final class GrainyFilter: CIFilter { } 13 | 14 | @ColorKernel 15 | final class DiagonalFilter: CIFilter { } 16 | 17 | @ColorKernel 18 | final class WarmInversionFilter: CIFilter { } 19 | 20 | @ColorKernel 21 | final class NormalizeFilter: CIFilter { } 22 | 23 | @ColorKernel 24 | final class WaveFilter: CIFilter { } 25 | 26 | @ColorKernel 27 | final class GallifreyFilter: CIFilter { } 28 | 29 | @ColorKernel 30 | final class AlienFilter: CIFilter { } 31 | 32 | @ColorKernel 33 | final class GrayscaleFilter: CIFilter { } 34 | 35 | @ColorKernel 36 | final class SpectralFilter: CIFilter { } 37 | 38 | @SamplerKernel 39 | final class ShiftFilter: CIFilter { } 40 | 41 | @SamplerKernel 42 | final class ThreeDGlassesFilter: CIFilter { } 43 | 44 | // Proof of concept 45 | final class PixellateFacesFilter: CIFilter { 46 | 47 | private let context = CIContext() 48 | 49 | @objc dynamic public var inputImage: CIImage? 50 | 51 | override public var outputImage: CIImage? { 52 | guard let input = inputImage else { 53 | return nil 54 | } 55 | 56 | // Facial detection only works on-device, not simulator 57 | let options = [CIDetectorAccuracy: CIDetectorAccuracyLow] 58 | let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: context, options: options)! 59 | let faces = faceDetector.features(in: input) 60 | 61 | guard !faces.isEmpty else { return input } 62 | 63 | let pixellateFilter = CIFilter(name: "CIPixellate")! 64 | pixellateFilter.setValue(inputImage, forKey: kCIInputImageKey) 65 | pixellateFilter.setValue(20, forKey: kCIInputScaleKey) 66 | guard let pixellatedImage = pixellateFilter.outputImage else { return input } 67 | 68 | var maskImage = CIImage(color: CIColor.clear).cropped(to: input.extent) 69 | 70 | faces.forEach { 71 | let bounds = CGRect(x: $0.bounds.minX, 72 | y: $0.bounds.minY - ($0.bounds.height / 2), 73 | width: $0.bounds.width, 74 | height: $0.bounds.height * 1.5) 75 | let faceRect = CIImage(color: CIColor.white).cropped(to: bounds) 76 | maskImage = faceRect.composited(over: maskImage) 77 | } 78 | 79 | let blendFilter = CIFilter(name: "CIBlendWithAlphaMask")! 80 | blendFilter.setValue(pixellatedImage, forKey: kCIInputImageKey) 81 | blendFilter.setValue(inputImage, forKey: kCIInputBackgroundImageKey) 82 | blendFilter.setValue(maskImage, forKey: kCIInputMaskImageKey) 83 | 84 | return blendFilter.outputImage 85 | } 86 | } 87 | 88 | // Proof of concept 89 | final class OmnidimensionalFaceFilter: CIFilter { 90 | 91 | private let context = CIContext() 92 | 93 | @objc dynamic public var inputImage: CIImage? 94 | 95 | override public var outputImage: CIImage? { 96 | guard let input = inputImage else { 97 | return nil 98 | } 99 | 100 | // Facial detection only works on-device, not simulator 101 | let options = [CIDetectorAccuracy: CIDetectorAccuracyLow] 102 | let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: context, options: options)! 103 | let faces = faceDetector.features(in: input) 104 | 105 | guard !faces.isEmpty else { return input } 106 | 107 | let threeDFilter = ThreeDGlassesFilter() 108 | threeDFilter.setValue(inputImage, forKey: kCIInputImageKey) 109 | guard let threeDImage = threeDFilter.outputImage else { return input } 110 | 111 | var maskImage = CIImage(color: CIColor.clear).cropped(to: input.extent) 112 | 113 | faces.forEach { 114 | let bounds = CGRect(x: $0.bounds.minX, 115 | y: $0.bounds.minY - ($0.bounds.height / 4), 116 | width: $0.bounds.width, 117 | height: $0.bounds.height * 1.5) 118 | let faceRect = CIImage(color: CIColor.white).cropped(to: bounds) 119 | maskImage = faceRect.composited(over: maskImage) 120 | } 121 | 122 | let blendFilter = CIFilter(name: "CIBlendWithAlphaMask")! 123 | blendFilter.setValue(threeDImage, forKey: kCIInputImageKey) 124 | blendFilter.setValue(inputImage, forKey: kCIInputBackgroundImageKey) 125 | blendFilter.setValue(maskImage, forKey: kCIInputMaskImageKey) 126 | 127 | return blendFilter.outputImage 128 | } 129 | } 130 | 131 | final class ThickGlassSquaresFilter: CIFilter { 132 | 133 | @objc dynamic let intensity: Float 134 | @objc dynamic public var inputImage: CIImage? 135 | 136 | override public var outputImage: CIImage? { 137 | guard let input = inputImage else { 138 | return nil 139 | } 140 | return Self.kernel.apply(extent: input.extent, 141 | roiCallback: { 142 | $1 143 | }, 144 | image: input, 145 | arguments: [intensity]) 146 | } 147 | 148 | init(intensity: Float) { 149 | self.intensity = intensity 150 | super.init() 151 | } 152 | 153 | required init?(coder: NSCoder) { 154 | fatalError("init(coder:) has not been implemented") 155 | } 156 | 157 | static private var kernel: CIWarpKernel = { () -> CIWarpKernel in 158 | getDistortionKernel(function: "thickGlassSquares") 159 | }() 160 | 161 | 162 | static private func getDistortionKernel(function: String) -> CIWarpKernel { 163 | let url = Bundle.main.url(forResource: "default", withExtension: "metallib")! 164 | let data = try! Data(contentsOf: url) 165 | return try! CIWarpKernel(functionName: function, fromMetalLibraryData: data) 166 | } 167 | } 168 | 169 | final class LensFilter: CIFilter { 170 | 171 | @objc dynamic var inputImage: CIImage? 172 | @objc dynamic var centerX: Float = 0.3 173 | @objc dynamic var centerY: Float = 0.6 174 | @objc dynamic var radius: Float = 0.2 175 | @objc dynamic var intensity: Float = 0.6 176 | 177 | override var outputImage: CIImage? { 178 | guard let input = inputImage else { 179 | return nil 180 | } 181 | return Self.kernel.apply(extent: input.extent, 182 | roiCallback: { 183 | $1 184 | }, 185 | image: input, 186 | arguments: [input.extent.width, 187 | input.extent.height, 188 | centerX, 189 | centerY, 190 | radius, 191 | intensity]) 192 | } 193 | 194 | static private var kernel: CIWarpKernel = { () -> CIWarpKernel in 195 | getDistortionKernel(function: "lensFilter") 196 | }() 197 | 198 | static private func getDistortionKernel(function: String) -> CIWarpKernel { 199 | let url = Bundle.main.url(forResource: "default", withExtension: "metallib")! 200 | let data = try! Data(contentsOf: url) 201 | return try! CIWarpKernel(functionName: function, fromMetalLibraryData: data) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /CoreImageToy/Model/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // CoreImageToy 4 | // 5 | // Created by Jacob Bartlett on 22/03/2024. 6 | // 7 | 8 | import CoreImage 9 | import Foundation 10 | 11 | enum Constants { 12 | 13 | static let filterCategoryNames: [String] = [kCICategoryColorEffect, 14 | kCICategoryStylize, 15 | kCICategoryBlur, 16 | kCICategoryDistortionEffect, 17 | kCICategoryColorAdjustment, 18 | kCICategoryGeometryAdjustment] 19 | 20 | static let customFilters = ImageFilterCategory( 21 | name: "My custom filters", 22 | filterSelection: [ 23 | ImageFilterSelection(filter: GrainyFilter()), 24 | ImageFilterSelection(filter: DiagonalFilter()), 25 | ImageFilterSelection(filter: WarmInversionFilter()), 26 | ImageFilterSelection(filter: NormalizeFilter()), 27 | ImageFilterSelection(filter: WaveFilter()), 28 | ImageFilterSelection(filter: GallifreyFilter()), 29 | ImageFilterSelection(filter: AlienFilter()), 30 | ImageFilterSelection(filter: GrayscaleFilter()), 31 | ImageFilterSelection(filter: SpectralFilter()), 32 | ImageFilterSelection(filter: ShiftFilter()), 33 | ImageFilterSelection(filter: ThreeDGlassesFilter()), 34 | ImageFilterSelection(filter: PixellateFacesFilter()), 35 | ImageFilterSelection(filter: OmnidimensionalFaceFilter()), 36 | ImageFilterSelection(filter: ThickGlassSquaresFilter(intensity: 50)), 37 | ImageFilterSelection(filter: LensFilter()) 38 | ] 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /CoreImageToy/Model/ImageFilterCategory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageFilterCategory.swift 3 | // CoreImageToy 4 | // 5 | // Created by Jacob Bartlett on 22/03/2024. 6 | // 7 | 8 | import CoreImage 9 | import Foundation 10 | 11 | struct ImageFilterCategory: Identifiable { 12 | var id: String { name } 13 | let name: String 14 | var filterSelection: [ImageFilterSelection] 15 | 16 | func filters(matching searchText: String) -> [ImageFilterSelection] { 17 | if searchText.isEmpty { 18 | return filterSelection 19 | 20 | } else { 21 | return filterSelection.filter { 22 | $0.filter.name.lowercased().contains(searchText.lowercased()) 23 | } 24 | } 25 | } 26 | } 27 | 28 | extension [ImageFilterCategory] { 29 | var selectedFilters: [CIFilter] { 30 | self.flatMap { $0.filterSelection } 31 | .filter { $0.selected } 32 | .sorted(by: { $0.sortOrder < $1.sortOrder }) 33 | .map { $0.filter } 34 | } 35 | } 36 | 37 | extension [ImageFilterCategory] { 38 | var nextSortOrder: Int { 39 | (self.flatMap { $0.filterSelection } 40 | .map { $0.sortOrder } 41 | .max() ?? 0) + 1 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /CoreImageToy/Model/ImageFilterSelection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageFilterSelection.swift 3 | // CoreImageToy 4 | // 5 | // Created by Jacob Bartlett on 22/03/2024. 6 | // 7 | 8 | import CoreImage 9 | import Foundation 10 | 11 | struct ImageFilterSelection: Identifiable { 12 | 13 | var id: String { filter.name } 14 | let filter: CIFilter 15 | var selected: Bool 16 | var sortOrder: Int = 0 17 | 18 | init(filter: CIFilter) { 19 | self.filter = filter 20 | self.selected = false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /CoreImageToy/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CoreImageToy/UI/FilterSelectionControls.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterSelectionControls.swift 3 | // CoreImageToy 4 | // 5 | // Created by Jacob Bartlett on 22/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FilterSelectionControls: View { 11 | 12 | enum Detent: CaseIterable { 13 | 14 | case small 15 | case medium 16 | case large 17 | 18 | var detent: PresentationDetent { 19 | switch self { 20 | case .small: 21 | return .fraction(0.25) 22 | case .medium: 23 | return .fraction(0.6) 24 | case .large: 25 | return .fraction(0.9) 26 | } 27 | } 28 | } 29 | 30 | @Binding var viewModel: PhotosViewModel 31 | @State private var selectedDetent: PresentationDetent = Detent.small.detent 32 | @State private var searchText: String = "" 33 | private let availableDetents: Set = Set(Detent.allCases.map { $0.detent }) 34 | 35 | var body: some View { 36 | NavigationStack { 37 | List { 38 | ForEach(viewModel.filterCategories) { category in 39 | Section(category.name) { 40 | ForEach(category.filters(matching: searchText)) { selection in 41 | Button(action: { 42 | viewModel.select(category: category, selection: selection) 43 | 44 | }, label: { 45 | HStack(spacing: 12) { 46 | Text(selection.id) 47 | .font(.body) 48 | .lineLimit(1) 49 | .truncationMode(.tail) 50 | .frame(maxWidth: .infinity, alignment: .leading) 51 | 52 | if viewModel.isSelected(category: category, selection: selection) { 53 | if selection.sortOrder > 0 { 54 | Text("(\(selection.sortOrder))") 55 | .font(.caption) 56 | .fontWeight(.medium) 57 | } 58 | 59 | Image(systemName: "checkmark") 60 | .fixedSize(horizontal: true, vertical: true) 61 | } 62 | } 63 | }) 64 | } 65 | } 66 | } 67 | } 68 | .searchable(text: $searchText, 69 | placement: (selectedDetent == Detent.large.detent) ? .navigationBarDrawer(displayMode: .always) : .automatic, 70 | prompt: "Search Filters") 71 | .toolbar { 72 | ToolbarItem(placement: .navigationBarTrailing) { 73 | Button(action: { 74 | withAnimation { 75 | searchText = "" 76 | viewModel.removeAllFilters() 77 | } 78 | 79 | }, label: { 80 | Image(systemName: "trash") 81 | }) 82 | } 83 | } 84 | .navigationTitle("Core Image Filters") 85 | .navigationBarTitleDisplayMode(.inline) 86 | } 87 | .presentationDetents(availableDetents, selection: $selectedDetent) 88 | .presentationDragIndicator(.visible) 89 | .presentationCornerRadius(12) 90 | .presentationBackgroundInteraction(.enabled) 91 | .interactiveDismissDisabled() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /CoreImageToy/UI/ImageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotosViewModel.swift 3 | // CoreImageToy 4 | // 5 | // Created by Jacob Bartlett on 21/03/2024. 6 | // 7 | 8 | import CoreImage 9 | import PhotosUI 10 | import SwiftUI 11 | 12 | @Observable 13 | final class PhotosViewModel { 14 | 15 | private let context = CIContext() 16 | 17 | var photoSelection: [UIImage?] = [] 18 | var filteredPhotos: [UIImage?] = [] 19 | var filterCategories: [ImageFilterCategory] 20 | 21 | // #error("app review") 22 | // @AppStorage("password") var password: String = "" 23 | // @Environment(\.requestReview) var requestReview 24 | 25 | init() { 26 | filterCategories = Constants.filterCategoryNames.toFilterCategories() + CollectionOfOne(Constants.customFilters) 27 | } 28 | 29 | func updateFilters() { 30 | filteredPhotos = photoSelection 31 | .map { apply(filters: filterCategories.selectedFilters, to: $0) } 32 | } 33 | 34 | func isSelected(category: ImageFilterCategory, selection: ImageFilterSelection) -> Bool { 35 | guard let categoryIndex = filterCategories.firstIndex(where: { $0.id == category.id }), 36 | let selectionIndex = filterCategories[categoryIndex].filterSelection.firstIndex(where: { $0.id == selection.id }) else { return false } 37 | return filterCategories[categoryIndex].filterSelection[selectionIndex].selected 38 | } 39 | 40 | func select(category: ImageFilterCategory, selection: ImageFilterSelection) { 41 | guard let categoryIndex = filterCategories.firstIndex(where: { $0.id == category.id }), 42 | let selectionIndex = filterCategories[categoryIndex].filterSelection.firstIndex(where: { $0.id == selection.id }) else { return } 43 | let isSelected = filterCategories[categoryIndex].filterSelection[selectionIndex].selected 44 | filterCategories[categoryIndex].filterSelection[selectionIndex].sortOrder = isSelected ? 0 : filterCategories.nextSortOrder 45 | filterCategories[categoryIndex].filterSelection[selectionIndex].selected.toggle() 46 | updateFilters() 47 | } 48 | 49 | func removeAllFilters() { 50 | filterCategories = Constants.filterCategoryNames.toFilterCategories() + CollectionOfOne(Constants.customFilters) 51 | updateFilters() 52 | } 53 | 54 | func removeAllPhotos() { 55 | withAnimation { 56 | photoSelection = [] 57 | filteredPhotos = [] 58 | } 59 | removeAllFilters() 60 | } 61 | 62 | // #error("share") 63 | // func sharePhotos() { 64 | // let filteredPhotos = filteredPhotos.compactMap { $0 } 65 | // guard !filteredPhotos.isEmpty, 66 | // let rootVC = UIApplication.shared.rootVC() else { return } 67 | // let activityVC = UIActivityViewController(activityItems: [filteredPhotos], applicationActivities: nil) 68 | // rootVC.present(activityVC, animated: true) 69 | // } 70 | 71 | private func apply(filters: [CIFilter], to image: UIImage?) -> UIImage? { 72 | guard let image else { return nil } 73 | guard let ciImage = CIImage(image: image) else { return image } 74 | let filteredImage = filters.reduce(ciImage) { image, filter in 75 | filter.setValue(image, forKey: kCIInputImageKey) 76 | guard let output = filter.outputImage else { return image } 77 | return output 78 | } 79 | guard let cgImage = context.createCGImage(filteredImage, from: filteredImage.extent) else { return image } 80 | return UIImage(cgImage: cgImage, scale: image.scale, orientation: image.imageOrientation) 81 | } 82 | } 83 | 84 | private extension [String] { 85 | 86 | func toFilterCategories() -> [ImageFilterCategory] { 87 | map { categoryName in 88 | ImageFilterCategory(name: categoryName, 89 | filterSelection: CIFilter 90 | .filterNames(inCategories: [categoryName]) 91 | .compactMap { CIFilter(name: $0) } 92 | .filter { $0.inputKeys.contains(kCIInputImageKey) } 93 | .compactMap { ImageFilterSelection(filter: $0) } 94 | ) 95 | } 96 | } 97 | } 98 | 99 | extension UIApplication { 100 | 101 | func rootVC() -> UIViewController? { 102 | connectedScenes 103 | .filter { 104 | $0.activationState == .foregroundActive 105 | } 106 | .compactMap { $0 as? UIWindowScene } 107 | .first? 108 | .windows 109 | .first { $0.isKeyWindow }? 110 | .rootViewController 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /CoreImageToy/UI/PhotosList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotosList.swift 3 | // CoreImageToy 4 | // 5 | // Created by Jacob Bartlett on 21/03/2024. 6 | // 7 | 8 | import SwiftUI 9 | import PhotosUI 10 | 11 | struct PhotosList: View { 12 | 13 | @State private var viewModel = PhotosViewModel() 14 | @State private var pickerItemSelection = [PhotosPickerItem]() 15 | 16 | var body: some View { 17 | NavigationStack { 18 | photosViewBody 19 | .onChange(of: pickerItemSelection) { 20 | viewModel.removeAllPhotos() 21 | Task { 22 | let images = await withTaskGroup(of: Data?.self, returning: [UIImage].self) { group in 23 | for item in pickerItemSelection { 24 | group.addTask { 25 | try? await item.loadTransferable(type: Data.self) 26 | } 27 | } 28 | return await group 29 | .compactMap { $0 } 30 | .reduce(into: [UIImage]()) { partialResult, data in 31 | partialResult.append(UIImage(data: data)) 32 | }.compactMap { $0 } 33 | } 34 | viewModel.photoSelection = images 35 | viewModel.filteredPhotos = images 36 | } 37 | } 38 | .toolbar { 39 | ToolbarItemGroup(placement: .navigationBarTrailing) { 40 | 41 | Button(action: { 42 | viewModel.removeAllPhotos() 43 | 44 | }, label: { 45 | Image(systemName: "trash") 46 | }) 47 | 48 | // Button(action: { 49 | // Task { 50 | // await viewModel.sharePhotos() 51 | // } 52 | // 53 | // }, label: { 54 | // Image(systemName: "square.and.arrow.up") 55 | // }) 56 | } 57 | } 58 | .navigationTitle("Core Image Toy") 59 | } 60 | } 61 | 62 | @ViewBuilder var photosViewBody: some View { 63 | if viewModel.filteredPhotos.isEmpty { 64 | PhotosPicker("Select Photos", selection: $pickerItemSelection, matching: .images) 65 | 66 | } else { 67 | List(viewModel.filteredPhotos.compactMap { $0 }, id: \.self) { 68 | Photo(image: $0) 69 | .listRowBackground(Color.clear) 70 | .listRowSeparator(.hidden) 71 | } 72 | .listStyle(PlainListStyle()) 73 | .padding(.bottom, 200) 74 | .sheet(isPresented: .constant(!viewModel.filteredPhotos.isEmpty)) { 75 | FilterSelectionControls(viewModel: $viewModel) 76 | } 77 | } 78 | } 79 | } 80 | 81 | struct Photo: View { 82 | 83 | let image: UIImage 84 | 85 | var body: some View { 86 | Image(uiImage: image) 87 | .resizable() 88 | .aspectRatio(contentMode: .fill) 89 | } 90 | } 91 | 92 | #Preview { 93 | PhotosList() 94 | } 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jacob's Apps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | --------------------------------------------------------------------------------