├── .github └── CurvyText.png ├── .gitignore ├── CurvyText.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CurvyText ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── ContentView.swift ├── CurvyText.entitlements ├── Draggable.swift ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── SceneDelegate.swift ├── CurvyTextMac ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── ContentView.swift ├── CurvyTextMac.entitlements ├── Draggable.swift ├── Info.plist └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── LICENSE ├── PathText ├── Package.swift ├── Sources │ └── PathText │ │ ├── AppKit.swift │ │ ├── GlyphRun.swift │ │ ├── LayoutManager.swift │ │ ├── PathSection.swift │ │ ├── Previews.swift │ │ ├── SwiftUI.swift │ │ └── UIKit.swift └── Tests │ └── PathTextTests │ └── PathTextTests.swift ├── PathTextDemo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist └── ViewController.swift ├── PathTextDemoMac ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── PathTextDemoMac.entitlements └── ViewController.swift └── README.md /.github/CurvyText.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rnapier/CurvyText/605cf8f4e4a75d7a9f6a5a029ee7cfcb54b2a665/.github/CurvyText.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,xcode,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,xcode,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### macOS Patch ### 33 | # iCloud generated files 34 | *.icloud 35 | 36 | ### Swift ### 37 | # Xcode 38 | # 39 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 40 | 41 | ## User settings 42 | xcuserdata/ 43 | 44 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 45 | *.xcscmblueprint 46 | *.xccheckout 47 | 48 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 49 | build/ 50 | DerivedData/ 51 | *.moved-aside 52 | *.pbxuser 53 | !default.pbxuser 54 | *.mode1v3 55 | !default.mode1v3 56 | *.mode2v3 57 | !default.mode2v3 58 | *.perspectivev3 59 | !default.perspectivev3 60 | 61 | ## Obj-C/Swift specific 62 | *.hmap 63 | 64 | ## App packaging 65 | *.ipa 66 | *.dSYM.zip 67 | *.dSYM 68 | 69 | ## Playgrounds 70 | timeline.xctimeline 71 | playground.xcworkspace 72 | 73 | # Swift Package Manager 74 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 75 | # Packages/ 76 | # Package.pins 77 | # Package.resolved 78 | # *.xcodeproj 79 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 80 | # hence it is not needed unless you have added a package configuration file to your project 81 | # .swiftpm 82 | 83 | .build/ 84 | 85 | # CocoaPods 86 | # We recommend against adding the Pods directory to your .gitignore. However 87 | # you should judge for yourself, the pros and cons are mentioned at: 88 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 89 | # Pods/ 90 | # Add this line if you want to avoid checking in source code from the Xcode workspace 91 | # *.xcworkspace 92 | 93 | # Carthage 94 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 95 | # Carthage/Checkouts 96 | 97 | Carthage/Build/ 98 | 99 | # Accio dependency management 100 | Dependencies/ 101 | .accio/ 102 | 103 | # fastlane 104 | # It is recommended to not store the screenshots in the git repo. 105 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 106 | # For more information about the recommended setup visit: 107 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 108 | 109 | fastlane/report.xml 110 | fastlane/Preview.html 111 | fastlane/screenshots/**/*.png 112 | fastlane/test_output 113 | 114 | # Code Injection 115 | # After new code Injection tools there's a generated folder /iOSInjectionProject 116 | # https://github.com/johnno1962/injectionforxcode 117 | 118 | iOSInjectionProject/ 119 | 120 | ### Xcode ### 121 | 122 | ## Xcode 8 and earlier 123 | 124 | ### Xcode Patch ### 125 | *.xcodeproj/* 126 | !*.xcodeproj/project.pbxproj 127 | !*.xcodeproj/xcshareddata/ 128 | !*.xcodeproj/project.xcworkspace/ 129 | !*.xcworkspace/contents.xcworkspacedata 130 | /*.gcno 131 | **/xcshareddata/WorkspaceSettings.xcsettings 132 | 133 | # End of https://www.toptal.com/developers/gitignore/api/swift,xcode,macos 134 | 135 | -------------------------------------------------------------------------------- /CurvyText.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FB05B63523C253EB00B01325 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB05B63423C253EB00B01325 /* AppDelegate.swift */; }; 11 | FB05B63723C253EB00B01325 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB05B63623C253EB00B01325 /* ViewController.swift */; }; 12 | FB05B63923C253EC00B01325 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FB05B63823C253EC00B01325 /* Assets.xcassets */; }; 13 | FB05B63C23C253EC00B01325 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB05B63A23C253EC00B01325 /* Main.storyboard */; }; 14 | FB05B64323C2542300B01325 /* PathText in Frameworks */ = {isa = PBXBuildFile; productRef = FB05B64223C2542300B01325 /* PathText */; }; 15 | FB061B4323C27C8B005AB1D5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB061B4223C27C8B005AB1D5 /* SceneDelegate.swift */; }; 16 | FB2C3664239AA4AC002DDB93 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB2C3663239AA4AC002DDB93 /* AppDelegate.swift */; }; 17 | FB2C3668239AA4AC002DDB93 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB2C3667239AA4AC002DDB93 /* ContentView.swift */; }; 18 | FB2C366A239AA4AF002DDB93 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FB2C3669239AA4AF002DDB93 /* Assets.xcassets */; }; 19 | FB2C366D239AA4AF002DDB93 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FB2C366C239AA4AF002DDB93 /* Preview Assets.xcassets */; }; 20 | FB2C3670239AA4AF002DDB93 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB2C366E239AA4AF002DDB93 /* LaunchScreen.storyboard */; }; 21 | FB2C367E239C09CE002DDB93 /* PathText in Frameworks */ = {isa = PBXBuildFile; productRef = FB2C367D239C09CE002DDB93 /* PathText */; }; 22 | FBF07A4523C24A59001E04DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBF07A4423C24A59001E04DD /* AppDelegate.swift */; }; 23 | FBF07A4723C24A59001E04DD /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBF07A4623C24A59001E04DD /* ContentView.swift */; }; 24 | FBF07A4923C24A5B001E04DD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FBF07A4823C24A5B001E04DD /* Assets.xcassets */; }; 25 | FBF07A4C23C24A5B001E04DD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FBF07A4B23C24A5B001E04DD /* Preview Assets.xcassets */; }; 26 | FBF07A4F23C24A5B001E04DD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FBF07A4D23C24A5B001E04DD /* Main.storyboard */; }; 27 | FBF07A5523C24B04001E04DD /* Draggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB2C367F239C0B0B002DDB93 /* Draggable.swift */; }; 28 | FBF07A5723C24B31001E04DD /* PathText in Frameworks */ = {isa = PBXBuildFile; productRef = FBF07A5623C24B31001E04DD /* PathText */; }; 29 | FBF07A5923C24C9A001E04DD /* Draggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBF07A5823C24C9A001E04DD /* Draggable.swift */; }; 30 | FBF07A8923C24EA1001E04DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBF07A8823C24EA1001E04DD /* AppDelegate.swift */; }; 31 | FBF07A8D23C24EA1001E04DD /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBF07A8C23C24EA1001E04DD /* ViewController.swift */; }; 32 | FBF07A9023C24EA1001E04DD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FBF07A8E23C24EA1001E04DD /* Main.storyboard */; }; 33 | FBF07A9223C24EA2001E04DD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FBF07A9123C24EA2001E04DD /* Assets.xcassets */; }; 34 | FBF07A9523C24EA2001E04DD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FBF07A9323C24EA2001E04DD /* LaunchScreen.storyboard */; }; 35 | FBF07A9B23C24EDE001E04DD /* PathText in Frameworks */ = {isa = PBXBuildFile; productRef = FBF07A9A23C24EDE001E04DD /* PathText */; }; 36 | /* End PBXBuildFile section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | FB05B63223C253EB00B01325 /* PathTextDemoMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PathTextDemoMac.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | FB05B63423C253EB00B01325 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 41 | FB05B63623C253EB00B01325 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 42 | FB05B63823C253EC00B01325 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | FB05B63B23C253EC00B01325 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 44 | FB05B63D23C253EC00B01325 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | FB05B63E23C253EC00B01325 /* PathTextDemoMac.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PathTextDemoMac.entitlements; sourceTree = ""; }; 46 | FB061B4223C27C8B005AB1D5 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 47 | FB2C3660239AA4AC002DDB93 /* CurvyText.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CurvyText.app; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | FB2C3663239AA4AC002DDB93 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 49 | FB2C3667239AA4AC002DDB93 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 50 | FB2C3669239AA4AF002DDB93 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 51 | FB2C366C239AA4AF002DDB93 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 52 | FB2C366F239AA4AF002DDB93 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 53 | FB2C3671239AA4AF002DDB93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 54 | FB2C367B239C0868002DDB93 /* PathText */ = {isa = PBXFileReference; lastKnownFileType = folder; path = PathText; sourceTree = ""; }; 55 | FB2C367F239C0B0B002DDB93 /* Draggable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Draggable.swift; sourceTree = ""; }; 56 | FBF07A3D23C248AE001E04DD /* CurvyText.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CurvyText.entitlements; sourceTree = ""; }; 57 | FBF07A4223C24A59001E04DD /* CurvyTextMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CurvyTextMac.app; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | FBF07A4423C24A59001E04DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 59 | FBF07A4623C24A59001E04DD /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 60 | FBF07A4823C24A5B001E04DD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 61 | FBF07A4B23C24A5B001E04DD /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 62 | FBF07A4E23C24A5B001E04DD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 63 | FBF07A5023C24A5B001E04DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64 | FBF07A5123C24A5B001E04DD /* CurvyTextMac.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CurvyTextMac.entitlements; sourceTree = ""; }; 65 | FBF07A5823C24C9A001E04DD /* Draggable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Draggable.swift; sourceTree = ""; }; 66 | FBF07A8623C24EA1001E04DD /* PathTextDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PathTextDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 67 | FBF07A8823C24EA1001E04DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 68 | FBF07A8C23C24EA1001E04DD /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 69 | FBF07A8F23C24EA1001E04DD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 70 | FBF07A9123C24EA2001E04DD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 71 | FBF07A9423C24EA2001E04DD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 72 | FBF07A9623C24EA2001E04DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | /* End PBXFileReference section */ 74 | 75 | /* Begin PBXFrameworksBuildPhase section */ 76 | FB05B62F23C253EB00B01325 /* Frameworks */ = { 77 | isa = PBXFrameworksBuildPhase; 78 | buildActionMask = 2147483647; 79 | files = ( 80 | FB05B64323C2542300B01325 /* PathText in Frameworks */, 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | FB2C365D239AA4AC002DDB93 /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | FB2C367E239C09CE002DDB93 /* PathText in Frameworks */, 89 | ); 90 | runOnlyForDeploymentPostprocessing = 0; 91 | }; 92 | FBF07A3F23C24A59001E04DD /* Frameworks */ = { 93 | isa = PBXFrameworksBuildPhase; 94 | buildActionMask = 2147483647; 95 | files = ( 96 | FBF07A5723C24B31001E04DD /* PathText in Frameworks */, 97 | ); 98 | runOnlyForDeploymentPostprocessing = 0; 99 | }; 100 | FBF07A8323C24EA1001E04DD /* Frameworks */ = { 101 | isa = PBXFrameworksBuildPhase; 102 | buildActionMask = 2147483647; 103 | files = ( 104 | FBF07A9B23C24EDE001E04DD /* PathText in Frameworks */, 105 | ); 106 | runOnlyForDeploymentPostprocessing = 0; 107 | }; 108 | /* End PBXFrameworksBuildPhase section */ 109 | 110 | /* Begin PBXGroup section */ 111 | FB05B63323C253EB00B01325 /* PathTextDemoMac */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | FB05B63423C253EB00B01325 /* AppDelegate.swift */, 115 | FB05B63623C253EB00B01325 /* ViewController.swift */, 116 | FB05B63823C253EC00B01325 /* Assets.xcassets */, 117 | FB05B63A23C253EC00B01325 /* Main.storyboard */, 118 | FB05B63D23C253EC00B01325 /* Info.plist */, 119 | FB05B63E23C253EC00B01325 /* PathTextDemoMac.entitlements */, 120 | ); 121 | path = PathTextDemoMac; 122 | sourceTree = ""; 123 | }; 124 | FB2C3657239AA4AC002DDB93 = { 125 | isa = PBXGroup; 126 | children = ( 127 | FB2C367B239C0868002DDB93 /* PathText */, 128 | FB2C3662239AA4AC002DDB93 /* CurvyText */, 129 | FBF07A4323C24A59001E04DD /* CurvyTextMac */, 130 | FBF07A8723C24EA1001E04DD /* PathTextDemo */, 131 | FB05B63323C253EB00B01325 /* PathTextDemoMac */, 132 | FB2C3661239AA4AC002DDB93 /* Products */, 133 | FB2C367C239C09CE002DDB93 /* Frameworks */, 134 | ); 135 | sourceTree = ""; 136 | }; 137 | FB2C3661239AA4AC002DDB93 /* Products */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | FB2C3660239AA4AC002DDB93 /* CurvyText.app */, 141 | FBF07A4223C24A59001E04DD /* CurvyTextMac.app */, 142 | FBF07A8623C24EA1001E04DD /* PathTextDemo.app */, 143 | FB05B63223C253EB00B01325 /* PathTextDemoMac.app */, 144 | ); 145 | name = Products; 146 | sourceTree = ""; 147 | }; 148 | FB2C3662239AA4AC002DDB93 /* CurvyText */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | FBF07A5823C24C9A001E04DD /* Draggable.swift */, 152 | FBF07A3D23C248AE001E04DD /* CurvyText.entitlements */, 153 | FB2C3663239AA4AC002DDB93 /* AppDelegate.swift */, 154 | FB061B4223C27C8B005AB1D5 /* SceneDelegate.swift */, 155 | FB2C3667239AA4AC002DDB93 /* ContentView.swift */, 156 | FB2C3669239AA4AF002DDB93 /* Assets.xcassets */, 157 | FB2C366E239AA4AF002DDB93 /* LaunchScreen.storyboard */, 158 | FB2C3671239AA4AF002DDB93 /* Info.plist */, 159 | FB2C366B239AA4AF002DDB93 /* Preview Content */, 160 | ); 161 | path = CurvyText; 162 | sourceTree = ""; 163 | }; 164 | FB2C366B239AA4AF002DDB93 /* Preview Content */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | FB2C366C239AA4AF002DDB93 /* Preview Assets.xcassets */, 168 | ); 169 | path = "Preview Content"; 170 | sourceTree = ""; 171 | }; 172 | FB2C367C239C09CE002DDB93 /* Frameworks */ = { 173 | isa = PBXGroup; 174 | children = ( 175 | ); 176 | name = Frameworks; 177 | sourceTree = ""; 178 | }; 179 | FBF07A4323C24A59001E04DD /* CurvyTextMac */ = { 180 | isa = PBXGroup; 181 | children = ( 182 | FBF07A4423C24A59001E04DD /* AppDelegate.swift */, 183 | FBF07A4623C24A59001E04DD /* ContentView.swift */, 184 | FB2C367F239C0B0B002DDB93 /* Draggable.swift */, 185 | FBF07A4823C24A5B001E04DD /* Assets.xcassets */, 186 | FBF07A4D23C24A5B001E04DD /* Main.storyboard */, 187 | FBF07A5023C24A5B001E04DD /* Info.plist */, 188 | FBF07A5123C24A5B001E04DD /* CurvyTextMac.entitlements */, 189 | FBF07A4A23C24A5B001E04DD /* Preview Content */, 190 | ); 191 | path = CurvyTextMac; 192 | sourceTree = ""; 193 | }; 194 | FBF07A4A23C24A5B001E04DD /* Preview Content */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | FBF07A4B23C24A5B001E04DD /* Preview Assets.xcassets */, 198 | ); 199 | path = "Preview Content"; 200 | sourceTree = ""; 201 | }; 202 | FBF07A8723C24EA1001E04DD /* PathTextDemo */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | FBF07A8823C24EA1001E04DD /* AppDelegate.swift */, 206 | FBF07A8C23C24EA1001E04DD /* ViewController.swift */, 207 | FBF07A8E23C24EA1001E04DD /* Main.storyboard */, 208 | FBF07A9123C24EA2001E04DD /* Assets.xcassets */, 209 | FBF07A9323C24EA2001E04DD /* LaunchScreen.storyboard */, 210 | FBF07A9623C24EA2001E04DD /* Info.plist */, 211 | ); 212 | path = PathTextDemo; 213 | sourceTree = ""; 214 | }; 215 | /* End PBXGroup section */ 216 | 217 | /* Begin PBXNativeTarget section */ 218 | FB05B63123C253EB00B01325 /* PathTextDemoMac */ = { 219 | isa = PBXNativeTarget; 220 | buildConfigurationList = FB05B63F23C253EC00B01325 /* Build configuration list for PBXNativeTarget "PathTextDemoMac" */; 221 | buildPhases = ( 222 | FB05B62E23C253EB00B01325 /* Sources */, 223 | FB05B62F23C253EB00B01325 /* Frameworks */, 224 | FB05B63023C253EB00B01325 /* Resources */, 225 | ); 226 | buildRules = ( 227 | ); 228 | dependencies = ( 229 | ); 230 | name = PathTextDemoMac; 231 | packageProductDependencies = ( 232 | FB05B64223C2542300B01325 /* PathText */, 233 | ); 234 | productName = PathTextDemoMac; 235 | productReference = FB05B63223C253EB00B01325 /* PathTextDemoMac.app */; 236 | productType = "com.apple.product-type.application"; 237 | }; 238 | FB2C365F239AA4AC002DDB93 /* CurvyText */ = { 239 | isa = PBXNativeTarget; 240 | buildConfigurationList = FB2C3674239AA4AF002DDB93 /* Build configuration list for PBXNativeTarget "CurvyText" */; 241 | buildPhases = ( 242 | FB2C365C239AA4AC002DDB93 /* Sources */, 243 | FB2C365D239AA4AC002DDB93 /* Frameworks */, 244 | FB2C365E239AA4AC002DDB93 /* Resources */, 245 | ); 246 | buildRules = ( 247 | ); 248 | dependencies = ( 249 | ); 250 | name = CurvyText; 251 | packageProductDependencies = ( 252 | FB2C367D239C09CE002DDB93 /* PathText */, 253 | ); 254 | productName = CurvyText; 255 | productReference = FB2C3660239AA4AC002DDB93 /* CurvyText.app */; 256 | productType = "com.apple.product-type.application"; 257 | }; 258 | FBF07A4123C24A59001E04DD /* CurvyTextMac */ = { 259 | isa = PBXNativeTarget; 260 | buildConfigurationList = FBF07A5223C24A5B001E04DD /* Build configuration list for PBXNativeTarget "CurvyTextMac" */; 261 | buildPhases = ( 262 | FBF07A3E23C24A59001E04DD /* Sources */, 263 | FBF07A3F23C24A59001E04DD /* Frameworks */, 264 | FBF07A4023C24A59001E04DD /* Resources */, 265 | ); 266 | buildRules = ( 267 | ); 268 | dependencies = ( 269 | ); 270 | name = CurvyTextMac; 271 | packageProductDependencies = ( 272 | FBF07A5623C24B31001E04DD /* PathText */, 273 | ); 274 | productName = CurvyTextMac; 275 | productReference = FBF07A4223C24A59001E04DD /* CurvyTextMac.app */; 276 | productType = "com.apple.product-type.application"; 277 | }; 278 | FBF07A8523C24EA1001E04DD /* PathTextDemo */ = { 279 | isa = PBXNativeTarget; 280 | buildConfigurationList = FBF07A9723C24EA2001E04DD /* Build configuration list for PBXNativeTarget "PathTextDemo" */; 281 | buildPhases = ( 282 | FBF07A8223C24EA1001E04DD /* Sources */, 283 | FBF07A8323C24EA1001E04DD /* Frameworks */, 284 | FBF07A8423C24EA1001E04DD /* Resources */, 285 | ); 286 | buildRules = ( 287 | ); 288 | dependencies = ( 289 | ); 290 | name = PathTextDemo; 291 | packageProductDependencies = ( 292 | FBF07A9A23C24EDE001E04DD /* PathText */, 293 | ); 294 | productName = PathTextDemo; 295 | productReference = FBF07A8623C24EA1001E04DD /* PathTextDemo.app */; 296 | productType = "com.apple.product-type.application"; 297 | }; 298 | /* End PBXNativeTarget section */ 299 | 300 | /* Begin PBXProject section */ 301 | FB2C3658239AA4AC002DDB93 /* Project object */ = { 302 | isa = PBXProject; 303 | attributes = { 304 | LastSwiftUpdateCheck = 1130; 305 | LastUpgradeCheck = 1120; 306 | ORGANIZATIONNAME = "Rob Napier"; 307 | TargetAttributes = { 308 | FB05B63123C253EB00B01325 = { 309 | CreatedOnToolsVersion = 11.3; 310 | }; 311 | FB2C365F239AA4AC002DDB93 = { 312 | CreatedOnToolsVersion = 11.2.1; 313 | }; 314 | FBF07A4123C24A59001E04DD = { 315 | CreatedOnToolsVersion = 11.3; 316 | }; 317 | FBF07A8523C24EA1001E04DD = { 318 | CreatedOnToolsVersion = 11.3; 319 | }; 320 | }; 321 | }; 322 | buildConfigurationList = FB2C365B239AA4AC002DDB93 /* Build configuration list for PBXProject "CurvyText" */; 323 | compatibilityVersion = "Xcode 9.3"; 324 | developmentRegion = en; 325 | hasScannedForEncodings = 0; 326 | knownRegions = ( 327 | en, 328 | Base, 329 | ); 330 | mainGroup = FB2C3657239AA4AC002DDB93; 331 | productRefGroup = FB2C3661239AA4AC002DDB93 /* Products */; 332 | projectDirPath = ""; 333 | projectRoot = ""; 334 | targets = ( 335 | FB2C365F239AA4AC002DDB93 /* CurvyText */, 336 | FBF07A4123C24A59001E04DD /* CurvyTextMac */, 337 | FBF07A8523C24EA1001E04DD /* PathTextDemo */, 338 | FB05B63123C253EB00B01325 /* PathTextDemoMac */, 339 | ); 340 | }; 341 | /* End PBXProject section */ 342 | 343 | /* Begin PBXResourcesBuildPhase section */ 344 | FB05B63023C253EB00B01325 /* Resources */ = { 345 | isa = PBXResourcesBuildPhase; 346 | buildActionMask = 2147483647; 347 | files = ( 348 | FB05B63923C253EC00B01325 /* Assets.xcassets in Resources */, 349 | FB05B63C23C253EC00B01325 /* Main.storyboard in Resources */, 350 | ); 351 | runOnlyForDeploymentPostprocessing = 0; 352 | }; 353 | FB2C365E239AA4AC002DDB93 /* Resources */ = { 354 | isa = PBXResourcesBuildPhase; 355 | buildActionMask = 2147483647; 356 | files = ( 357 | FB2C3670239AA4AF002DDB93 /* LaunchScreen.storyboard in Resources */, 358 | FB2C366D239AA4AF002DDB93 /* Preview Assets.xcassets in Resources */, 359 | FB2C366A239AA4AF002DDB93 /* Assets.xcassets in Resources */, 360 | ); 361 | runOnlyForDeploymentPostprocessing = 0; 362 | }; 363 | FBF07A4023C24A59001E04DD /* Resources */ = { 364 | isa = PBXResourcesBuildPhase; 365 | buildActionMask = 2147483647; 366 | files = ( 367 | FBF07A4F23C24A5B001E04DD /* Main.storyboard in Resources */, 368 | FBF07A4C23C24A5B001E04DD /* Preview Assets.xcassets in Resources */, 369 | FBF07A4923C24A5B001E04DD /* Assets.xcassets in Resources */, 370 | ); 371 | runOnlyForDeploymentPostprocessing = 0; 372 | }; 373 | FBF07A8423C24EA1001E04DD /* Resources */ = { 374 | isa = PBXResourcesBuildPhase; 375 | buildActionMask = 2147483647; 376 | files = ( 377 | FBF07A9523C24EA2001E04DD /* LaunchScreen.storyboard in Resources */, 378 | FBF07A9223C24EA2001E04DD /* Assets.xcassets in Resources */, 379 | FBF07A9023C24EA1001E04DD /* Main.storyboard in Resources */, 380 | ); 381 | runOnlyForDeploymentPostprocessing = 0; 382 | }; 383 | /* End PBXResourcesBuildPhase section */ 384 | 385 | /* Begin PBXSourcesBuildPhase section */ 386 | FB05B62E23C253EB00B01325 /* Sources */ = { 387 | isa = PBXSourcesBuildPhase; 388 | buildActionMask = 2147483647; 389 | files = ( 390 | FB05B63723C253EB00B01325 /* ViewController.swift in Sources */, 391 | FB05B63523C253EB00B01325 /* AppDelegate.swift in Sources */, 392 | ); 393 | runOnlyForDeploymentPostprocessing = 0; 394 | }; 395 | FB2C365C239AA4AC002DDB93 /* Sources */ = { 396 | isa = PBXSourcesBuildPhase; 397 | buildActionMask = 2147483647; 398 | files = ( 399 | FB2C3664239AA4AC002DDB93 /* AppDelegate.swift in Sources */, 400 | FB2C3668239AA4AC002DDB93 /* ContentView.swift in Sources */, 401 | FBF07A5923C24C9A001E04DD /* Draggable.swift in Sources */, 402 | FB061B4323C27C8B005AB1D5 /* SceneDelegate.swift in Sources */, 403 | ); 404 | runOnlyForDeploymentPostprocessing = 0; 405 | }; 406 | FBF07A3E23C24A59001E04DD /* Sources */ = { 407 | isa = PBXSourcesBuildPhase; 408 | buildActionMask = 2147483647; 409 | files = ( 410 | FBF07A5523C24B04001E04DD /* Draggable.swift in Sources */, 411 | FBF07A4723C24A59001E04DD /* ContentView.swift in Sources */, 412 | FBF07A4523C24A59001E04DD /* AppDelegate.swift in Sources */, 413 | ); 414 | runOnlyForDeploymentPostprocessing = 0; 415 | }; 416 | FBF07A8223C24EA1001E04DD /* Sources */ = { 417 | isa = PBXSourcesBuildPhase; 418 | buildActionMask = 2147483647; 419 | files = ( 420 | FBF07A8D23C24EA1001E04DD /* ViewController.swift in Sources */, 421 | FBF07A8923C24EA1001E04DD /* AppDelegate.swift in Sources */, 422 | ); 423 | runOnlyForDeploymentPostprocessing = 0; 424 | }; 425 | /* End PBXSourcesBuildPhase section */ 426 | 427 | /* Begin PBXVariantGroup section */ 428 | FB05B63A23C253EC00B01325 /* Main.storyboard */ = { 429 | isa = PBXVariantGroup; 430 | children = ( 431 | FB05B63B23C253EC00B01325 /* Base */, 432 | ); 433 | name = Main.storyboard; 434 | sourceTree = ""; 435 | }; 436 | FB2C366E239AA4AF002DDB93 /* LaunchScreen.storyboard */ = { 437 | isa = PBXVariantGroup; 438 | children = ( 439 | FB2C366F239AA4AF002DDB93 /* Base */, 440 | ); 441 | name = LaunchScreen.storyboard; 442 | sourceTree = ""; 443 | }; 444 | FBF07A4D23C24A5B001E04DD /* Main.storyboard */ = { 445 | isa = PBXVariantGroup; 446 | children = ( 447 | FBF07A4E23C24A5B001E04DD /* Base */, 448 | ); 449 | name = Main.storyboard; 450 | sourceTree = ""; 451 | }; 452 | FBF07A8E23C24EA1001E04DD /* Main.storyboard */ = { 453 | isa = PBXVariantGroup; 454 | children = ( 455 | FBF07A8F23C24EA1001E04DD /* Base */, 456 | ); 457 | name = Main.storyboard; 458 | sourceTree = ""; 459 | }; 460 | FBF07A9323C24EA2001E04DD /* LaunchScreen.storyboard */ = { 461 | isa = PBXVariantGroup; 462 | children = ( 463 | FBF07A9423C24EA2001E04DD /* Base */, 464 | ); 465 | name = LaunchScreen.storyboard; 466 | sourceTree = ""; 467 | }; 468 | /* End PBXVariantGroup section */ 469 | 470 | /* Begin XCBuildConfiguration section */ 471 | FB05B64023C253EC00B01325 /* Debug */ = { 472 | isa = XCBuildConfiguration; 473 | buildSettings = { 474 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 475 | CODE_SIGN_ENTITLEMENTS = PathTextDemoMac/PathTextDemoMac.entitlements; 476 | CODE_SIGN_STYLE = Automatic; 477 | COMBINE_HIDPI_IMAGES = YES; 478 | DEVELOPMENT_TEAM = J374TBL6J6; 479 | ENABLE_HARDENED_RUNTIME = YES; 480 | INFOPLIST_FILE = PathTextDemoMac/Info.plist; 481 | LD_RUNPATH_SEARCH_PATHS = ( 482 | "$(inherited)", 483 | "@executable_path/../Frameworks", 484 | ); 485 | MACOSX_DEPLOYMENT_TARGET = 10.10; 486 | PRODUCT_BUNDLE_IDENTIFIER = net.robnapier.PathTextDemoMac; 487 | PRODUCT_NAME = "$(TARGET_NAME)"; 488 | SDKROOT = macosx; 489 | SWIFT_VERSION = 5.0; 490 | }; 491 | name = Debug; 492 | }; 493 | FB05B64123C253EC00B01325 /* Release */ = { 494 | isa = XCBuildConfiguration; 495 | buildSettings = { 496 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 497 | CODE_SIGN_ENTITLEMENTS = PathTextDemoMac/PathTextDemoMac.entitlements; 498 | CODE_SIGN_STYLE = Automatic; 499 | COMBINE_HIDPI_IMAGES = YES; 500 | DEVELOPMENT_TEAM = J374TBL6J6; 501 | ENABLE_HARDENED_RUNTIME = YES; 502 | INFOPLIST_FILE = PathTextDemoMac/Info.plist; 503 | LD_RUNPATH_SEARCH_PATHS = ( 504 | "$(inherited)", 505 | "@executable_path/../Frameworks", 506 | ); 507 | MACOSX_DEPLOYMENT_TARGET = 10.10; 508 | PRODUCT_BUNDLE_IDENTIFIER = net.robnapier.PathTextDemoMac; 509 | PRODUCT_NAME = "$(TARGET_NAME)"; 510 | SDKROOT = macosx; 511 | SWIFT_VERSION = 5.0; 512 | }; 513 | name = Release; 514 | }; 515 | FB2C3672239AA4AF002DDB93 /* Debug */ = { 516 | isa = XCBuildConfiguration; 517 | buildSettings = { 518 | ALWAYS_SEARCH_USER_PATHS = NO; 519 | CLANG_ANALYZER_NONNULL = YES; 520 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 521 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 522 | CLANG_CXX_LIBRARY = "libc++"; 523 | CLANG_ENABLE_MODULES = YES; 524 | CLANG_ENABLE_OBJC_ARC = YES; 525 | CLANG_ENABLE_OBJC_WEAK = YES; 526 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 527 | CLANG_WARN_BOOL_CONVERSION = YES; 528 | CLANG_WARN_COMMA = YES; 529 | CLANG_WARN_CONSTANT_CONVERSION = YES; 530 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 531 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 532 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 533 | CLANG_WARN_EMPTY_BODY = YES; 534 | CLANG_WARN_ENUM_CONVERSION = YES; 535 | CLANG_WARN_INFINITE_RECURSION = YES; 536 | CLANG_WARN_INT_CONVERSION = YES; 537 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 538 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 539 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 540 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 541 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 542 | CLANG_WARN_STRICT_PROTOTYPES = YES; 543 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 544 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 545 | CLANG_WARN_UNREACHABLE_CODE = YES; 546 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 547 | COPY_PHASE_STRIP = NO; 548 | DEBUG_INFORMATION_FORMAT = dwarf; 549 | ENABLE_STRICT_OBJC_MSGSEND = YES; 550 | ENABLE_TESTABILITY = YES; 551 | GCC_C_LANGUAGE_STANDARD = gnu11; 552 | GCC_DYNAMIC_NO_PIC = NO; 553 | GCC_NO_COMMON_BLOCKS = YES; 554 | GCC_OPTIMIZATION_LEVEL = 0; 555 | GCC_PREPROCESSOR_DEFINITIONS = ( 556 | "DEBUG=1", 557 | "$(inherited)", 558 | ); 559 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 560 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 561 | GCC_WARN_UNDECLARED_SELECTOR = YES; 562 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 563 | GCC_WARN_UNUSED_FUNCTION = YES; 564 | GCC_WARN_UNUSED_VARIABLE = YES; 565 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 566 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 567 | MTL_FAST_MATH = YES; 568 | ONLY_ACTIVE_ARCH = YES; 569 | SDKROOT = iphoneos; 570 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 571 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 572 | }; 573 | name = Debug; 574 | }; 575 | FB2C3673239AA4AF002DDB93 /* Release */ = { 576 | isa = XCBuildConfiguration; 577 | buildSettings = { 578 | ALWAYS_SEARCH_USER_PATHS = NO; 579 | CLANG_ANALYZER_NONNULL = YES; 580 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 581 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 582 | CLANG_CXX_LIBRARY = "libc++"; 583 | CLANG_ENABLE_MODULES = YES; 584 | CLANG_ENABLE_OBJC_ARC = YES; 585 | CLANG_ENABLE_OBJC_WEAK = YES; 586 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 587 | CLANG_WARN_BOOL_CONVERSION = YES; 588 | CLANG_WARN_COMMA = YES; 589 | CLANG_WARN_CONSTANT_CONVERSION = YES; 590 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 591 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 592 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 593 | CLANG_WARN_EMPTY_BODY = YES; 594 | CLANG_WARN_ENUM_CONVERSION = YES; 595 | CLANG_WARN_INFINITE_RECURSION = YES; 596 | CLANG_WARN_INT_CONVERSION = YES; 597 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 598 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 599 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 600 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 601 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 602 | CLANG_WARN_STRICT_PROTOTYPES = YES; 603 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 604 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 605 | CLANG_WARN_UNREACHABLE_CODE = YES; 606 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 607 | COPY_PHASE_STRIP = NO; 608 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 609 | ENABLE_NS_ASSERTIONS = NO; 610 | ENABLE_STRICT_OBJC_MSGSEND = YES; 611 | GCC_C_LANGUAGE_STANDARD = gnu11; 612 | GCC_NO_COMMON_BLOCKS = YES; 613 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 614 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 615 | GCC_WARN_UNDECLARED_SELECTOR = YES; 616 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 617 | GCC_WARN_UNUSED_FUNCTION = YES; 618 | GCC_WARN_UNUSED_VARIABLE = YES; 619 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 620 | MTL_ENABLE_DEBUG_INFO = NO; 621 | MTL_FAST_MATH = YES; 622 | SDKROOT = iphoneos; 623 | SWIFT_COMPILATION_MODE = wholemodule; 624 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 625 | VALIDATE_PRODUCT = YES; 626 | }; 627 | name = Release; 628 | }; 629 | FB2C3675239AA4AF002DDB93 /* Debug */ = { 630 | isa = XCBuildConfiguration; 631 | buildSettings = { 632 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 633 | CODE_SIGN_ENTITLEMENTS = CurvyText/CurvyText.entitlements; 634 | CODE_SIGN_STYLE = Automatic; 635 | DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; 636 | DEVELOPMENT_ASSET_PATHS = "\"CurvyText/Preview Content\""; 637 | DEVELOPMENT_TEAM = J374TBL6J6; 638 | ENABLE_PREVIEWS = YES; 639 | INFOPLIST_FILE = CurvyText/Info.plist; 640 | LD_RUNPATH_SEARCH_PATHS = ( 641 | "$(inherited)", 642 | "@executable_path/Frameworks", 643 | ); 644 | PRODUCT_BUNDLE_IDENTIFIER = net.robnapier.CurvyText; 645 | PRODUCT_NAME = "$(TARGET_NAME)"; 646 | SUPPORTS_MACCATALYST = YES; 647 | SWIFT_VERSION = 5.0; 648 | TARGETED_DEVICE_FAMILY = "1,2"; 649 | }; 650 | name = Debug; 651 | }; 652 | FB2C3676239AA4AF002DDB93 /* Release */ = { 653 | isa = XCBuildConfiguration; 654 | buildSettings = { 655 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 656 | CODE_SIGN_ENTITLEMENTS = CurvyText/CurvyText.entitlements; 657 | CODE_SIGN_STYLE = Automatic; 658 | DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; 659 | DEVELOPMENT_ASSET_PATHS = "\"CurvyText/Preview Content\""; 660 | DEVELOPMENT_TEAM = J374TBL6J6; 661 | ENABLE_PREVIEWS = YES; 662 | INFOPLIST_FILE = CurvyText/Info.plist; 663 | LD_RUNPATH_SEARCH_PATHS = ( 664 | "$(inherited)", 665 | "@executable_path/Frameworks", 666 | ); 667 | PRODUCT_BUNDLE_IDENTIFIER = net.robnapier.CurvyText; 668 | PRODUCT_NAME = "$(TARGET_NAME)"; 669 | SUPPORTS_MACCATALYST = YES; 670 | SWIFT_VERSION = 5.0; 671 | TARGETED_DEVICE_FAMILY = "1,2"; 672 | }; 673 | name = Release; 674 | }; 675 | FBF07A5323C24A5B001E04DD /* Debug */ = { 676 | isa = XCBuildConfiguration; 677 | buildSettings = { 678 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 679 | CODE_SIGN_ENTITLEMENTS = CurvyTextMac/CurvyTextMac.entitlements; 680 | CODE_SIGN_STYLE = Automatic; 681 | COMBINE_HIDPI_IMAGES = YES; 682 | DEVELOPMENT_ASSET_PATHS = "\"CurvyTextMac/Preview Content\""; 683 | DEVELOPMENT_TEAM = J374TBL6J6; 684 | ENABLE_HARDENED_RUNTIME = YES; 685 | ENABLE_PREVIEWS = YES; 686 | INFOPLIST_FILE = CurvyTextMac/Info.plist; 687 | LD_RUNPATH_SEARCH_PATHS = ( 688 | "$(inherited)", 689 | "@executable_path/../Frameworks", 690 | ); 691 | MACOSX_DEPLOYMENT_TARGET = 10.15; 692 | PRODUCT_BUNDLE_IDENTIFIER = net.robnapier.CurvyTextMac; 693 | PRODUCT_NAME = "$(TARGET_NAME)"; 694 | SDKROOT = macosx; 695 | SWIFT_VERSION = 5.0; 696 | }; 697 | name = Debug; 698 | }; 699 | FBF07A5423C24A5B001E04DD /* Release */ = { 700 | isa = XCBuildConfiguration; 701 | buildSettings = { 702 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 703 | CODE_SIGN_ENTITLEMENTS = CurvyTextMac/CurvyTextMac.entitlements; 704 | CODE_SIGN_STYLE = Automatic; 705 | COMBINE_HIDPI_IMAGES = YES; 706 | DEVELOPMENT_ASSET_PATHS = "\"CurvyTextMac/Preview Content\""; 707 | DEVELOPMENT_TEAM = J374TBL6J6; 708 | ENABLE_HARDENED_RUNTIME = YES; 709 | ENABLE_PREVIEWS = YES; 710 | INFOPLIST_FILE = CurvyTextMac/Info.plist; 711 | LD_RUNPATH_SEARCH_PATHS = ( 712 | "$(inherited)", 713 | "@executable_path/../Frameworks", 714 | ); 715 | MACOSX_DEPLOYMENT_TARGET = 10.15; 716 | PRODUCT_BUNDLE_IDENTIFIER = net.robnapier.CurvyTextMac; 717 | PRODUCT_NAME = "$(TARGET_NAME)"; 718 | SDKROOT = macosx; 719 | SWIFT_VERSION = 5.0; 720 | }; 721 | name = Release; 722 | }; 723 | FBF07A9823C24EA2001E04DD /* Debug */ = { 724 | isa = XCBuildConfiguration; 725 | buildSettings = { 726 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 727 | CODE_SIGN_STYLE = Automatic; 728 | DEVELOPMENT_TEAM = J374TBL6J6; 729 | INFOPLIST_FILE = PathTextDemo/Info.plist; 730 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 731 | LD_RUNPATH_SEARCH_PATHS = ( 732 | "$(inherited)", 733 | "@executable_path/Frameworks", 734 | ); 735 | PRODUCT_BUNDLE_IDENTIFIER = net.robnapier.PathTextDemo; 736 | PRODUCT_NAME = "$(TARGET_NAME)"; 737 | SWIFT_VERSION = 5.0; 738 | TARGETED_DEVICE_FAMILY = "1,2"; 739 | }; 740 | name = Debug; 741 | }; 742 | FBF07A9923C24EA2001E04DD /* Release */ = { 743 | isa = XCBuildConfiguration; 744 | buildSettings = { 745 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 746 | CODE_SIGN_STYLE = Automatic; 747 | DEVELOPMENT_TEAM = J374TBL6J6; 748 | INFOPLIST_FILE = PathTextDemo/Info.plist; 749 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 750 | LD_RUNPATH_SEARCH_PATHS = ( 751 | "$(inherited)", 752 | "@executable_path/Frameworks", 753 | ); 754 | PRODUCT_BUNDLE_IDENTIFIER = net.robnapier.PathTextDemo; 755 | PRODUCT_NAME = "$(TARGET_NAME)"; 756 | SWIFT_VERSION = 5.0; 757 | TARGETED_DEVICE_FAMILY = "1,2"; 758 | }; 759 | name = Release; 760 | }; 761 | /* End XCBuildConfiguration section */ 762 | 763 | /* Begin XCConfigurationList section */ 764 | FB05B63F23C253EC00B01325 /* Build configuration list for PBXNativeTarget "PathTextDemoMac" */ = { 765 | isa = XCConfigurationList; 766 | buildConfigurations = ( 767 | FB05B64023C253EC00B01325 /* Debug */, 768 | FB05B64123C253EC00B01325 /* Release */, 769 | ); 770 | defaultConfigurationIsVisible = 0; 771 | defaultConfigurationName = Release; 772 | }; 773 | FB2C365B239AA4AC002DDB93 /* Build configuration list for PBXProject "CurvyText" */ = { 774 | isa = XCConfigurationList; 775 | buildConfigurations = ( 776 | FB2C3672239AA4AF002DDB93 /* Debug */, 777 | FB2C3673239AA4AF002DDB93 /* Release */, 778 | ); 779 | defaultConfigurationIsVisible = 0; 780 | defaultConfigurationName = Release; 781 | }; 782 | FB2C3674239AA4AF002DDB93 /* Build configuration list for PBXNativeTarget "CurvyText" */ = { 783 | isa = XCConfigurationList; 784 | buildConfigurations = ( 785 | FB2C3675239AA4AF002DDB93 /* Debug */, 786 | FB2C3676239AA4AF002DDB93 /* Release */, 787 | ); 788 | defaultConfigurationIsVisible = 0; 789 | defaultConfigurationName = Release; 790 | }; 791 | FBF07A5223C24A5B001E04DD /* Build configuration list for PBXNativeTarget "CurvyTextMac" */ = { 792 | isa = XCConfigurationList; 793 | buildConfigurations = ( 794 | FBF07A5323C24A5B001E04DD /* Debug */, 795 | FBF07A5423C24A5B001E04DD /* Release */, 796 | ); 797 | defaultConfigurationIsVisible = 0; 798 | defaultConfigurationName = Release; 799 | }; 800 | FBF07A9723C24EA2001E04DD /* Build configuration list for PBXNativeTarget "PathTextDemo" */ = { 801 | isa = XCConfigurationList; 802 | buildConfigurations = ( 803 | FBF07A9823C24EA2001E04DD /* Debug */, 804 | FBF07A9923C24EA2001E04DD /* Release */, 805 | ); 806 | defaultConfigurationIsVisible = 0; 807 | defaultConfigurationName = Release; 808 | }; 809 | /* End XCConfigurationList section */ 810 | 811 | /* Begin XCSwiftPackageProductDependency section */ 812 | FB05B64223C2542300B01325 /* PathText */ = { 813 | isa = XCSwiftPackageProductDependency; 814 | productName = PathText; 815 | }; 816 | FB2C367D239C09CE002DDB93 /* PathText */ = { 817 | isa = XCSwiftPackageProductDependency; 818 | productName = PathText; 819 | }; 820 | FBF07A5623C24B31001E04DD /* PathText */ = { 821 | isa = XCSwiftPackageProductDependency; 822 | productName = PathText; 823 | }; 824 | FBF07A9A23C24EDE001E04DD /* PathText */ = { 825 | isa = XCSwiftPackageProductDependency; 826 | productName = PathText; 827 | }; 828 | /* End XCSwiftPackageProductDependency section */ 829 | }; 830 | rootObject = FB2C3658239AA4AC002DDB93 /* Project object */; 831 | } 832 | -------------------------------------------------------------------------------- /CurvyText.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CurvyText.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CurvyText/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CurvyText 4 | // 5 | // Created by Rob Napier on 12/6/19. 6 | // Copyright © 2019 Rob Napier. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /CurvyText/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /CurvyText/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CurvyText/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 | -------------------------------------------------------------------------------- /CurvyText/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // CurvyText 4 | // 5 | // Created by Rob Napier on 12/6/19. 6 | // Copyright © 2019 Rob Napier. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import PathText 11 | 12 | struct ContentView: View { 13 | @State var P0 = CGPoint(x: 50, y: 500) 14 | @State var P1 = CGPoint(x: 300, y: 300) 15 | @State var P2 = CGPoint(x: 400, y: 700) 16 | @State var P3 = CGPoint(x: 650, y: 500) 17 | 18 | let text: NSAttributedString = { 19 | let string = NSString("You can display text along a curve, with bold, color, and big text.") 20 | 21 | let s = NSMutableAttributedString(string: string as String, 22 | attributes: [.font: UIFont.systemFont(ofSize: 16)]) 23 | 24 | s.addAttributes([.font: UIFont.boldSystemFont(ofSize: 16)], range: string.range(of: "bold")) 25 | s.addAttributes([.foregroundColor: UIColor.red], range: string.range(of: "color")) 26 | s.addAttributes([.font: UIFont.systemFont(ofSize: 32)], range: string.range(of: "big text")) 27 | return s 28 | }() 29 | 30 | var body: some View { 31 | 32 | let path = Path() { 33 | $0.move(to: P0) 34 | $0.addCurve(to: P3, control1: P1, control2: P2) 35 | } 36 | 37 | return ZStack{ 38 | Path() { 39 | $0.move(to: P0) 40 | $0.addCurve(to: P3, control1: P1, control2: P2) 41 | } 42 | .stroke(Color.blue, lineWidth: 2) 43 | 44 | PathText(text: text, path: path) 45 | 46 | ControlPoint(position: $P0) 47 | .foregroundColor(.green) 48 | 49 | ControlPoint(position: $P1) 50 | .foregroundColor(.black) 51 | 52 | ControlPoint(position: $P2) 53 | .foregroundColor(.black) 54 | 55 | ControlPoint(position: $P3) 56 | .foregroundColor(.red) 57 | } 58 | } 59 | } 60 | 61 | struct ContentView_Previews: PreviewProvider { 62 | static var previews: some View { 63 | ContentView() 64 | } 65 | } 66 | 67 | struct ControlPoint: View { 68 | let size = CGSize(width: 13, height: 13) 69 | 70 | @Binding var position: CGPoint 71 | 72 | var body: some View { 73 | Rectangle() 74 | .frame(width: size.width, height: size.height) // Size of fill 75 | .frame(width: size.width * 3, height: size.height * 3) // Increase hit area 76 | .contentShape(Rectangle()) // Make whole area hittable 77 | .draggable(position: $position) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /CurvyText/CurvyText.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /CurvyText/Draggable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Draggable.swift 3 | // CurvyText 4 | // 5 | // Created by Rob Napier on 12/6/19. 6 | // Copyright © 2019 Rob Napier. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @available(iOS 13.0, *) 12 | struct Draggable: View { 13 | let content: Content 14 | @Binding var position: CGPoint 15 | 16 | @State private var dragStart: CGPoint? // Drag based on initial touch-point, not center 17 | 18 | var body: some View { 19 | content 20 | .position(position) 21 | .gesture( 22 | DragGesture().onChanged { 23 | if self.dragStart == nil { 24 | self.dragStart = self.position 25 | } 26 | 27 | if let dragStart = self.dragStart { 28 | self.position = dragStart + $0.translation 29 | } 30 | } 31 | .onEnded { _ in 32 | self.dragStart = nil 33 | } 34 | ) 35 | } 36 | } 37 | 38 | @available(iOS 13.0, *) 39 | extension View { 40 | func draggable(position: Binding) -> some View { 41 | Draggable(content: self, position: position) 42 | } 43 | } 44 | 45 | extension CGPoint { 46 | static func + (lhs: CGPoint, rhs: CGSize) -> CGPoint { 47 | return CGPoint(x: lhs.x + rhs.width, 48 | y: lhs.y + rhs.height) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CurvyText/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /CurvyText/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CurvyText/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // CurvyText 4 | // 5 | // Created by Rob Napier on 12/6/19. 6 | // Copyright © 2019 Rob Napier. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Create the SwiftUI view that provides the window contents. 23 | let contentView = ContentView() 24 | 25 | // Use a UIHostingController as window root view controller. 26 | if let windowScene = scene as? UIWindowScene { 27 | let window = UIWindow(windowScene: windowScene) 28 | window.rootViewController = UIHostingController(rootView: contentView) 29 | self.window = window 30 | window.makeKeyAndVisible() 31 | } 32 | } 33 | 34 | func sceneDidDisconnect(_ scene: UIScene) { 35 | // Called as the scene is being released by the system. 36 | // This occurs shortly after the scene enters the background, or when its session is discarded. 37 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 38 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 39 | } 40 | 41 | func sceneDidBecomeActive(_ scene: UIScene) { 42 | // Called when the scene has moved from an inactive state to an active state. 43 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 44 | } 45 | 46 | func sceneWillResignActive(_ scene: UIScene) { 47 | // Called when the scene will move from an active state to an inactive state. 48 | // This may occur due to temporary interruptions (ex. an incoming phone call). 49 | } 50 | 51 | func sceneWillEnterForeground(_ scene: UIScene) { 52 | // Called as the scene transitions from the background to the foreground. 53 | // Use this method to undo the changes made on entering the background. 54 | } 55 | 56 | func sceneDidEnterBackground(_ scene: UIScene) { 57 | // Called as the scene transitions from the foreground to the background. 58 | // Use this method to save data, release shared resources, and store enough scene-specific state information 59 | // to restore the scene back to its current state. 60 | } 61 | 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /CurvyTextMac/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CurvyTextMac 4 | // 5 | // Created by Rob Napier on 1/5/20. 6 | // Copyright © 2020 Rob Napier. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SwiftUI 11 | 12 | @NSApplicationMain 13 | class AppDelegate: NSObject, NSApplicationDelegate { 14 | 15 | var window: NSWindow! 16 | 17 | 18 | func applicationDidFinishLaunching(_ aNotification: Notification) { 19 | // Create the SwiftUI view that provides the window contents. 20 | let contentView = ContentView() 21 | 22 | // Create the window and set the content view. 23 | window = NSWindow( 24 | contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), 25 | styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], 26 | backing: .buffered, defer: false) 27 | window.center() 28 | window.setFrameAutosaveName("Main Window") 29 | window.contentView = NSHostingView(rootView: contentView) 30 | window.makeKeyAndOrderFront(nil) 31 | } 32 | 33 | func applicationWillTerminate(_ aNotification: Notification) { 34 | // Insert code here to tear down your application 35 | } 36 | 37 | 38 | } 39 | 40 | -------------------------------------------------------------------------------- /CurvyTextMac/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /CurvyTextMac/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CurvyTextMac/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | Default 529 | 530 | 531 | 532 | 533 | 534 | 535 | Left to Right 536 | 537 | 538 | 539 | 540 | 541 | 542 | Right to Left 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | Default 554 | 555 | 556 | 557 | 558 | 559 | 560 | Left to Right 561 | 562 | 563 | 564 | 565 | 566 | 567 | Right to Left 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | -------------------------------------------------------------------------------- /CurvyTextMac/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // CurvyTextMac 4 | // 5 | // Created by Rob Napier on 1/5/20. 6 | // Copyright © 2020 Rob Napier. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | import SwiftUI 11 | import PathText 12 | 13 | struct ContentView: View { 14 | @State var P0 = CGPoint(x: 50, y: 500) 15 | @State var P1 = CGPoint(x: 300, y: 300) 16 | @State var P2 = CGPoint(x: 400, y: 700) 17 | @State var P3 = CGPoint(x: 650, y: 500) 18 | 19 | let text: NSAttributedString = { 20 | let string = NSString("You can display text along a curve, with bold, color, and big text.") 21 | 22 | let s = NSMutableAttributedString(string: string as String, 23 | attributes: [.font: NSFont.systemFont(ofSize: 16)]) 24 | 25 | s.addAttributes([.font: NSFont.boldSystemFont(ofSize: 16)], range: string.range(of: "bold")) 26 | s.addAttributes([.foregroundColor: NSColor.red], range: string.range(of: "color")) 27 | s.addAttributes([.font: NSFont.systemFont(ofSize: 32)], range: string.range(of: "big text")) 28 | return s 29 | }() 30 | 31 | var body: some View { 32 | 33 | let path = Path() { 34 | $0.move(to: P0) 35 | $0.addCurve(to: P3, control1: P1, control2: P2) 36 | } 37 | 38 | return ZStack{ 39 | Path() { 40 | $0.move(to: P0) 41 | $0.addCurve(to: P3, control1: P1, control2: P2) 42 | } 43 | .stroke(Color.blue, lineWidth: 2) 44 | 45 | PathText(text: text, path: path) 46 | 47 | ControlPoint(position: $P0) 48 | .foregroundColor(.green) 49 | 50 | ControlPoint(position: $P1) 51 | .foregroundColor(.black) 52 | 53 | ControlPoint(position: $P2) 54 | .foregroundColor(.black) 55 | 56 | ControlPoint(position: $P3) 57 | .foregroundColor(.red) 58 | } 59 | } 60 | } 61 | 62 | struct ContentView_Previews: PreviewProvider { 63 | static var previews: some View { 64 | ContentView() 65 | } 66 | } 67 | 68 | struct ControlPoint: View { 69 | let size = CGSize(width: 13, height: 13) 70 | 71 | @Binding var position: CGPoint 72 | 73 | var body: some View { 74 | Rectangle() 75 | .frame(width: size.width, height: size.height) // Size of fill 76 | .frame(width: size.width * 3, height: size.height * 3) // Increase hit area 77 | .contentShape(Rectangle()) // Make whole area hittable 78 | .draggable(position: $position) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /CurvyTextMac/CurvyTextMac.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /CurvyTextMac/Draggable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Draggable.swift 3 | // CurvyText 4 | // 5 | // Created by Rob Napier on 12/6/19. 6 | // Copyright © 2019 Rob Napier. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @available(OSX 10.15, *) 12 | struct Draggable: View { 13 | let content: Content 14 | @Binding var position: CGPoint 15 | 16 | @State private var dragStart: CGPoint? // Drag based on initial touch-point, not center 17 | 18 | var body: some View { 19 | content 20 | .position(position) 21 | .gesture( 22 | DragGesture().onChanged { 23 | if self.dragStart == nil { 24 | self.dragStart = self.position 25 | } 26 | 27 | if let dragStart = self.dragStart { 28 | self.position = dragStart + $0.translation 29 | } 30 | } 31 | .onEnded { _ in 32 | self.dragStart = nil 33 | } 34 | ) 35 | } 36 | } 37 | 38 | @available(OSX 10.15, *) 39 | extension View { 40 | func draggable(position: Binding) -> some View { 41 | Draggable(content: self, position: position) 42 | } 43 | } 44 | 45 | extension CGPoint { 46 | static func + (lhs: CGPoint, rhs: CGSize) -> CGPoint { 47 | return CGPoint(x: lhs.x + rhs.width, 48 | y: lhs.y + rhs.height) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CurvyTextMac/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2020 Rob Napier. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | NSSupportsAutomaticTermination 32 | 33 | NSSupportsSuddenTermination 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /CurvyTextMac/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rob Napier 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 | -------------------------------------------------------------------------------- /PathText/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "PathText", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "PathText", 12 | targets: ["PathText"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "PathText", 23 | dependencies: []), 24 | .testTarget( 25 | name: "PathTextTests", 26 | dependencies: ["PathText"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /PathText/Sources/PathText/AppKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppKit.swift 3 | // 4 | // 5 | // Created by Rob Napier on 1/4/20. 6 | // 7 | 8 | #if canImport(AppKit) && !targetEnvironment(macCatalyst) 9 | import AppKit 10 | import SwiftUI 11 | 12 | typealias PlatformColor = NSColor 13 | 14 | @available(OSX, introduced: 10.15) 15 | extension PathText: NSViewRepresentable { 16 | public func makeNSView(context: Context) -> PathTextView { 17 | PathTextView(flipped: true) 18 | } 19 | 20 | public func updateNSView(_ nsView: PathTextView, context: Context) { 21 | nsView.text = text 22 | nsView.path = path 23 | } 24 | } 25 | 26 | public class PathTextView: NSView { 27 | private var layoutManager = PathTextLayoutManager() 28 | 29 | private let _isFlipped: Bool 30 | override public var isFlipped: Bool { return _isFlipped } 31 | 32 | public var text: NSAttributedString { 33 | get { layoutManager.text } 34 | set { 35 | layoutManager.text = newValue 36 | setNeedsDisplay(self.bounds) 37 | } 38 | } 39 | 40 | public var path: CGPath { 41 | get { layoutManager.path } 42 | set { 43 | layoutManager.path = newValue 44 | setNeedsDisplay(self.bounds) 45 | } 46 | } 47 | 48 | public init(frame: CGRect = .zero, 49 | text: NSAttributedString = NSAttributedString(), 50 | path: CGPath = CGMutablePath(), 51 | flipped: Bool = false) { 52 | 53 | self._isFlipped = flipped 54 | super.init(frame: frame) 55 | self.text = text 56 | self.path = path 57 | } 58 | 59 | required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 60 | 61 | public override func draw(_ rect: CGRect) { 62 | let context = NSGraphicsContext.current!.cgContext 63 | 64 | if isFlipped { 65 | context.textMatrix = CGAffineTransform(scaleX: 1, y: -1) 66 | } 67 | 68 | layoutManager.draw(in: context) 69 | } 70 | } 71 | #endif 72 | -------------------------------------------------------------------------------- /PathText/Sources/PathText/GlyphRun.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Rob Napier on 1/1/20. 6 | // 7 | 8 | import Foundation 9 | import CoreText 10 | 11 | // For NSShadow 12 | #if canImport(UIKit) 13 | import UIKit 14 | #elseif canImport(AppKit) 15 | import AppKit 16 | #endif 17 | 18 | private extension Sequence { 19 | func mapUntilNil(_ transform: (Self.Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] { 20 | try map(transform) 21 | .prefix(while: { $0 != nil }) 22 | .compactMap {$0} 23 | } 24 | } 25 | 26 | // Location in text space 27 | struct GlyphBoxes { 28 | var glyph: CGGlyph 29 | var bounds: CGRect 30 | var baseline: CGFloat // Distance from bottom of bounds to baseline 31 | } 32 | 33 | extension GlyphBoxes { 34 | // Location of left (leading?) baseline in text space. 35 | var position: CGPoint { CGPoint(x: bounds.minX, y: bounds.maxY - baseline)} 36 | var width: CGFloat { bounds.width } 37 | var anchor: CGFloat { position.x + width / 2 } // Point on baseline to connect to tangent 38 | var height: CGFloat { bounds.height } 39 | var ascent: CGFloat { height - baseline } 40 | 41 | init(run: CTRun, index: CFIndex, glyph: CGGlyph, position: CGPoint) { 42 | var ascent: CGFloat = 0 43 | var descent: CGFloat = 0 44 | let width = CGFloat(CTRunGetTypographicBounds(run, 45 | CFRange(location: index, length: 1), 46 | &ascent, &descent, nil)) 47 | self.glyph = glyph 48 | self.bounds = CGRect(x: position.x, y: position.y - ascent, width: width, height: ascent + descent) 49 | self.baseline = descent 50 | } 51 | } 52 | 53 | private func makeCGColor(_ value: Any) -> CGColor { 54 | if let color = value as? PlatformColor { 55 | return color.cgColor 56 | } else { 57 | return value as! CGColor 58 | } 59 | } 60 | 61 | private extension CGContext { 62 | func apply(attributes: [NSAttributedString.Key : Any]) { 63 | for (key, value) in attributes { 64 | switch key { 65 | case .font: 66 | let ctFont = value as! CTFont 67 | let cgFont = CTFontCopyGraphicsFont(ctFont, nil) 68 | self.setFont(cgFont) 69 | self.setFontSize(CTFontGetSize(ctFont)) 70 | 71 | case .foregroundColor: 72 | self.setFillColor(makeCGColor(value)) 73 | 74 | case .strokeColor: 75 | self.setStrokeColor(makeCGColor(value)) 76 | 77 | case .strokeWidth: 78 | let width = value as! CGFloat 79 | 80 | let mode: CGTextDrawingMode 81 | if width < 0 { 82 | mode = .fillStroke 83 | } else if width == 0 { 84 | mode = .fill 85 | } else { 86 | mode = .stroke 87 | } 88 | self.setTextDrawingMode(mode) 89 | self.setLineWidth(width) 90 | 91 | // Remember: NSShadow does not honor CTM. It is always in the default user coordinates. 92 | case .shadow: 93 | let shadow = value as! NSShadow 94 | if let color = shadow.shadowColor { 95 | self.setShadow(offset: shadow.shadowOffset, blur: shadow.shadowBlurRadius, color: makeCGColor(color)) 96 | } else { 97 | self.setShadow(offset: shadow.shadowOffset, blur: shadow.shadowBlurRadius) 98 | } 99 | 100 | // 101 | // Ignore for various reasons 102 | // 103 | 104 | // Ignore because CTRun already handles it 105 | case .kern, 106 | .ligature, 107 | .writingDirection: 108 | break 109 | 110 | // Ingore because other methods already handle it 111 | case .baselineOffset: 112 | break 113 | 114 | // Ignore because they are unsupported by CoreText 115 | case .expansion, // Expansion is not fully supported; it'll act more like tracking 116 | .link, 117 | .obliqueness, 118 | .textEffect: 119 | break 120 | 121 | // Ignore because it would look bad if implemented 122 | case .backgroundColor, 123 | .paragraphStyle, 124 | .strikethroughStyle, 125 | .strikethroughColor, 126 | .underlineStyle, 127 | .underlineColor: 128 | break 129 | 130 | // Ignore because it's unneeded information 131 | case .init("NSOriginalFont"): // Original font before substitution. 132 | break 133 | 134 | default: 135 | print("Unknown attribute: \(key) = \(value)") // FIXME: Just for debugging. 136 | } 137 | } 138 | } 139 | } 140 | 141 | struct GlyphRun { 142 | let run: CTRun 143 | let boxes: [GlyphBoxes] 144 | let attributes: [NSAttributedString.Key : Any] 145 | 146 | init(run: CTRun, boxes: [GlyphBoxes]) { 147 | self.run = run 148 | self.boxes = boxes 149 | self.attributes = CTRunGetAttributes(run) as! [NSAttributedString.Key : Any] 150 | } 151 | 152 | private(set) var tangents: [PathTangent] = [] { 153 | didSet { 154 | updateTypographicBounds() 155 | } 156 | } 157 | 158 | var baselineOffset: CGFloat { 159 | attributes[.baselineOffset] as? CGFloat ?? 0 160 | } 161 | 162 | mutating func updateTangets(with tangentGenerator: inout TangentGenerator) { 163 | tangents = boxes.mapUntilNil { tangentGenerator.getTangent(at: $0.anchor) } 164 | } 165 | 166 | private mutating func updateTypographicBounds() { 167 | let transformed: [CGRect] = zip(boxes, tangents).map { (arg) in 168 | let (location, tangent) = arg 169 | 170 | let tangentPoint = tangent.point 171 | let angle = tangent.angle 172 | 173 | return location.bounds 174 | .offsetBy(dx: -location.anchor, dy: -(location.position.y + baselineOffset)) // Move anchor to .zero 175 | .applying(.init(rotationAngle: angle)) // Rotate 176 | .offsetBy(dx: tangentPoint.x, dy: tangentPoint.y) // Translate in rotated context 177 | } 178 | 179 | typographicBounds = transformed.reduce(.null) { $0.union($1) } 180 | } 181 | 182 | var typographicBounds: CGRect = .null 183 | 184 | func draw(in context: CGContext) { 185 | // DEBUGGING 186 | // context.stroke(typographicBounds) 187 | 188 | context.saveGState() 189 | defer { context.restoreGState() } 190 | 191 | context.apply(attributes: attributes) 192 | 193 | for (location, tangent) in zip(boxes, tangents) { 194 | context.saveGState() 195 | defer { context.restoreGState() } 196 | 197 | let tangentPoint = tangent.point 198 | let angle = tangent.angle 199 | 200 | context.translateBy(x: tangentPoint.x, y: tangentPoint.y) // y is flipped 201 | context.rotate(by: angle) 202 | 203 | context.textPosition = CGPoint(x: -location.width / 2, 204 | y: -(location.position.y + baselineOffset)) // y is flipped 205 | 206 | // Use CGContext rather than CTFontDrawGlyphs to get context features like shadow 207 | context.showGlyphs([location.glyph], at: [.zero]) 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /PathText/Sources/PathText/LayoutManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutManager.swift 3 | // 4 | // 5 | // Created by Rob Napier on 1/5/20. 6 | // 7 | 8 | import Foundation 9 | import CoreGraphics 10 | import CoreText 11 | 12 | struct PathTextLayoutManager { 13 | public var text: NSAttributedString = NSAttributedString() { 14 | didSet { 15 | invalidateGlyphs() 16 | } 17 | } 18 | 19 | public var path: CGPath = CGMutablePath() { 20 | didSet { 21 | invalidateLayout() 22 | } 23 | } 24 | 25 | public var typographicBounds: CGRect { 26 | // FIXME: ensureLayout? Maybe pre-calculate this? 27 | glyphRuns.reduce(.null) { $0.union($1.typographicBounds) } 28 | } 29 | 30 | mutating func ensureGlyphs() { 31 | if needsGlyphGeneration { updateGlyphs() } 32 | } 33 | 34 | mutating func ensureLayout() { 35 | if needsLayout { updateLayout() } 36 | } 37 | 38 | private var needsGlyphGeneration = false 39 | public mutating func invalidateGlyphs() { needsGlyphGeneration = true } 40 | 41 | private var needsLayout = false 42 | public mutating func invalidateLayout() { needsLayout = true } 43 | 44 | private var glyphRuns: [GlyphRun] = [] 45 | 46 | private mutating func updateGlyphs() { 47 | let line = CTLineCreateWithAttributedString(text) 48 | let runs = CTLineGetGlyphRuns(line) as! [CTRun] 49 | 50 | glyphRuns = runs.map { run in 51 | let glyphCount = CTRunGetGlyphCount(run) 52 | 53 | let positions: [CGPoint] = Array(unsafeUninitializedCapacity: glyphCount) { (buffer, initialized) in 54 | CTRunGetPositions(run, CFRange(), buffer.baseAddress!) 55 | initialized = glyphCount 56 | } 57 | 58 | let glyphs = Array(unsafeUninitializedCapacity: glyphCount) { (buffer, initialized) in 59 | CTRunGetGlyphs(run, CFRange(), buffer.baseAddress!) 60 | initialized = glyphCount 61 | } 62 | 63 | let locations: [GlyphBoxes] = (0.. PathTangent 14 | func nextTangent(linearDistance: CGFloat, after: PathTangent) -> NextTangent 15 | } 16 | 17 | extension PathSection { 18 | // Default impl 19 | func nextTangent(linearDistance: CGFloat, after lastTangent: PathTangent) -> NextTangent { 20 | // Simplistic routine to find the t along Bezier that is a linear distance away from a previous tangent. 21 | // This routine just walks forward, accumulating Euclidean approximations until it finds 22 | // a point at least linearDistance away. Good optimizations here would reduce the number 23 | // of guesses, but this is tricky since if we go too far out, the 24 | // curve might loop back on leading to incorrect results. Tuning 25 | // kStep is good start. 26 | let step: CGFloat = 0.001 // 0.0001 - 0.001 work well 27 | var lastTangent = lastTangent 28 | 29 | var approximateLinearDistance: CGFloat = 0 30 | 31 | var nextTangent = lastTangent 32 | while approximateLinearDistance < linearDistance && nextTangent.t <= 1.0 { 33 | lastTangent = nextTangent 34 | nextTangent = getTangent(t: nextTangent.t + step) 35 | approximateLinearDistance += lastTangent.point.distance(to: nextTangent.point) 36 | } 37 | 38 | if nextTangent.t > 1.0 { 39 | return .insufficient(remainingLinearDistance: approximateLinearDistance) 40 | } else { 41 | return .found(nextTangent) 42 | } 43 | } 44 | } 45 | 46 | private extension CGPoint { 47 | func distance(to other: CGPoint) -> CGFloat { 48 | let dx = x - other.x 49 | let dy = y - other.y 50 | return hypot(dx, dy) 51 | } 52 | } 53 | 54 | struct PathTangent: Equatable { 55 | var t: CGFloat 56 | var point: CGPoint 57 | var angle: CGFloat 58 | } 59 | 60 | enum NextTangent { 61 | case found(PathTangent) 62 | case insufficient(remainingLinearDistance: CGFloat) 63 | } 64 | 65 | struct TangentGenerator { 66 | private var sections: ArraySlice 67 | private var lastLocation: CGFloat = 0 68 | private var lastTangent: PathTangent? 69 | 70 | init(path: CGPath) { 71 | sections = path.sections()[...] 72 | } 73 | 74 | // Locations must be in ascending order 75 | mutating func getTangent(at location: CGFloat) -> PathTangent? { 76 | assert(location >= lastLocation) 77 | 78 | while let section = sections.first { 79 | let currentTangent = lastTangent ?? section.getTangent(t: 0) 80 | let linearDistance = location - lastLocation 81 | 82 | switch section.nextTangent(linearDistance: linearDistance, 83 | after: currentTangent) { 84 | case .found(let tangent): 85 | lastTangent = tangent 86 | lastLocation = location 87 | return tangent 88 | 89 | case let .insufficient(remainingLinearDistance: remain): 90 | lastLocation += remain 91 | lastTangent = nil 92 | sections.removeFirst() 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | } 99 | 100 | extension CGPath { 101 | func sections() -> [PathSection] { 102 | class Applier { 103 | var sections: [PathSection] = [] 104 | var start: CGPoint? 105 | var current: CGPoint? 106 | 107 | func add(element: CGPathElement) { 108 | // FIXME: Filter zero-length? 109 | switch element.type { 110 | case .closeSubpath: 111 | sections.append(PathLineSection(start: current ?? .zero, end: start ?? .zero)) 112 | current = start 113 | start = nil 114 | 115 | case .moveToPoint: 116 | let p = element.points[0] 117 | start = start ?? p 118 | current = p 119 | 120 | case .addCurveToPoint: 121 | let points = element.points 122 | let (p1, p2, p3) = (points[0], points[1], points[2]) 123 | sections.append(PathCurveSection(p0: current ?? .zero, p1: p1, p2: p2, p3: p3)) 124 | start = start ?? .zero 125 | current = p3 126 | 127 | case .addLineToPoint: 128 | let p = element.points[0] 129 | sections.append(PathLineSection(start: current ?? .zero, end: p)) 130 | start = start ?? .zero 131 | current = p 132 | 133 | case .addQuadCurveToPoint: 134 | let points = element.points 135 | let (p1, p2) = (points[0], points[1]) 136 | sections.append(PathQuadCurveSection(p0: current ?? .zero, p1: p1, p2: p2)) 137 | start = start ?? .zero 138 | current = p2 139 | 140 | @unknown default: 141 | break 142 | } 143 | } 144 | } 145 | 146 | func f(info: UnsafeMutableRawPointer?, elementPtr: UnsafePointer) { 147 | info? 148 | .assumingMemoryBound(to: Applier.self) 149 | .pointee 150 | .add(element: elementPtr.pointee) 151 | } 152 | 153 | var applier = Applier() 154 | 155 | self.apply(info: &applier, function: f) 156 | 157 | return applier.sections 158 | } 159 | } 160 | 161 | struct PathLineSection: PathSection { 162 | let start, end: CGPoint 163 | 164 | func getTangent(t: CGFloat) -> PathTangent { 165 | let dx = end.x - start.x 166 | let dy = end.y - start.y 167 | 168 | let x = start.x + dx * t 169 | let y = start.y + dy * t 170 | 171 | return PathTangent(t: t, 172 | point: CGPoint(x: x, y: y), 173 | angle: atan2(dy, dx)) 174 | } 175 | } 176 | 177 | struct PathQuadCurveSection: PathSection { 178 | let p0, p1, p2: CGPoint 179 | var start: CGPoint { p0 } 180 | var end: CGPoint { p2 } 181 | 182 | func getTangent(t: CGFloat) -> PathTangent { 183 | let dx = bezierPrime(t, p0.x, p1.x, p2.x) 184 | let dy = bezierPrime(t, p0.y, p1.y, p2.y) 185 | 186 | let x = bezier(t, p0.x, p1.x, p2.x) 187 | let y = bezier(t, p0.y, p1.y, p2.y) 188 | 189 | return PathTangent(t: t, 190 | point: CGPoint(x: x, y: y), 191 | angle: atan2(dy, dx)) 192 | } 193 | 194 | // The quadratic Bezier function at t 195 | private func bezier(_ t: CGFloat, _ P0: CGFloat, _ P1: CGFloat, _ P2: CGFloat) -> CGFloat { 196 | (1-t)*(1-t) * P0 197 | + 2 * (1-t) * t * P1 198 | + t*t * P2 199 | } 200 | 201 | // The slope of the quadratic Bezier function at t 202 | private func bezierPrime(_ t: CGFloat, _ P0: CGFloat, _ P1: CGFloat, _ P2: CGFloat) -> CGFloat { 203 | 2 * (1-t) * (P1 - P0) 204 | + 2 * t * (P2 - P1) 205 | } 206 | } 207 | 208 | struct PathCurveSection: PathSection { 209 | 210 | let p0, p1, p2, p3: CGPoint 211 | var start: CGPoint { p0 } 212 | var end: CGPoint { p3 } 213 | 214 | func getTangent(t: CGFloat) -> PathTangent { 215 | let dx = bezierPrime(t, p0.x, p1.x, p2.x, p3.x) 216 | let dy = bezierPrime(t, p0.y, p1.y, p2.y, p3.y) 217 | 218 | let x = bezier(t, p0.x, p1.x, p2.x, p3.x) 219 | let y = bezier(t, p0.y, p1.y, p2.y, p3.y) 220 | 221 | return PathTangent(t: t, 222 | point: CGPoint(x: x, y: y), 223 | angle: atan2(dy, dx)) 224 | } 225 | 226 | // The cubic Bezier function at t 227 | private func bezier(_ t: CGFloat, _ P0: CGFloat, _ P1: CGFloat, _ P2: CGFloat, _ P3: CGFloat) -> CGFloat { 228 | (1-t)*(1-t)*(1-t) * P0 229 | + 3 * (1-t)*(1-t) * t * P1 230 | + 3 * (1-t) * t*t * P2 231 | + t*t*t * P3 232 | } 233 | 234 | // The slope of the cubic Bezier function at t 235 | private func bezierPrime(_ t: CGFloat, _ P0: CGFloat, _ P1: CGFloat, _ P2: CGFloat, _ P3: CGFloat) -> CGFloat { 236 | 0 237 | - 3 * (1-t)*(1-t) * P0 238 | + (3 * (1-t)*(1-t) * P1) - (6 * t * (1-t) * P1) 239 | - (3 * t*t * P2) + (6 * t * (1-t) * P2) 240 | + 3 * t*t * P3 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /PathText/Sources/PathText/Previews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Previews.swift 3 | // 4 | // 5 | // Created by Rob Napier on 1/5/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | #if canImport(UIKit) 11 | import UIKit 12 | private typealias PlatformFont = UIFont 13 | #elseif canImport(AppKit) 14 | private typealias PlatformFont = NSFont 15 | #else 16 | #error("Unsupported platform") 17 | #endif 18 | 19 | @available(iOS, introduced: 13) 20 | @available(OSX, introduced: 10.15) 21 | struct PathText_Previews: PreviewProvider { 22 | // static let text: NSAttributedString = { 23 | // let string = NSString("You can display text along a curve, with bold, color, and big text.") 24 | // 25 | // let s = NSMutableAttributedString(string: string as String, 26 | // attributes: [.font: PlatformFont.systemFont(ofSize: 16)]) 27 | // 28 | // s.addAttributes([.font: PlatformFont.boldSystemFont(ofSize: 16)], range: string.range(of: "bold")) 29 | // s.addAttributes([.foregroundColor: PlatformColor.red], range: string.range(of: "color")) 30 | // s.addAttributes([.font: PlatformFont.systemFont(ofSize: 32)], range: string.range(of: "big text")) 31 | // return s 32 | // }() 33 | 34 | static let text: NSAttributedString = { 35 | let string = NSString("mmii can d\u{030a}isplay العربية tëxt along a cu\u{0327}rve, with bold, color, and big text.") 36 | 37 | let s = NSMutableAttributedString(string: string as String, 38 | attributes: [.font: PlatformFont.systemFont(ofSize: 48)]) 39 | 40 | s.addAttribute(.font, value: PlatformFont.boldSystemFont(ofSize: 48), range: string.range(of: "tëxt")) 41 | s.addAttribute(.foregroundColor, value: PlatformColor.red, range: string.range(of: "d\u{030a}isplay")) 42 | s.addAttribute(.font, value: PlatformFont.systemFont(ofSize: 32), range: string.range(of: "big text")) 43 | 44 | s.addAttribute(.strokeColor, value: PlatformColor.blue, range: string.range(of: "can")) 45 | s.addAttribute(.strokeWidth, value: 2, range: string.range(of: "can")) 46 | 47 | s.addAttribute(.baselineOffset, value: 20, range: string.range(of: "along")) 48 | 49 | let shadow = NSShadow() 50 | shadow.shadowBlurRadius = 5 51 | shadow.shadowColor = PlatformColor.green 52 | shadow.shadowOffset = CGSize(width: 5, height: 10) 53 | s.addAttribute(.shadow, value: shadow, range: string.range(of: "can")) 54 | 55 | s.addAttribute(.writingDirection, value: [3], range: string.range(of: "mmii")) 56 | 57 | return s 58 | }() 59 | 60 | static func CurveView() -> some View { 61 | let P0 = CGPoint(x: 50, y: 300) 62 | let P1 = CGPoint(x: 300, y: 100) 63 | let P2 = CGPoint(x: 400, y: 500) 64 | let P3 = CGPoint(x: 650, y: 300) 65 | 66 | let path = Path() { 67 | $0.move(to: P0) 68 | $0.addCurve(to: P3, control1: P1, control2: P2) 69 | } 70 | 71 | return ZStack { 72 | PathText(text: text, path: path) 73 | path.stroke(Color.blue, lineWidth: 2) 74 | } 75 | } 76 | 77 | static func LineView() -> some View { 78 | let P0 = CGPoint(x: 50, y: 300) 79 | let P1 = CGPoint(x: 650, y: 300) 80 | 81 | let path = Path() { 82 | $0.move(to: P0) 83 | $0.addLine(to: P1) 84 | } 85 | 86 | return VStack { 87 | Text(verbatim: text.string) 88 | .font(.system(size: 48)) 89 | .padding() 90 | .lineLimit(1) 91 | ZStack { 92 | PathText(text: text, path: path) 93 | path.stroke(Color.blue, lineWidth: 2) 94 | } 95 | } 96 | } 97 | 98 | static func LinesView() -> some View { 99 | let P0 = CGPoint(x: 50, y: 400) 100 | let P1 = CGPoint(x: 150, y: 100) 101 | let P2 = CGPoint(x: 650, y: 400) 102 | 103 | let path = Path() { 104 | $0.move(to: P0) 105 | $0.addLine(to: P1) 106 | $0.addLine(to: P2) 107 | } 108 | 109 | return ZStack { 110 | PathText(text: text, path: path) 111 | path.stroke(Color.blue, lineWidth: 2) 112 | } 113 | } 114 | 115 | static func LineAndCurveView() -> some View { 116 | let P0 = CGPoint(x: 50, y: 400) 117 | let P1 = CGPoint(x: 150, y: 200) 118 | let C1 = CGPoint(x: 300, y: 100) 119 | let C2 = CGPoint(x: 300, y: 400) 120 | let P3 = CGPoint(x: 650, y: 400) 121 | 122 | let path = Path() { 123 | $0.move(to: P0) 124 | $0.addLine(to: P1) 125 | $0.addCurve(to: P3, control1: C1, control2: C2) 126 | } 127 | 128 | return ZStack { 129 | PathText(text: text, path: path) 130 | path.stroke(Color.blue, lineWidth: 2) 131 | } 132 | } 133 | 134 | static func QuadCurveView() -> some View { 135 | let P0 = CGPoint(x: 50, y: 300) 136 | let P1 = CGPoint(x: 300, y: 100) 137 | let P2 = CGPoint(x: 650, y: 300) 138 | 139 | let path = Path() { 140 | $0.move(to: P0) 141 | $0.addQuadCurve(to: P2, control: P1) 142 | } 143 | 144 | return PathText(text: text, path: path) 145 | } 146 | 147 | static func RoundedRectView() -> some View { 148 | 149 | let P0 = CGPoint(x: 100, y: 100) 150 | let size = CGSize(width: 300, height: 200) 151 | let cornerSize = CGSize(width: 50, height: 50) 152 | 153 | let path = Path() { 154 | $0.addRoundedRect(in: CGRect(origin: P0, size: size), cornerSize: cornerSize) 155 | } 156 | 157 | return ZStack { 158 | PathText(text: text, path: path) 159 | // path.stroke(Color.blue, lineWidth: 2) 160 | } 161 | } 162 | 163 | static func TwoGlyphCharacter() -> some View { 164 | let P0 = CGPoint(x: 50, y: 300) 165 | let P1 = CGPoint(x: 650, y: 300) 166 | 167 | let path = Path() { 168 | $0.move(to: P0) 169 | $0.addLine(to: P1) 170 | } 171 | 172 | return VStack { 173 | Text("ÅX̊") // "X\u{030A}") 174 | .font(.system(size: 48)) 175 | ZStack { 176 | PathText(text: NSAttributedString(string: "ÅX̊Z", 177 | attributes: [.font: PlatformFont.systemFont(ofSize: 48)]), path: path) 178 | path.stroke(Color.blue, lineWidth: 2) 179 | } 180 | } 181 | } 182 | static var previews: some View { 183 | Group { 184 | CurveView() 185 | LineView() 186 | LinesView() 187 | LineAndCurveView() 188 | QuadCurveView() 189 | RoundedRectView() 190 | TwoGlyphCharacter() 191 | }.previewLayout(.fixed(width: 700, height: 500)) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /PathText/Sources/PathText/SwiftUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUI.swift 3 | // 4 | // 5 | // Created by Rob Napier on 1/4/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS, introduced: 13) 11 | @available(OSX, introduced: 10.15) 12 | public struct PathText { 13 | public var text: NSAttributedString 14 | public var path: CGPath 15 | 16 | public init(text: NSAttributedString, path: Path) { 17 | self.init(text: text, path: path.cgPath) 18 | } 19 | 20 | public init(text: NSAttributedString, path: CGPath) { 21 | self.text = text 22 | self.path = path 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PathText/Sources/PathText/UIKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKit.swift 3 | // 4 | // 5 | // Created by Rob Napier on 1/4/20. 6 | // 7 | 8 | #if canImport(UIKit) 9 | import UIKit 10 | import SwiftUI 11 | 12 | typealias PlatformColor = UIColor 13 | 14 | @available(iOS 13, *) 15 | extension PathText: UIViewRepresentable { 16 | public func makeUIView(context: Context) -> PathTextView { PathTextView() } 17 | 18 | public func updateUIView(_ uiView: PathTextView, context: Context) { 19 | uiView.text = text 20 | uiView.path = path 21 | } 22 | } 23 | 24 | /* 25 | Draws attributed text along a cubic Bezier path defined by P0, P1, P2, and P3 26 | */ 27 | public class PathTextView: UIView { 28 | 29 | private var layoutManager = PathTextLayoutManager() 30 | 31 | public var text: NSAttributedString { 32 | get { layoutManager.text } 33 | set { 34 | layoutManager.text = newValue 35 | setNeedsDisplay() 36 | } 37 | } 38 | 39 | public var path: CGPath { 40 | get { layoutManager.path } 41 | set { 42 | layoutManager.path = newValue 43 | setNeedsDisplay() 44 | } 45 | } 46 | 47 | public init(frame: CGRect = .zero, text: NSAttributedString = NSAttributedString(), path: CGPath = CGMutablePath()) { 48 | super.init(frame: frame) 49 | self.text = text 50 | self.path = path 51 | 52 | backgroundColor = .clear 53 | } 54 | 55 | required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 56 | 57 | public override func draw(_ rect: CGRect) { 58 | let context = UIGraphicsGetCurrentContext()! 59 | context.textMatrix = CGAffineTransform(scaleX: 1, y: -1) 60 | layoutManager.draw(in: context) 61 | } 62 | 63 | public var typographicBounds: CGRect { 64 | layoutManager.ensureLayout() 65 | return layoutManager.typographicBounds 66 | } 67 | } 68 | #endif 69 | -------------------------------------------------------------------------------- /PathText/Tests/PathTextTests/PathTextTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PathText 3 | 4 | #if canImport(UIKit) 5 | import UIKit 6 | private typealias PlatformFont = UIFont 7 | private typealias PlatformColor = UIColor 8 | #elseif canImport(AppKit) 9 | private typealias PlatformFont = NSFont 10 | private typealias PlatformColor = NSColor 11 | #else 12 | #error("Unsupported platform") 13 | #endif 14 | 15 | func AssertPathTangentsEqual(_ expression1: [PathTangent], _ expression2: [PathTangent]) { 16 | for (tangent1, tangent2) in zip(expression1, expression2) { 17 | XCTAssertEqual(tangent1.t, tangent2.t, accuracy: 0.01) 18 | XCTAssertEqual(tangent1.angle, tangent2.angle, accuracy: 0.01) 19 | XCTAssertEqual(tangent1.point.x, tangent2.point.x, accuracy: 1.0) 20 | XCTAssertEqual(tangent1.point.y, tangent2.point.y, accuracy: 1.0) 21 | } 22 | } 23 | 24 | @available(iOS, introduced: 13.0) 25 | @available(OSX, introduced: 10.15) 26 | final class PathTextTests: XCTestCase { 27 | static let text: NSAttributedString = { 28 | let string = NSString("You can display text along a curve, with bold, color, and big text.") 29 | 30 | let s = NSMutableAttributedString(string: string as String, 31 | attributes: [.font: PlatformFont.systemFont(ofSize: 16)]) 32 | 33 | s.addAttributes([.font: PlatformFont.boldSystemFont(ofSize: 16)], range: string.range(of: "bold")) 34 | s.addAttributes([.foregroundColor: PlatformColor.red], range: string.range(of: "color")) 35 | s.addAttributes([.font: PlatformFont.systemFont(ofSize: 32)], range: string.range(of: "big text")) 36 | return s 37 | }() 38 | 39 | func testCurve() { 40 | let P0 = CGPoint(x: 50, y: 500) 41 | let P1 = CGPoint(x: 300, y: 300) 42 | let P2 = CGPoint(x: 400, y: 700) 43 | let P3 = CGPoint(x: 650, y: 500) 44 | 45 | let path = CGMutablePath() 46 | path.move(to: P0) 47 | path.addCurve(to: P3, control1: P1, control2: P2) 48 | 49 | let sections = path.sections() 50 | 51 | XCTAssertEqual(sections.count, 1) 52 | 53 | guard let section = sections.first as? PathCurveSection else { 54 | XCTFail() 55 | return 56 | } 57 | 58 | XCTAssertEqual(section.p0, P0) 59 | XCTAssertEqual(section.p1, P1) 60 | XCTAssertEqual(section.p2, P2) 61 | XCTAssertEqual(section.p3, P3) 62 | 63 | var generator = TangentGenerator(path: path) 64 | let tangents = [0, 100, 200, 300, 400, 500, 600].compactMap{ generator.getTangent(at: $0) } 65 | 66 | AssertPathTangentsEqual(tangents, [ 67 | PathTangent(t: 0, point: P0, angle: -0.674), 68 | PathTangent(t: 0.124, point: CGPoint(x: 137, y: 451), angle: -0.310), 69 | PathTangent(t: 0.288, point: CGPoint(x: 236, y: 448), angle: 0.234), 70 | PathTangent(t: 0.461, point: CGPoint(x: 328, y: 488), angle: 0.510), 71 | PathTangent(t: 0.628, point: CGPoint(x: 416, y: 536), angle: 0.420), 72 | PathTangent(t: 0.800, point: CGPoint(x: 513, y: 558), angle: -0.026), 73 | PathTangent(t: 0.947, point: CGPoint(x: 607, y: 528), angle: -0.522), 74 | ]) 75 | } 76 | 77 | func testFlatLine() { 78 | 79 | let P0 = CGPoint(x: 0, y: 0) 80 | let P1 = CGPoint(x: 800, y: 0) 81 | 82 | let path = CGMutablePath() 83 | path.move(to: P0) 84 | path.addLine(to: P1) 85 | 86 | let sections = path.sections() 87 | 88 | XCTAssertEqual(sections.count, 1) 89 | 90 | guard let section = sections.first as? PathLineSection else { 91 | XCTFail() 92 | return 93 | } 94 | 95 | XCTAssertEqual(section.start, P0) 96 | XCTAssertEqual(section.end, P1) 97 | 98 | var generator = TangentGenerator(path: path) 99 | let tangents = [0, 100, 200, 300, 400, 500, 600, 700].compactMap{ generator.getTangent(at: $0) } 100 | 101 | AssertPathTangentsEqual(tangents, [ 102 | PathTangent(t: 0, point: CGPoint(x: 0, y: 0), angle: 0), 103 | PathTangent(t: 0.125, point: CGPoint(x: 100, y: 0), angle: 0), 104 | PathTangent(t: 0.250, point: CGPoint(x: 200, y: 0), angle: 0), 105 | PathTangent(t: 0.375, point: CGPoint(x: 300, y: 0), angle: 0), 106 | PathTangent(t: 0.500, point: CGPoint(x: 400, y: 0), angle: 0), 107 | PathTangent(t: 0.625, point: CGPoint(x: 500, y: 0), angle: 0), 108 | PathTangent(t: 0.750, point: CGPoint(x: 600, y: 0), angle: 0), 109 | PathTangent(t: 0.875, point: CGPoint(x: 700, y: 0), angle: 0), 110 | ]) 111 | } 112 | 113 | func testLine() { 114 | 115 | let P0 = CGPoint(x: 0, y: 0) 116 | let P1 = CGPoint(x: 800, y: 800) 117 | 118 | let path = CGMutablePath() 119 | path.move(to: P0) 120 | path.addLine(to: P1) 121 | 122 | let sections = path.sections() 123 | 124 | XCTAssertEqual(sections.count, 1) 125 | 126 | guard let section = sections.first as? PathLineSection else { 127 | XCTFail() 128 | return 129 | } 130 | 131 | XCTAssertEqual(section.start, P0) 132 | XCTAssertEqual(section.end, P1) 133 | 134 | var generator = TangentGenerator(path: path) 135 | let tangents = [0, 100, 200, 300, 400, 500, 600, 700].compactMap{ generator.getTangent(at: $0) } 136 | 137 | let angle: CGFloat = atan(1) 138 | 139 | AssertPathTangentsEqual(tangents, [ 140 | PathTangent(t: 0.000, point: CGPoint(x: 0.0, y: 0.0), angle: angle), 141 | PathTangent(t: 0.089, point: CGPoint(x: 71, y: 71), angle: angle), 142 | PathTangent(t: 0.178, point: CGPoint(x: 142, y: 142), angle: angle), 143 | PathTangent(t: 0.267, point: CGPoint(x: 214, y: 214), angle: angle), 144 | PathTangent(t: 0.356, point: CGPoint(x: 285, y: 285), angle: angle), 145 | PathTangent(t: 0.445, point: CGPoint(x: 356, y: 356), angle: angle), 146 | PathTangent(t: 0.534, point: CGPoint(x: 427, y: 427), angle: angle), 147 | PathTangent(t: 0.623, point: CGPoint(x: 498, y: 498), angle: angle), 148 | ]) 149 | } 150 | 151 | func testTwoLines() { 152 | 153 | let P0 = CGPoint(x: 0, y: 0) 154 | let P1 = CGPoint(x: 400, y: 400) 155 | let P2 = CGPoint(x: 800, y: 0) 156 | 157 | let path = CGMutablePath() 158 | path.move(to: P0) 159 | path.addLine(to: P1) 160 | path.addLine(to: P2) 161 | 162 | let sections = path.sections() 163 | 164 | XCTAssertEqual(sections.count, 2) 165 | 166 | guard let section1 = sections.first as? PathLineSection else { 167 | XCTFail() 168 | return 169 | } 170 | 171 | XCTAssertEqual(section1.start, P0) 172 | XCTAssertEqual(section1.end, P1) 173 | 174 | guard let section2 = sections.dropFirst().first as? PathLineSection else { 175 | XCTFail() 176 | return 177 | } 178 | 179 | XCTAssertEqual(section2.start, P1) 180 | XCTAssertEqual(section2.end, P2) 181 | 182 | var generator = TangentGenerator(path: path) 183 | let tangents = [0, 100, 200, 300, 400, 500, 600, 700].compactMap{ generator.getTangent(at: $0) } 184 | 185 | AssertPathTangentsEqual(tangents, [ 186 | PathTangent(t: 0.000, point: CGPoint(x: 0, y: 0), angle: 0.785), 187 | PathTangent(t: 0.177, point: CGPoint(x: 70, y: 71), angle: 0.785), 188 | PathTangent(t: 0.354, point: CGPoint(x: 141, y: 142), angle: 0.785), 189 | PathTangent(t: 0.531, point: CGPoint(x: 212, y: 212), angle: 0.785), 190 | PathTangent(t: 0.708, point: CGPoint(x: 283, y: 283), angle: 0.785), 191 | PathTangent(t: 0.885, point: CGPoint(x: 354, y: 354), angle: 0.785), 192 | PathTangent(t: 0.062, point: CGPoint(x: 425, y: 375), angle: -0.785), 193 | PathTangent(t: 0.239, point: CGPoint(x: 496, y: 304), angle: -0.785), 194 | ]) 195 | } 196 | 197 | func testQuadCurve() { 198 | 199 | let P0 = CGPoint(x: 50, y: 500) 200 | let P1 = CGPoint(x: 300, y: 300) 201 | let P2 = CGPoint(x: 650, y: 500) 202 | 203 | let path = CGMutablePath() 204 | path.move(to: P0) 205 | path.addQuadCurve(to: P2, control: P1) 206 | 207 | let sections = path.sections() 208 | 209 | XCTAssertEqual(sections.count, 1) 210 | 211 | guard let section1 = sections.first as? PathQuadCurveSection else { 212 | XCTFail() 213 | return 214 | } 215 | 216 | XCTAssertEqual(section1.p0, P0) 217 | XCTAssertEqual(section1.p1, P1) 218 | XCTAssertEqual(section1.p2, P2) 219 | 220 | var generator = TangentGenerator(path: path) 221 | let tangents = [0, 100, 200, 300, 400, 500, 600, 700].compactMap{ generator.getTangent(at: $0) } 222 | 223 | AssertPathTangentsEqual(tangents, [ 224 | PathTangent(t: 0.000, point: CGPoint(x: 50, y: 500), angle: -0.675), 225 | PathTangent(t: 0.163, point: CGPoint(x: 134, y: 445), angle: -0.469), 226 | PathTangent(t: 0.334, point: CGPoint(x: 228, y: 411), angle: -0.230), 227 | PathTangent(t: 0.505, point: CGPoint(x: 328, y: 400), angle: 0.007), 228 | PathTangent(t: 0.667, point: CGPoint(x: 427, y: 411), angle: 0.208), 229 | PathTangent(t: 0.815, point: CGPoint(x: 523, y: 440), angle: 0.363), 230 | PathTangent(t: 0.950, point: CGPoint(x: 614, y: 481), angle: 0.481), 231 | ]) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /PathTextDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PathTextDemo 4 | // 5 | // Created by Rob Napier on 1/5/20. 6 | // Copyright © 2020 Rob Napier. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /PathTextDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /PathTextDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /PathTextDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /PathTextDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /PathTextDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /PathTextDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // PathTextDemo 4 | // 5 | // Created by Rob Napier on 1/5/20. 6 | // Copyright © 2020 Rob Napier. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import PathText 11 | 12 | class ViewController: UIViewController { 13 | 14 | let path: CGPath = { 15 | let P0 = CGPoint(x: 50, y: 100) 16 | let P1 = CGPoint(x: 300, y: 0) 17 | let P2 = CGPoint(x: 400, y: 200) 18 | let P3 = CGPoint(x: 650, y: 100) 19 | 20 | let path = CGMutablePath() 21 | path.move(to: P0) 22 | path.addCurve(to: P3, control1: P1, control2: P2) 23 | return path 24 | }() 25 | 26 | let text: NSAttributedString = { 27 | let string = NSString("You can display text along a curve, with bold, color, and BIG text.") 28 | 29 | let s = NSMutableAttributedString(string: string as String, 30 | attributes: [.font: UIFont.systemFont(ofSize: 16)]) 31 | 32 | s.addAttributes([.font: UIFont.boldSystemFont(ofSize: 16)], range: string.range(of: "bold")) 33 | s.addAttributes([.foregroundColor: UIColor.red], range: string.range(of: "color")) 34 | s.addAttributes([.font: UIFont.systemFont(ofSize: 32)], range: string.range(of: "BIG text")) 35 | return s 36 | }() 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | 41 | let frame = CGRect(origin: CGPoint(x: 50, y: 100), 42 | size: CGSize(width: 650, height: 200)) 43 | 44 | let textView = PathTextView(frame: frame, text: text, path: path) 45 | 46 | textView.layer.borderColor = UIColor.red.cgColor 47 | textView.layer.borderWidth = 1 48 | view.addSubview(textView) 49 | 50 | let pathView = PathView(frame: frame, path: path) 51 | view.addSubview(pathView) 52 | 53 | var tightFrame = frame 54 | tightFrame.origin.y = 400 55 | let tightTextView = PathTextView(frame: tightFrame, text: text, path: path) 56 | tightTextView.bounds = tightTextView.typographicBounds 57 | tightTextView.layer.borderColor = UIColor.red.cgColor 58 | tightTextView.layer.borderWidth = 1 59 | view.addSubview(tightTextView) 60 | 61 | let line = CGMutablePath() 62 | line.move(to: CGPoint(x: 50, y: 50)) 63 | line.addLine(to: CGPoint(x: 600, y: 50)) 64 | var lineFrame = tightFrame 65 | lineFrame.origin.y = 700 66 | 67 | let lineTextView = PathTextView(frame: lineFrame, text: text, path: line) 68 | lineTextView.layer.borderColor = UIColor.red.cgColor 69 | lineTextView.layer.borderWidth = 1 70 | view.addSubview(lineTextView) 71 | } 72 | } 73 | 74 | class PathView: UIView { 75 | var path: CGPath 76 | init(frame: CGRect = .zero, path: CGPath) { 77 | self.path = path 78 | super.init(frame: frame) 79 | self.backgroundColor = UIColor.clear 80 | } 81 | 82 | required init?(coder: NSCoder) { 83 | fatalError("init(coder:) has not been implemented") 84 | } 85 | 86 | override func draw(_ rect: CGRect) { 87 | let ctx = UIGraphicsGetCurrentContext()! 88 | ctx.addPath(path) 89 | ctx.strokePath() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /PathTextDemoMac/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PathTextDemoMac 4 | // 5 | // Created by Rob Napier on 1/5/20. 6 | // Copyright © 2020 Rob Napier. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @NSApplicationMain 12 | class AppDelegate: NSObject, NSApplicationDelegate { 13 | 14 | 15 | 16 | func applicationDidFinishLaunching(_ aNotification: Notification) { 17 | // Insert code here to initialize your application 18 | } 19 | 20 | func applicationWillTerminate(_ aNotification: Notification) { 21 | // Insert code here to tear down your application 22 | } 23 | 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /PathTextDemoMac/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /PathTextDemoMac/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /PathTextDemoMac/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2020 Rob Napier. All rights reserved. 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | NSSupportsAutomaticTermination 32 | 33 | NSSupportsSuddenTermination 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /PathTextDemoMac/PathTextDemoMac.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /PathTextDemoMac/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // PathTextDemoMac 4 | // 5 | // Created by Rob Napier on 1/5/20. 6 | // Copyright © 2020 Rob Napier. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import PathText 11 | 12 | class ViewController: NSViewController { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | let P0 = CGPoint(x: 50, y: 100) 18 | let P1 = CGPoint(x: 300, y: 0) 19 | let P2 = CGPoint(x: 400, y: 200) 20 | let P3 = CGPoint(x: 650, y: 100) 21 | 22 | let path = CGMutablePath() 23 | path.move(to: P0) 24 | path.addCurve(to: P3, control1: P1, control2: P2) 25 | 26 | let text: NSAttributedString = { 27 | let string = NSString("You can display text along a curve, with bold, color, and big text.") 28 | 29 | let s = NSMutableAttributedString(string: string as String, 30 | attributes: [.font: NSFont.systemFont(ofSize: 16)]) 31 | 32 | s.addAttributes([.font: NSFont.boldSystemFont(ofSize: 16)], range: string.range(of: "bold")) 33 | s.addAttributes([.foregroundColor: NSColor.red], range: string.range(of: "color")) 34 | s.addAttributes([.font: NSFont.systemFont(ofSize: 32)], range: string.range(of: "big text")) 35 | return s 36 | }() 37 | 38 | let frame = CGRect(origin: CGPoint(x: 50, y: 100), 39 | size: CGSize(width: 650, height: 200)) 40 | 41 | let textView = PathTextView(frame: frame, text: text, path: path) 42 | 43 | textView.wantsLayer = true 44 | textView.layer?.borderColor = NSColor.red.cgColor 45 | textView.layer?.borderWidth = 1 46 | view.addSubview(textView) 47 | 48 | let pathView = PathView(frame: frame, path: path) 49 | view.addSubview(pathView) 50 | 51 | } 52 | 53 | override var representedObject: Any? { 54 | didSet { 55 | // Update the view, if already loaded. 56 | } 57 | } 58 | } 59 | 60 | class PathView: NSView { 61 | var path: CGPath 62 | init(frame: NSRect = .zero, path: CGPath) { 63 | self.path = path 64 | super.init(frame: frame) 65 | } 66 | 67 | required init?(coder: NSCoder) { 68 | fatalError("init(coder:) has not been implemented") 69 | } 70 | 71 | override func draw(_ dirtyRect: NSRect) { 72 | let ctx = NSGraphicsContext.current!.cgContext 73 | ctx.addPath(path) 74 | ctx.strokePath() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CurvyText 2 | 3 | ![Screenshot of text along a curvy line. The text reads, “You can display text along a curve, with bold, color, and big text.”](.github/CurvyText.png) 4 | --------------------------------------------------------------------------------