├── .gitignore ├── LICENSE.md ├── MagnificationLoupe.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcuserdata │ └── janum.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── MagnificationLoupe ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── maps.imageset │ │ ├── Contents.json │ │ └── maps.png ├── ContentView.swift ├── MagnificationLoupe.metal ├── MagnificationLoupeApp.swift └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, 4 | Objective-C.gitignore & Swift.gitignore 5 | 6 | ## User settings 7 | xcuserdata/ 8 | 9 | ## compatibility with Xcode 8 and earlier (ignoring not required starting 10 | Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting 15 | Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift 43 | Package Manager dependencies. 44 | # Packages/ 45 | # Package.pins 46 | # Package.resolved 47 | # *.xcodeproj 48 | # 49 | # Xcode automatically generates this directory with a .xcworkspacedata 50 | file and xcuserdata 51 | # hence it is not needed unless you have added a package configuration 52 | file to your project 53 | # .swiftpm 54 | 55 | .build/ 56 | 57 | # CocoaPods 58 | # 59 | # We recommend against adding the Pods directory to your .gitignore. 60 | However 61 | # you should judge for yourself, the pros and cons are mentioned at: 62 | # 63 | https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 64 | # 65 | # Pods/ 66 | # 67 | # Add this line if you want to avoid checking in source code from the 68 | Xcode workspace 69 | # *.xcworkspace 70 | 71 | # Carthage 72 | # 73 | # Add this line if you want to avoid checking in source code from Carthage 74 | dependencies. 75 | # Carthage/Checkouts 76 | 77 | Carthage/Build/ 78 | 79 | # Accio dependency management 80 | Dependencies/ 81 | .accio/ 82 | 83 | # fastlane 84 | # 85 | # It is recommended to not store the screenshots in the git repo. 86 | # Instead, use fastlane to re-generate the screenshots whenever they are 87 | needed. 88 | # For more information about the recommended setup visit: 89 | # 90 | https://docs.fastlane.tools/best-practices/source-control/#source-control 91 | 92 | fastlane/report.xml 93 | fastlane/Preview.html 94 | fastlane/screenshots/**/*.png 95 | fastlane/test_output 96 | 97 | # Code Injection 98 | # 99 | # After new code Injection tools there's a generated folder 100 | /iOSInjectionProject 101 | # https://github.com/johnno1962/injectionforxcode 102 | 103 | iOSInjectionProject/ 104 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Janum Trivedi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MagnificationLoupe.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F73E88162A3B8B0F00617F46 /* MagnificationLoupeApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73E88152A3B8B0F00617F46 /* MagnificationLoupeApp.swift */; }; 11 | F73E88182A3B8B0F00617F46 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73E88172A3B8B0F00617F46 /* ContentView.swift */; }; 12 | F73E881A2A3B8B0F00617F46 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F73E88192A3B8B0F00617F46 /* Assets.xcassets */; }; 13 | F73E881D2A3B8B0F00617F46 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F73E881C2A3B8B0F00617F46 /* Preview Assets.xcassets */; }; 14 | F73E88252A3B8B2B00617F46 /* Wave in Frameworks */ = {isa = PBXBuildFile; productRef = F73E88242A3B8B2B00617F46 /* Wave */; }; 15 | F73E88272A3B8B4600617F46 /* MagnificationLoupe.metal in Sources */ = {isa = PBXBuildFile; fileRef = F73E88262A3B8B4600617F46 /* MagnificationLoupe.metal */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | F73E88122A3B8B0F00617F46 /* MagnificationLoupe.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MagnificationLoupe.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | F73E88152A3B8B0F00617F46 /* MagnificationLoupeApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagnificationLoupeApp.swift; sourceTree = ""; }; 21 | F73E88172A3B8B0F00617F46 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 22 | F73E88192A3B8B0F00617F46 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23 | F73E881C2A3B8B0F00617F46 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 24 | F73E88262A3B8B4600617F46 /* MagnificationLoupe.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = MagnificationLoupe.metal; sourceTree = ""; }; 25 | /* End PBXFileReference section */ 26 | 27 | /* Begin PBXFrameworksBuildPhase section */ 28 | F73E880F2A3B8B0F00617F46 /* Frameworks */ = { 29 | isa = PBXFrameworksBuildPhase; 30 | buildActionMask = 2147483647; 31 | files = ( 32 | F73E88252A3B8B2B00617F46 /* Wave in Frameworks */, 33 | ); 34 | runOnlyForDeploymentPostprocessing = 0; 35 | }; 36 | /* End PBXFrameworksBuildPhase section */ 37 | 38 | /* Begin PBXGroup section */ 39 | F73E88092A3B8B0F00617F46 = { 40 | isa = PBXGroup; 41 | children = ( 42 | F73E88142A3B8B0F00617F46 /* MagnificationLoupe */, 43 | F73E88132A3B8B0F00617F46 /* Products */, 44 | ); 45 | sourceTree = ""; 46 | }; 47 | F73E88132A3B8B0F00617F46 /* Products */ = { 48 | isa = PBXGroup; 49 | children = ( 50 | F73E88122A3B8B0F00617F46 /* MagnificationLoupe.app */, 51 | ); 52 | name = Products; 53 | sourceTree = ""; 54 | }; 55 | F73E88142A3B8B0F00617F46 /* MagnificationLoupe */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | F73E88152A3B8B0F00617F46 /* MagnificationLoupeApp.swift */, 59 | F73E88172A3B8B0F00617F46 /* ContentView.swift */, 60 | F73E88262A3B8B4600617F46 /* MagnificationLoupe.metal */, 61 | F73E88192A3B8B0F00617F46 /* Assets.xcassets */, 62 | F73E881B2A3B8B0F00617F46 /* Preview Content */, 63 | ); 64 | path = MagnificationLoupe; 65 | sourceTree = ""; 66 | }; 67 | F73E881B2A3B8B0F00617F46 /* Preview Content */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | F73E881C2A3B8B0F00617F46 /* Preview Assets.xcassets */, 71 | ); 72 | path = "Preview Content"; 73 | sourceTree = ""; 74 | }; 75 | /* End PBXGroup section */ 76 | 77 | /* Begin PBXNativeTarget section */ 78 | F73E88112A3B8B0F00617F46 /* MagnificationLoupe */ = { 79 | isa = PBXNativeTarget; 80 | buildConfigurationList = F73E88202A3B8B0F00617F46 /* Build configuration list for PBXNativeTarget "MagnificationLoupe" */; 81 | buildPhases = ( 82 | F73E880E2A3B8B0F00617F46 /* Sources */, 83 | F73E880F2A3B8B0F00617F46 /* Frameworks */, 84 | F73E88102A3B8B0F00617F46 /* Resources */, 85 | ); 86 | buildRules = ( 87 | ); 88 | dependencies = ( 89 | ); 90 | name = MagnificationLoupe; 91 | packageProductDependencies = ( 92 | F73E88242A3B8B2B00617F46 /* Wave */, 93 | ); 94 | productName = MagnificationLoupe; 95 | productReference = F73E88122A3B8B0F00617F46 /* MagnificationLoupe.app */; 96 | productType = "com.apple.product-type.application"; 97 | }; 98 | /* End PBXNativeTarget section */ 99 | 100 | /* Begin PBXProject section */ 101 | F73E880A2A3B8B0F00617F46 /* Project object */ = { 102 | isa = PBXProject; 103 | attributes = { 104 | BuildIndependentTargetsInParallel = 1; 105 | LastSwiftUpdateCheck = 1500; 106 | LastUpgradeCheck = 1500; 107 | TargetAttributes = { 108 | F73E88112A3B8B0F00617F46 = { 109 | CreatedOnToolsVersion = 15.0; 110 | }; 111 | }; 112 | }; 113 | buildConfigurationList = F73E880D2A3B8B0F00617F46 /* Build configuration list for PBXProject "MagnificationLoupe" */; 114 | compatibilityVersion = "Xcode 14.0"; 115 | developmentRegion = en; 116 | hasScannedForEncodings = 0; 117 | knownRegions = ( 118 | en, 119 | Base, 120 | ); 121 | mainGroup = F73E88092A3B8B0F00617F46; 122 | packageReferences = ( 123 | F73E88232A3B8B2B00617F46 /* XCRemoteSwiftPackageReference "Wave" */, 124 | ); 125 | productRefGroup = F73E88132A3B8B0F00617F46 /* Products */; 126 | projectDirPath = ""; 127 | projectRoot = ""; 128 | targets = ( 129 | F73E88112A3B8B0F00617F46 /* MagnificationLoupe */, 130 | ); 131 | }; 132 | /* End PBXProject section */ 133 | 134 | /* Begin PBXResourcesBuildPhase section */ 135 | F73E88102A3B8B0F00617F46 /* Resources */ = { 136 | isa = PBXResourcesBuildPhase; 137 | buildActionMask = 2147483647; 138 | files = ( 139 | F73E881D2A3B8B0F00617F46 /* Preview Assets.xcassets in Resources */, 140 | F73E881A2A3B8B0F00617F46 /* Assets.xcassets in Resources */, 141 | ); 142 | runOnlyForDeploymentPostprocessing = 0; 143 | }; 144 | /* End PBXResourcesBuildPhase section */ 145 | 146 | /* Begin PBXSourcesBuildPhase section */ 147 | F73E880E2A3B8B0F00617F46 /* Sources */ = { 148 | isa = PBXSourcesBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | F73E88272A3B8B4600617F46 /* MagnificationLoupe.metal in Sources */, 152 | F73E88182A3B8B0F00617F46 /* ContentView.swift in Sources */, 153 | F73E88162A3B8B0F00617F46 /* MagnificationLoupeApp.swift in Sources */, 154 | ); 155 | runOnlyForDeploymentPostprocessing = 0; 156 | }; 157 | /* End PBXSourcesBuildPhase section */ 158 | 159 | /* Begin XCBuildConfiguration section */ 160 | F73E881E2A3B8B0F00617F46 /* Debug */ = { 161 | isa = XCBuildConfiguration; 162 | buildSettings = { 163 | ALWAYS_SEARCH_USER_PATHS = NO; 164 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 165 | CLANG_ANALYZER_NONNULL = YES; 166 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 167 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 168 | CLANG_ENABLE_MODULES = YES; 169 | CLANG_ENABLE_OBJC_ARC = YES; 170 | CLANG_ENABLE_OBJC_WEAK = YES; 171 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 172 | CLANG_WARN_BOOL_CONVERSION = YES; 173 | CLANG_WARN_COMMA = YES; 174 | CLANG_WARN_CONSTANT_CONVERSION = YES; 175 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 176 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 177 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 178 | CLANG_WARN_EMPTY_BODY = YES; 179 | CLANG_WARN_ENUM_CONVERSION = YES; 180 | CLANG_WARN_INFINITE_RECURSION = YES; 181 | CLANG_WARN_INT_CONVERSION = YES; 182 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 183 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 184 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 185 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 186 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 187 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 188 | CLANG_WARN_STRICT_PROTOTYPES = YES; 189 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 190 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 191 | CLANG_WARN_UNREACHABLE_CODE = YES; 192 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 193 | COPY_PHASE_STRIP = NO; 194 | DEBUG_INFORMATION_FORMAT = dwarf; 195 | ENABLE_STRICT_OBJC_MSGSEND = YES; 196 | ENABLE_TESTABILITY = YES; 197 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 198 | GCC_C_LANGUAGE_STANDARD = gnu17; 199 | GCC_DYNAMIC_NO_PIC = NO; 200 | GCC_NO_COMMON_BLOCKS = YES; 201 | GCC_OPTIMIZATION_LEVEL = 0; 202 | GCC_PREPROCESSOR_DEFINITIONS = ( 203 | "DEBUG=1", 204 | "$(inherited)", 205 | ); 206 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 207 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 208 | GCC_WARN_UNDECLARED_SELECTOR = YES; 209 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 210 | GCC_WARN_UNUSED_FUNCTION = YES; 211 | GCC_WARN_UNUSED_VARIABLE = YES; 212 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 213 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 214 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 215 | MTL_FAST_MATH = YES; 216 | ONLY_ACTIVE_ARCH = YES; 217 | SDKROOT = iphoneos; 218 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 219 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 220 | }; 221 | name = Debug; 222 | }; 223 | F73E881F2A3B8B0F00617F46 /* Release */ = { 224 | isa = XCBuildConfiguration; 225 | buildSettings = { 226 | ALWAYS_SEARCH_USER_PATHS = NO; 227 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 228 | CLANG_ANALYZER_NONNULL = YES; 229 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 230 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 231 | CLANG_ENABLE_MODULES = YES; 232 | CLANG_ENABLE_OBJC_ARC = YES; 233 | CLANG_ENABLE_OBJC_WEAK = YES; 234 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 235 | CLANG_WARN_BOOL_CONVERSION = YES; 236 | CLANG_WARN_COMMA = YES; 237 | CLANG_WARN_CONSTANT_CONVERSION = YES; 238 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 239 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 240 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 241 | CLANG_WARN_EMPTY_BODY = YES; 242 | CLANG_WARN_ENUM_CONVERSION = YES; 243 | CLANG_WARN_INFINITE_RECURSION = YES; 244 | CLANG_WARN_INT_CONVERSION = YES; 245 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 246 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 247 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 248 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 249 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 250 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 251 | CLANG_WARN_STRICT_PROTOTYPES = YES; 252 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 253 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 254 | CLANG_WARN_UNREACHABLE_CODE = YES; 255 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 256 | COPY_PHASE_STRIP = NO; 257 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 258 | ENABLE_NS_ASSERTIONS = NO; 259 | ENABLE_STRICT_OBJC_MSGSEND = YES; 260 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 261 | GCC_C_LANGUAGE_STANDARD = gnu17; 262 | GCC_NO_COMMON_BLOCKS = YES; 263 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 264 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 265 | GCC_WARN_UNDECLARED_SELECTOR = YES; 266 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 267 | GCC_WARN_UNUSED_FUNCTION = YES; 268 | GCC_WARN_UNUSED_VARIABLE = YES; 269 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 270 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 271 | MTL_ENABLE_DEBUG_INFO = NO; 272 | MTL_FAST_MATH = YES; 273 | SDKROOT = iphoneos; 274 | SWIFT_COMPILATION_MODE = wholemodule; 275 | VALIDATE_PRODUCT = YES; 276 | }; 277 | name = Release; 278 | }; 279 | F73E88212A3B8B0F00617F46 /* Debug */ = { 280 | isa = XCBuildConfiguration; 281 | buildSettings = { 282 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 283 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 284 | CODE_SIGN_STYLE = Automatic; 285 | CURRENT_PROJECT_VERSION = 1; 286 | DEVELOPMENT_ASSET_PATHS = "\"MagnificationLoupe/Preview Content\""; 287 | DEVELOPMENT_TEAM = GP8VNBM7WP; 288 | ENABLE_PREVIEWS = YES; 289 | GENERATE_INFOPLIST_FILE = YES; 290 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 291 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 292 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 293 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 294 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 295 | LD_RUNPATH_SEARCH_PATHS = ( 296 | "$(inherited)", 297 | "@executable_path/Frameworks", 298 | ); 299 | MARKETING_VERSION = 1.0; 300 | PRODUCT_BUNDLE_IDENTIFIER = com.janumtrivedi.MagnificationLoupe; 301 | PRODUCT_NAME = "$(TARGET_NAME)"; 302 | SWIFT_EMIT_LOC_STRINGS = YES; 303 | SWIFT_VERSION = 5.0; 304 | TARGETED_DEVICE_FAMILY = "1,2"; 305 | }; 306 | name = Debug; 307 | }; 308 | F73E88222A3B8B0F00617F46 /* Release */ = { 309 | isa = XCBuildConfiguration; 310 | buildSettings = { 311 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 312 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 313 | CODE_SIGN_STYLE = Automatic; 314 | CURRENT_PROJECT_VERSION = 1; 315 | DEVELOPMENT_ASSET_PATHS = "\"MagnificationLoupe/Preview Content\""; 316 | DEVELOPMENT_TEAM = GP8VNBM7WP; 317 | ENABLE_PREVIEWS = YES; 318 | GENERATE_INFOPLIST_FILE = YES; 319 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 320 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 321 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 322 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 323 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 324 | LD_RUNPATH_SEARCH_PATHS = ( 325 | "$(inherited)", 326 | "@executable_path/Frameworks", 327 | ); 328 | MARKETING_VERSION = 1.0; 329 | PRODUCT_BUNDLE_IDENTIFIER = com.janumtrivedi.MagnificationLoupe; 330 | PRODUCT_NAME = "$(TARGET_NAME)"; 331 | SWIFT_EMIT_LOC_STRINGS = YES; 332 | SWIFT_VERSION = 5.0; 333 | TARGETED_DEVICE_FAMILY = "1,2"; 334 | }; 335 | name = Release; 336 | }; 337 | /* End XCBuildConfiguration section */ 338 | 339 | /* Begin XCConfigurationList section */ 340 | F73E880D2A3B8B0F00617F46 /* Build configuration list for PBXProject "MagnificationLoupe" */ = { 341 | isa = XCConfigurationList; 342 | buildConfigurations = ( 343 | F73E881E2A3B8B0F00617F46 /* Debug */, 344 | F73E881F2A3B8B0F00617F46 /* Release */, 345 | ); 346 | defaultConfigurationIsVisible = 0; 347 | defaultConfigurationName = Release; 348 | }; 349 | F73E88202A3B8B0F00617F46 /* Build configuration list for PBXNativeTarget "MagnificationLoupe" */ = { 350 | isa = XCConfigurationList; 351 | buildConfigurations = ( 352 | F73E88212A3B8B0F00617F46 /* Debug */, 353 | F73E88222A3B8B0F00617F46 /* Release */, 354 | ); 355 | defaultConfigurationIsVisible = 0; 356 | defaultConfigurationName = Release; 357 | }; 358 | /* End XCConfigurationList section */ 359 | 360 | /* Begin XCRemoteSwiftPackageReference section */ 361 | F73E88232A3B8B2B00617F46 /* XCRemoteSwiftPackageReference "Wave" */ = { 362 | isa = XCRemoteSwiftPackageReference; 363 | repositoryURL = "https://github.com/jtrivedi/Wave"; 364 | requirement = { 365 | kind = upToNextMajorVersion; 366 | minimumVersion = 0.3.2; 367 | }; 368 | }; 369 | /* End XCRemoteSwiftPackageReference section */ 370 | 371 | /* Begin XCSwiftPackageProductDependency section */ 372 | F73E88242A3B8B2B00617F46 /* Wave */ = { 373 | isa = XCSwiftPackageProductDependency; 374 | package = F73E88232A3B8B2B00617F46 /* XCRemoteSwiftPackageReference "Wave" */; 375 | productName = Wave; 376 | }; 377 | /* End XCSwiftPackageProductDependency section */ 378 | }; 379 | rootObject = F73E880A2A3B8B0F00617F46 /* Project object */; 380 | } 381 | -------------------------------------------------------------------------------- /MagnificationLoupe.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MagnificationLoupe.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MagnificationLoupe.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "wave", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/jtrivedi/Wave", 7 | "state" : { 8 | "revision" : "d95ce41fa52c42d9790388e9086c892930e9e48b", 9 | "version" : "0.3.2" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /MagnificationLoupe.xcodeproj/xcuserdata/janum.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MagnificationLoupe.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MagnificationLoupe/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 | -------------------------------------------------------------------------------- /MagnificationLoupe/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MagnificationLoupe/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MagnificationLoupe/Assets.xcassets/maps.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "maps.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MagnificationLoupe/Assets.xcassets/maps.imageset/maps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtrivedi/MagnificationLoupe/45c1cb07881bd9551935d25d6f3a5a30bb2424f6/MagnificationLoupe/Assets.xcassets/maps.imageset/maps.png -------------------------------------------------------------------------------- /MagnificationLoupe/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // MagnificationLoupe 4 | // 5 | // Created by Janum Trivedi on 6/15/23. 6 | // 7 | 8 | import SwiftUI 9 | import Wave 10 | 11 | struct ContentView: View { 12 | 13 | static let loupeRestingPosition = CGPoint(x: 0.15, y: 0.85) 14 | 15 | static let loupeRestingSize = 0.1 16 | static let loupeDraggingSize = 0.2 17 | 18 | @State var loupeCenter = loupeRestingPosition 19 | @State var loupeSize = loupeRestingSize 20 | 21 | @State var isDragging: Bool = false 22 | @State var initialTouchLocation: CGPoint? = nil 23 | 24 | @State var loupePositionAnimator = SpringAnimator( 25 | spring: .init(dampingRatio: 0.92, response: 0.2), 26 | value: loupeRestingPosition 27 | ) 28 | 29 | @State var loupeSizeAnimator = SpringAnimator( 30 | spring: .init(dampingRatio: 0.72, response: 0.7), 31 | value: loupeRestingSize 32 | ) 33 | 34 | func loupeEffect(center: CGPoint, size: CGFloat) -> Shader { 35 | Shader(function: .init(library: .default, name: "loupe"), arguments: [ 36 | .boundingRect, 37 | .float2(center.x, center.y), 38 | .float(size), 39 | ]) 40 | } 41 | 42 | func rotation3D(for loupeCenter: CGPoint, isDragging: Bool) -> CGPoint { 43 | let maxRotationDegrees = isDragging ? 10.0 : 0 44 | let rotX = mapRange(loupeCenter.x, 0, 1, maxRotationDegrees, -maxRotationDegrees) 45 | let rotY = mapRange(loupeCenter.y, 0, 1, -maxRotationDegrees, maxRotationDegrees) 46 | return CGPoint(x: rotX, y: rotY) 47 | } 48 | 49 | var body: some View { 50 | TimelineView(.animation) { context in 51 | GeometryReader { proxy in 52 | let size = proxy.size 53 | let maxSampleOffset = CGSize(width: 40, height: 40) 54 | 55 | let rotation = rotation3D(for: loupeCenter, isDragging: isDragging) 56 | 57 | VStack { 58 | Spacer() 59 | Image("maps") 60 | .resizable() 61 | .aspectRatio(contentMode: .fit) 62 | .scaleEffect(x: isDragging ? 1.05 : 1, y: isDragging ? 1.05 : 1) 63 | .rotation3DEffect( 64 | .degrees(rotation.x), axis: (x: 0.0, y: 1.0, z: 0.0) 65 | ) 66 | .rotation3DEffect( 67 | .degrees(rotation.y), axis: (x: 1.0, y: 0.0, z: 0.0) 68 | ) 69 | .layerEffect(loupeEffect(center: loupeCenter, size: loupeSize), maxSampleOffset: maxSampleOffset) 70 | 71 | Spacer() 72 | } 73 | .gesture( 74 | DragGesture(minimumDistance: 0) 75 | .onChanged{ value in 76 | if initialTouchLocation == nil { 77 | initialTouchLocation = loupeCenter 78 | } 79 | 80 | guard let initialTouchLocation else { 81 | return 82 | } 83 | 84 | let translation = value.translation 85 | let normTranslation = CGPoint( 86 | x: translation.width / size.width, 87 | y: translation.height / size.width 88 | ) 89 | 90 | let newLoupeCenter = CGPoint( 91 | x: initialTouchLocation.x + normTranslation.x, 92 | y: initialTouchLocation.y + normTranslation.y 93 | ) 94 | 95 | loupePositionAnimator.spring = .init(dampingRatio: 0.92, response: 0.2) 96 | loupePositionAnimator.target = newLoupeCenter 97 | loupePositionAnimator.start() 98 | 99 | loupeSizeAnimator.target = Self.loupeDraggingSize 100 | loupeSizeAnimator.start() 101 | 102 | withAnimation(.spring(response: 0.4, dampingFraction: 1.1)) { 103 | isDragging = true 104 | } 105 | } 106 | .onEnded { value in 107 | let liftOffVelocity = value.velocity 108 | loupePositionAnimator.velocity = CGPoint( 109 | x: liftOffVelocity.width / size.width, 110 | y: liftOffVelocity.height / size.height 111 | ) 112 | 113 | loupePositionAnimator.spring = .init(dampingRatio: 0.72, response: 0.7) 114 | loupePositionAnimator.target = Self.loupeRestingPosition 115 | loupePositionAnimator.start() 116 | 117 | loupeSizeAnimator.target = Self.loupeRestingSize 118 | loupeSizeAnimator.start() 119 | 120 | initialTouchLocation = nil 121 | 122 | withAnimation(.spring(response: 0.4, dampingFraction: 1.1)) { 123 | isDragging = false 124 | } 125 | } 126 | ) 127 | .onAppear { 128 | loupePositionAnimator.valueChanged = { value in 129 | loupeCenter = value 130 | } 131 | 132 | loupeSizeAnimator.valueChanged = { value in 133 | loupeSize = value 134 | } 135 | } 136 | } 137 | } 138 | .background { 139 | Color.black 140 | } 141 | .ignoresSafeArea(.all) 142 | } 143 | } 144 | 145 | #Preview { 146 | ContentView() 147 | } 148 | -------------------------------------------------------------------------------- /MagnificationLoupe/MagnificationLoupe.metal: -------------------------------------------------------------------------------- 1 | // 2 | // MagnificationLoupe.metal 3 | // MagnificationLoupe 4 | // 5 | // Created by Janum Trivedi on 6/15/23. 6 | // 7 | 8 | #include 9 | #include 10 | 11 | using namespace metal; 12 | 13 | float mapRange(float value, float inMin, float inMax, float outMin, float outMax) { 14 | return ((value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin); 15 | } 16 | 17 | float easeInQuart(float x) { 18 | return x * x * x * x; 19 | } 20 | 21 | [[ stitchable ]] half4 loupe(float2 position, SwiftUI::Layer layer, float4 bounds, float2 loupeCenter, float loupeSize) { 22 | // The overall size/resolution of the shader view's bounds 23 | float2 size = float2(bounds[2], bounds[3]); 24 | 25 | // Normalize `position` into unit coordinates (i.e. [0, 1]) 26 | float2 p = position / size; 27 | float px = p[0]; 28 | float py = p[1]; 29 | 30 | // Sample the original color of the pixel 31 | half4 color = layer.sample(position); 32 | 33 | // Calculate how far the current pixel is from the loupe's center. 34 | float d = distance(loupeCenter, p); 35 | 36 | float r = loupeSize; 37 | float shadowRadius = 0.08; 38 | 39 | // Draw the magnified contents within the loupe: 40 | if (d <= r) { 41 | // How "zoomed-in" the magnification effect is. 42 | float offset = 0.10; 43 | 44 | // Kinda hacky, but keep the magnification radius constant even as the loupe's bounds change. 45 | r = 0.18; 46 | 47 | // Calculate the bounds of the loupe given its center and radius: 48 | float loupeMinX = loupeCenter[0] - r; 49 | float loupeMaxX = loupeCenter[0] + r; 50 | float loupeMinY = loupeCenter[1] - r; 51 | float loupeMaxY = loupeCenter[1] + r; 52 | 53 | // The pixels within the loupe need to sample across a smaller range from the underlying texture. 54 | // This is what gives that "magnification" effect. 55 | float zoomRangeMinX = loupeMinX + offset; 56 | float zoomRangeMaxX = loupeMaxX - offset; 57 | 58 | float zoomRangeMinY = loupeMinY + offset; 59 | float zoomRangeMaxY = loupeMaxY - offset; 60 | 61 | // Calculate the new coordinates to sample. 62 | // For example, when `px` == `loupeMinX`, it'll be converted to `zoomRangeMinX`. 63 | float zoomPosX = mapRange(px, loupeMinX, loupeMaxX, zoomRangeMinX, zoomRangeMaxX); 64 | float zoomPosY = mapRange(py, loupeMinY, loupeMaxY, zoomRangeMinY, zoomRangeMaxY); 65 | 66 | // un-normalize position back to user-space 67 | // We've been working with normalized unit values, but we need convert those values back to "user-space". 68 | // These values are considered "denormalized", and we can use them to sample the SwiftUI::Layer texture. 69 | float2 normalizedSamplePosition = float2(zoomPosX, zoomPosY); 70 | float2 denormalizedSamplePosition = float2( 71 | normalizedSamplePosition[0] * size[0], 72 | normalizedSamplePosition[1] * size[1] 73 | ); 74 | 75 | // Finally, sample the layer with the new, "magnified" coordinate. 76 | color = layer.sample(denormalizedSamplePosition); 77 | } 78 | // This block draws the shadow around the loupe: 79 | else if (d > r && d <= r + shadowRadius) { 80 | float distanceFromEdge = d - r; 81 | 82 | // Progress is normalized within the [0, 1] range. 83 | float progress = mapRange(distanceFromEdge, 0, shadowRadius, 1, 0); 84 | 85 | // Decay progress really quickly to create a more realistic shadow. 86 | progress = easeInQuart(progress); 87 | 88 | // Finally, do some alpha compositing to blend the black shadow color with the original pixel's color: 89 | float shadowOpacity = mapRange(progress, 1, 0, 0.2, 0); 90 | color = mix(color, half4(half3(0), 1), half4(shadowOpacity)); 91 | } 92 | 93 | return color; 94 | } 95 | 96 | -------------------------------------------------------------------------------- /MagnificationLoupe/MagnificationLoupeApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MagnificationLoupeApp.swift 3 | // MagnificationLoupe 4 | // 5 | // Created by Janum Trivedi on 6/15/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct MagnificationLoupeApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MagnificationLoupe/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magnification Loupe 2 | 3 | Built with SwiftUI, Metal, and Wave. Requires iOS 17. 4 | 5 | Video/demo: [Twitter](https://twitter.com/jmtrivedi/status/1669469698348699652) 6 | --------------------------------------------------------------------------------