├── MercurialText.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── simongladman.xcuserdatad │ └── xcschemes │ ├── MercurialText.xcscheme │ └── xcschememanagement.plist ├── MercurialText ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── ViewController.swift └── components │ ├── ItemRenderer.swift │ ├── ShadingImageEditor.swift │ ├── SupportClasses.swift │ └── TextEditor.swift ├── README.md └── screen_shot.png /MercurialText.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3E02B1751C10CC1F00258127 /* screen_shot.png in Resources */ = {isa = PBXBuildFile; fileRef = 3E02B1741C10CC1F00258127 /* screen_shot.png */; }; 11 | 3EFCAC851C0C2E9B00D0121B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EFCAC841C0C2E9B00D0121B /* AppDelegate.swift */; }; 12 | 3EFCAC871C0C2E9B00D0121B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EFCAC861C0C2E9B00D0121B /* ViewController.swift */; }; 13 | 3EFCAC8A1C0C2E9B00D0121B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3EFCAC881C0C2E9B00D0121B /* Main.storyboard */; }; 14 | 3EFCAC8C1C0C2E9B00D0121B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3EFCAC8B1C0C2E9B00D0121B /* Assets.xcassets */; }; 15 | 3EFCAC8F1C0C2E9B00D0121B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3EFCAC8D1C0C2E9B00D0121B /* LaunchScreen.storyboard */; }; 16 | 3EFCAC981C0C2EDA00D0121B /* ShadingImageEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EFCAC971C0C2EDA00D0121B /* ShadingImageEditor.swift */; }; 17 | 3EFCAC9A1C0CC91600D0121B /* ItemRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EFCAC991C0CC91600D0121B /* ItemRenderer.swift */; }; 18 | 3EFCAC9C1C0CCCEB00D0121B /* SupportClasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EFCAC9B1C0CCCEB00D0121B /* SupportClasses.swift */; }; 19 | 3EFCAC9E1C0D87F300D0121B /* TextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EFCAC9D1C0D87F300D0121B /* TextEditor.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 3E02B1741C10CC1F00258127 /* screen_shot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = screen_shot.png; sourceTree = ""; }; 24 | 3EFCAC811C0C2E9B00D0121B /* MercurialText.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MercurialText.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 3EFCAC841C0C2E9B00D0121B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 26 | 3EFCAC861C0C2E9B00D0121B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 27 | 3EFCAC891C0C2E9B00D0121B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 28 | 3EFCAC8B1C0C2E9B00D0121B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 29 | 3EFCAC8E1C0C2E9B00D0121B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 30 | 3EFCAC901C0C2E9B00D0121B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 31 | 3EFCAC971C0C2EDA00D0121B /* ShadingImageEditor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShadingImageEditor.swift; path = components/ShadingImageEditor.swift; sourceTree = ""; }; 32 | 3EFCAC991C0CC91600D0121B /* ItemRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ItemRenderer.swift; path = components/ItemRenderer.swift; sourceTree = ""; }; 33 | 3EFCAC9B1C0CCCEB00D0121B /* SupportClasses.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SupportClasses.swift; path = components/SupportClasses.swift; sourceTree = ""; }; 34 | 3EFCAC9D1C0D87F300D0121B /* TextEditor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TextEditor.swift; path = components/TextEditor.swift; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | 3EFCAC7E1C0C2E9B00D0121B /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | 3EFCAC781C0C2E9B00D0121B = { 49 | isa = PBXGroup; 50 | children = ( 51 | 3E02B1741C10CC1F00258127 /* screen_shot.png */, 52 | 3EFCAC831C0C2E9B00D0121B /* MercurialText */, 53 | 3EFCAC821C0C2E9B00D0121B /* Products */, 54 | ); 55 | sourceTree = ""; 56 | }; 57 | 3EFCAC821C0C2E9B00D0121B /* Products */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 3EFCAC811C0C2E9B00D0121B /* MercurialText.app */, 61 | ); 62 | name = Products; 63 | sourceTree = ""; 64 | }; 65 | 3EFCAC831C0C2E9B00D0121B /* MercurialText */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | 3EFCAC961C0C2EA700D0121B /* components */, 69 | 3EFCAC841C0C2E9B00D0121B /* AppDelegate.swift */, 70 | 3EFCAC861C0C2E9B00D0121B /* ViewController.swift */, 71 | 3EFCAC881C0C2E9B00D0121B /* Main.storyboard */, 72 | 3EFCAC8B1C0C2E9B00D0121B /* Assets.xcassets */, 73 | 3EFCAC8D1C0C2E9B00D0121B /* LaunchScreen.storyboard */, 74 | 3EFCAC901C0C2E9B00D0121B /* Info.plist */, 75 | ); 76 | path = MercurialText; 77 | sourceTree = ""; 78 | }; 79 | 3EFCAC961C0C2EA700D0121B /* components */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 3EFCAC971C0C2EDA00D0121B /* ShadingImageEditor.swift */, 83 | 3EFCAC991C0CC91600D0121B /* ItemRenderer.swift */, 84 | 3EFCAC9B1C0CCCEB00D0121B /* SupportClasses.swift */, 85 | 3EFCAC9D1C0D87F300D0121B /* TextEditor.swift */, 86 | ); 87 | name = components; 88 | sourceTree = ""; 89 | }; 90 | /* End PBXGroup section */ 91 | 92 | /* Begin PBXNativeTarget section */ 93 | 3EFCAC801C0C2E9B00D0121B /* MercurialText */ = { 94 | isa = PBXNativeTarget; 95 | buildConfigurationList = 3EFCAC931C0C2E9B00D0121B /* Build configuration list for PBXNativeTarget "MercurialText" */; 96 | buildPhases = ( 97 | 3EFCAC7D1C0C2E9B00D0121B /* Sources */, 98 | 3EFCAC7E1C0C2E9B00D0121B /* Frameworks */, 99 | 3EFCAC7F1C0C2E9B00D0121B /* Resources */, 100 | ); 101 | buildRules = ( 102 | ); 103 | dependencies = ( 104 | ); 105 | name = MercurialText; 106 | productName = MercurialText; 107 | productReference = 3EFCAC811C0C2E9B00D0121B /* MercurialText.app */; 108 | productType = "com.apple.product-type.application"; 109 | }; 110 | /* End PBXNativeTarget section */ 111 | 112 | /* Begin PBXProject section */ 113 | 3EFCAC791C0C2E9B00D0121B /* Project object */ = { 114 | isa = PBXProject; 115 | attributes = { 116 | LastSwiftUpdateCheck = 0710; 117 | LastUpgradeCheck = 0710; 118 | ORGANIZATIONNAME = "Simon Gladman"; 119 | TargetAttributes = { 120 | 3EFCAC801C0C2E9B00D0121B = { 121 | CreatedOnToolsVersion = 7.1.1; 122 | }; 123 | }; 124 | }; 125 | buildConfigurationList = 3EFCAC7C1C0C2E9B00D0121B /* Build configuration list for PBXProject "MercurialText" */; 126 | compatibilityVersion = "Xcode 3.2"; 127 | developmentRegion = English; 128 | hasScannedForEncodings = 0; 129 | knownRegions = ( 130 | en, 131 | Base, 132 | ); 133 | mainGroup = 3EFCAC781C0C2E9B00D0121B; 134 | productRefGroup = 3EFCAC821C0C2E9B00D0121B /* Products */; 135 | projectDirPath = ""; 136 | projectRoot = ""; 137 | targets = ( 138 | 3EFCAC801C0C2E9B00D0121B /* MercurialText */, 139 | ); 140 | }; 141 | /* End PBXProject section */ 142 | 143 | /* Begin PBXResourcesBuildPhase section */ 144 | 3EFCAC7F1C0C2E9B00D0121B /* Resources */ = { 145 | isa = PBXResourcesBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | 3EFCAC8F1C0C2E9B00D0121B /* LaunchScreen.storyboard in Resources */, 149 | 3EFCAC8C1C0C2E9B00D0121B /* Assets.xcassets in Resources */, 150 | 3EFCAC8A1C0C2E9B00D0121B /* Main.storyboard in Resources */, 151 | 3E02B1751C10CC1F00258127 /* screen_shot.png in Resources */, 152 | ); 153 | runOnlyForDeploymentPostprocessing = 0; 154 | }; 155 | /* End PBXResourcesBuildPhase section */ 156 | 157 | /* Begin PBXSourcesBuildPhase section */ 158 | 3EFCAC7D1C0C2E9B00D0121B /* Sources */ = { 159 | isa = PBXSourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 3EFCAC871C0C2E9B00D0121B /* ViewController.swift in Sources */, 163 | 3EFCAC9E1C0D87F300D0121B /* TextEditor.swift in Sources */, 164 | 3EFCAC981C0C2EDA00D0121B /* ShadingImageEditor.swift in Sources */, 165 | 3EFCAC851C0C2E9B00D0121B /* AppDelegate.swift in Sources */, 166 | 3EFCAC9A1C0CC91600D0121B /* ItemRenderer.swift in Sources */, 167 | 3EFCAC9C1C0CCCEB00D0121B /* SupportClasses.swift in Sources */, 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | }; 171 | /* End PBXSourcesBuildPhase section */ 172 | 173 | /* Begin PBXVariantGroup section */ 174 | 3EFCAC881C0C2E9B00D0121B /* Main.storyboard */ = { 175 | isa = PBXVariantGroup; 176 | children = ( 177 | 3EFCAC891C0C2E9B00D0121B /* Base */, 178 | ); 179 | name = Main.storyboard; 180 | sourceTree = ""; 181 | }; 182 | 3EFCAC8D1C0C2E9B00D0121B /* LaunchScreen.storyboard */ = { 183 | isa = PBXVariantGroup; 184 | children = ( 185 | 3EFCAC8E1C0C2E9B00D0121B /* Base */, 186 | ); 187 | name = LaunchScreen.storyboard; 188 | sourceTree = ""; 189 | }; 190 | /* End PBXVariantGroup section */ 191 | 192 | /* Begin XCBuildConfiguration section */ 193 | 3EFCAC911C0C2E9B00D0121B /* Debug */ = { 194 | isa = XCBuildConfiguration; 195 | buildSettings = { 196 | ALWAYS_SEARCH_USER_PATHS = NO; 197 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 198 | CLANG_CXX_LIBRARY = "libc++"; 199 | CLANG_ENABLE_MODULES = YES; 200 | CLANG_ENABLE_OBJC_ARC = YES; 201 | CLANG_WARN_BOOL_CONVERSION = YES; 202 | CLANG_WARN_CONSTANT_CONVERSION = YES; 203 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 204 | CLANG_WARN_EMPTY_BODY = YES; 205 | CLANG_WARN_ENUM_CONVERSION = YES; 206 | CLANG_WARN_INT_CONVERSION = YES; 207 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 208 | CLANG_WARN_UNREACHABLE_CODE = YES; 209 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 210 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 211 | COPY_PHASE_STRIP = NO; 212 | DEBUG_INFORMATION_FORMAT = dwarf; 213 | ENABLE_STRICT_OBJC_MSGSEND = YES; 214 | ENABLE_TESTABILITY = YES; 215 | GCC_C_LANGUAGE_STANDARD = gnu99; 216 | GCC_DYNAMIC_NO_PIC = NO; 217 | GCC_NO_COMMON_BLOCKS = YES; 218 | GCC_OPTIMIZATION_LEVEL = 0; 219 | GCC_PREPROCESSOR_DEFINITIONS = ( 220 | "DEBUG=1", 221 | "$(inherited)", 222 | ); 223 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 224 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 225 | GCC_WARN_UNDECLARED_SELECTOR = YES; 226 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 227 | GCC_WARN_UNUSED_FUNCTION = YES; 228 | GCC_WARN_UNUSED_VARIABLE = YES; 229 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 230 | MTL_ENABLE_DEBUG_INFO = YES; 231 | ONLY_ACTIVE_ARCH = YES; 232 | SDKROOT = iphoneos; 233 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 234 | TARGETED_DEVICE_FAMILY = 2; 235 | }; 236 | name = Debug; 237 | }; 238 | 3EFCAC921C0C2E9B00D0121B /* Release */ = { 239 | isa = XCBuildConfiguration; 240 | buildSettings = { 241 | ALWAYS_SEARCH_USER_PATHS = NO; 242 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 243 | CLANG_CXX_LIBRARY = "libc++"; 244 | CLANG_ENABLE_MODULES = YES; 245 | CLANG_ENABLE_OBJC_ARC = YES; 246 | CLANG_WARN_BOOL_CONVERSION = YES; 247 | CLANG_WARN_CONSTANT_CONVERSION = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INT_CONVERSION = YES; 252 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 253 | CLANG_WARN_UNREACHABLE_CODE = YES; 254 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 255 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 256 | COPY_PHASE_STRIP = NO; 257 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 258 | ENABLE_NS_ASSERTIONS = NO; 259 | ENABLE_STRICT_OBJC_MSGSEND = YES; 260 | GCC_C_LANGUAGE_STANDARD = gnu99; 261 | GCC_NO_COMMON_BLOCKS = YES; 262 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 263 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 264 | GCC_WARN_UNDECLARED_SELECTOR = YES; 265 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 266 | GCC_WARN_UNUSED_FUNCTION = YES; 267 | GCC_WARN_UNUSED_VARIABLE = YES; 268 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 269 | MTL_ENABLE_DEBUG_INFO = NO; 270 | SDKROOT = iphoneos; 271 | TARGETED_DEVICE_FAMILY = 2; 272 | VALIDATE_PRODUCT = YES; 273 | }; 274 | name = Release; 275 | }; 276 | 3EFCAC941C0C2E9B00D0121B /* Debug */ = { 277 | isa = XCBuildConfiguration; 278 | buildSettings = { 279 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 280 | INFOPLIST_FILE = MercurialText/Info.plist; 281 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 282 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.flexmonkey.MercurialText; 283 | PRODUCT_NAME = "$(TARGET_NAME)"; 284 | }; 285 | name = Debug; 286 | }; 287 | 3EFCAC951C0C2E9B00D0121B /* Release */ = { 288 | isa = XCBuildConfiguration; 289 | buildSettings = { 290 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 291 | INFOPLIST_FILE = MercurialText/Info.plist; 292 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 293 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.flexmonkey.MercurialText; 294 | PRODUCT_NAME = "$(TARGET_NAME)"; 295 | }; 296 | name = Release; 297 | }; 298 | /* End XCBuildConfiguration section */ 299 | 300 | /* Begin XCConfigurationList section */ 301 | 3EFCAC7C1C0C2E9B00D0121B /* Build configuration list for PBXProject "MercurialText" */ = { 302 | isa = XCConfigurationList; 303 | buildConfigurations = ( 304 | 3EFCAC911C0C2E9B00D0121B /* Debug */, 305 | 3EFCAC921C0C2E9B00D0121B /* Release */, 306 | ); 307 | defaultConfigurationIsVisible = 0; 308 | defaultConfigurationName = Release; 309 | }; 310 | 3EFCAC931C0C2E9B00D0121B /* Build configuration list for PBXNativeTarget "MercurialText" */ = { 311 | isa = XCConfigurationList; 312 | buildConfigurations = ( 313 | 3EFCAC941C0C2E9B00D0121B /* Debug */, 314 | 3EFCAC951C0C2E9B00D0121B /* Release */, 315 | ); 316 | defaultConfigurationIsVisible = 0; 317 | defaultConfigurationName = Release; 318 | }; 319 | /* End XCConfigurationList section */ 320 | }; 321 | rootObject = 3EFCAC791C0C2E9B00D0121B /* Project object */; 322 | } 323 | -------------------------------------------------------------------------------- /MercurialText.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MercurialText.xcodeproj/xcuserdata/simongladman.xcuserdatad/xcschemes/MercurialText.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /MercurialText.xcodeproj/xcuserdata/simongladman.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MercurialText.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 3EFCAC801C0C2E9B00D0121B 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /MercurialText/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MercurialText 4 | // 5 | // Created by Simon Gladman on 30/11/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /MercurialText/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "ipad", 5 | "size" : "29x29", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "ipad", 10 | "size" : "29x29", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "ipad", 15 | "size" : "40x40", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "ipad", 20 | "size" : "40x40", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "76x76", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "76x76", 31 | "scale" : "2x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /MercurialText/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /MercurialText/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /MercurialText/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 | UIInterfaceOrientationLandscapeLeft 36 | UIInterfaceOrientationLandscapeRight 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /MercurialText/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // MercurialText 4 | // 5 | // Created by Simon Gladman on 30/11/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController 12 | { 13 | 14 | let textEditor = TextEditor() 15 | let shadingImageEditor = ShadingImageEditor() 16 | 17 | override func viewDidLoad() 18 | { 19 | super.viewDidLoad() 20 | 21 | view.backgroundColor = UIColor.blackColor() 22 | 23 | shadingImageEditor.addTarget(self, 24 | action: "shadingImageChange", 25 | forControlEvents: UIControlEvents.ValueChanged) 26 | 27 | view.addSubview(shadingImageEditor) 28 | 29 | view.addSubview(textEditor) 30 | } 31 | 32 | override func viewDidAppear(animated: Bool) 33 | { 34 | shadingImageChange() 35 | } 36 | 37 | func shadingImageChange() 38 | { 39 | textEditor.shadingImage = shadingImageEditor.image 40 | 41 | textEditor.createImage() 42 | } 43 | 44 | override func viewDidLayoutSubviews() 45 | { 46 | let top = topLayoutGuide.length 47 | let shadingImageEditorWidth = CGFloat(300) 48 | 49 | shadingImageEditor.frame = CGRect(x: view.frame.width - shadingImageEditorWidth, 50 | y: top, 51 | width: shadingImageEditorWidth, 52 | height: view.frame.height - top) 53 | 54 | textEditor.frame = CGRect(x: 0, 55 | y: top, 56 | width: view.frame.width - shadingImageEditorWidth, 57 | height: view.frame.height - top) 58 | } 59 | 60 | override func preferredStatusBarStyle() -> UIStatusBarStyle 61 | { 62 | return UIStatusBarStyle.LightContent 63 | } 64 | 65 | override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask 66 | { 67 | return UIInterfaceOrientationMask.Landscape 68 | } 69 | } 70 | 71 | -------------------------------------------------------------------------------- /MercurialText/components/ItemRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelledSlider.swift 3 | // MercurialText 4 | // 5 | // Created by Simon Gladman on 30/11/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | 12 | 13 | class ItemRenderer: UITableViewCell 14 | { 15 | let slider = LabelledSlider() 16 | 17 | var enabled: Bool = true 18 | { 19 | didSet 20 | { 21 | slider.enabled = enabled 22 | } 23 | } 24 | 25 | var parameter: Parameter? 26 | { 27 | didSet 28 | { 29 | slider.parameter = parameter 30 | } 31 | } 32 | 33 | override init(style: UITableViewCellStyle, reuseIdentifier: String?) 34 | { 35 | super.init(style: style, reuseIdentifier: "ItemRenderer") 36 | 37 | backgroundColor = UIColor.blackColor() 38 | 39 | contentView.addSubview(slider) 40 | } 41 | 42 | override func layoutSubviews() 43 | { 44 | slider.frame = bounds 45 | } 46 | 47 | required init?(coder aDecoder: NSCoder) 48 | { 49 | fatalError("init(coder:) has not been implemented") 50 | } 51 | } 52 | 53 | // ------------------- 54 | 55 | class LabelledSlider: UIControl 56 | { 57 | let label = UILabel() 58 | let valueLabel = UILabel() 59 | let slider = UISlider() 60 | 61 | override var enabled: Bool 62 | { 63 | didSet 64 | { 65 | userInteractionEnabled = enabled 66 | 67 | label.enabled = enabled 68 | valueLabel.enabled = enabled 69 | slider.enabled = enabled 70 | } 71 | } 72 | 73 | var parameter: Parameter? 74 | { 75 | didSet 76 | { 77 | guard let parameter = parameter else 78 | { 79 | label.text = "" 80 | return 81 | } 82 | 83 | label.text = parameter.name 84 | 85 | slider.minimumValue = parameter.minMax.min 86 | slider.maximumValue = parameter.minMax.max 87 | 88 | valueLabel.text = String(format: "%.2f", parameter.value) 89 | 90 | slider.value = Float(parameter.value) 91 | } 92 | } 93 | 94 | override init(frame: CGRect) 95 | { 96 | super.init(frame: frame) 97 | 98 | label.textColor = UIColor.whiteColor() 99 | valueLabel.textColor = UIColor.whiteColor() 100 | 101 | label.adjustsFontSizeToFitWidth = true 102 | valueLabel.adjustsFontSizeToFitWidth = true 103 | 104 | valueLabel.textAlignment = NSTextAlignment.Right 105 | 106 | addSubview(label) 107 | addSubview(valueLabel) 108 | addSubview(slider) 109 | 110 | label.numberOfLines = 0 111 | 112 | slider.addTarget(self, action: "sliderChangeHandler", forControlEvents: UIControlEvents.ValueChanged) 113 | } 114 | 115 | required init?(coder aDecoder: NSCoder) 116 | { 117 | fatalError("init(coder:) has not been implemented") 118 | } 119 | 120 | func sliderChangeHandler() 121 | { 122 | parameter?.value = CGFloat(slider.value) 123 | 124 | guard let parameter = parameter else 125 | { 126 | return 127 | } 128 | 129 | valueLabel.text = String(format: "%.2f", parameter.value) 130 | 131 | sendActionsForControlEvents(UIControlEvents.ValueChanged) 132 | } 133 | 134 | override func layoutSubviews() 135 | { 136 | label.frame = CGRect(x: 5, 137 | y: 2, 138 | width: frame.width / 2, 139 | height: label.intrinsicContentSize().height).insetBy(dx: 2, dy: 0) 140 | 141 | valueLabel.frame = CGRect(x: frame.width / 2, 142 | y: 2, 143 | width: frame.width / 2, 144 | height: valueLabel.intrinsicContentSize().height).insetBy(dx: 2, dy: 0) 145 | 146 | slider.frame = CGRect(x: 0, 147 | y: frame.height / 2 - 2, 148 | width: frame.width, 149 | height: slider.intrinsicContentSize().height).insetBy(dx: 2, dy: 0) 150 | 151 | layer.borderColor = UIColor.darkGrayColor().CGColor 152 | layer.borderWidth = 1 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /MercurialText/components/ShadingImageEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShadingImageEditor.swift 3 | // MercurialText 4 | // 5 | // Created by Simon Gladman on 30/11/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SceneKit 11 | 12 | 13 | class ShadingImageEditor: UIControl 14 | { 15 | let lights = [ 16 | OmniLight(), 17 | OmniLight(), 18 | OmniLight(), 19 | OmniLight() 20 | ] 21 | 22 | let parameterGroups = [ 23 | ParameterGroup(name: "Material", parameters: [ 24 | Parameter(name: "Shininess", parameterFunction: .AdjustMaterialShininess, value: 0.02, minMax: MinMax(min:0.001, max: 0.25)) 25 | ]), 26 | 27 | ParameterGroup(name: "Light 1", parameters: [ 28 | Parameter(name: "Hue", parameterFunction: .AdjustLightHue(index: 0), value: 0.1, minMax: MinMaxNorm), 29 | Parameter(name: "Brightness", parameterFunction: .AdjustLightBrightness(index: 0), value: 1, minMax: MinMaxNorm), 30 | Parameter(name: "x Position", parameterFunction: .AdjustLightPosition(index: 0, axis: .X), value: 0, minMax: MinMaxXY), 31 | Parameter(name: "y Position", parameterFunction: .AdjustLightPosition(index: 0, axis: .Y), value: 25, minMax: MinMaxXY), 32 | Parameter(name: "z Position", parameterFunction: .AdjustLightPosition(index: 0, axis: .Z), value: 0, minMax: MinMaxZ) 33 | ]), 34 | 35 | ParameterGroup(name: "Light 2", parameters: [ 36 | Parameter(name: "Hue", parameterFunction: .AdjustLightHue(index: 1), value: 0.48, minMax: MinMaxNorm), 37 | Parameter(name: "Brightness", parameterFunction: .AdjustLightBrightness(index: 1), value: 1, minMax: MinMaxNorm), 38 | Parameter(name: "x Position", parameterFunction: .AdjustLightPosition(index: 1, axis: .X), value: 25, minMax: MinMaxXY), 39 | Parameter(name: "y Position", parameterFunction: .AdjustLightPosition(index: 1, axis: .Y), value: -35, minMax: MinMaxXY), 40 | Parameter(name: "z Position", parameterFunction: .AdjustLightPosition(index: 1, axis: .Z), value: 0, minMax: MinMaxZ) 41 | ]), 42 | 43 | ParameterGroup(name: "Light 3", parameters: [ 44 | Parameter(name: "Hue", parameterFunction: .AdjustLightHue(index: 2), value: 0.85, minMax: MinMaxNorm), 45 | Parameter(name: "Brightness", parameterFunction: .AdjustLightBrightness(index: 2), value: 1, minMax: MinMaxNorm), 46 | Parameter(name: "x Position", parameterFunction: .AdjustLightPosition(index: 2, axis: .X), value: -35, minMax: MinMaxXY), 47 | Parameter(name: "y Position", parameterFunction: .AdjustLightPosition(index: 2, axis: .Y), value: -20, minMax: MinMaxXY), 48 | Parameter(name: "z Position", parameterFunction: .AdjustLightPosition(index: 2, axis: .Z), value: -10, minMax: MinMaxZ) 49 | ]), 50 | 51 | ParameterGroup(name: "Light 4", parameters: [ 52 | Parameter(name: "Hue", parameterFunction: .AdjustLightHue(index: 3), value: 0.18, minMax: MinMaxNorm), 53 | Parameter(name: "Brightness", parameterFunction: .AdjustLightBrightness(index: 3), value: 1, minMax: MinMaxNorm), 54 | Parameter(name: "x Position", parameterFunction: .AdjustLightPosition(index: 3, axis: .X), value: -35, minMax: MinMaxXY), 55 | Parameter(name: "y Position", parameterFunction: .AdjustLightPosition(index: 3, axis: .Y), value: 10, minMax: MinMaxXY), 56 | Parameter(name: "z Position", parameterFunction: .AdjustLightPosition(index: 3, axis: .Z), value: 35, minMax: MinMaxZ) 57 | ]) 58 | ] 59 | 60 | var lightPositionWidgets = [LightPositionWidget]() 61 | 62 | let material = SCNMaterial() 63 | 64 | let sceneKitView = SCNView() 65 | let tableView = UITableView() 66 | 67 | var sceneChanged = false 68 | 69 | var image: UIImage? 70 | { 71 | return sceneKitView.snapshot() 72 | } 73 | 74 | override init(frame: CGRect) 75 | { 76 | super.init(frame: frame) 77 | 78 | sceneKitView.layer.borderColor = UIColor.darkGrayColor().CGColor 79 | sceneKitView.layer.borderWidth = 1 80 | 81 | sceneKitView.delegate = self 82 | 83 | addSubview(sceneKitView) 84 | addSubview(tableView) 85 | 86 | tableView.dataSource = self 87 | tableView.delegate = self 88 | 89 | setUpSceneKit() 90 | applyAllParameters() 91 | 92 | tableView.rowHeight = 60 93 | 94 | tableView.registerClass(ItemRenderer.self, 95 | forCellReuseIdentifier: "ItemRenderer") 96 | 97 | for (index, _) in lights.enumerate() 98 | { 99 | let lightWidget = LightPositionWidget(index: index) 100 | 101 | lightPositionWidgets.append(lightWidget) 102 | sceneKitView.addSubview(lightWidget) 103 | } 104 | 105 | positionLightWidgets() 106 | colorLightWidgets() 107 | } 108 | 109 | required init?(coder aDecoder: NSCoder) 110 | { 111 | fatalError("init(coder:) has not been implemented") 112 | } 113 | 114 | func positionLightWidgets() 115 | { 116 | let width = frame.width 117 | 118 | for (widget, light) in zip(lightPositionWidgets, lights) 119 | { 120 | let lightPosition = CGPoint(x: width * CGFloat((light.position.x - MinMaxXY.min) / (MinMaxXY.max - MinMaxXY.min)) - 10, 121 | y: width - (width * CGFloat((light.position.y - MinMaxXY.min) / (MinMaxXY.max - MinMaxXY.min))) - 10 122 | ) 123 | 124 | widget.frame = CGRect(origin: lightPosition, 125 | size: CGSize(width: 20, height: 20)) 126 | } 127 | } 128 | 129 | func colorLightWidgets() 130 | { 131 | for (widget, light) in zip(lightPositionWidgets, lights) 132 | { 133 | widget.backgroundColor = UIColor(hue: light.hue, 134 | saturation: 1, 135 | brightness: light.brightness, 136 | alpha: 1) 137 | } 138 | } 139 | 140 | func setUpSceneKit() 141 | { 142 | sceneKitView.backgroundColor = UIColor.blackColor() 143 | 144 | let sphere = SCNSphere(radius: 1) 145 | let sphereNode = SCNNode(geometry: sphere) 146 | 147 | let scene = SCNScene() 148 | 149 | sceneKitView.scene = scene 150 | 151 | let camera = SCNCamera() 152 | 153 | camera.usesOrthographicProjection = true 154 | camera.orthographicScale = 1 155 | 156 | let cameraNode = SCNNode() 157 | 158 | cameraNode.camera = camera 159 | cameraNode.position = SCNVector3(x: 0, y: 0, z: 2) 160 | 161 | scene.rootNode.addChildNode(cameraNode) 162 | 163 | // sphere... 164 | 165 | sphereNode.position = SCNVector3(x: 0, y: 0, z: 0) 166 | scene.rootNode.addChildNode(sphereNode) 167 | 168 | for light in lights 169 | { 170 | scene.rootNode.addChildNode(light) 171 | } 172 | 173 | material.lightingModelName = SCNLightingModelPhong 174 | material.specular.contents = UIColor.whiteColor() 175 | material.diffuse.contents = UIColor.darkGrayColor() 176 | material.shininess = 0.15 177 | 178 | sphere.materials = [material] 179 | } 180 | 181 | func sliderChangeHandler(slider: LabelledSlider) 182 | { 183 | guard let parameter = slider.parameter else 184 | { 185 | return 186 | } 187 | 188 | updateSceneFromParameter(parameter) 189 | } 190 | 191 | func applyAllParameters() 192 | { 193 | for group in parameterGroups 194 | { 195 | for parameter in group.parameters 196 | { 197 | updateSceneFromParameter(parameter) 198 | } 199 | } 200 | } 201 | 202 | func updateSceneFromParameter(parameter: Parameter) 203 | { 204 | switch parameter.parameterFunction 205 | { 206 | case let .AdjustLightPosition(index, axis): 207 | switch axis 208 | { 209 | case .X: 210 | lights[index].position.x = Float(parameter.value) 211 | case .Y: 212 | lights[index].position.y = Float(parameter.value) 213 | case .Z: 214 | lights[index].position.z = Float(parameter.value) 215 | } 216 | 217 | positionLightWidgets() 218 | 219 | case .AdjustMaterialShininess: 220 | material.shininess = parameter.value 221 | 222 | case let .AdjustLightHue(index): 223 | lights[index].hue = parameter.value 224 | colorLightWidgets() 225 | 226 | case let .AdjustLightBrightness(index): 227 | lights[index].brightness = parameter.value 228 | colorLightWidgets() 229 | } 230 | 231 | sceneChanged = true 232 | } 233 | 234 | override func layoutSubviews() 235 | { 236 | sceneKitView.frame = CGRect(x: 0, 237 | y: 0, 238 | width: frame.width, 239 | height: frame.width) 240 | 241 | tableView.frame = CGRect(x: 0, 242 | y: frame.width, 243 | width: frame.width, 244 | height: frame.height - frame.width) 245 | 246 | tableView.separatorStyle = UITableViewCellSeparatorStyle.None 247 | 248 | positionLightWidgets() 249 | } 250 | 251 | } 252 | 253 | // MARK: scene renderer delegate 254 | 255 | extension ShadingImageEditor: SCNSceneRendererDelegate 256 | { 257 | func renderer(renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: NSTimeInterval) 258 | { 259 | guard sceneChanged else 260 | { 261 | return 262 | } 263 | 264 | sceneChanged = false 265 | 266 | dispatch_async(dispatch_get_main_queue()) 267 | { 268 | self.sendActionsForControlEvents(UIControlEvents.ValueChanged) 269 | } 270 | } 271 | } 272 | 273 | // MARK: table delegate 274 | 275 | extension ShadingImageEditor: UITableViewDelegate 276 | { 277 | 278 | } 279 | 280 | // MARK: table view datasource 281 | 282 | extension ShadingImageEditor: UITableViewDataSource 283 | { 284 | func numberOfSectionsInTableView(tableView: UITableView) -> Int 285 | { 286 | return parameterGroups.count 287 | } 288 | 289 | func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int 290 | { 291 | return parameterGroups[section].parameters.count 292 | } 293 | 294 | func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell 295 | { 296 | let cell = tableView.dequeueReusableCellWithIdentifier("ItemRenderer", 297 | forIndexPath: indexPath) as! ItemRenderer 298 | 299 | cell.parameter = parameterGroups[indexPath.section].parameters[indexPath.item] 300 | 301 | cell.slider.addTarget(self, action: "sliderChangeHandler:", forControlEvents: UIControlEvents.ValueChanged) 302 | 303 | return cell 304 | } 305 | 306 | func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? 307 | { 308 | return parameterGroups[section].name 309 | } 310 | } 311 | 312 | // MARK: Light position widget 313 | 314 | class LightPositionWidget: UIView 315 | { 316 | let label = UILabel() 317 | 318 | required init(index: Int) 319 | { 320 | super.init(frame: CGRectZero) 321 | 322 | layer.borderWidth = 1 323 | layer.borderColor = UIColor.whiteColor().CGColor 324 | 325 | label.text = "\(index + 1)" 326 | label.font = UIFont.boldSystemFontOfSize(12) 327 | label.textColor = UIColor.darkGrayColor() 328 | label.textAlignment = NSTextAlignment.Center 329 | 330 | addSubview(label) 331 | } 332 | 333 | required init?(coder aDecoder: NSCoder) 334 | { 335 | fatalError("init(coder:) has not been implemented") 336 | } 337 | 338 | override var backgroundColor:UIColor? 339 | { 340 | didSet 341 | { 342 | if let color = backgroundColor 343 | { 344 | var white: CGFloat = 0 345 | var alpha: CGFloat = 0 346 | 347 | color.getWhite(&white, alpha: &alpha) 348 | 349 | label.textColor = white < 0.5 ? 350 | UIColor.whiteColor() : 351 | UIColor.blackColor() 352 | } 353 | } 354 | } 355 | 356 | override func layoutSubviews() 357 | { 358 | layer.cornerRadius = min(frame.width, frame.height) / 2 359 | 360 | label.frame = bounds 361 | 362 | label.shadowColor = UIColor.whiteColor() 363 | label.shadowOffset = CGSize(width: 0, height: 0) 364 | } 365 | 366 | } 367 | -------------------------------------------------------------------------------- /MercurialText/components/SupportClasses.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SupportClasses.swift 3 | // MercurialText 4 | // 5 | // Created by Simon Gladman on 30/11/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import SceneKit 10 | 11 | class ParameterGroup 12 | { 13 | init(name: String, parameters: [Parameter]) 14 | { 15 | self.name = name 16 | self.parameters = parameters 17 | } 18 | 19 | let name: String 20 | let parameters: [Parameter] 21 | } 22 | 23 | class Parameter 24 | { 25 | init(name: String, parameterFunction: ParameterFunction, value: CGFloat, minMax: MinMax) 26 | { 27 | self.name = name 28 | self.parameterFunction = parameterFunction 29 | self.value = value 30 | self.minMax = minMax 31 | } 32 | 33 | let name: String 34 | let parameterFunction: ParameterFunction 35 | var value: CGFloat 36 | let minMax: MinMax 37 | } 38 | 39 | enum ParameterFunction 40 | { 41 | case AdjustLightPosition(index: Int, axis: PositionAxis) 42 | case AdjustLightHue(index: Int) 43 | case AdjustLightBrightness(index: Int) 44 | case AdjustMaterialShininess 45 | } 46 | 47 | enum PositionAxis 48 | { 49 | case X 50 | case Y 51 | case Z 52 | } 53 | 54 | typealias MinMax = (min: Float, max: Float) 55 | 56 | let MinMaxNorm = MinMax(min: 0, max: 1) 57 | let MinMaxXY = MinMax(min: -50, max: 50) 58 | let MinMaxZ = MinMax(min: -10, max: 50) 59 | 60 | class OmniLight: SCNNode 61 | { 62 | init(x: Float = 0, y: Float = 0, z: Float = 0, hue: CGFloat = 0, brightness: CGFloat = 0) 63 | { 64 | self.x = x 65 | self.y = y 66 | self.z = z 67 | 68 | self.hue = hue 69 | self.brightness = brightness 70 | 71 | super.init() 72 | 73 | let omniLight = SCNLight() 74 | omniLight.type = SCNLightTypeOmni 75 | 76 | light = omniLight 77 | 78 | position = SCNVector3(x: x, y: y, z: z) 79 | 80 | updateLightColor() 81 | } 82 | 83 | required init?(coder aDecoder: NSCoder) 84 | { 85 | fatalError("init(coder:) has not been implemented") 86 | } 87 | 88 | let x: Float 89 | let y: Float 90 | let z: Float 91 | 92 | var hue: CGFloat 93 | { 94 | didSet 95 | { 96 | updateLightColor() 97 | } 98 | } 99 | 100 | var brightness: CGFloat 101 | { 102 | didSet 103 | { 104 | updateLightColor() 105 | } 106 | } 107 | 108 | func updateLightColor() 109 | { 110 | light?.color = UIColor(hue: hue, 111 | saturation: 1, 112 | brightness: brightness, 113 | alpha: 1) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /MercurialText/components/TextEditor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextEditor.swift 3 | // MercurialText 4 | // 5 | // Created by Simon Gladman on 01/12/2015. 6 | // Copyright © 2015 Simon Gladman. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import GLKit 11 | 12 | class TextEditor: UIView 13 | { 14 | let imageView: GLKView 15 | 16 | let toolbar = UIToolbar() 17 | let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.WhiteLarge) 18 | 19 | let fonts = UIFont.familyNames().sort() 20 | 21 | var pendingUpdate = false 22 | 23 | var shadingImage: UIImage? 24 | var filteredImageData: CIImage? 25 | 26 | let heightMapFilter = CIFilter(name: "CIHeightFieldFromMask")! 27 | let shadedMaterialFilter = CIFilter(name: "CIShadedMaterial")! 28 | 29 | var isBusy = false 30 | { 31 | didSet 32 | { 33 | if isBusy 34 | { 35 | activityIndicator.startAnimating() 36 | } 37 | else 38 | { 39 | activityIndicator.stopAnimating() 40 | } 41 | } 42 | } 43 | 44 | lazy var ciContext: CIContext = 45 | { 46 | [unowned self] in 47 | 48 | return CIContext(EAGLContext: self.imageView.context, options: [kCIContextWorkingColorSpace: NSNull()]) 49 | }() 50 | 51 | lazy var fontPicker: UIPickerView = 52 | { 53 | [unowned self] in 54 | 55 | let fontPicker = UIPickerView() 56 | 57 | fontPicker.dataSource = self 58 | fontPicker.delegate = self 59 | fontPicker.backgroundColor = UIColor.lightGrayColor() 60 | 61 | return fontPicker 62 | }() 63 | 64 | lazy var label: UILabel = 65 | { 66 | [unowned self] in 67 | 68 | let label = UILabel() 69 | 70 | label.textAlignment = NSTextAlignment.Center 71 | label.font = UIFont(name: self.fonts.first!, size: 300) 72 | label.numberOfLines = 5 73 | label.adjustsFontSizeToFitWidth = true 74 | label.text = "Flex Monkey Mercurial Text" 75 | label.textColor = UIColor.whiteColor() 76 | 77 | return label 78 | }() 79 | 80 | override init(frame: CGRect) 81 | { 82 | imageView = GLKView(frame: frame, context: EAGLContext(API: .OpenGLES2)) 83 | 84 | super.init(frame: frame) 85 | 86 | imageView.delegate = self 87 | 88 | backgroundColor = UIColor.blackColor() 89 | imageView.backgroundColor = UIColor.blackColor() 90 | imageView.contentMode = UIViewContentMode.ScaleAspectFit 91 | 92 | let editButton = UIBarButtonItem(barButtonSystemItem: .Edit, target: self, action: "editTextClicked") 93 | let saveButton = UIBarButtonItem(barButtonSystemItem: .Save, target: self, action: "saveImageClicked") 94 | 95 | toolbar.setItems([editButton, saveButton], animated: false) 96 | 97 | activityIndicator.stopAnimating() 98 | 99 | addSubview(label) 100 | addSubview(fontPicker) 101 | addSubview(imageView) 102 | addSubview(toolbar) 103 | addSubview(activityIndicator) 104 | } 105 | 106 | required init?(coder aDecoder: NSCoder) 107 | { 108 | fatalError("init(coder:) has not been implemented") 109 | } 110 | 111 | func editTextClicked() 112 | { 113 | guard let rootController = UIApplication.sharedApplication().keyWindow!.rootViewController else 114 | { 115 | return 116 | } 117 | 118 | let editTextController = UIAlertController(title: "Mercurial Text", message: nil, preferredStyle: .Alert) 119 | 120 | let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default) 121 | { 122 | (_: UIAlertAction) in 123 | 124 | if let updatedText = editTextController.textFields?.first?.text 125 | { 126 | self.label.text = updatedText 127 | 128 | self.createImage() 129 | } 130 | } 131 | 132 | let cancelAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: nil) 133 | 134 | editTextController.addTextFieldWithConfigurationHandler 135 | { 136 | (textField: UITextField) in 137 | 138 | textField.text = self.label.text 139 | } 140 | 141 | editTextController.addAction(okAction) 142 | editTextController.addAction(cancelAction) 143 | 144 | rootController.presentViewController(editTextController, animated: false, completion: nil) 145 | } 146 | 147 | func saveImageClicked() 148 | { 149 | guard let filteredImageData = filteredImageData else 150 | { 151 | return 152 | } 153 | 154 | toolbar.items?.forEach 155 | { 156 | $0.enabled = false 157 | } 158 | 159 | let cgImage = ciContext.createCGImage(filteredImageData, fromRect: filteredImageData.extent) 160 | 161 | UIImageWriteToSavedPhotosAlbum(UIImage(CGImage: cgImage), self, "image:didFinishSavingWithError:contextInfo:", nil) 162 | } 163 | 164 | func image(image: UIImage, didFinishSavingWithError error: NSError?, contextInfo:UnsafePointer) 165 | { 166 | toolbar.items?.forEach 167 | { 168 | $0.enabled = true 169 | } 170 | } 171 | 172 | func createImage() 173 | { 174 | guard !isBusy else 175 | { 176 | pendingUpdate = true 177 | return 178 | } 179 | 180 | guard let shadingImage = shadingImage, ciShadingImage = CIImage(image: shadingImage) else 181 | { 182 | return 183 | } 184 | 185 | isBusy = true 186 | 187 | UIGraphicsBeginImageContextWithOptions(CGSize(width: self.label.frame.width, 188 | height: self.label.frame.height), false, 1) 189 | 190 | self.label.layer.renderInContext(UIGraphicsGetCurrentContext()!) 191 | 192 | let textImage = UIGraphicsGetImageFromCurrentImageContext() 193 | 194 | UIGraphicsEndImageContext(); 195 | 196 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) 197 | { 198 | let heightMapFilter = self.heightMapFilter.copy() 199 | let shadedMaterialFilter = self.shadedMaterialFilter.copy() 200 | 201 | heightMapFilter.setValue(CIImage(image: textImage), 202 | forKey: kCIInputImageKey) 203 | 204 | shadedMaterialFilter.setValue(heightMapFilter.valueForKey(kCIOutputImageKey), 205 | forKey: kCIInputImageKey) 206 | 207 | shadedMaterialFilter.setValue(ciShadingImage, 208 | forKey: "inputShadingImage") 209 | 210 | self.filteredImageData = shadedMaterialFilter.valueForKey(kCIOutputImageKey) as? CIImage 211 | 212 | dispatch_async(dispatch_get_main_queue()) 213 | { 214 | self.imageView.setNeedsDisplay() 215 | } 216 | } 217 | } 218 | 219 | // MARK: Layout stuff 220 | 221 | override func layoutSubviews() 222 | { 223 | let availableHeight = frame.height - fontPicker.intrinsicContentSize().height 224 | let toolbarHeight = toolbar.intrinsicContentSize().height 225 | 226 | let mainFrame = CGRect(x: 0, 227 | y: 0, 228 | width: frame.width, 229 | height: availableHeight - toolbarHeight) 230 | 231 | label.frame = mainFrame 232 | imageView.frame = mainFrame 233 | activityIndicator.frame = mainFrame 234 | 235 | fontPicker.frame = CGRect(x: 0, 236 | y: frame.height - fontPicker.intrinsicContentSize().height - toolbarHeight, 237 | width: frame.width, 238 | height: fontPicker.intrinsicContentSize().height) 239 | 240 | toolbar.frame = CGRect(x: 0, 241 | y: frame.height - toolbarHeight, 242 | width: frame.width, 243 | height: toolbarHeight) 244 | } 245 | } 246 | 247 | extension TextEditor: GLKViewDelegate 248 | { 249 | func glkView(view: GLKView, drawInRect rect: CGRect) 250 | { 251 | guard let filteredImageData = filteredImageData else 252 | { 253 | return 254 | } 255 | 256 | ciContext.drawImage(filteredImageData, 257 | inRect: CGRect(x: 0, y: 0, width: imageView.drawableWidth, height: imageView.drawableHeight), 258 | fromRect: filteredImageData.extent) 259 | 260 | isBusy = false 261 | 262 | if pendingUpdate 263 | { 264 | pendingUpdate = false 265 | 266 | createImage() 267 | } 268 | } 269 | } 270 | 271 | extension TextEditor: UIPickerViewDataSource 272 | { 273 | func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int 274 | { 275 | return fonts.count 276 | } 277 | 278 | func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int 279 | { 280 | return 1 281 | } 282 | } 283 | 284 | extension TextEditor: UIPickerViewDelegate 285 | { 286 | func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? 287 | { 288 | return fonts[row] 289 | } 290 | 291 | func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) 292 | { 293 | label.font = UIFont(name: fonts[row], size: 300) 294 | 295 | createImage() 296 | } 297 | 298 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MercurialText 2 | ##### Embossed Type using SceneKit and CIShadedMaterial 3 | ##### _Companion project to http://flexmonkey.blogspot.co.uk/2015/12/mercurialtext-embossed-type-using.html_ 4 | 5 | ![screenshot](screen_shot.png) 6 | 7 | iOS 9 saw the introduction of two new Core Image filters for iOS, `CIHeightFieldFromMask` and `CIShadedMaterial`, which together allow developers to create a 3D embossed image from a monochrome source, such as text or line art. The source for the shading is an image of a hemisphere and it's this that defines the surface appearance. Of course, an external image can be used for shading shading, but since we have SceneKit for rendering 3D, we can use that and add some dynamism to the embossing. 8 | 9 | With that in mind, I present *MercurialText*, a proof-of-concept app that allows users to edit a source material, tweak the lighting and apply that material to user defined text in a variety of fonts. `CIShadedMaterial` does a fantastic job of generating a beautiful metallic embossed surface. 10 | 11 | MercurialText is composed of two main classes, `ShadingImageEditor`, which allows the user to edit the material and its lighting and generates the shading image and `TextEditor` which allows the user to edit their text and applies the Core Image filters to generate the embossed image. 12 | 13 | ## Shading Image Editor 14 | 15 | The shading image editor's user interface consists of a SceneKit view, to display the hemisphere and a table view to display the editable parameters such as shininess and light positions. 16 | 17 | The SceneKit's scene comprises of a solitary sphere and four omni lights. Because the sphere's radius is 1 and I'm using an orthographic camera with an orthographicScale of 1, the sphere nicely fills the frame. 18 | 19 | To populate the table view, I've created an array of `ParameterGroup` each of which contain an array of `Parameter` children. The `Parameter` is interesting (at least IMHO) in that along with fundamental properties such as name and value: 20 | 21 | ```swift 22 | struct Parameter 23 | { 24 | let name: String 25 | let parameterFunction: ParameterFunction 26 | var value: CGFloat 27 | let minMax: MinMax 28 | } 29 | ``` 30 | 31 | ...it also has an enumeration with associated value of type `ParameterFunction`: 32 | 33 | ```swift 34 | enum ParameterFunction 35 | { 36 | case AdjustLightPosition(index: Int, axis: PositionAxis) 37 | case AdjustLightHue(index: Int) 38 | case AdjustLightBrightness(index: Int) 39 | case AdjustMaterialShininess 40 | } 41 | ``` 42 | 43 | ...which the shading image editor uses to update the scene: 44 | 45 | ```swift 46 | func updateSceneFromParameter(parameter: Parameter) 47 | { 48 | switch parameter.parameterFunction 49 | { 50 | case let .AdjustLightPosition(index, axis): 51 | switch axis 52 | { 53 | case .X: 54 | lights[index].position.x = Float(parameter.value) 55 | case .Y: 56 | lights[index].position.y = Float(parameter.value) 57 | case .Z: 58 | lights[index].position.z = Float(parameter.value) 59 | } 60 | 61 | case .AdjustMaterialShininess: 62 | material.shininess = parameter.value 63 | 64 | case let .AdjustLightHue(index): 65 | lights[index].hue = parameter.value 66 | 67 | case let .AdjustLightBrightness(index): 68 | lights[index].brightness = parameter.value 69 | } 70 | 71 | sceneChanged = true 72 | } 73 | ``` 74 | 75 | Whenever the SceneKit scene changes, the editor, which implements `SCNSceneRendererDelegate`, checks the `sceneChanged` flag and, if the change has originated from a user gesture, dispatches a `UIControlEvents.ValueChanged` which is picked up in the view controller. 76 | 77 | To access the rendered image, the editor exposes a computed property, `image`, which is simply a `snapshot()` of the SceneKit view: 78 | 79 | ```swift 80 | var image: UIImage? 81 | { 82 | return sceneKitView.snapshot() 83 | } 84 | ``` 85 | 86 | ## Text Editor / Renderer 87 | 88 | The view controller mediates between the shading image editor and the text editor / renderer. After the image editor dispatches the value changed action, the view controller sets the `shadingImage` property on the text editor instance and invokes `createImage()`: 89 | 90 | ```swift 91 | func shadingImageChange() 92 | { 93 | textEditor.shadingImage = shadingImageEditor.image 94 | 95 | textEditor.createImage() 96 | } 97 | ``` 98 | 99 | It's the `createImage()` function that does all the hard work. A few opening guard statements ensure that it's not already rendering and the necessary images are available: 100 | 101 | ```swift 102 | guard !isBusy else 103 | { 104 | pendingUpdate = true 105 | return 106 | } 107 | 108 | guard let shadingImage = shadingImage, ciShadingImage = CIImage(image: shadingImage) else 109 | { 110 | return 111 | } 112 | ``` 113 | 114 | Then I set the busy flag to true and create a `UIImage` of the original text from its label: 115 | 116 | ```swift 117 | isBusy = true 118 | 119 | UIGraphicsBeginImageContextWithOptions(CGSize(width: self.label.frame.width, 120 | height: self.label.frame.height), false, 1) 121 | 122 | label.layer.renderInContext(UIGraphicsGetCurrentContext()!) 123 | 124 | let textImage = UIGraphicsGetImageFromCurrentImageContext() 125 | 126 | UIGraphicsEndImageContext(); 127 | ``` 128 | 129 | The actual image filtering is done in a background thread to keep the user interface responsive. I create copies of my two Core Image filters, this is probably not necessary in this project, but I think is probably best practice according to this guidance from Apple: 130 | 131 | > _CIContext and CIImage objects are immutable, which means each can be shared safely among threads. Multiple threads can use the same GPU or CPU CIContext object to render CIImage objects. However, this is not the case for CIFilter objects, which are mutable. A CIFilter object cannot be shared safely among threads. If your app is multithreaded, each thread must create its own CIFilter objects. Otherwise, your app could behave unexpectedly._ 132 | 133 | With those copies of the two filters, I set the required values: 134 | 135 | ```swift 136 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) 137 | { 138 | let heightMapFilter = self.heightMapFilter.copy() 139 | let shadedMaterialFilter = self.shadedMaterialFilter.copy() 140 | 141 | heightMapFilter.setValue(CIImage(image: textImage), 142 | forKey: kCIInputImageKey) 143 | 144 | shadedMaterialFilter.setValue(heightMapFilter.valueForKey(kCIOutputImageKey), 145 | forKey: kCIInputImageKey) 146 | 147 | shadedMaterialFilter.setValue(ciShadingImage, 148 | forKey: "inputShadingImage") 149 | 150 | [...] 151 | ``` 152 | 153 | ...and now I'm ready to get the final image from the shaded material filter and generate a `UIImage`: 154 | 155 | ```swift 156 | let filteredImageData = shadedMaterialFilter.valueForKey(kCIOutputImageKey) as! CIImage 157 | let filteredImageRef = self.ciContext.createCGImage(filteredImageData, 158 | fromRect: filteredImageData.extent) 159 | 160 | let finalImage = UIImage(CGImage: filteredImageRef) 161 | ``` 162 | 163 | To ensure the screen updates, setting my image view's image property needs to happen in the main thread. After doing that, I also see if there's a pending update and re-invoke `self.createImage` if that's the case: 164 | 165 | ```swift 166 | dispatch_async(dispatch_get_main_queue()) 167 | { 168 | self.imageView.image = finalImage 169 | self.isBusy = false 170 | 171 | if self.pendingUpdate 172 | { 173 | self.pendingUpdate = false 174 | 175 | self.createImage() 176 | } 177 | } 178 | ``` 179 | 180 | To get the best performance, I've followed Apple's guidelines and created my Core Image context from an EAGL context and turned off colour management: 181 | 182 | ```swift 183 | let ciContext = CIContext(EAGLContext: EAGLContext(API: EAGLRenderingAPI.OpenGLES2), 184 | options: [kCIContextWorkingColorSpace: NSNull()]) 185 | ``` 186 | 187 | ## In Conclusion 188 | 189 | `CIHeightFieldFromMask` and `CIShadedMaterial` do a fantastic job of creating gorgeous looking 3D renderings from flat text. Executing those filters in background threads mitigates the fact they can take a few moments to execute and keep the user interface responsive. Using SceneKit rather than an external image editor allows the fine tweaking of the final results. 190 | 191 | As always, the source code for this project is available at my GitHub repository here. Enjoy! 192 | -------------------------------------------------------------------------------- /screen_shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlexMonkey/MercurialText/b407b73af2e190171fff0a052e260cfbba513408/screen_shot.png --------------------------------------------------------------------------------