├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Example ├── ShapeUpExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── ShapeUpExample.xcscheme ├── ShapeUpExample │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-MacOS-128x128@1x.png │ │ │ ├── Icon-MacOS-128x128@2x.png │ │ │ ├── Icon-MacOS-16x16@1x.png │ │ │ ├── Icon-MacOS-16x16@2x.png │ │ │ ├── Icon-MacOS-256x256@1x.png │ │ │ ├── Icon-MacOS-256x256@2x.png │ │ │ ├── Icon-MacOS-32x32@1x.png │ │ │ ├── Icon-MacOS-32x32@2x.png │ │ │ ├── Icon-MacOS-512x512@1x.png │ │ │ ├── Icon-MacOS-512x512@2x.png │ │ │ └── app-icon-1024@1x~ios-marketing.png │ │ ├── AppIcon.brandassets │ │ │ ├── App Icon - App Store.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── ShapeUp-icon-tvOS-back.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ └── ShapeUp-icon-tvOS-front.png │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── ShapeUp-icon-tvOS-middle.png │ │ │ │ │ └── Contents.json │ │ │ ├── App Icon.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── ShapeUp-icon-tvOS-back400.png │ │ │ │ │ │ └── ShapeUp-icon-tvOS-back800.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ ├── Contents.json │ │ │ │ │ │ ├── ShapeUp-icon-tvOS-front400.png │ │ │ │ │ │ └── ShapeUp-icon-tvOS-front800.png │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── ShapeUp-icon-tvOS-middle400.png │ │ │ │ │ └── ShapeUp-icon-tvOS-middle800.png │ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Top Shelf Image Wide.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── ShapeUp-topShelfWide-tvOS.png │ │ │ │ └── ShapeUp-topShelfWide-tvOS1440.png │ │ │ └── Top Shelf Image.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── ShapeUp-topShelf-tvOS.png │ │ │ │ └── ShapeUp-topShelf-tvOS1440.png │ │ ├── AppIcon.solidimagestack │ │ │ ├── Back.solidimagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── ShapeUp-icon-visionOS-back.png │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Front.solidimagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── ShapeUp-icon-visionOS-front.png │ │ │ │ └── Contents.json │ │ │ └── Middle.solidimagestacklayer │ │ │ │ ├── Content.imageset │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── BasicShapes │ │ ├── AnimatableTestShape.swift │ │ ├── BasicShapes.swift │ │ ├── CornerPentagonExample.swift │ │ ├── CornerPentagonExample2.swift │ │ ├── CornerRectangleExample.swift │ │ ├── CornerTriangleExample.swift │ │ └── CornerTriangleExample2.swift │ ├── CodeFeatures │ │ ├── BasicCompareExample.swift │ │ └── MessageBubble │ │ │ ├── MessageBubble0Example.swift │ │ │ ├── MessageBubble1Example.swift │ │ │ ├── MessageBubble2Example.swift │ │ │ ├── MessageBubble3Example.swift │ │ │ ├── MessageBubble4Example.swift │ │ │ ├── MessageBubbleExamples.swift │ │ │ └── MessageBubbleInsetExample.swift │ ├── ContentView.swift │ ├── Corner │ │ ├── AddOpenCornerShapeExample.swift │ │ ├── CornerExample.swift │ │ ├── CornerShapeExample.swift │ │ ├── CornerStyleExampleOld.swift │ │ └── NestedCornerStyleExample.swift │ ├── CornerCustomExample.swift │ ├── Info.plist │ ├── NotchedShapes │ │ ├── NotchedExamples.swift │ │ ├── NotchedPentagonExample.swift │ │ ├── NotchedRectangleExample.swift │ │ └── NotchedTriangleExample.swift │ ├── Other Tools │ │ ├── AnimatablePackExample.swift │ │ ├── EmbossExample.swift │ │ ├── InsettableShapeByPropertyExample.swift │ │ └── SketchyLineExample.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── RectangleExample.swift │ ├── RectangleExample2.swift │ └── ShapeUpExampleApp.swift ├── Shared │ ├── CrossPlatform │ │ ├── CrossPlatformSlider.swift │ │ └── CrossPlatformStepper.swift │ ├── CustomShapes │ │ ├── FoldButton.swift │ │ ├── HexagonExample.swift │ │ ├── InsetCornerShapeExample.swift │ │ └── ShapeUpLogoView.swift │ └── SharedAssets.xcassets │ │ ├── Contents.json │ │ ├── SUBlack.colorset │ │ └── Contents.json │ │ ├── SUCyan.colorset │ │ └── Contents.json │ │ ├── SUPink.colorset │ │ └── Contents.json │ │ ├── SUPurple.colorset │ │ └── Contents.json │ │ ├── SUWhite.colorset │ │ └── Contents.json │ │ ├── SUYellow.colorset │ │ └── Contents.json │ │ └── ShapeUp-logo.imageset │ │ ├── Contents.json │ │ └── ShapeUp-logo.png └── WatchShapeUpExample │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── app-icon-1024@1x~ios-marketing.png │ └── Contents.json │ ├── ContentView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── WatchShapeUpExampleApp.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── ShapeUp │ ├── Angle │ ├── AngleRepresentable.swift │ └── AngleType.swift │ ├── AnimatablePack │ └── AnimatablePack.swift │ ├── Corner │ ├── Corner+Animatable.swift │ ├── Corner+extensions+Array.swift │ ├── Corner+extensions.swift │ ├── Corner.swift │ ├── CornerStyle+Animatable.swift │ ├── CornerStyle+CutoutLocation.swift │ ├── CornerStyle.swift │ └── Dimensions │ │ ├── Corner+Dimensions+Array.swift │ │ ├── Corner+Dimensions+concave.swift │ │ ├── Corner+Dimensions+exensions.swift │ │ ├── Corner+Dimensions+flattened.swift │ │ ├── Corner+Dimensions+inset.swift │ │ ├── Corner+Dimensions+path.swift │ │ └── Corner+Dimensions.swift │ ├── CornerShape │ ├── CornerCustom.swift │ ├── CornerShape.swift │ ├── EnumeratedCornerShape │ │ ├── CornerPentagon.swift │ │ ├── CornerRectangle.swift │ │ ├── CornerTriangle.swift │ │ └── EnumeratedCornerShape.swift │ └── InsettableShapeByProperty.swift │ ├── Emboss │ └── EmbossViewModifier.swift │ ├── GeoMath.swift │ ├── Notch │ ├── Notch+extensions.swift │ ├── Notch+staticInit.swift │ ├── Notch.swift │ ├── NotchStyle+staticInit.swift │ └── NotchStyle.swift │ ├── PrivacyInfo.xcprivacy │ ├── Rect │ ├── RectAnchor+extensions+Array.swift │ └── RectAnchor.swift │ ├── RelatableValue │ ├── RelatableValue+Arithmatic.swift │ ├── RelatableValue+VectorArithmetic.swift │ └── RelatableValue.swift │ ├── SketchyLines │ ├── SketchyLine+staticInit.swift │ ├── SketchyLine.swift │ └── SketchyLines.swift │ ├── Vector2 │ ├── Vector2.swift │ ├── Vector2Algebraic.swift │ ├── Vector2Representable+Array.swift │ ├── Vector2Representable.swift │ ├── Vector2Transformable+Array.swift │ └── Vector2Transformable.swift │ ├── _Experimental │ └── RelativeCornerShape.swift │ ├── _Extension-Internal │ ├── Angle+extensions.swift │ ├── CGRect+extensions.swift │ └── CGSize+extensions.swift │ └── _Extension-Public │ ├── CGPoint+publicExtensions.swift │ ├── CGRect+publicExtensions.swift │ ├── InsettableShape+publicExtensions.swift │ ├── Path+publicExtensions.swift │ ├── Rectangle+publicExtensions.swift │ ├── Shape+publicExtensions.swift │ └── View+publicExtensions.swift └── Tests └── ShapeUpTests ├── AngleRepresentableTests.swift ├── AngleTypeTests.swift ├── GeoMathTests.swift ├── RectAnchorArrayTests.swift ├── RectAnchorTests.swift ├── RelatableValueTests.swift ├── Vector2AlgebraicTests.swift ├── Vector2RepresentableArrayTests.swift ├── Vector2RepresentableTests.swift └── Vector2Tests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Thumbnails 11 | ._* 12 | 13 | ### Swift ### 14 | # Xcode 15 | # 16 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 17 | 18 | ## User settings 19 | xcuserdata/ 20 | 21 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 22 | *.xcscmblueprint 23 | *.xccheckout 24 | 25 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 26 | build/ 27 | DerivedData/ 28 | *.moved-aside 29 | *.pbxuser 30 | !default.pbxuser 31 | *.mode1v3 32 | !default.mode1v3 33 | *.mode2v3 34 | !default.mode2v3 35 | *.perspectivev3 36 | !default.perspectivev3 37 | 38 | ## Obj-C/Swift specific 39 | *.hmap 40 | 41 | ## App packaging 42 | *.ipa 43 | *.dSYM.zip 44 | *.dSYM 45 | 46 | ## Playgrounds 47 | timeline.xctimeline 48 | playground.xcworkspace 49 | 50 | # Swift Package Manager 51 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 52 | Packages/ 53 | Package.pins 54 | Package.resolved 55 | # *.xcodeproj 56 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 57 | # hence it is not needed unless you have added a package configuration file to your project 58 | .swiftpm 59 | 60 | .build/ 61 | 62 | # CocoaPods 63 | # We recommend against adding the Pods directory to your .gitignore. However 64 | # you should judge for yourself, the pros and cons are mentioned at: 65 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 66 | # Pods/ 67 | # Add this line if you want to avoid checking in source code from the Xcode workspace 68 | # *.xcworkspace 69 | 70 | # Carthage 71 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 72 | # Carthage/Checkouts 73 | 74 | Carthage/Build/ 75 | 76 | # Accio dependency management 77 | Dependencies/ 78 | .accio/ 79 | 80 | # fastlane 81 | # It is recommended to not store the screenshots in the git repo. 82 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 83 | # For more information about the recommended setup visit: 84 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 85 | 86 | fastlane/report.xml 87 | fastlane/Preview.html 88 | fastlane/screenshots/**/*.png 89 | fastlane/test_output 90 | 91 | # Code Injection 92 | # After new code Injection tools there's a generated folder /iOSInjectionProject 93 | # https://github.com/johnno1962/injectionforxcode 94 | 95 | iOSInjectionProject/ 96 | 97 | # End of https://www.toptal.com/developers/gitignore/api/swift,macos -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/ShapeUpExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/ShapeUpExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/ShapeUpExample.xcodeproj/xcshareddata/xcschemes/ShapeUpExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024@1x~ios-marketing.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "filename" : "Icon-MacOS-16x16@1x.png", 11 | "idiom" : "mac", 12 | "scale" : "1x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Icon-MacOS-16x16@2x.png", 17 | "idiom" : "mac", 18 | "scale" : "2x", 19 | "size" : "16x16" 20 | }, 21 | { 22 | "filename" : "Icon-MacOS-32x32@1x.png", 23 | "idiom" : "mac", 24 | "scale" : "1x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Icon-MacOS-32x32@2x.png", 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "32x32" 32 | }, 33 | { 34 | "filename" : "Icon-MacOS-128x128@1x.png", 35 | "idiom" : "mac", 36 | "scale" : "1x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Icon-MacOS-128x128@2x.png", 41 | "idiom" : "mac", 42 | "scale" : "2x", 43 | "size" : "128x128" 44 | }, 45 | { 46 | "filename" : "Icon-MacOS-256x256@1x.png", 47 | "idiom" : "mac", 48 | "scale" : "1x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Icon-MacOS-256x256@2x.png", 53 | "idiom" : "mac", 54 | "scale" : "2x", 55 | "size" : "256x256" 56 | }, 57 | { 58 | "filename" : "Icon-MacOS-512x512@1x.png", 59 | "idiom" : "mac", 60 | "scale" : "1x", 61 | "size" : "512x512" 62 | }, 63 | { 64 | "filename" : "Icon-MacOS-512x512@2x.png", 65 | "idiom" : "mac", 66 | "scale" : "2x", 67 | "size" : "512x512" 68 | } 69 | ], 70 | "info" : { 71 | "author" : "xcode", 72 | "version" : 1 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@1x.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@2x.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@1x.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@2x.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@1x.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@2x.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@1x.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@2x.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@1x.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@2x.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/app-icon-1024@1x~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.appiconset/app-icon-1024@1x~ios-marketing.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShapeUp-icon-tvOS-back.png", 5 | "idiom" : "tv" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-back.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShapeUp-icon-tvOS-front.png", 5 | "idiom" : "tv" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-front.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShapeUp-icon-tvOS-middle.png", 5 | "idiom" : "tv" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-middle.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShapeUp-icon-tvOS-back400.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ShapeUp-icon-tvOS-back800.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-back400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-back400.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-back800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-back800.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShapeUp-icon-tvOS-front400.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ShapeUp-icon-tvOS-front800.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-front400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-front400.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-front800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-front800.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShapeUp-icon-tvOS-middle400.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ShapeUp-icon-tvOS-middle800.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-middle400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-middle400.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-middle800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/ShapeUp-icon-tvOS-middle800.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShapeUp-topShelfWide-tvOS.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ShapeUp-topShelfWide-tvOS1440.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/ShapeUp-topShelfWide-tvOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/ShapeUp-topShelfWide-tvOS.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/ShapeUp-topShelfWide-tvOS1440.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image Wide.imageset/ShapeUp-topShelfWide-tvOS1440.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShapeUp-topShelf-tvOS.png", 5 | "idiom" : "tv", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ShapeUp-topShelf-tvOS1440.png", 10 | "idiom" : "tv", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "author" : "xcode", 16 | "version" : 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/ShapeUp-topShelf-tvOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/ShapeUp-topShelf-tvOS.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/ShapeUp-topShelf-tvOS1440.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/ShapeUp-topShelf-tvOS1440.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShapeUp-icon-visionOS-back.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/ShapeUp-icon-visionOS-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/ShapeUp-icon-visionOS-back.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.solidimagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.solidimagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.solidimagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.solidimagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShapeUp-icon-visionOS-front.png", 5 | "idiom" : "vision", 6 | "scale" : "2x" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/ShapeUp-icon-visionOS-front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/ShapeUpExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/ShapeUp-icon-visionOS-front.png -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "vision", 5 | "scale" : "2x" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/BasicShapes/AnimatableTestShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatableTestShape.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-08-09. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AnimatableTestShape: Shape { 11 | var insetAmount: Double 12 | 13 | var animatableData: Double { 14 | get { insetAmount } 15 | set { insetAmount = newValue } 16 | } 17 | 18 | func path(in rect: CGRect) -> Path { 19 | var path = Path() 20 | path.addRect(rect.insetBy(dx: insetAmount, dy: insetAmount)) 21 | return path 22 | } 23 | } 24 | 25 | #Preview { 26 | AnimatableTestShape(insetAmount: 2) 27 | .fill() 28 | } 29 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/BasicShapes/BasicShapes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicShapes.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-08-13. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct BasicShapes: View { 12 | var body: some View { 13 | Section { 14 | NavigationLink(destination: CornerRectangleExample()) { 15 | Label("CornerRectangle", systemImage: "rectangle") 16 | } 17 | 18 | NavigationLink(destination: CornerTriangleExample2()) { 19 | Label("CornerTriangle", systemImage: "triangle") 20 | } 21 | 22 | NavigationLink(destination: CornerPentagonExample2()) { 23 | Label("CornerPentagon", systemImage: "pentagon") 24 | } 25 | } header: { 26 | Text("Basic Shapes") 27 | } 28 | } 29 | } 30 | 31 | struct BasicShapes_Previews: PreviewProvider { 32 | static var previews: some View { 33 | List { 34 | BasicShapes() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/BasicShapes/CornerPentagonExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerPentagonExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-09. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct CornerPentagonExample: View { 12 | var body: some View { 13 | VStack { 14 | CornerPentagon( 15 | pointHeight: .relative(0.3), 16 | topTaper: .relative(0.1), 17 | bottomTaper: .relative(0.3), 18 | styles: [ 19 | .topRight: .concave(radius: 30), 20 | .bottomLeft: .straight(radius: .relative(0.3)) 21 | ] 22 | ) 23 | .fill(Color.suCyan) 24 | .frame(width: 200, height: 100) 25 | 26 | CornerPentagon(pointHeight: 10) 27 | .applyingStyle(.concave(radius: 10)) 28 | .strokeBorder(Color.suPink, lineWidth: 8) 29 | .frame(width: 200, height: 100) 30 | 31 | CornerPentagon(pointHeight: .relative(0.5), topTaper: .relative(0.3)) 32 | .applyingStyle(.rounded(radius: .relative(0.3)), shapeCorners: [.bottomLeft, .bottomRight]) 33 | .fill(Color.suYellow) 34 | .frame(width: 200, height: 100) 35 | } 36 | .navigationTitle("CornerPentagon") 37 | } 38 | } 39 | 40 | struct CornerPentagonExample_Previews: PreviewProvider { 41 | static var previews: some View { 42 | NavigationView { 43 | CornerPentagonExample() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/BasicShapes/CornerPentagonExample2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerPentagonExample2.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-20. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct CornerPentagonExample2: View { 12 | @State private var pointHeight = 0.6 13 | @State private var topTaper = 0.6 14 | @State private var bottomTaper = 0.6 15 | @State private var inset = 0.0 16 | 17 | var body: some View { 18 | VStack { 19 | Color.clear.overlay( 20 | CornerPentagon( 21 | pointHeight: .relative(pointHeight), 22 | topTaper: .relative(topTaper), 23 | bottomTaper: .relative(bottomTaper) 24 | ) 25 | .applyingStyle(.rounded(radius: .relative(0.2))) 26 | .inset(by: inset) 27 | .fill(Color.suCyan) 28 | .animation(.default, value: pointHeight) 29 | .animation(.default, value: topTaper) 30 | .animation(.default, value: bottomTaper) 31 | .animation(.default, value: inset) 32 | ) 33 | 34 | CrossPlatformStepper( 35 | label: "Point Height", 36 | value: $pointHeight, 37 | minValue: 0, 38 | maxValue: 1, 39 | step: 0.1, 40 | decimalPlaces: 1 41 | ) 42 | 43 | CrossPlatformStepper( 44 | label: "Top Taper", 45 | value: $topTaper, 46 | minValue: 0, 47 | maxValue: 1, 48 | step: 0.1, 49 | decimalPlaces: 1 50 | ) 51 | 52 | CrossPlatformStepper( 53 | label: "Bottom Taper", 54 | value: $bottomTaper, 55 | minValue: 0, 56 | maxValue: 1, 57 | step: 0.1, 58 | decimalPlaces: 1 59 | ) 60 | 61 | CrossPlatformStepper( 62 | label: "Inset", 63 | value: $inset, 64 | minValue: -30, 65 | maxValue: 30, 66 | step: 10 67 | ) 68 | } 69 | .padding() 70 | .navigationTitle("CornerPentagon") 71 | } 72 | } 73 | 74 | struct CornerPentagonExample2_Previews: PreviewProvider { 75 | static var previews: some View { 76 | NavigationView { 77 | CornerPentagonExample2() 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/BasicShapes/CornerRectangleExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerRectangleExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-08-13. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct CornerRectangleExample: View { 12 | var body: some View { 13 | VStack { 14 | CornerRectangle([ 15 | .topLeft: .straight(radius: 60), 16 | .topRight: .cutout(radius: .relative(0.2)), 17 | .bottomRight: .rounded(radius: .relative(0.6)), 18 | .bottomLeft: .concave(radius: .relative(0.2)) 19 | ]) 20 | .fill(Color.suCyan) 21 | .frame(width: 200, height: 100) 22 | 23 | CornerRectangle() 24 | .applyingStyle(.straight(radius: 20)) 25 | .strokeBorder(Color.suPink, lineWidth: 8) 26 | .frame(width: 200, height: 100) 27 | 28 | CornerRectangle() 29 | .applyingStyle(.rounded(radius: 30), shapeCorners: [.bottomLeft, .bottomRight]) 30 | .fill(Color.suYellow) 31 | .frame(width: 100, height: 100) 32 | } 33 | .navigationTitle("CornerRectangle") 34 | } 35 | } 36 | 37 | struct CornerRectangleExample_Previews: PreviewProvider { 38 | static var previews: some View { 39 | CornerRectangleExample() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/BasicShapes/CornerTriangleExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerTriangleExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-09. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct CornerTriangleExample: View { 12 | var body: some View { 13 | VStack { 14 | CornerTriangle(topPoint: .relative(0.6), styles: [ 15 | .top: .straight(radius: 10), 16 | .bottomRight: .rounded(radius: .relative(0.3)), 17 | .bottomLeft: .concave(radius: .relative(0.2)) 18 | ]) 19 | .fill(Color.suCyan) 20 | .frame(width: 200, height: 100) 21 | 22 | CornerTriangle(topPoint: .zero) 23 | .applyingStyle(.concave(radius: 10)) 24 | .strokeBorder(Color.suPink, lineWidth: 8) 25 | .frame(width: 200, height: 100) 26 | 27 | CornerTriangle() 28 | .applyingStyle(.rounded(radius: 10), shapeCorners: [.top, .bottomRight]) 29 | .fill(Color.suYellow) 30 | .frame(width: 200, height: 100) 31 | } 32 | .navigationTitle("CornerTriangle") 33 | } 34 | } 35 | 36 | struct CornerTriangleExample_Previews: PreviewProvider { 37 | static var previews: some View { 38 | NavigationView { 39 | CornerTriangleExample() 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/BasicShapes/CornerTriangleExample2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerTriangleExample2.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-20. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct CornerTriangleExample2: View { 12 | @State private var topPoint = 0.6 13 | @State private var inset = 0.0 14 | 15 | var body: some View { 16 | VStack { 17 | Color.clear.overlay( 18 | CornerTriangle(topPoint: .relative(topPoint), styles: [ 19 | .top: .straight(radius: 10), 20 | .bottomRight: .rounded(radius: .relative(0.3)), 21 | .bottomLeft: .concave(radius: .relative(0.2)) 22 | ]) 23 | .inset(by: inset) 24 | .fill(Color.suCyan) 25 | .animation(.default, value: topPoint) 26 | .animation(.default, value: inset) 27 | ) 28 | 29 | CrossPlatformStepper( 30 | label: "Top Point", 31 | value: $topPoint, 32 | minValue: 0, 33 | maxValue: 1, 34 | step: 0.1 35 | ) 36 | 37 | #if !os(tvOS) 38 | Slider(value: $topPoint, in: 0...1) { 39 | Text("Top Point") 40 | } minimumValueLabel: { 41 | Text("0") 42 | } maximumValueLabel: { 43 | Text("1") 44 | } 45 | #endif 46 | 47 | CrossPlatformStepper( 48 | label: "Inset", 49 | value: $inset, 50 | minValue: -30, 51 | maxValue: 30, 52 | step: 10 53 | ) 54 | 55 | #if !os(tvOS) 56 | Slider(value: $inset, in: -30...30) { 57 | Text("Inset") 58 | } minimumValueLabel: { 59 | Text("-30") 60 | } maximumValueLabel: { 61 | Text("30") 62 | } 63 | #endif 64 | } 65 | .padding() 66 | .navigationTitle("CornerTriangle") 67 | } 68 | } 69 | 70 | struct CornerTriangleExample2_Previews: PreviewProvider { 71 | static var previews: some View { 72 | NavigationView { 73 | CornerTriangleExample2() 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/CodeFeatures/BasicCompareExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicCompareExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-10. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct SwiftUIBasicShape: Shape { 12 | func path(in rect: CGRect) -> Path { 13 | var path = Path() 14 | path.move(to: CGPoint(x: rect.minX, y: rect.midY)) 15 | path.addLine(to: CGPoint(x: rect.minX + rect.width * 0.25, y: rect.minY + rect.height * 0.25)) 16 | 17 | let cutLengthSquared: Double = sqrt(pow(rect.width * 0.25, 2) + pow(rect.height * 0.25, 2)) 18 | let radius = (rect.width / rect.height) * cutLengthSquared 19 | 20 | path.addArc( 21 | tangent1End: CGPoint(x: rect.midX, y: rect.minY), 22 | tangent2End: CGPoint(x: rect.minX + rect.width * 0.75, y: rect.minY + rect.height * 0.25), 23 | radius: radius 24 | ) 25 | path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) 26 | path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY)) 27 | path.closeSubpath() 28 | return path 29 | } 30 | } 31 | 32 | struct ShapeUpBasicShape: CornerShape { 33 | let closed: Bool = true 34 | var insetAmount: CGFloat = 0 35 | 36 | func corners(in rect: CGRect) -> [Corner] { 37 | [ 38 | Corner(x: rect.minX, y: rect.midY), 39 | Corner(.rounded(radius: .relative(0.5)), x: rect.midX, y: rect.minY), 40 | Corner(x: rect.maxX, y: rect.midY), 41 | Corner(x: rect.midX, y: rect.maxY) 42 | ] 43 | } 44 | } 45 | 46 | struct BasicCompareExample: View { 47 | var body: some View { 48 | VStack { 49 | SwiftUIBasicShape() 50 | .fill(Color.suPurple) 51 | 52 | Text("SwiftUI Shape - 30 lines of code\n(Not insettable)") 53 | 54 | ShapeUpBasicShape() 55 | .fill(Color.suPink) 56 | 57 | Text("ShapeUp CornerShape - 12 lines of code\n(Insettable)") 58 | 59 | CornerCustom { $0.points(.top, .right, .bottom, .left).corners([.rounded(radius: .relative(0.5))]) } 60 | .fill(Color.suCyan) 61 | 62 | Text("ShapeUp CornerCustom - 1 line of code\n(Insettable)") 63 | } 64 | .multilineTextAlignment(.center) 65 | .padding() 66 | } 67 | } 68 | 69 | struct BasicCompareExample_Previews: PreviewProvider { 70 | static var previews: some View { 71 | BasicCompareExample() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/CodeFeatures/MessageBubble/MessageBubble0Example.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageBubble0Example.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-24. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct MessageBubble0: Shape { 12 | let cornerRadius: CGFloat 13 | let pointSize: CGFloat 14 | let pointRadius: CGFloat 15 | 16 | func path(in rect: CGRect) -> Path { 17 | var path = Path() 18 | path.move(to: CGPoint(x: rect.midX, y: rect.minY)) 19 | path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.minY), tangent2End: CGPoint(x: rect.maxX, y: rect.maxY), radius: cornerRadius) 20 | path.addArc(tangent1End: CGPoint(x: rect.maxX, y: rect.maxY), tangent2End: CGPoint(x: rect.midX, y: rect.maxY), radius: cornerRadius) 21 | path.addArc(tangent1End: CGPoint(x: rect.midX + (pointSize / 2), y: rect.maxY), tangent2End: CGPoint(x: rect.midX, y: rect.maxY + pointSize), radius: pointRadius) 22 | path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY + pointSize)) 23 | path.addArc(tangent1End: CGPoint(x: rect.midX - (pointSize / 2), y: rect.maxY), tangent2End: CGPoint(x: rect.minX, y: rect.maxY), radius: pointRadius) 24 | path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.maxY), tangent2End: CGPoint(x: rect.minX, y: rect.minY), radius: cornerRadius) 25 | path.addArc(tangent1End: CGPoint(x: rect.minX, y: rect.minY), tangent2End: CGPoint(x: rect.midX, y: rect.minY), radius: cornerRadius) 26 | path.closeSubpath() 27 | return path 28 | } 29 | } 30 | 31 | struct MessageBubble0Example: View { 32 | var body: some View { 33 | MessageBubble0(cornerRadius: 20, pointSize: 20, pointRadius: 10) 34 | .fill(Color.suPurple) 35 | .frame(width: 200, height: 120) 36 | } 37 | } 38 | 39 | struct MessageBubble0Example_Previews: PreviewProvider { 40 | static var previews: some View { 41 | MessageBubble0Example() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/CodeFeatures/MessageBubble/MessageBubble1Example.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageBubble1Example.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-24. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct MessageBubble1: CornerShape { 12 | var closed: Bool = true 13 | var insetAmount: CGFloat = 0 14 | 15 | let cornerRadius: RelatableValue 16 | let pointSize: CGFloat 17 | let pointRadius: RelatableValue 18 | 19 | func corners(in rect: CGRect) -> [Corner] { 20 | [ 21 | Corner(.rounded(radius: cornerRadius), x: rect.minX, y: rect.minY), 22 | Corner(.rounded(radius: cornerRadius), x: rect.maxX, y: rect.minY), 23 | Corner(.rounded(radius: cornerRadius), x: rect.maxX, y: rect.maxY), 24 | Corner(.rounded(radius: pointRadius), x: rect.midX + (pointSize / 2), y: rect.maxY), 25 | Corner(.point, x: rect.midX, y: rect.maxY + pointSize), 26 | Corner(.rounded(radius: pointRadius), x: rect.midX - (pointSize / 2), y: rect.maxY), 27 | Corner(.rounded(radius: cornerRadius), x: rect.minX, y: rect.maxY) 28 | ] 29 | } 30 | } 31 | 32 | struct MessageBubble1Example: View { 33 | var body: some View { 34 | MessageBubble1(cornerRadius: 20, pointSize: 20, pointRadius: 10) 35 | .fill(Color.suPurple) 36 | .frame(width: 200, height: 120) 37 | } 38 | } 39 | 40 | struct MessageBubble1Example_Previews: PreviewProvider { 41 | static var previews: some View { 42 | MessageBubble1Example() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/CodeFeatures/MessageBubble/MessageBubble2Example.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageBubble2Example.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-24. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct MessageBubble2: CornerShape { 12 | let closed: Bool = true 13 | var insetAmount: CGFloat = 0 14 | 15 | let cornerRadius: RelatableValue 16 | let pointSize: CGFloat 17 | let pointRadius: RelatableValue 18 | 19 | func corners(in rect: CGRect) -> [Corner] { 20 | let rightSide = [ 21 | Corner(.rounded(radius: cornerRadius), x: rect.maxX, y: rect.minY), 22 | Corner(.rounded(radius: cornerRadius), x: rect.maxX, y: rect.maxY), 23 | Corner(.rounded(radius: pointRadius), x: rect.midX + (pointSize / 2), y: rect.maxY) 24 | ] 25 | 26 | return rightSide 27 | + [Corner(x: rect.midX, y: rect.maxY + pointSize)] 28 | + rightSide.flippedHorizontally(across: rect.midX).reversed() 29 | } 30 | } 31 | 32 | struct MessageBubble2Example: View { 33 | var body: some View { 34 | MessageBubble1(cornerRadius: 20, pointSize: 20, pointRadius: 10) 35 | .fill(Color.suPurple) 36 | .frame(width: 200, height: 120) 37 | } 38 | } 39 | 40 | struct MessageBubble2Example_Previews: PreviewProvider { 41 | static var previews: some View { 42 | MessageBubble2Example() 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/CodeFeatures/MessageBubble/MessageBubble3Example.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageBubble3Example.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-24. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct MessageBubble3: CornerShape { 12 | var closed: Bool = true 13 | var insetAmount: CGFloat = 0 14 | 15 | let cornerRadius: RelatableValue 16 | let pointSize: RelatableValue 17 | let pointRadius: RelatableValue 18 | 19 | func corners(in rect: CGRect) -> [Corner] { 20 | rect 21 | .corners(.rounded(radius: cornerRadius)) 22 | .addingNotch( 23 | .triangle(depth: -pointSize, cornerStyles: [ 24 | .rounded(radius: pointRadius), 25 | .point, 26 | .rounded(radius: pointRadius) 27 | ]), 28 | afterCornerIndex: 2 29 | ) 30 | } 31 | } 32 | 33 | struct MessageBubble3Example: View { 34 | @State private var cornerRadius: RelatableValue = 20 35 | 36 | var body: some View { 37 | VStack { 38 | MessageBubble3(cornerRadius: cornerRadius, pointSize: 20, pointRadius: 10) 39 | .fill(Color.suPurple) 40 | .frame(width: 200, height: 120) 41 | .animation(.spring(), value: cornerRadius) 42 | } 43 | } 44 | } 45 | 46 | struct MessageBubble3Example_Previews: PreviewProvider { 47 | static var previews: some View { 48 | MessageBubble3Example() 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/CodeFeatures/MessageBubble/MessageBubble4Example.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageBubble4Example.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-24. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct MessageBubble4: View { 12 | let cornerRadius: RelatableValue 13 | let pointSize: RelatableValue 14 | let pointRadius: RelatableValue 15 | let insetAmount: CGFloat 16 | 17 | var body: some View { 18 | CornerCustom { rect in 19 | rect 20 | .corners(.rounded(radius: cornerRadius)) 21 | .addingNotch( 22 | .triangle(depth: -pointSize, cornerStyles: [ 23 | .rounded(radius: pointRadius), 24 | .point, 25 | .rounded(radius: pointRadius) 26 | ]), 27 | afterCornerIndex: 2 28 | ) 29 | } 30 | .inset(by: insetAmount) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/CodeFeatures/MessageBubble/MessageBubbleInsetExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageBubble5Example.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-24. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct MessageBubbleInsetExample: View { 12 | var messageBubble: CornerCustom { 13 | CornerCustom { rect in 14 | rect.corners(.rounded(radius: 20)) 15 | .addingNotch( 16 | .triangle(depth: -20, cornerStyles: [ 17 | .rounded(radius: 10), 18 | .point, 19 | .rounded(radius: 10) 20 | ]), 21 | afterCornerIndex: 2 22 | ) 23 | } 24 | } 25 | 26 | let colors: [Color] = [.suCyan, .suPink, .suWhite, .suYellow] 27 | 28 | var body: some View { 29 | ZStack { 30 | messageBubble 31 | .inset(by: -15) 32 | .fill(Color.suPurple) 33 | 34 | ForEach(Array(zip(colors.indices, colors)), id: \.0) { (i, color) in 35 | messageBubble 36 | .inset(by: CGFloat((i * 5) - 10)) 37 | .stroke(color, lineWidth: 3) 38 | } 39 | } 40 | .frame(width: 200, height: 120) 41 | } 42 | } 43 | 44 | struct MessageBubbleInsetExample_Previews: PreviewProvider { 45 | static var previews: some View { 46 | MessageBubbleInsetExample() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Corner/AddOpenCornerShapeExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddOpenCornerShapeExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-02-11. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct OpenCornerShape: Shape { 12 | func path(in rect: CGRect) -> Path { 13 | let corners = [ 14 | Corner(x: rect.minX, y: rect.minY), 15 | Corner(x: rect.midX, y: rect.midY), 16 | Corner(x: rect.maxX, y: rect.minY), 17 | Corner(x: rect.maxX, y: rect.midY) 18 | ] 19 | .corners([ 20 | .straight(radius: .relative(0.2)), 21 | .cutout(radius: .relative(0.2), cornerStyles: [ 22 | .rounded(radius: .relative(0.4)), 23 | .point, 24 | .straight(radius: .relative(0.4)) 25 | ]), 26 | .cutout(radius: .relative(0.2), cornerStyles: [.rounded(radius: .relative(0.2))]), 27 | .rounded(radius: 20) 28 | ]) 29 | 30 | var path = Path() 31 | path.move(to: CGPoint(x: rect.minX + rect.width * 0.25, y: rect.maxY)) 32 | path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.midY), control: CGPoint(x: rect.midX, y: rect.midY)) 33 | 34 | path.addOpenCornerShape( 35 | corners, 36 | previousPoint: CGPoint(x: rect.minX, y: rect.midY), 37 | nextPoint: CGPoint(x: rect.midX, y: rect.midY), 38 | moveToStart: false 39 | ) 40 | 41 | path.addQuadCurve(to: CGPoint(x: rect.maxX - rect.width * 0.25, y: rect.maxY), control: CGPoint(x: rect.midX, y: rect.midY)) 42 | 43 | return path 44 | } 45 | } 46 | 47 | struct AddOpenCornerShapeExample: View { 48 | var body: some View { 49 | VStack { 50 | VStack(alignment: .leading) { 51 | Text("`CornerShape` does not currently support curves but you can always use an array of `Corner` to draw a part of your path.") 52 | 53 | Text("Below is a SwiftUI `Shape` but some of the difficult corner details were added with `path.addOpenCornerShape()`") 54 | } 55 | 56 | Spacer() 57 | 58 | OpenCornerShape() 59 | .stroke(Color.suPink, lineWidth: 10) 60 | .frame(width: 200, height: 200) 61 | 62 | Spacer() 63 | } 64 | .padding() 65 | .navigationTitle("AddOpenCornerShape") 66 | } 67 | } 68 | 69 | struct AddOpenCornerShapeExample_Previews: PreviewProvider { 70 | static var previews: some View { 71 | NavigationView { 72 | AddOpenCornerShapeExample() 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Corner/CornerExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-05-19. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct CornerExample: View { 12 | let shapes = ["Rectangle", "Triangle", "Pentagon"] 13 | let styles: [CornerStyle] = [.point, .rounded(radius: .zero), .concave(radius: .zero), .straight(radius: .zero), .cutout(radius: .zero)] 14 | let radii: [RelatableValue] = [.absolute(.zero), .relative(.zero)] 15 | 16 | @State private var shape = "Rectangle" 17 | @State private var style = CornerStyle.rounded(radius: .zero) 18 | @State private var relativeRadius = true 19 | @State private var relative = 0.2 20 | @State private var absolute = 25.0 21 | 22 | var adjustedStyle: CornerStyle { 23 | style.changingRadius(to: relativeRadius ? .relative(relative) : .absolute(absolute)) 24 | } 25 | 26 | var body: some View { 27 | VStack { 28 | VStack(alignment: .leading) { 29 | Text("Make shapes using `Corner`, pick a `style` and set the `radius` using either `absolute` or `relative` values.") 30 | } 31 | 32 | Color.clear.overlay( 33 | Group { 34 | switch shape { 35 | case "Rectangle": 36 | CornerRectangle() 37 | .applyingStyle(adjustedStyle) 38 | case "Triangle": 39 | CornerTriangle() 40 | .applyingStyle(adjustedStyle) 41 | default: 42 | CornerPentagon(pointHeight: .relative(0.3), bottomTaper: .relative(0.2)) 43 | .applyingStyle(adjustedStyle) 44 | } 45 | } 46 | ) 47 | .foregroundColor(Color.suPink) 48 | .padding() 49 | 50 | Picker("Base Shape", selection: $shape) { 51 | ForEach(shapes, id: \.self) { shape in 52 | Text(shape) 53 | } 54 | } 55 | .pickerStyle(.segmented) 56 | 57 | Picker("CornerStyle", selection: $style) { 58 | ForEach(styles, id: \.self) { style in 59 | Text(style.name) 60 | } 61 | } 62 | .pickerStyle(.segmented) 63 | 64 | Group { 65 | Section { 66 | Picker("Radius", selection: $relativeRadius) { 67 | Text("Relative").tag(true) 68 | Text("Absolute").tag(false) 69 | } 70 | .pickerStyle(.segmented) 71 | } header: { 72 | if relativeRadius { 73 | CrossPlatformStepper( 74 | label: "Radius", 75 | value: $relative, 76 | minValue: 0, 77 | maxValue: 1, 78 | step: 0.1, 79 | decimalPlaces: 1 80 | ) 81 | } else { 82 | CrossPlatformStepper( 83 | label: "Radius", 84 | value: $absolute, 85 | minValue: 0, 86 | maxValue: 300, 87 | step: 10, 88 | decimalPlaces: 0 89 | ) 90 | } 91 | } 92 | 93 | #if !os(tvOS) 94 | if relativeRadius { 95 | Slider(value: $relative, in: 0...0.5) { 96 | Text("Relative Value") 97 | } minimumValueLabel: { 98 | Text("0") 99 | } maximumValueLabel: { 100 | Text("0.5") 101 | } 102 | } else { 103 | Slider(value: $absolute, in: 0...150) { 104 | Text("Absolute Value") 105 | } minimumValueLabel: { 106 | Text("0") 107 | } maximumValueLabel: { 108 | Text("150") 109 | } 110 | } 111 | #endif 112 | } 113 | .disabled(style == .point) 114 | 115 | } 116 | .padding() 117 | .navigationTitle("Corner") 118 | } 119 | } 120 | 121 | struct CornerExample_Previews: PreviewProvider { 122 | static var previews: some View { 123 | NavigationView { 124 | CornerExample() 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Corner/CornerShapeExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerShapeView.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-08-13. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct TestClosedShape: CornerShape { 12 | let closed: Bool 13 | var insetAmount: CGFloat = 0 14 | 15 | func corners(in rect: CGRect) -> [Corner] { 16 | [ 17 | Corner(.rounded(radius: .relative(0.3)),x: rect.minX, y: rect.minY), 18 | Corner(.straight(radius: .relative(0.1)), x: rect.midX, y: rect.midY), 19 | Corner(.cutout(radius: 20),x: rect.maxX, y: rect.minY), 20 | Corner(.concave(radius: .relative(0.3)),x: rect.maxX, y: rect.maxY), 21 | Corner(x: rect.midX, y: rect.maxY), 22 | ] 23 | } 24 | } 25 | 26 | struct TestOpenShape: CornerShape { 27 | let closed: Bool 28 | var insetAmount: CGFloat = 0 29 | 30 | func corners(in rect: CGRect) -> [Corner] { 31 | rect.points(.bottomLeft, .left, .bottom, .top, .right, .topRight) 32 | .corners([ 33 | nil, 34 | .rounded(radius: .relative(0.4)), 35 | .concave(radius: .relative(0.3)), 36 | .straight(radius: .relative(0.3)), 37 | .cutout(radius: .relative(0.1)), 38 | nil 39 | ]) 40 | } 41 | } 42 | 43 | struct CornerShapeExample: View { 44 | @State private var closed = true 45 | @State private var insetAmount: CGFloat = 0 46 | 47 | var body: some View { 48 | VStack { 49 | VStack(alignment: .leading) { 50 | Text("Create any array of `Corner` using") 51 | Text("`corners(in: CGRect) -> [Corner]`").padding(.vertical, 4) 52 | Text("instead of the `Shape` function") 53 | Text("`path(in: CGRect) -> Path`").padding(.vertical, 4) 54 | Text("to make a `CornerShape` that can be open or closed and is insettable automatically!") 55 | } 56 | Spacer() 57 | 58 | ZStack { 59 | TestClosedShape(closed: closed, insetAmount: insetAmount) 60 | .fill(Color.suCyan) 61 | 62 | TestClosedShape(closed: closed, insetAmount: insetAmount) 63 | .stroke(Color.suPink, lineWidth: 12) 64 | } 65 | .frame(width: 200, height: 150) 66 | 67 | Spacer() 68 | 69 | ZStack { 70 | TestOpenShape(closed: closed) 71 | .inset(by: insetAmount) 72 | .fill(Color.suYellow) 73 | 74 | TestOpenShape(closed: closed) 75 | .inset(by: insetAmount) 76 | .stroke(Color.suPurple, lineWidth: 12) 77 | } 78 | .frame(width: 200, height: 150) 79 | 80 | Spacer() 81 | 82 | Picker("Shape Style", selection: $closed) { 83 | Text("Closed").tag(true) 84 | Text("Open").tag(false) 85 | } 86 | .pickerStyle(.segmented) 87 | 88 | CrossPlatformSlider( 89 | label: "Inset Amount", 90 | value: $insetAmount, 91 | minValue: -30, 92 | maxValue: 30, 93 | step: 5, 94 | labelPrefix: true 95 | ) 96 | } 97 | .padding() 98 | .navigationTitle("CornerShape") 99 | } 100 | } 101 | 102 | struct CornerShapeView_Previews: PreviewProvider { 103 | static var previews: some View { 104 | NavigationView { 105 | CornerShapeExample() 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Corner/CornerStyleExampleOld.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerStyleExampleOld.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-10. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct CornerStyleExampleOld: View { 12 | let styles: [CornerStyle] = [ 13 | .point, 14 | .rounded(radius: 25), 15 | .concave(radius: 25), 16 | .straight(radius: 25), 17 | .cutout(radius: 25) 18 | ] 19 | 20 | var body: some View { 21 | VStack { 22 | ForEach(styles, id: \.self) { style in 23 | ZStack { 24 | CornerTriangle() 25 | .applyingStyles([.top: style]) 26 | .fill(Color.suPink) 27 | 28 | Text(style.name) 29 | } 30 | .padding(5) 31 | .padding(.horizontal, 50) 32 | } 33 | } 34 | .padding() 35 | .navigationTitle("CornerStyle") 36 | } 37 | } 38 | 39 | struct CornerStyleExampleOld_Previews: PreviewProvider { 40 | static var previews: some View { 41 | CornerStyleExampleOld() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Corner/NestedCornerStyleExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NestedCornerStyleExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-09. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct NestedCornerStyleExample: View { 12 | let straightCutout: CornerStyle = { 13 | let radius = RelatableValue.relative(0.25) 14 | 15 | return CornerStyle.straight(radius: radius, cornerStyle: .cutout(radius: radius)) 16 | }() 17 | 18 | let cutoutStraight: CornerStyle = { 19 | let radius = RelatableValue.relative(0.3) 20 | 21 | return CornerStyle.cutout(radius: radius, cornerStyle: .straight(radius: radius)) 22 | }() 23 | 24 | var body: some View { 25 | VStack { 26 | Text("Some `CornerStyle` options like `.straight` and `.cutout` create more corners. These corners can also have corner styles applied to them to create detailed nested corners.") 27 | 28 | Spacer() 29 | 30 | CornerTriangle() 31 | .applyingStyle(straightCutout) 32 | .strokeBorder(Color.suPink, lineWidth: 8) 33 | .frame(width: 150, height: 150) 34 | 35 | Spacer() 36 | 37 | CornerPentagon(pointHeight: .relative(0.3), bottomTaper: .relative(0.2)) 38 | .applyingStyle(cutoutStraight) 39 | .strokeBorder(Color.suCyan, lineWidth: 8) 40 | .frame(width: 150, height: 150) 41 | 42 | Spacer() 43 | } 44 | .padding() 45 | .navigationTitle("Nested Corners") 46 | } 47 | } 48 | 49 | struct NestedCornerStyleExample_Previews: PreviewProvider { 50 | static var previews: some View { 51 | NavigationView { 52 | NestedCornerStyleExample() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/CornerCustomExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerCustomExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-10. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct CornerCustomExample: View { 12 | @State private var inset = 10.0 13 | 14 | var body: some View { 15 | VStack { 16 | CornerCustom { rect in 17 | [ 18 | Corner(.straight(radius: .relative(0.4)),x: rect.minX, y: rect.minY), 19 | Corner(.rounded(radius: .relative(0.1)), x: rect.midX, y: rect.midY), 20 | Corner(.concave(radius: 20),x: rect.maxX, y: rect.minY), 21 | Corner(.cutout(radius: .relative(0.3)),x: rect.maxX, y: rect.maxY), 22 | Corner(.concave(radius: 40), x: rect.midX, y: rect.midY + (rect.height * 0.1)), 23 | Corner(x: rect.minX, y: rect.maxY) 24 | ] 25 | } 26 | .inset(by: inset) 27 | .fill(Color.suPink) 28 | .frame(width: 200, height: 150) 29 | .animation(.default, value: inset) 30 | 31 | CrossPlatformStepper( 32 | label: "Inset", 33 | value: $inset, 34 | minValue: -30, 35 | maxValue: 30, 36 | step: 10 37 | ) 38 | 39 | Text("Closed Shape") 40 | 41 | CornerCustom(closed: false) { rect in 42 | rect 43 | .points(relativeLocations: [ 44 | (0.0, 1.0), 45 | (0.0, 0.4), 46 | (0.4, 0.7), 47 | (0.4, 0.1), 48 | (0.7, 0.3), 49 | (1.0, 0), 50 | (0.8, 1.0) 51 | ]) 52 | .corners([ 53 | nil, 54 | .rounded(radius: .relative(0.4)), 55 | .concave(radius: .relative(0.3)), 56 | .straight(radius: .relative(0.3)), 57 | .cutout(radius: .relative(0.1)), 58 | nil 59 | ]) 60 | } 61 | .stroke(Color.suYellow, lineWidth: 10) 62 | .frame(width: 200, height: 150) 63 | 64 | Text("Open Shape") 65 | } 66 | .navigationTitle("CornerCustom") 67 | } 68 | } 69 | 70 | struct CornerCustomExample_Previews: PreviewProvider { 71 | static var previews: some View { 72 | CornerCustomExample() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDisplayName 6 | ShapeUp 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 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 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UIApplicationSupportsIndirectInputEvents 31 | 32 | UILaunchScreen 33 | 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/NotchedShapes/NotchedExamples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotchedExamples.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-09. 6 | // 7 | 8 | import SwiftUI 9 | 10 | import ShapeUp 11 | import SwiftUI 12 | 13 | struct NotchedExamples: View { 14 | var body: some View { 15 | Section { 16 | NavigationLink(destination: NotchedRectangleExample()) { 17 | Label("NotchedRectangle", systemImage: "rectangle") 18 | } 19 | 20 | NavigationLink(destination: NotchedTriangleExample()) { 21 | Label("NotchedTriangle", systemImage: "triangle") 22 | } 23 | 24 | NavigationLink(destination: NotchedPentagonExample()) { 25 | Label("NotchedPentagon", systemImage: "pentagon") 26 | } 27 | } header: { 28 | Text("Notched") 29 | } 30 | } 31 | } 32 | 33 | struct NotchedExamples_Previews: PreviewProvider { 34 | static var previews: some View { 35 | NotchedExamples() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/NotchedShapes/NotchedPentagonExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotchedPentagonExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-09. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct NotchedPentagonExample: View { 12 | var body: some View { 13 | CornerCustom { rect in 14 | CornerPentagon( 15 | pointHeight: .relative(0.2), 16 | topTaper: .relative(0.15), 17 | bottomTaper: .zero 18 | ) 19 | .corners(in: rect) 20 | .applyingStyle(.rounded(radius: 20)) 21 | .addingNotches([ 22 | .triangle(depth: .relative(0.2)), 23 | nil, 24 | nil, 25 | .triangle(depth: .relative(0.2)), 26 | .rectangle(length: 20, depth: 10, cornerStyle: .rounded(radius: .relative(0.4))) 27 | ]) 28 | } 29 | .fill(Color.suYellow) 30 | .frame(width: 300, height: 300) 31 | .navigationTitle("NotchedPentagon") 32 | } 33 | } 34 | 35 | struct NotchedPentagonExample_Previews: PreviewProvider { 36 | static var previews: some View { 37 | NotchedPentagonExample() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/NotchedShapes/NotchedRectangleExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotchedRectangleExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-08-13. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct NotchedRectangleExample: View { 12 | var body: some View { 13 | CornerCustom { rect in 14 | rect 15 | .corners([ 16 | .rounded(radius: 20), 17 | .cutout(radius: .relative(0.3)), 18 | .straight(radius: 70), 19 | .rounded(radius: 20) 20 | ]) 21 | .addingNotches( 22 | [ 23 | .rectangle(depth: 50, cornerStyle: .rounded(radius: 10)), 24 | nil, 25 | .triangle(position: .relative(0.5), length: .relative(0.2), depth: .relative(0.1)), 26 | .custom(depth: 60) { rect in 27 | [ 28 | Corner(x: rect.midX, y: rect.minY), 29 | Corner(x: rect.minX, y: rect.maxY), 30 | Corner(.rounded(radius: 15), x: rect.midX, y: rect.maxY), 31 | Corner(x: rect.maxX, y: rect.minY) 32 | ] 33 | } 34 | ] 35 | ) 36 | } 37 | .strokeBorder(Color.suPink, style: StrokeStyle(lineWidth: 20)) 38 | .frame(width: 300, height: 300) 39 | .navigationTitle("NotchedRectangle") 40 | } 41 | } 42 | 43 | struct NotchedRectangleExample_Previews: PreviewProvider { 44 | static var previews: some View { 45 | NotchedRectangleExample() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/NotchedShapes/NotchedTriangleExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotchedTriangleExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-09. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct NotchedTriangleExample: View { 12 | var body: some View { 13 | CornerCustom { rect in 14 | CornerTriangle() 15 | .corners(in: rect) 16 | .applyingStyle(.rounded(radius: 20)) 17 | .addingNotches([ 18 | .triangle(depth: .relative(0.2)), 19 | nil, 20 | .rectangle(length: 50, depth: 30, cornerStyle: .rounded(radius: .relative(0.4))) 21 | ]) 22 | } 23 | .fill(Color.suPurple) 24 | .frame(width: 300, height: 300) 25 | .navigationTitle("NotchedTriangle") 26 | } 27 | } 28 | 29 | struct NotchedTriangleExample_Previews: PreviewProvider { 30 | static var previews: some View { 31 | NotchedTriangleExample() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Other Tools/AnimatablePackExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatablePackExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2023-09-06. 6 | // 7 | 8 | /// AnimatablePack uses parameter pack iteration that is only available when using the Swift 6.0 compiler (Xcode 16+) 9 | /// https://forums.swift.org/t/pitch-enable-pack-iteration/66168 10 | #if compiler(>=6.0) 11 | import ShapeUp 12 | import SwiftUI 13 | 14 | @available(iOS 17, macOS 14, watchOS 10, tvOS 17, *) 15 | struct AnimatablePackShape: CornerShape { 16 | var closed: Bool = true 17 | var insetAmount: CGFloat = 0 18 | 19 | var cornerRadius: RelatableValue 20 | var rotation: Angle 21 | 22 | func corners(in rect: CGRect) -> [Corner] { 23 | rect 24 | .corners(.rounded(radius: cornerRadius)) 25 | .rotated(rotation, anchor: rect.point(.center)) 26 | } 27 | 28 | var animatableData: AnimatablePack { 29 | get { 30 | AnimatablePack(insetAmount, cornerRadius, rotation.radians) 31 | } 32 | set { 33 | (insetAmount, cornerRadius, rotation.radians) = newValue() 34 | } 35 | } 36 | } 37 | 38 | @available(iOS 17, macOS 14, watchOS 10, tvOS 17, *) 39 | struct AnimatablePackExample: View { 40 | @State private var insetAmount: Double = 0 41 | @State private var cornerRadius: Double = 40 42 | @State private var rotation: Double = 0 43 | 44 | var body: some View { 45 | VStack { 46 | VStack(alignment: .leading) { 47 | Text("Animate lots of properties in a `Shape` using `AnimatablePack` instead of nesting `AnimatablePair` types") 48 | } 49 | 50 | AnimatablePackShape( 51 | insetAmount: insetAmount, 52 | cornerRadius: .absolute(cornerRadius), 53 | rotation: .degrees(rotation) 54 | ) 55 | .fill(Color.suPink) 56 | .padding() 57 | .animation(.easeInOut.speed(0.2), value: insetAmount) 58 | .animation(.easeInOut.speed(0.2), value: cornerRadius) 59 | .animation(.easeInOut.speed(0.2), value: rotation) 60 | 61 | CrossPlatformStepper( 62 | label: "Inset", 63 | value: $insetAmount, 64 | minValue: -30, 65 | maxValue: 30, 66 | step: 10 67 | ) 68 | 69 | CrossPlatformStepper( 70 | label: "Corner Radius", 71 | value: $cornerRadius, 72 | minValue: 0, 73 | maxValue: 200, 74 | step: 20 75 | ) 76 | 77 | CrossPlatformStepper( 78 | label: "Rotation Angle", 79 | value: $rotation, 80 | minValue: -720, 81 | maxValue: 720, 82 | step: 45 83 | ) 84 | } 85 | .padding() 86 | .navigationTitle("AnimatablePack") 87 | } 88 | } 89 | 90 | @available(iOS 17, macOS 14, watchOS 10, tvOS 17, *) 91 | struct AnimatablePackExample_Previews: PreviewProvider { 92 | static var previews: some View { 93 | NavigationView { 94 | AnimatablePackExample() 95 | } 96 | } 97 | } 98 | #endif 99 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Other Tools/EmbossExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmbossExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-09-16. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct EmbossExample: View { 12 | let angle: Angle = .degrees(45) 13 | let color = Color.suPurple 14 | 15 | var body: some View { 16 | ScrollView { 17 | VStack(spacing: 40) { 18 | Circle() 19 | .fill(color) 20 | .emboss(using: Circle(), size: 4, angle: angle, opacity: 1) 21 | .frame(width: 200) 22 | 23 | Image(systemName: "heart") 24 | .resizable() 25 | .scaledToFit() 26 | .deboss(baseColor: color, amount: 1, blur: 1, angle: angle, opacity: 0.3) 27 | .frame(width: 200) 28 | 29 | Text("Hello World") 30 | .font(.largeTitle) 31 | .bold() 32 | .foregroundColor(color) 33 | .deboss(amount: 0.5, angle: angle, opacity: 0.5) 34 | 35 | CornerRectangle([ 36 | .topLeft: .straight(radius: 60), 37 | .topRight: .cutout(radius: .relative(0.2)), 38 | .bottomRight: .rounded(radius: .relative(0.6)), 39 | .bottomLeft: .concave(radius: .relative(0.2)) 40 | ]) 41 | .embossEdges(size: 2, angle: angle, opacity: 1) 42 | .padding() 43 | .background(color) 44 | .frame(width: 200, height: 200) 45 | 46 | CornerPentagon(pointHeight: 20, topTaper: .relative(0.5), bottomTaper: .relative(0.2)) 47 | .embossEdges(size: 4, angle: angle, opacity: 1) 48 | .padding() 49 | .background(color) 50 | .frame(width: 200, height: 200) 51 | } 52 | .frame(maxWidth: .infinity) 53 | .padding() 54 | } 55 | .navigationTitle("Emboss") 56 | } 57 | } 58 | 59 | struct EmbossExample_Previews: PreviewProvider { 60 | static var previews: some View { 61 | NavigationView { 62 | EmbossExample() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Other Tools/InsettableShapeByPropertyExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsettableShapeByPropertyExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-09-02. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct InsettableShapeWithHole: InsettableShapeByProperty { 12 | var insetAmount: CGFloat = 0 13 | 14 | func path(in rect: CGRect) -> Path { 15 | var path = rect 16 | .corners(.rounded(radius: 10)) 17 | .inset(by: insetAmount) 18 | .path() 19 | 20 | path.closeSubpath() 21 | 22 | var hole = Path() 23 | hole.addArc(center: rect.point(.center), radius: rect.width * 0.25 + insetAmount, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true) 24 | hole.closeSubpath() 25 | 26 | hole.addPath(path) 27 | 28 | return hole 29 | } 30 | } 31 | 32 | struct InsettableShapeByPropertyExample: View { 33 | @State private var inset: CGFloat = 10.0 34 | var body: some View { 35 | VStack { 36 | VStack(alignment: .leading) { 37 | Text("Turning a `Shape` into an `InsettableShape` can be cumbersome. In some cases it's possible to include the inset value as a parameter. The `InsettableShapeByProperty` protocol will generate the rest of the insetting code for you.") 38 | } 39 | 40 | Spacer() 41 | 42 | ZStack { 43 | InsettableShapeWithHole() 44 | .fill(Color.suPurple) 45 | 46 | InsettableShapeWithHole() 47 | .inset(by: inset) 48 | .stroke(lineWidth: 5) 49 | .foregroundColor(.suPink) 50 | } 51 | .frame(width: 200, height: 200) 52 | 53 | Spacer() 54 | 55 | CrossPlatformSlider( 56 | label: "Inset", 57 | value: $inset, 58 | minValue: -20, 59 | maxValue: 20, 60 | step: 5, 61 | labelPrefix: true 62 | ) 63 | } 64 | .padding() 65 | .navigationTitle("InsetByProperty") 66 | } 67 | } 68 | struct InsettableShapeByPropertyExample_Previews: PreviewProvider { 69 | static var previews: some View { 70 | NavigationView { 71 | InsettableShapeByPropertyExample() 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Other Tools/SketchyLineExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SketchyLineExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-08-13. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct SketchyLineExample: View { 12 | @State private var drawAmount: CGFloat = 1 13 | 14 | var body: some View { 15 | VStack { 16 | VStack(alignment: .leading) { 17 | Text("An easy way to draw lines with edges that extend beyond the frame.") 18 | Text("Adjust the extension amount and offset of each line using `RelatableValue.absolute` or `.relative`.") 19 | Text("Animate the `drawAmount` and set the `drawDirection`") 20 | } 21 | 22 | Spacer() 23 | 24 | Text("Baseline") 25 | .font(.system(size: 32)) 26 | .alignmentGuide(.bottom) { d in 27 | d[.firstTextBaseline] 28 | } 29 | .background( 30 | SketchyLines(lines: [ 31 | .leading(startExtension: -2, endExtension: 10), 32 | .bottom(startExtension: 5, endExtension: 5, offset: .relative(0.05)) 33 | ], drawAmount: drawAmount) 34 | .stroke(Color.suPink, lineWidth: 2) 35 | , alignment: .bottom 36 | ) 37 | 38 | Spacer() 39 | 40 | Text("Boxed In") 41 | .font(.system(size: 32)) 42 | .padding(.horizontal, 5) 43 | .background( 44 | SketchyLines(lines: [ 45 | .top(startExtension: 5, endExtension: 5, drawDirection: .toTopLeading), 46 | .bottom(startExtension: 5, endExtension: 5), 47 | .leading(startExtension: 5, endExtension: 5), 48 | .trailing(startExtension: 5, endExtension: 5) 49 | ], drawAmount: drawAmount) 50 | .stroke(Color.suPink, lineWidth: 2) 51 | , alignment: .bottom 52 | ) 53 | 54 | Spacer() 55 | 56 | Button("Animate") { 57 | withAnimation { 58 | drawAmount = drawAmount < 1 ? 1 : 0 59 | } 60 | } 61 | 62 | Spacer() 63 | 64 | CrossPlatformSlider( 65 | label: "Draw Amount", 66 | value: $drawAmount, 67 | minValue: 0, 68 | maxValue: 1, 69 | step: 0.1, 70 | labelPrefix: true 71 | ) 72 | } 73 | .padding() 74 | .navigationTitle("SketchyLine") 75 | } 76 | } 77 | 78 | struct SketchyLineExample_Previews: PreviewProvider { 79 | static var previews: some View { 80 | NavigationView { 81 | SketchyLineExample() 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/RectangleExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectangleExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-24. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct RectangleExample: View { 12 | let styles: [CornerStyle] = [ 13 | .rounded(radius: 30), 14 | .cutout(radius: 30), 15 | .straight(radius: 30), 16 | .concave(radius: 30) 17 | ] 18 | 19 | var body: some View { 20 | VStack { 21 | ForEach(styles, id: \.self) { style in 22 | Rectangle() 23 | .applyingStyle(style) 24 | .fill(Color.suPurple) 25 | .padding() 26 | } 27 | } 28 | } 29 | } 30 | 31 | struct RectangleExample_Previews: PreviewProvider { 32 | static var previews: some View { 33 | RectangleExample() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/RectangleExample2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectangleExample2.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-24. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct RectangleExample2: View { 12 | var body: some View { 13 | Rectangle() 14 | .applyingStyles([ 15 | .topLeft: .rounded(radius: 100), 16 | .topRight: .concave(radius: 80), 17 | .bottomRight: .straight(radius: 50), 18 | .bottomLeft: .cutout(radius: 50) 19 | ]) 20 | .fill(Color.suPurple) 21 | .padding() 22 | .frame(height: 400) 23 | } 24 | } 25 | 26 | struct RectangleExample2_Previews: PreviewProvider { 27 | static var previews: some View { 28 | RectangleExample2() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Example/ShapeUpExample/ShapeUpExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShapeUpExampleApp.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2021-08-13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct ShapeUpExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | .accentColor(.suPink) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Example/Shared/CrossPlatform/CrossPlatformSlider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrossPlatformSlider.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-02-29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CrossPlatformSlider: View where V: BinaryFloatingPoint, V: CVarArg, V.Stride: BinaryFloatingPoint { 11 | let label: String 12 | @Binding var value: V 13 | let minValue: V 14 | let maxValue: V 15 | let step: V.Stride 16 | let decimalPlaces: Int 17 | let labelPrefix: Bool 18 | 19 | init( 20 | label: String, 21 | value: Binding, 22 | minValue: V, 23 | maxValue: V, 24 | step: V.Stride, 25 | decimalPlaces: Int = 0, 26 | labelPrefix: Bool = false 27 | ) { 28 | self.label = label 29 | self._value = value 30 | self.minValue = minValue 31 | self.maxValue = maxValue 32 | self.step = step 33 | self.decimalPlaces = decimalPlaces 34 | self.labelPrefix = labelPrefix 35 | } 36 | 37 | var format: String { 38 | "%.\(decimalPlaces)f" 39 | } 40 | 41 | var body: some View { 42 | #if os(tvOS) 43 | HStack { 44 | Text("\(label) \(String(format: format, value))") 45 | Button("-") { value = max(minValue, value.advanced(by: -step)) } 46 | Button("+") { value = min(maxValue, value.advanced(by: step)) } 47 | } 48 | #else 49 | Slider(value: $value, in: minValue...maxValue) { 50 | Text(label) 51 | } minimumValueLabel: { 52 | Text("\(labelPrefix ? "\(label): " : "")\(String(format: format, minValue))") 53 | } maximumValueLabel: { 54 | Text(String(format: format, maxValue)) 55 | } 56 | #endif 57 | } 58 | } 59 | 60 | #Preview { 61 | CrossPlatformSlider( 62 | label: "Inset", 63 | value: .constant(2.0), 64 | minValue: -30, 65 | maxValue: 30, 66 | step: 10, 67 | decimalPlaces: 0, 68 | labelPrefix: true 69 | ) 70 | .padding() 71 | } 72 | -------------------------------------------------------------------------------- /Example/Shared/CrossPlatform/CrossPlatformStepper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CrossPlatformStepper.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2024-02-29. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CrossPlatformStepper: View where V: Strideable, V: CVarArg { 11 | let label: String 12 | @Binding var value: V 13 | let minValue: V 14 | let maxValue: V 15 | let step: V.Stride 16 | let decimalPlaces: Int 17 | 18 | init( 19 | label: String, 20 | value: Binding, 21 | minValue: V, 22 | maxValue: V, 23 | step: V.Stride, 24 | decimalPlaces: Int = 0 25 | ) { 26 | self.label = label 27 | self._value = value 28 | self.minValue = minValue 29 | self.maxValue = maxValue 30 | self.step = step 31 | self.decimalPlaces = decimalPlaces 32 | } 33 | 34 | var format: String { 35 | "%.\(decimalPlaces)f" 36 | } 37 | 38 | var hStackStepper: some View { 39 | HStack { 40 | Text("\(label) \(String(format: format, value))") 41 | Button("-") { value = max(minValue, value.advanced(by: -step)) } 42 | Button("+") { value = min(maxValue, value.advanced(by: step)) } 43 | } 44 | } 45 | 46 | var body: some View { 47 | #if os(tvOS) 48 | hStackStepper 49 | #else 50 | if #available(watchOS 9.0, *) { 51 | Stepper("\(label) \(String(format: format, value))", value: $value, in: minValue...maxValue, step: step) 52 | } else { 53 | hStackStepper 54 | } 55 | #endif 56 | } 57 | } 58 | 59 | #Preview { 60 | CrossPlatformStepper( 61 | label: "Inset", 62 | value: .constant(2.0), 63 | minValue: -30, 64 | maxValue: 30, 65 | step: 10, 66 | decimalPlaces: 0 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /Example/Shared/CustomShapes/FoldButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FoldButton.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-30. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct FoldButton: View { 12 | var body: some View { 13 | ZStack(alignment: .trailing) { 14 | Rectangle() 15 | .applyingStyle(.rounded(radius: .relative(0.5))) 16 | .applyingStyle(.point, shapeCorners: [.topRight]) 17 | .fill(.purple) 18 | .frame(width: 300) 19 | 20 | CornerCustom { rect in 21 | rect 22 | .points(relativeLocations: [ 23 | (0,0), 24 | (1,0), 25 | (1,-0.5), 26 | (1,1), 27 | (0,1) 28 | ]) 29 | .corners([ 30 | .rounded(radius: .absolute(rect.height * 0.5)), 31 | .rounded(radius: .relative(1)), 32 | .point, 33 | .rounded(radius: .absolute(rect.height * 0.5)), 34 | .rounded(radius: .absolute(rect.height * 0.5)) 35 | ]) 36 | } 37 | .fill(.blue) 38 | .frame(width: 100) 39 | } 40 | .frame(height: 50) 41 | } 42 | } 43 | 44 | struct ReplyButton_Previews: PreviewProvider { 45 | static var previews: some View { 46 | FoldButton() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Example/Shared/CustomShapes/HexagonExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HexagonExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-03-28. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct QuickHexagonExample: View { 12 | /// This property cannot be animated as it goes in the CornerCustom closure 13 | let taper: CGFloat 14 | 15 | var body: some View { 16 | CornerCustom { rect in 17 | rect 18 | .points(relativeLocations: [ 19 | (taper, 0), 20 | (1 - taper, 0), 21 | (1, 0.5), 22 | (1 - taper, 1), 23 | (taper, 1), 24 | (0, 0.5) 25 | ]) 26 | .corners(.rounded(radius: 20)) 27 | } 28 | } 29 | } 30 | 31 | struct AdjustableQuickHexagonExample: View { 32 | @State private var taper = 0.25 33 | 34 | var body: some View { 35 | VStack { 36 | QuickHexagonExample(taper: taper) 37 | .aspectRatio(1.1, contentMode: .fit) 38 | 39 | CrossPlatformSlider( 40 | label: "Taper", 41 | value: $taper, 42 | minValue: 0, 43 | maxValue: 0.5, 44 | step: 0.1 45 | ) 46 | } 47 | .padding() 48 | } 49 | } 50 | 51 | struct HexagonExample_Previews: PreviewProvider { 52 | static var previews: some View { 53 | ScrollView { 54 | AdjustableQuickHexagonExample() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Example/Shared/CustomShapes/InsetCornerShapeExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsetCornerShapeExample.swift 3 | // ShapeUpExample 4 | // 5 | // Created by Ryan Lintott on 2022-02-11. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct InsetCornerShape: CornerShape { 12 | var closed = true 13 | var insetAmount: CGFloat = 0 14 | 15 | func corners(in rect: CGRect) -> [Corner] { 16 | [ 17 | Corner(x: rect.minX, y: rect.minY), 18 | Corner(x: rect.midX, y: rect.midY), 19 | Corner(x: rect.maxX, y: rect.minY), 20 | Corner(x: rect.maxX, y: rect.maxY), 21 | Corner(x: rect.minX, y: rect.maxY) 22 | ] 23 | .applyingStyle(.concave(radius: 25)) 24 | } 25 | } 26 | 27 | struct InsetCornerShapeExample: View { 28 | var body: some View { 29 | ZStack { 30 | InsetCornerShape() 31 | .strokeBorder(Color.suCyan, lineWidth: 15) 32 | } 33 | .frame(width: 200, height: 200) 34 | .navigationTitle("InsetCornerShape") 35 | } 36 | } 37 | 38 | struct InsetCornerShapeExample_Previews: PreviewProvider { 39 | static var previews: some View { 40 | NavigationView { 41 | InsetCornerShapeExample() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/SUBlack.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/SUCyan.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.996", 9 | "green" : "0.875", 10 | "red" : "0.016" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/SUPink.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.690", 9 | "green" : "0.271", 10 | "red" : "0.980" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/SUPurple.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.780", 9 | "green" : "0.318", 10 | "red" : "0.271" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/SUWhite.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/SUYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.545", 9 | "green" : "0.855", 10 | "red" : "0.937" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/ShapeUp-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ShapeUp-logo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Example/Shared/SharedAssets.xcassets/ShapeUp-logo.imageset/ShapeUp-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/Shared/SharedAssets.xcassets/ShapeUp-logo.imageset/ShapeUp-logo.png -------------------------------------------------------------------------------- /Example/WatchShapeUpExample/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Example/WatchShapeUpExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024@1x~ios-marketing.png", 5 | "idiom" : "universal", 6 | "platform" : "watchos", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Example/WatchShapeUpExample/Assets.xcassets/AppIcon.appiconset/app-icon-1024@1x~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanlintott/ShapeUp/9a7b12905bcf192a4ed3c391ffa7b4e10efeea01/Example/WatchShapeUpExample/Assets.xcassets/AppIcon.appiconset/app-icon-1024@1x~ios-marketing.png -------------------------------------------------------------------------------- /Example/WatchShapeUpExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/WatchShapeUpExample/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // WatchShapeUpExample Watch App 4 | // 5 | // Created by Ryan Lintott on 2024-03-02. 6 | // 7 | 8 | import ShapeUp 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | var body: some View { 13 | ShapeUpLogoView() 14 | .padding() 15 | } 16 | } 17 | 18 | #Preview { 19 | ContentView() 20 | } 21 | -------------------------------------------------------------------------------- /Example/WatchShapeUpExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/WatchShapeUpExample/WatchShapeUpExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WatchShapeUpExampleApp.swift 3 | // WatchShapeUpExample Watch App 4 | // 5 | // Created by Ryan Lintott on 2024-03-02. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct WatchShapeUpExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ryan Lintott 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 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: "ShapeUp", 8 | platforms: [ 9 | .iOS(.v14), 10 | .macOS(.v11), 11 | .watchOS(.v7), 12 | .tvOS(.v14), 13 | .visionOS(.v1) 14 | ], 15 | products: [ 16 | // Products define the executables and libraries a package produces, and make them visible to other packages. 17 | .library( 18 | name: "ShapeUp", 19 | targets: ["ShapeUp"]), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "ShapeUp", 26 | resources: [.copy("PrivacyInfo.xcprivacy")]), 27 | .testTarget( 28 | name: "ShapeUpTests", 29 | dependencies: ["ShapeUp"]), 30 | ], 31 | swiftLanguageVersions: [.v5, .version("6")] 32 | ) 33 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Angle/AngleType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AngleType.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-02-04. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An enumeration representing the type of an angle. 11 | public enum AngleType: Int, Comparable, CaseIterable, Sendable { 12 | /// An angle of zero degrees with initial and terminal sides in the same location. 13 | case zero 14 | /// An angle with a magnitude greater than 0 but less than 90 degrees. 15 | case acute 16 | /// An angle with a magnitude of 90 degrees. 17 | case rightAngle 18 | /// An angle with a magnitude greater than 90 but less than 180 degrees. 19 | case obtuse 20 | /// An angle with a magnitude of 180 degrees. 21 | case straight 22 | /// An angle with a magnitude greater than 180 but less than 360 degrees. 23 | case reflex 24 | /// An angle with a magnitude of 360 degrees. 25 | case fullRotation 26 | /// An angle with a magnitude greater than 360 degrees. 27 | case over360 28 | 29 | public static func < (lhs: AngleType, rhs: AngleType) -> Bool { 30 | lhs.rawValue < rhs.rawValue 31 | } 32 | } 33 | 34 | public extension AngleType { 35 | /// Creates an angle type based on a supplied radians. 36 | /// - Parameter radians: The magnitude of this value is used to determine the type. 37 | init(radians: Double) { 38 | self = Self.type(of: .radians(radians)) 39 | } 40 | 41 | /// Creates an angle type based on a supplied degrees. 42 | /// - Parameter degrees: The magnitude of this value is used to determine the type. 43 | init(degrees: Double) { 44 | self = Self.type(of: .degrees(degrees)) 45 | } 46 | 47 | /// Creates an angle type based on the supplied angle. 48 | /// - Parameter angle: The positive value of this angle is used to determine the type. 49 | /// - Returns: The type of the supplied angle. 50 | static func type(of angle: Angle) -> Self { 51 | switch angle.positive.radians { 52 | case 0: 53 | return .zero 54 | case (.pi * 0.5): 55 | return .rightAngle 56 | case (.pi): 57 | return .straight 58 | case (.pi * 2): 59 | return .fullRotation 60 | case 0...(.pi * 0.5): 61 | return .acute 62 | case (.pi * 0.5)...(.pi): 63 | return obtuse 64 | case (.pi)...(.pi * 2): 65 | return .reflex 66 | default: 67 | return .over360 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/ShapeUp/AnimatablePack/AnimatablePack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatablePack.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2023-08-02. 6 | // 7 | 8 | /// AnimatablePack uses parameter pack iteration that is only available when using the Swift 6.0 compiler (Xcode 16+) 9 | /// https://forums.swift.org/t/pitch-enable-pack-iteration/66168 10 | #if compiler(>=6.0) 11 | import SwiftUI 12 | 13 | /** 14 | A parameter pack implementation of `AnimatablePair` 15 | 16 | Conforming to Animatable with AnimatablePair: 17 | 18 | ```swift 19 | struct MyShape: Animatable { 20 | var animatableData: AnimatablePair> { 21 | get { AnimatablePair(insetAmount, AnimatablePair(cornerRadius, rotation)) } 22 | set { 23 | insetAmount = newValue.first 24 | cornerRadius = newValue.second.first 25 | rotation = newValue.second.second 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | Conforming to Animatable with AnimatablePack: 32 | ```swift 33 | struct MyShape: Animatable { 34 | var animatableData: AnimatablePack { 35 | get { AnimatablePack(insetAmount, cornerRadius, rotation) } 36 | set { (insetAmount, cornerRadius, rotation) = newValue() } 37 | } 38 | } 39 | ``` 40 | */ 41 | @available(iOS 17, macOS 14, watchOS 10, tvOS 17, *) 42 | @dynamicMemberLookup 43 | public struct AnimatablePack: VectorArithmetic { 44 | /// Pack of items that conform to ``VectorArithmetic`` 45 | public var item: (repeat each Item) 46 | 47 | /// Creates an `Animatable` pack of items 48 | /// - Parameter item: Pack of items that conform to ``VectorArithmetic`` 49 | public init(_ item: repeat each Item) { 50 | self.item = (repeat each item) 51 | } 52 | 53 | /// Access elements in the same was as a tuple using pack.1, pack.2, etc... 54 | public subscript(dynamicMember keyPath: WritableKeyPath<(repeat each Item), V>) -> V { 55 | get { item[keyPath: keyPath] } 56 | set { item[keyPath: keyPath] = newValue } 57 | } 58 | 59 | /// Call as function to easily return the item tuple. 60 | public func callAsFunction() -> (repeat each Item) { 61 | item 62 | } 63 | } 64 | 65 | @available(iOS 17, macOS 14, watchOS 10, tvOS 17, *) 66 | extension AnimatablePack: Sendable where repeat each Item: Sendable { } 67 | 68 | 69 | @available(iOS 17, macOS 14, watchOS 10, tvOS 17, *) 70 | public extension AnimatablePack { 71 | static var zero: Self { 72 | .init(repeat (each Item).zero) 73 | } 74 | 75 | static func + (lhs: Self, rhs: Self) -> Self { 76 | .init(repeat (each lhs.item) + (each rhs.item)) 77 | } 78 | 79 | static func - (lhs: Self, rhs: Self) -> Self { 80 | .init(repeat (each lhs.item) - (each rhs.item)) 81 | } 82 | 83 | mutating func scale(by rhs: Double) { 84 | item = (repeat (each item).scaled(by: rhs)) 85 | } 86 | 87 | static func == (lhs: Self, rhs: Self) -> Bool { 88 | (lhs - rhs).magnitudeSquared == .zero 89 | } 90 | 91 | var magnitudeSquared: Double { 92 | var value = 0.0 93 | for item in repeat each item { 94 | value += item.magnitudeSquared 95 | } 96 | return value 97 | } 98 | } 99 | #endif 100 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Corner/Corner+Animatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Corner+Animatable.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2023-05-19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Corner: Animatable { 11 | public var animatableData: AnimatablePair { 12 | get { 13 | .init(Vector2(dx: x, dy: y), style.animatableData) 14 | } 15 | set { 16 | self.update(with: newValue) 17 | } 18 | } 19 | 20 | /// Updates this Corner with new values based on animatable data 21 | /// - Parameter newValue: Data used to update this Corner 22 | mutating func update(with newValue: AnimatableData) { 23 | x = newValue.first.dx 24 | y = newValue.first.dy 25 | style.update(with: newValue.second) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Corner/Corner+extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Corner+extensions.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-08-13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Corner: Vector2Transformable { 11 | public var vector: Vector2 { 12 | Vector2(dx: x, dy: y) 13 | } 14 | 15 | public init(vector: Vector2) { 16 | x = vector.dx 17 | y = vector.dy 18 | style = .point 19 | } 20 | 21 | public func repositioned(to point: some Vector2Representable) -> Corner { 22 | Corner(style, point: point) 23 | } 24 | } 25 | 26 | extension Corner { 27 | /// Radius of corner based on the style. 28 | public var radius: RelatableValue { 29 | style.radius 30 | } 31 | 32 | /// Creates a corner at the same position but with the supplied style. 33 | /// - Parameter style: Corner style to apply. 34 | /// - Returns: A corner at the same position but with the supplied style. 35 | public func applyingStyle(_ style: CornerStyle) -> Corner { 36 | if style == self.style { 37 | return self 38 | } 39 | return Corner(style, point: point) 40 | } 41 | 42 | /// Creates a corner with the same style at the same position but with a new supplied radius. 43 | /// - Parameter radius: Radius to apply to the corner. 44 | /// - Returns: A corner with the same style at the same position but with a new supplied radius. 45 | public func changingRadius(to radius: RelatableValue) -> Corner { 46 | if radius == self.radius { 47 | return self 48 | } 49 | return applyingStyle(style.changingRadius(to: radius)) 50 | } 51 | 52 | /// Creates a set of saved dimensions based on the corner style and provided previous and next points. 53 | /// 54 | /// Used for creating paths, insetting, flattening, etc. 55 | /// - Parameters: 56 | /// - previousPoint: Point before the corner. 57 | /// - nextPoint: Point after the corner. 58 | /// - Returns: A set of saved dimensions based on the corner style and provided previous and next points. 59 | public func dimensions(previousPoint: CGPoint, nextPoint: CGPoint) -> Self.Dimensions { 60 | .init(corner: self, previousPoint: previousPoint, nextPoint: nextPoint) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Corner/Corner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Corner.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-01-21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A point with a specified corner style used to draw corner shapes. 11 | /// 12 | /// Corners in an array are used instead of generating a path in ``CornerShape``, or when creating one inline with ``CornerCustom``. 13 | /// 14 | /// CornerCustom { rect in 15 | /// [ 16 | /// Corner(x: rect.midX, y: rect.minY), 17 | /// Corner(.rounded(radius: 5), x: rect.maxX, y: rect.maxY), 18 | /// Corner(.rounded(radius: 5), x: rect.minX, y: rect.maxY) 19 | /// ] 20 | /// } 21 | /// .fill() 22 | /// 23 | /// They can generate a path using ``Foundation/Array/Corner/path()``also be easily added to a path in a SwiftUI `Shape` using ``SwiftUICore/Path/addClosedCornerShape(_:)``, ``SwiftUICore/Path/addOpenCornerShape(_:previousPoint:nextPoint:moveToStart:)``. 24 | /// 25 | /// struct MyShape: Shape { 26 | /// let corners: [Corner] 27 | /// 28 | /// func path(in rect: CGRect) -> Path { 29 | /// let corners = [ 30 | /// Corner(x: rect.minX, y: rect.minY), 31 | /// Corner(x: rect.midX, y: rect.midY), 32 | /// Corner(x: rect.maxX, y: rect.minY), 33 | /// Corner(x: rect.maxX, y: rect.midY) 34 | /// ] 35 | /// .corners([ 36 | /// .straight(radius: .relative(0.2)), 37 | /// .cutout(radius: .relative(0.2), cornerStyles: [ 38 | /// .rounded(radius: .relative(0.4)), 39 | /// .point, 40 | /// .straight(radius: .relative(0.4)) 41 | /// ]), 42 | /// .cutout(radius: .relative(0.2), cornerStyles: [.rounded(radius: .relative(0.2))]), 43 | /// .rounded(radius: 20) 44 | /// ]) 45 | /// 46 | /// var path = Path() 47 | /// path.move(to: CGPoint(x: rect.minX + rect.width * 0.25, y: rect.maxY)) 48 | /// path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.midY), control: CGPoint(x: rect.midX, y: rect.midY)) 49 | /// 50 | /// path.addOpenCornerShape( 51 | /// corners, 52 | /// previousPoint: CGPoint(x: rect.minX, y: rect.midY), 53 | /// nextPoint: CGPoint(x: rect.midX, y: rect.midY), 54 | /// moveToStart: false 55 | /// ) 56 | /// 57 | /// path.addQuadCurve(to: CGPoint(x: rect.maxX - rect.width * 0.25, y: rect.maxY), control: CGPoint(x: rect.midX, y: rect.midY)) 58 | /// 59 | /// return path 60 | /// } 61 | /// } 62 | /// 63 | public struct Corner: Hashable, Codable, Sendable { 64 | public var x: CGFloat 65 | public var y: CGFloat 66 | public var style: CornerStyle 67 | 68 | /// Create a corner with a specified style and two-dimensional point. 69 | /// - Parameters: 70 | /// - style: Corner style. Default is .point. 71 | /// - x: x coordinate of corner. 72 | /// - y: y coordinate of corner. 73 | public init(_ style: CornerStyle? = nil, x: CGFloat, y: CGFloat) { 74 | self.x = x 75 | self.y = y 76 | self.style = style ?? .point 77 | } 78 | 79 | /// Create a corner with a specified style and two-dimensional point. 80 | /// - Parameters: 81 | /// - style: Corner style. Default is .point. 82 | /// - point: Location of corner. 83 | public init(_ style: CornerStyle? = nil, point: some Vector2Representable) { 84 | x = point.vector.dx 85 | y = point.vector.dy 86 | self.style = style ?? .point 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Corner/CornerStyle+Animatable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerStyle+Animatable.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2023-05-20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension CornerStyle: Animatable { 11 | public var animatableData: AnimatablePair { 12 | get { 13 | switch self { 14 | case .point: 15 | return .init(.zero, .zero) 16 | case let .rounded(radius): 17 | return .init(radius, .zero) 18 | case let .concave(radius, radiusOffset): 19 | return .init(radius, radiusOffset) 20 | case let .straight(radius, _): 21 | return .init(radius, .zero) 22 | case let .cutout(radius, _): 23 | return .init(radius, .zero) 24 | } 25 | } 26 | set { 27 | self.update(with: newValue) 28 | } 29 | } 30 | 31 | /// Updates this value based on new animatable data 32 | /// - Parameter newValue: Animatable Data representing the new value. 33 | mutating func update(with newValue: AnimatableData) { 34 | switch self { 35 | case .point, .rounded, .straight, .cutout: 36 | self = self.changingRadius(to: newValue.first) 37 | case .concave: 38 | self = .concave(radius: newValue.first, radiusOffset: newValue.second) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Corner/CornerStyle+CutoutLocation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerStyle+CutoutLocation.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-02-10. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | internal extension CornerStyle { 12 | /// An enumeration representing the cutout location on cutout style corners. 13 | /// 14 | /// Not in use yet. 15 | enum CutoutLocation { 16 | /// The center of the corner radius. 17 | case radiusCenter 18 | /// A point opposite the corner point across the line from corner start to corner end. 19 | case mirrorPoint 20 | /// The shallowest cut between radius center and mirror point. 21 | case shallow 22 | /// The deepest cut between radius center and mirror point. 23 | case deep 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Corner/Dimensions/Corner+Dimensions+Array.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Corner+Dimensions+Array.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-02-18. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension Array where Element == Corner.Dimensions { 11 | var corners: [Corner] { 12 | map { $0.corner } 13 | } 14 | 15 | func corners(inset: CGFloat) -> [Corner] { 16 | inset == 0 ? corners : map { $0.corner(inset: inset) } 17 | } 18 | 19 | /// Adds an open corner shape defined by this array of corners to the provided path. 20 | /// - Parameters: 21 | /// - path: Path where corner shape is added. 22 | /// - moveToStart: A boolean value determining if the first point should be moved to. If this value is false a line will be added from wherever the path currrently is to the first corner. 23 | func addOpenCornerShape(to path: inout Path, moveToStart: Bool) { 24 | self.enumerated().forEach { i, dims in 25 | // If it's the first corner and moveToStart is active, the first point will be a move. 26 | dims.addCornerShape(to: &path, moveToStart: i == 0 && moveToStart) 27 | } 28 | } 29 | 30 | /// Adds a closed corner shape defined by this array of corner dimensions to the provided path. 31 | /// - Parameters: 32 | /// - path: Path where corner shape is added. 33 | /// - closed: Boolean determining if the path is closed. Default is true. 34 | func addCornerShape(to path: inout Path, closed: Bool = true) { 35 | addOpenCornerShape(to: &path, moveToStart: true) 36 | if closed { 37 | path.closeSubpath() 38 | } 39 | } 40 | 41 | /// An flattened array of corners based on these corner dimensions. 42 | /// 43 | /// All corners will have their radius changed to absolute values and corners with nested styles will change to an array of corners with those styles. This process is recursive leaving no corners with nested corner styles or relative radius values. 44 | internal var flattened: [Corner] { 45 | flatMap { $0.flattened } 46 | } 47 | 48 | /// Returns an array of corners based on these corner dimensions flattened by the number of levels provided. 49 | /// 50 | /// All corners on this level will have their radius changed to absolute values and corners with nested styles will change to an array of corners with those styles. For each level higher than one this process will be repeated for those new nested corners. 51 | /// - Parameter levels: Number of levels to flatten. 52 | /// - Returns: An array of corners based on these corner dimensions flattened by the number of levels provided. 53 | internal func flattened(levels: Int) -> [Corner] { 54 | flatMap { $0.flattened(levels: levels)} 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Corner/Dimensions/Corner+Dimensions+exensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Corner+Dimensions+exensions.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-03-03. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension Corner.Dimensions { 11 | /// An array of corners created from nested corner styles. Point, rounded and concave corners will return empty arrays. Straight will return an array of 2 points (corner start and corner end), and cutout will return an array of 3 points (corner start, cutout, corner end) 12 | var subCorners: [Corner] { 13 | switch corner.style { 14 | case .point, .rounded, .concave: 15 | return [] 16 | case let .straight(_, cornerStyles): 17 | return [cornerStart, cornerEnd].corners(cornerStyles) 18 | case let .cutout(_, cornerStyles): 19 | return [cornerStart, cutoutPoint, cornerEnd].corners(cornerStyles) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Corner/Dimensions/Corner+Dimensions+flattened.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Corner+Dimensions+flattened.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-03-02. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Corner.Dimensions { 11 | /// An flattened array of corners based on these dimensions. 12 | /// 13 | /// Corner radius will change to an absolute value. Nested corner styles will change to an array of corners with those styles. This process is recursive leaving no corners with nested corner styles or relative radius values. 14 | internal var flattened: [Corner] { 15 | switch corner.style { 16 | case .point, .rounded, .concave: 17 | if case .relative = corner.radius { 18 | return [corner.changingRadius(to: .absolute(absoluteRadius))] 19 | } 20 | return [corner] 21 | case .straight, .cutout: 22 | return subCorners.flattened 23 | } 24 | } 25 | 26 | /// Returns an array of corners based on these corner dimensions flattened by the number of levels provided. 27 | /// 28 | /// Corner radius will change to an absolute value. Nested styles will change to an array of corners with those styles. For each level higher than one this process will be repeated for those new nested corners. 29 | /// - Parameter levels: Number of levels to flatten. 30 | /// - Returns: An array of corners based on these corner dimensions flattened by the number of levels provided. 31 | internal func flattened(levels: Int) -> [Corner] { 32 | guard levels > 0 else { return [corner] } 33 | 34 | switch corner.style { 35 | case .point, .rounded, .concave: 36 | if case .relative = corner.radius { 37 | return [corner.changingRadius(to: .absolute(absoluteRadius))] 38 | } 39 | return [corner] 40 | case .straight, .cutout: 41 | return subCorners.flattened(levels: levels - 1) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/ShapeUp/CornerShape/CornerCustom.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerCustom.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-01-23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A custom open or closed insettable shape built out of corners, aligned inside the frame of the view containing it. 12 | 13 | This shape can either be used in a SwiftUI View like any other `InsettableShape` 14 | 15 | CornerCustom { rect in 16 | [ 17 | Corner(x: rect.midX, y: rect.minY), 18 | Corner(.rounded(radius: 5), x: rect.maxX, y: rect.maxY), 19 | Corner(.rounded(radius: 5), x: rect.minX, y: rect.maxY) 20 | ] 21 | } 22 | .fill() 23 | 24 | CornerCustom { rect in 25 | rect 26 | .points(.top, .bottomRight, .left) 27 | .corners(.rounded(radius: .relative(0.1))) 28 | } 29 | .strokeBorder(lineWidth: 10) 30 | */ 31 | public struct CornerCustom: CornerShape { 32 | public let closed: Bool 33 | public var insetAmount: CGFloat = 0 34 | 35 | public var animatableData: CGFloat { 36 | get { insetAmount } 37 | set { insetAmount = newValue } 38 | } 39 | 40 | internal let corners: @Sendable (CGRect) -> [Corner] 41 | 42 | /// Creates a custom insettable shape out of corners. 43 | /// - Parameters: 44 | /// - closed: A boolean determining if the shape should be closed. Default is true. 45 | /// - corners: Closure used to draw corners in a defined frame. 46 | public init(closed: Bool = true, _ corners: @Sendable @escaping (CGRect) -> [Corner]) { 47 | self.closed = closed 48 | self.corners = corners 49 | } 50 | 51 | public func corners(in rect: CGRect) -> [Corner] { 52 | corners(rect) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ShapeUp/CornerShape/CornerShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerShape.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-10. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An 2D insettable shape that you can use when drawing a view or as an array of corners to use as a starting point for a more complex shape. 11 | /// 12 | /// You can define an insetAmount of zero as this property is mainly used if the shape is later inset. Use the closed property to define if your shape should be closed or left open. Write a function that returns an array of corners. 13 | /// 14 | /// public struct MyShape: CornerShape { 15 | /// public var insetAmount: CGFloat = .zero 16 | /// public let closed = true 17 | /// 18 | /// public func corners(in rect: CGRect) -> [Corner] { 19 | /// [ 20 | /// Corner(x: rect.midX, y: rect.minY), 21 | /// Corner(.rounded(radius: 5), x: rect.maxX, y: rect.maxY), 22 | /// Corner(.rounded(radius: 5), x: rect.minX, y: rect.maxY) 23 | /// ] 24 | /// } 25 | /// } 26 | /// 27 | /// The path function is already implemented and will use this array to create a single path, applying any inset, and closing it if the closed parameter is true. 28 | /// 29 | /// A `CornerShape` is an `InsettableShape` so it can be used in SwiftUI Views in the same way as `RoundedRectangle` or similar. 30 | /// 31 | /// MyShape() 32 | /// .fill() 33 | /// 34 | /// Or the corners can be accessed directly for use in a more complex shape 35 | /// 36 | /// public func corners(in rect: CGRect) -> [Corner] { 37 | /// MyShape() 38 | /// .corners(in: rect) 39 | /// .inset(by: 10) 40 | /// .addingNotch(Notch(.rectangle, depth: 5), afterCornerIndex: 0) 41 | /// } 42 | /// 43 | public protocol CornerShape: InsettableShapeByProperty { 44 | /// Creates an array of corners that will form a single closed shape with zero inset. 45 | /// 46 | /// Do not apply any inset amount in this function as it is automatically applied before creating the path. 47 | /// - Parameter rect: Frame in which the corners are defined. 48 | /// - Returns: An array of corners defining the shape with zero inset. 49 | func corners(in rect: CGRect) -> [Corner] 50 | 51 | /// A boolean determining if the shape is closed or open. 52 | var closed: Bool { get } 53 | } 54 | 55 | public extension CornerShape { 56 | /// Creates an array of corners inset by the insetAmount property. 57 | /// - Parameter rect: Frame in which the corners are defined. 58 | /// - Returns: An array of corners inset by the insetAmount property. 59 | func insetCorners(in rect: CGRect) -> [Corner] { 60 | corners(in: rect) 61 | .inset(by: insetAmount) 62 | } 63 | 64 | /// Creates a path from the array of inset corners. 65 | /// - Parameter rect: Frame in which the path is drawn. 66 | /// - Returns: Path that describes this corner shape. 67 | func path(in rect: CGRect) -> Path { 68 | insetCorners(in: rect) 69 | .path(closed: closed) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/ShapeUp/CornerShape/EnumeratedCornerShape/CornerPentagon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerPentagon.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-10. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A pentagon shape pointing upwards with individually stylable corners, aligned inside the frame of the view containing it. 12 | 13 | This shape can either be used in a SwiftUI View like any other `InsettableShape` 14 | 15 | CornerPentagon( 16 | pointHeight: .relative(0.3), 17 | topTaper: .relative(0.1), 18 | bottomTaper: .relative(0.3), 19 | styles: [ 20 | .topRight: .concave(radius: 30), 21 | .bottomLeft: .straight(radius: .relative(0.3)) 22 | ] 23 | ) 24 | .fill() 25 | 26 | The corners can be accessed directly for use in a more complex shape 27 | 28 | public func corners(in rect: CGRect) -> [Corner] { 29 | CornerPentagon(pointHeight: .relative(0.2), topTaper: .relative(0.15), bottomTaper: .zero) 30 | .corners(in: rect) 31 | .inset(by: 10) 32 | .addingNotch(Notch(.rectangle, depth: 5), afterCornerIndex: 0) 33 | } 34 | */ 35 | public struct CornerPentagon: EnumeratedCornerShape { 36 | public let closed = true 37 | public var insetAmount: CGFloat = 0 38 | 39 | /// An enumeration to indicate the corners of a pentagon. 40 | public enum ShapeCorner: EnumeratedCorner { 41 | case topLeft 42 | case top 43 | case topRight 44 | case bottomRight 45 | case bottomLeft 46 | } 47 | 48 | public var pointHeight: RelatableValue 49 | public var topTaper: RelatableValue 50 | public var bottomTaper: RelatableValue 51 | public var styles: [ShapeCorner: CornerStyle?] 52 | 53 | /// Creates a pentagon shape with corners that can be styled. 54 | /// - Parameters: 55 | /// - pointHeight: The vertical distance from the central point to the two points on either side. 56 | /// - topTaper: The horizontal inset of the two points closest to the top. 57 | /// - bottomTaper: The horizontal inset of the bottom two points. 58 | public init(pointHeight: RelatableValue, topTaper: RelatableValue = .zero, bottomTaper: RelatableValue = .zero, styles: [ShapeCorner: CornerStyle] = [:]) { 59 | self.pointHeight = pointHeight 60 | self.topTaper = topTaper 61 | self.bottomTaper = bottomTaper 62 | self.styles = styles 63 | } 64 | 65 | public func points(in rect: CGRect) -> [ShapeCorner: CGPoint] { 66 | let bottomInset = bottomTaper.value(using: rect.width / 2) 67 | let topInset = topTaper.value(using: rect.width / 2) 68 | let pointHeight = pointHeight.value(using: rect.height) 69 | 70 | return [ 71 | .bottomLeft: rect.point(.bottomLeft).moved(dx: bottomInset), 72 | .bottomRight: rect.point(.bottomRight).moved(dx: -bottomInset), 73 | .topLeft: rect.point(.topLeft).moved(dx: topInset, dy: pointHeight), 74 | .topRight: rect.point(.topRight).moved(dx: -topInset, dy: pointHeight), 75 | .top: rect.point(.top) 76 | ] 77 | } 78 | } 79 | 80 | /// Animatable Extension 81 | extension CornerPentagon { 82 | public typealias AnimatableData = 83 | AnimatablePair< 84 | CGFloat, 85 | AnimatablePair< 86 | RelatableValue, 87 | AnimatablePair< 88 | RelatableValue, 89 | RelatableValue 90 | > 91 | > 92 | > 93 | 94 | public var animatableData: AnimatableData { 95 | get { 96 | .init( 97 | insetAmount, 98 | .init( 99 | pointHeight, 100 | .init( 101 | topTaper, 102 | bottomTaper 103 | ) 104 | ) 105 | ) 106 | } 107 | set { 108 | insetAmount = newValue.first 109 | pointHeight = newValue.second.first 110 | topTaper = newValue.second.second.first 111 | bottomTaper = newValue.second.second.second 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Sources/ShapeUp/CornerShape/EnumeratedCornerShape/CornerRectangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerRectangle.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-10. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A rectangular `CornerShape` similar to `RoundedRectangle` with individually stylable corners, aligned inside the frame of the view containing it. 12 | 13 | This shape can either be used in a SwiftUI View like any other `InsettableShape` 14 | 15 | CornerRectangle([ 16 | .topLeft: .straight(radius: 60), 17 | .topRight: .cutout(radius: .relative(0.2)), 18 | .bottomRight: .rounded(radius: .relative(0.8)), 19 | .bottomLeft: .concave(radius: .relative(0.2)) 20 | ]) 21 | .fill() 22 | 23 | The corners can be accessed directly for use in a more complex shape 24 | 25 | public func corners(in rect: CGRect) -> [Corner] { 26 | CornerRectangle([.topLeft: .rounded(radius: 20)]) 27 | .corners(in: rect) 28 | .inset(by: 10) 29 | .addingNotch(Notch(.rectangle, depth: 5), afterCornerIndex: 0) 30 | } 31 | */ 32 | public struct CornerRectangle: EnumeratedCornerShape { 33 | public let closed = true 34 | public var insetAmount: CGFloat = 0 35 | 36 | public enum ShapeCorner: EnumeratedCorner { 37 | case topLeft 38 | case topRight 39 | case bottomRight 40 | case bottomLeft 41 | } 42 | 43 | public var styles: [ShapeCorner : CornerStyle?] 44 | 45 | /// Creates a 2d rectangular shape with specified styles for each corner. 46 | /// - Parameters: 47 | /// - styles: A dictionary describing the style of each shape corner. 48 | public init(_ styles: [ShapeCorner: CornerStyle] = [:]) { 49 | self.styles = styles 50 | } 51 | 52 | public func points(in rect: CGRect) -> [ShapeCorner : CGPoint] { 53 | [ 54 | .topLeft: rect.point(.topLeft), 55 | .topRight: rect.point(.topRight), 56 | .bottomRight: rect.point(.bottomRight), 57 | .bottomLeft: rect.point(.bottomLeft) 58 | ] 59 | } 60 | } 61 | 62 | /// Animatable Extension 63 | extension CornerRectangle { 64 | public var animatableData: CGFloat { 65 | get { insetAmount } 66 | set { insetAmount = newValue } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/ShapeUp/CornerShape/EnumeratedCornerShape/CornerTriangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerTriangle.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-03-08. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /** 11 | A triangular shape with an adjustable top point and individually stylable corners, aligned inside the frame of the view containing it. 12 | 13 | The top point is positioned relative to the top left corner and the value is a `RelatableValue` relative to the width of the frame provided. The default is in the middle. 14 | 15 | This shape can either be used in a SwiftUI View like any other `InsettableShape` 16 | 17 | CornerTriangle(topPoint: .relative(0.6), styles: [ 18 | .top: .straight(radius: 10), 19 | .bottomRight: .rounded(radius: .relative(0.3)), 20 | .bottomLeft: .concave(radius: .relative(0.2)) 21 | ]) 22 | .fill() 23 | 24 | The corners can be accessed directly for use in a more complex shape 25 | 26 | public func corners(in rect: CGRect) -> [Corner] { 27 | CornerTriangle(topPoint: 30) 28 | .corners(in: rect) 29 | .inset(by: 10) 30 | .addingNotch(Notch(.rectangle, depth: 5), afterCornerIndex: 0) 31 | } 32 | */ 33 | public struct CornerTriangle: EnumeratedCornerShape { 34 | public let closed = true 35 | public var insetAmount: CGFloat = 0 36 | 37 | /// An enumeration to indicate the three corners of a triangle. 38 | public enum ShapeCorner: EnumeratedCorner { 39 | case top 40 | case bottomRight 41 | case bottomLeft 42 | } 43 | 44 | public var topPoint: RelatableValue 45 | public var styles: [ShapeCorner: CornerStyle?] 46 | 47 | /// Creates a 2d triangular shape with specified top point and styles for each corner. 48 | /// - Parameters: 49 | /// - topPoint: Position of the top point from the top left corner of the frame. Relative values are relative to width. 50 | /// - styles: A dictionary describing the style of each shape corner. 51 | public init(topPoint: RelatableValue = .relative(0.5), styles: [ShapeCorner: CornerStyle] = [:]) { 52 | self.topPoint = topPoint 53 | self.styles = styles 54 | } 55 | 56 | public func points(in rect: CGRect) -> [ShapeCorner: CGPoint] { 57 | [ 58 | .top: rect.point(.topLeft).moved(dx: topPoint.value(using: rect.width)), 59 | .bottomRight: rect.point(.bottomRight), 60 | .bottomLeft: rect.point(.bottomLeft) 61 | ] 62 | } 63 | } 64 | 65 | /// Animatable Extension 66 | extension CornerTriangle { 67 | public var animatableData: AnimatablePair { 68 | get { 69 | .init(insetAmount, topPoint) 70 | } 71 | set { 72 | insetAmount = newValue.first 73 | topPoint = newValue.second 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/ShapeUp/CornerShape/EnumeratedCornerShape/EnumeratedCornerShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnumeratedCornerShape.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-03-09. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A protocol used inside ``EnumeratedCornerShape`` to ensure `ShapeCorner` is `CaseIterable`, `Hashable`, and `Sendable` 11 | public protocol EnumeratedCorner: CaseIterable, Hashable, Sendable { } 12 | 13 | /// A corner shape defined by a named set of shape corners. 14 | /// 15 | /// For example a triangle would include the corners top, bottom left, and bottom right. 16 | public protocol EnumeratedCornerShape: CornerShape { 17 | /// An enumeration containing each named corner in the order they will be drawn. 18 | associatedtype ShapeCorner: EnumeratedCorner 19 | 20 | /// A dictionary storing the style of each corner by it's shape corner label. 21 | var styles: [ShapeCorner: CornerStyle?] { get set } 22 | 23 | /// Returns a dictionary with each point used to draw the shape stored with it's shape corner label. 24 | /// - Returns: A dictionary with each point used to draw the shape stored with it's shape corner label. 25 | func points(in rect: CGRect) -> [ShapeCorner: CGPoint] 26 | } 27 | 28 | public extension EnumeratedCornerShape { 29 | func corners(in rect: CGRect) -> [Corner] { 30 | let points = points(in: rect) 31 | return ShapeCorner.allCases.compactMap { 32 | points[$0]?.corner(styles[$0] ?? nil) 33 | } 34 | } 35 | 36 | /// Creates a copy of this shape changing the style of specified corners to the provided style. 37 | /// - Parameters: 38 | /// - style: Style to apply to specified shape corners. 39 | /// - shapeCorners: Shape corners on which to apply the specified style. Missing values will keep current style. 40 | /// - Returns: A copy of this shape changing the style of specified corners to the provided style. 41 | func applyingStyle(_ style: CornerStyle, shapeCorners: Set = Set(ShapeCorner.allCases)) -> Self { 42 | var shape = self 43 | shapeCorners.forEach { shape.styles[$0] = style } 44 | return shape 45 | } 46 | 47 | /// Creates a copy of this shape changing the styles of specified corners. 48 | /// - Parameters: 49 | /// - styles: Styles to apply to each specified shape corner. Nil or missing values will keep current style. 50 | /// - Returns: A copy of this shape changing the styles of specified corners. 51 | func applyingStyles(_ styles: [ShapeCorner: CornerStyle?]) -> Self { 52 | var shape = self 53 | styles.forEach { (shapeCorner, style) in 54 | if let style = style { 55 | shape.styles[shapeCorner] = style 56 | } 57 | } 58 | return shape 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/ShapeUp/CornerShape/InsettableShapeByProperty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsettableShapeByProperty.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-16. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An insettable shape that stores its inset amount in a property rather than using the function. 11 | /// 12 | /// Inset versions of this type will always share the same type using the insetAmount property in the path function to draw the shape with the appropriate inset. 13 | public protocol InsettableShapeByProperty: InsettableShape { 14 | /// Inset amount stored as a property. 15 | /// 16 | /// Initialize this value with zero and the inset function will adjust it whenever you inset the shape. 17 | /// 18 | /// var insetAmount: CGFloat = 0 19 | /// 20 | /// Do not use this value in the corners function as that function needs to output corners with zero inset. 21 | var insetAmount: CGFloat { get set } 22 | } 23 | 24 | public extension InsettableShapeByProperty { 25 | /// Creates the same shape with an updated inset amount property. 26 | /// 27 | /// The shape must use this property in its path function to draw with the appropriate inset. 28 | /// - Parameter amount: Inset amount 29 | /// - Returns: The same shape type with the inset amount saved to 30 | func inset(by amount: CGFloat) -> Self { 31 | var shape = self 32 | shape.insetAmount += amount 33 | return shape 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Emboss/EmbossViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmbossViewModifier.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-01-19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// View modifier that overlays an emboss effect on a view. 11 | /// 12 | /// Used in `.emboss()` and `.deboss()` view extensions 13 | struct EmbossViewModifier: ViewModifier { 14 | let baseColor: Color? 15 | let blur: CGFloat 16 | let opacity: Double 17 | let offsetX: CGFloat 18 | let offsetY: CGFloat 19 | 20 | init(baseColor: Color? = nil, amount: CGFloat, blur: CGFloat? = nil, angle: Angle? = nil, opacity: Double? = nil, deboss: Bool = false) { 21 | self.baseColor = baseColor 22 | self.blur = blur ?? amount 23 | self.opacity = opacity ?? 1.0 24 | let angle = (angle ?? .degrees(45)) + (deboss ? .degrees(180) : .zero) 25 | self.offsetX = amount * 1.5 * CGFloat(cos(angle.radians)) 26 | self.offsetY = amount * 1.5 * CGFloat(sin(angle.radians)) 27 | } 28 | 29 | func body(content: Content) -> some View { 30 | content 31 | .opacity(baseColor == nil ? 1 : 0) 32 | .overlay( 33 | ZStack { 34 | baseColor 35 | 36 | Color.white 37 | .accessibilityIgnoresInvertColors() 38 | .mask( 39 | Color.white 40 | .overlay( 41 | Color.black 42 | .mask( 43 | content 44 | .offset(x: offsetX, y: offsetY) 45 | ) 46 | ) 47 | .blur(radius: blur) 48 | .drawingGroup() 49 | .luminanceToAlpha() 50 | ) 51 | .opacity(opacity) 52 | .allowsHitTesting(false) 53 | .accessibility(hidden: true) 54 | 55 | Color.black 56 | .accessibilityIgnoresInvertColors() 57 | .mask( 58 | Color.white 59 | .overlay( 60 | Color.black 61 | .mask( 62 | content 63 | .offset(x: -offsetX, y: -offsetY) 64 | ) 65 | ) 66 | .blur(radius: blur) 67 | .drawingGroup() 68 | .luminanceToAlpha() 69 | ) 70 | .opacity(opacity) 71 | .allowsHitTesting(false) 72 | .accessibility(hidden: true) 73 | } 74 | ) 75 | .mask(content) 76 | } 77 | } 78 | 79 | 80 | -------------------------------------------------------------------------------- /Sources/ShapeUp/GeoMath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeoMath.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-02-28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | internal enum GeoMath { 11 | /// Returns any intersection points between a line and a circle. 12 | /// - Parameters: 13 | /// - line: A line defined by two points. 14 | /// - circle: A circle defined by a center point and a radius. 15 | /// - Returns: An array of intersection points between the line and the circle. May be 0, 1, or 2 points. 16 | static func intersectionPoints(line: (point1: some Vector2Representable, point2: some Vector2Representable), circle: (center: some Vector2Representable, radius: CGFloat)) -> [CGPoint] { 17 | let deltaLine = line.point2.vector - line.point1.vector 18 | let centerToP1 = line.point1.vector - circle.center.vector 19 | 20 | let a = pow(deltaLine.dx,2) + pow(deltaLine.dy,2) 21 | let b = 2 * (deltaLine.dx * centerToP1.dx + deltaLine.dy * centerToP1.dy) 22 | let c = pow(centerToP1.dx, 2) + pow(centerToP1.dy, 2) - pow(circle.radius, 2) 23 | 24 | let det = b * b - 4 * a * c 25 | var detRoot = [CGFloat]() 26 | if a <= 0.000001 || det < 0 { 27 | // No real solutions 28 | return [] 29 | } else if det == 0 { 30 | // One solution 31 | detRoot += [0] 32 | } else { 33 | let root = sqrt(det) 34 | // Two solutions 35 | detRoot += [root, -root] 36 | } 37 | return detRoot 38 | .map { (-b + $0) / (2.0 * a) } 39 | .map { (line.point1.vector + ($0 * deltaLine)).point } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Notch/Notch+extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notch+extensions.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-08-13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension Notch { 11 | /// Creates an array of corners describing a notch between two points. 12 | /// 13 | /// Although any `Vector2Representable` object can be passed in, the output is always an array of corners as notch styles can contain corner styles. 14 | /// - Parameters: 15 | /// - start: Start point of the line where a notch is added. 16 | /// - end: End point of the line where a notch is added. 17 | /// - Returns: An array of corners describing a notch between two points. 18 | func between(start: some Vector2Representable, end: some Vector2Representable) -> [Corner] { 19 | let vector = end.vector - start.vector 20 | 21 | // Check if vector has a direction. If not then a notch can't be created between these two points. 22 | guard let direction = vector.direction else { 23 | return [] 24 | } 25 | 26 | let totalLength = vector.magnitude 27 | let normalizedVector = vector.normalized 28 | let notchPosition = position.value(using: totalLength) 29 | let notchLength = length.value(using: totalLength) 30 | let notchDepth = depth.value(using: totalLength) 31 | let notchStartPoint = start.vector + normalizedVector * (notchPosition - (notchLength / 2)) 32 | 33 | let rect = CGRect(x: 0, y: 0, width: notchLength, height: abs(notchDepth)) 34 | 35 | let notchCorners = style.corners(in: rect) 36 | let signedNotchCorners: [Corner] 37 | if notchDepth < 0 { 38 | signedNotchCorners = notchCorners.flippedVertically(across: 0) 39 | } else { 40 | signedNotchCorners = notchCorners 41 | } 42 | 43 | return signedNotchCorners 44 | .rotated(direction) 45 | .moved(notchStartPoint) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Notch/Notch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notch.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-01-21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A notch in a line. 11 | public struct Notch: Sendable { 12 | /// Style of the notch. 13 | public let style: NotchStyle 14 | 15 | /// Center position of the notch relative to the length of the line and measured from the start. 16 | public let position: RelatableValue 17 | 18 | /// Length of the notch relative to the length of the line. 19 | public let length: RelatableValue 20 | 21 | /// Depth of the notch relative to the length of the line. 22 | public let depth: RelatableValue 23 | 24 | /// Creates a notch that will be drawn relative to a line segment between two points. 25 | /// 26 | /// Notch depth assumes a clockwise order of points. 27 | /// 28 | /// Negative depth will create a tab instead of a notch. 29 | /// - Parameters: 30 | /// - style: Style of the notch. 31 | /// - position: Center position of the notch relative to the length of the line and measured from the start. Default is the midpoint of the line. 32 | /// - length: Length of the notch relative to the length of the line. Default is equal to the depth. 33 | /// - depth: Depth of the notch relative to the length of the line. 34 | public init(_ style: NotchStyle, position: RelatableValue? = nil, length: RelatableValue? = nil, depth: RelatableValue) { 35 | self.style = style 36 | self.position = position ?? .relative(0.5) 37 | self.length = length ?? depth 38 | self.depth = depth 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Notch/NotchStyle+staticInit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotchStyle+staticInit.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-02-09. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension NotchStyle { 11 | /// A triangular shaped notch with default corner styles. 12 | static let triangle = NotchStyle.triangle(cornerStyles: []) 13 | /// A rectangular shaped notch with default corner styles. 14 | static let rectangle = NotchStyle.rectangle(cornerStyles: []) 15 | 16 | /// Creates a triangular shaped notch with a specified corner style for all 3 corners. 17 | /// - Parameter cornerStyle: Corner style to apply to all 3 corners. 18 | /// - Returns: A triangular notch with styled corners. 19 | static func triangle(cornerStyle: CornerStyle? = nil) -> NotchStyle { 20 | .triangle(cornerStyles: Array(repeating: cornerStyle, count: 3)) 21 | } 22 | 23 | /// Creates a rectangular shaped notch with a specified corner style for all 4 corners. 24 | /// - Parameter cornerStyle: Corner style to apply to all 4 corners. 25 | /// - Returns: A rectangular notch with styled corners. 26 | static func rectangle(cornerStyle: CornerStyle? = nil) -> NotchStyle { 27 | .rectangle(cornerStyles: Array(repeating: cornerStyle, count: 4)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Notch/NotchStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotchStyle.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-08-13. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An enumeration to indicate the style of a notch. 11 | public enum NotchStyle: Sendable { 12 | /// A triangular shaped notch. 13 | /// - Parameters: 14 | /// - conerStyles: Corner styles for each corner in the notch. Nil values will use a .point style. 15 | case triangle(cornerStyles: [CornerStyle?]) 16 | 17 | /// A rectangular shaped notch. 18 | /// - Parameters: 19 | /// - conerStyles: Corner styles for each corner in the notch. Nil values will use a .point style. 20 | case rectangle(cornerStyles: [CornerStyle?]) 21 | 22 | /// A custom shaped notch defined by corners in a reference frame equal to the notch's length and depth. 23 | /// - Parameters: 24 | /// - coners: A closure used to create corners in a rectangle defined by the length and depth of the notch. Start and end points are at the top left and top right of the rectangle and do not need to be included. 25 | case custom(corners: @Sendable (_ in: CGRect) -> [Corner]) 26 | } 27 | 28 | public extension NotchStyle { 29 | /// Corner styles for all corners of the notch. 30 | var cornerStyles: [CornerStyle?] { 31 | switch self { 32 | case let .triangle(cornerStyles): 33 | return cornerStyles 34 | case let .rectangle(cornerStyles): 35 | return cornerStyles 36 | case let .custom(corners): 37 | return corners(.zero).cornerStyles 38 | } 39 | } 40 | 41 | func corners(in rect: CGRect) -> [Corner] { 42 | switch self { 43 | case .triangle(let cornerStyles): 44 | return rect.points( 45 | .topLeft, 46 | .bottom, 47 | .topRight 48 | ).corners(cornerStyles) 49 | case .rectangle(let cornerStyles): 50 | return rect.points( 51 | .topLeft, 52 | .bottomLeft, 53 | .bottomRight, 54 | .topRight 55 | ).corners(cornerStyles) 56 | case .custom(let corners): 57 | return corners(rect) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/ShapeUp/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Rect/RectAnchor+extensions+Array.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectAnchor+extensions+Array.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-02-03. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension Array where Element == RectAnchor { 11 | /// Creates an array of points corresponding to the locations of the anchors. 12 | /// - Parameter rect: Rectangle where anchors are positioned. 13 | /// - Returns: An array of points where the anchors are located. 14 | func points(in rect: CGRect) -> [CGPoint] { 15 | map { $0.point(in: rect) } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Rect/RectAnchor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectAnchor.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-10. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An enumeration to indicate the type of anchor 11 | public enum AnchorType: Sendable { 12 | /// Anchor positioned on a vertex 13 | case vertex 14 | /// Anchor positioned on an edge 15 | case edge 16 | /// Anchor positioned in the center 17 | case center 18 | } 19 | 20 | /// An enumeration to indicate an anchor location on a rectangle. 21 | /// 22 | /// Cases start with Center and are in clockwise order from top left. 23 | public enum RectAnchor: CaseIterable, Sendable { 24 | case center 25 | case topLeft 26 | case top 27 | case topRight 28 | case right 29 | case bottomRight 30 | case bottom 31 | case bottomLeft 32 | case left 33 | 34 | /// Creates a point in the location of an anchor. 35 | /// - Parameter rect: Rectangle where anchor is positioned. 36 | /// - Returns: The point where the anchor is located. 37 | public func point(in rect: CGRect) -> CGPoint { 38 | switch self { 39 | case .topLeft: 40 | return CGPoint(x: rect.minX, y: rect.minY) 41 | case .top: 42 | return CGPoint(x: rect.midX, y: rect.minY) 43 | case .topRight: 44 | return CGPoint(x: rect.maxX, y: rect.minY) 45 | case .left: 46 | return CGPoint(x: rect.minX, y: rect.midY) 47 | case .center: 48 | return CGPoint(x: rect.midX, y: rect.midY) 49 | case .right: 50 | return CGPoint(x: rect.maxX, y: rect.midY) 51 | case .bottomLeft: 52 | return CGPoint(x: rect.minX, y: rect.maxY) 53 | case .bottom: 54 | return CGPoint(x: rect.midX, y: rect.maxY) 55 | case .bottomRight: 56 | return CGPoint(x: rect.maxX, y: rect.maxY) 57 | } 58 | } 59 | 60 | /// The type of the anchor. 61 | public var type: AnchorType { 62 | switch self { 63 | case .center: return .center 64 | case .topLeft, .topRight, .bottomRight, .bottomLeft: return .vertex 65 | case .top, .right, .bottom, .left: return .edge 66 | } 67 | } 68 | 69 | /// Edge anchors in clockwise order from the top left. 70 | public static var edgeAnchors: [Self] { 71 | allCases.filter { $0.type == .edge } 72 | } 73 | 74 | /// Corner anchors in clockwise order from the top left. 75 | public static var vertexAnchors: [Self] { 76 | allCases.filter { $0.type == .vertex } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/ShapeUp/RelatableValue/RelatableValue+Arithmatic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RelatableValue+Arithmatic.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-03-24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension RelatableValue { 11 | static func + (lhs: Self, rhs: Self) -> Self { 12 | switch (lhs, rhs) { 13 | case let (.absolute(lhsValue), .absolute(rhsValue)): 14 | return .absolute(lhsValue + rhsValue) 15 | 16 | case let (.relative(lhsValue), .relative(rhsValue)): 17 | return .relative(lhsValue + rhsValue) 18 | 19 | case let (.mixed(lhsAbsolute, lhsRelative), .mixed(rhsAbsolute, rhsRelative)): 20 | return .mixed(absolute: lhsAbsolute + rhsAbsolute, relative: lhsRelative + rhsRelative) 21 | 22 | case (.absolute, .relative), (.relative, .absolute), (.absolute, .mixed), (.mixed, .absolute), (.relative, .mixed), (.mixed, .relative): 23 | return lhs.mixed + rhs.mixed 24 | } 25 | } 26 | 27 | static prefix func - (x: Self) -> Self { 28 | switch x { 29 | case let .absolute(value): 30 | return .absolute(-value) 31 | case let .relative(value): 32 | return .relative(-value) 33 | case let .mixed(absolute, relative): 34 | return .mixed(absolute: -absolute, relative: -relative) 35 | } 36 | } 37 | 38 | static func - (lhs: Self, rhs: Self) -> Self { 39 | lhs + -rhs 40 | } 41 | 42 | /// RelatableValue addition assignment 43 | static func += (lhs: inout Self, rhs: Self) { 44 | lhs = lhs + rhs 45 | } 46 | 47 | /// RelatableValue subtraction assignment 48 | static func -= (lhs: inout Self, rhs: Self) { 49 | lhs = lhs - rhs 50 | } 51 | 52 | static func * (lhs: RelatableValue, rhs: CGFloat) -> RelatableValue { 53 | switch lhs { 54 | case let .absolute(lhsValue): 55 | return .absolute(lhsValue * rhs) 56 | 57 | case let .relative(lhsValue): 58 | return .relative(lhsValue * rhs) 59 | 60 | case let .mixed(lhsAbsolute, lhsRelative): 61 | return .mixed(absolute: lhsAbsolute * rhs, relative: lhsRelative * rhs) 62 | } 63 | } 64 | 65 | static func / (lhs: RelatableValue, rhs: CGFloat) -> RelatableValue { 66 | lhs * (1 / rhs) 67 | } 68 | 69 | /// RelatableValue multiplication assignment 70 | static func *= (lhs: inout Self, rhs: CGFloat) { 71 | lhs = lhs * rhs 72 | } 73 | 74 | /// RelatableValue division assignment 75 | static func /= (lhs: inout Self, rhs: CGFloat) { 76 | lhs = lhs / rhs 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/ShapeUp/RelatableValue/RelatableValue+VectorArithmetic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RelatableValue+Animatable.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2023-05-19. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension RelatableValue: VectorArithmetic { 11 | public mutating func scale(by rhs: Double) { 12 | switch self { 13 | case let .absolute(absolute): 14 | self = .absolute(absolute * rhs) 15 | case let .relative(relative): 16 | self = .relative(relative * rhs) 17 | case let .mixed(absolute, relative): 18 | self = .mixed(absolute: absolute * rhs, relative: relative * rhs) 19 | } 20 | } 21 | 22 | public var magnitudeSquared: Double { 23 | switch self { 24 | case let .absolute(absolute): 25 | return absolute * absolute 26 | case let .relative(relative): 27 | return relative * relative 28 | case let .mixed(absolute, relative): 29 | return absolute * absolute + relative * relative 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ShapeUp/RelatableValue/RelatableValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RelatableValue.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-01-22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An enumeration that represents either a relative or absolute value. 11 | public enum RelatableValue: Hashable, AdditiveArithmetic, Codable, Sendable { 12 | case absolute(_ value: CGFloat) 13 | case relative(_ value: CGFloat) 14 | case mixed(absolute: CGFloat, relative: CGFloat) 15 | } 16 | 17 | public extension RelatableValue { 18 | /// The absolute value zero 19 | static let zero = RelatableValue.absolute(0) 20 | 21 | /// Returns the absolute value based on a provided total. 22 | /// - Parameter total: Total used to calculate absolute values of relative values. 23 | /// - Returns: The absolute value based on a provided total. 24 | func value(using total: CGFloat) -> CGFloat { 25 | switch self { 26 | case let .absolute(value): 27 | return value 28 | case let .relative(value): 29 | return value * total 30 | case let .mixed(absolute, relative): 31 | return absolute + (relative * total) 32 | } 33 | } 34 | 35 | /// Returns an absolute relatable value of this value based on a provided total. 36 | /// - Parameter total: Total used to create absolute values from relative values. 37 | /// - Returns: An absolute relatable value of this value based on a provided total. 38 | func absolute(using total: CGFloat) -> Self { 39 | .absolute(value(using: total)) 40 | } 41 | 42 | /// Returns a relative relatable value of this value based on a provided total. 43 | /// - Parameter total: Total used to create relative values from absolute values. 44 | /// - Returns: A relative relatable value of this value based on a provided total. 45 | func relative(total: CGFloat) -> Self { 46 | .relative(value(using: total) / total) 47 | } 48 | 49 | /// Returns a mixed relatable value of this value based on a provided total. 50 | /// - Returns: A mixed relatable value of this value. 51 | var mixed: Self { 52 | switch self { 53 | case let .absolute(value): 54 | return .mixed(absolute: value, relative: 0) 55 | case let .relative(value): 56 | return .mixed(absolute: 0, relative: value) 57 | case .mixed: 58 | return self 59 | } 60 | } 61 | } 62 | 63 | extension RelatableValue: ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral { 64 | public typealias FloatLiteralType = Double 65 | public typealias IntegerLiteralType = Int 66 | 67 | /// Creates an absolute RelatableValue from the provided literal Double 68 | /// 69 | /// Usefull when providing fixed values for RelatableValue properties. 70 | public init(floatLiteral value: Double) { 71 | self = .absolute(value) 72 | } 73 | 74 | /// Creates an absolute RelatableValue from the provided literal Int 75 | /// 76 | /// Usefull when providing fixed values for RelatableValue properties. 77 | public init(integerLiteral value: Int) { 78 | self = .absolute(CGFloat(value)) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/ShapeUp/SketchyLines/SketchyLines.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SketchyLines.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2020-10-28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Lines with ends that can extend and a position that can offset perpendicular to its direction. 11 | /// 12 | /// All lines can be animated with a single draw amount. Each line's draw amount will be ignored. 13 | public struct SketchyLines: Shape { 14 | public var animatableData: CGFloat { 15 | get { drawAmount } 16 | set { self.drawAmount = newValue } 17 | } 18 | 19 | public var lines: [SketchyLine] 20 | public var drawAmount: CGFloat 21 | 22 | /// Creates a collection of sketchy lines. 23 | /// - Parameters: 24 | /// - lines: Lines that will be drawn using hte drawAmount. 25 | /// - drawAmount: Amount to draw. Defaults to 1 and overrides all lines. 26 | public init(lines: [SketchyLine], drawAmount: CGFloat = 1) { 27 | self.lines = lines 28 | self.drawAmount = drawAmount 29 | } 30 | } 31 | 32 | public extension SketchyLines { 33 | func path(in rect: CGRect) -> Path { 34 | var path = Path() 35 | 36 | for line in lines { 37 | path.addPath(line.path(in: rect, drawAmount: drawAmount)) 38 | } 39 | return path 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Vector2/Vector2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Vector2.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-03. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A vector type used as an alternative to CGPoint 11 | /// 12 | /// This type is mainly used internally so that CGPoint isn't publically extended with too many functions that may conflict with other user functions. 13 | public struct Vector2: Equatable, Hashable, Codable, Sendable { 14 | /// Delta x 15 | public var dx: CGFloat 16 | /// Delta y 17 | public var dy: CGFloat 18 | 19 | /// Creates a vector 20 | /// - Parameters: 21 | /// - dx: The x component 22 | /// - dy: The y component 23 | public init(dx: CGFloat, dy: CGFloat) { 24 | self.dx = dx 25 | self.dy = dy 26 | } 27 | } 28 | 29 | public extension Vector2 { 30 | /// The zero vector 31 | static let zero = Self.init(dx: 0, dy: 0) 32 | 33 | /// A CGSize representation 34 | var size: CGSize { CGSize(width: dx, height: dy) } 35 | 36 | /// A CGRect representation with the origin at zero. 37 | var rect: CGRect { CGRect(x: 0, y: 0, width: dx, height: dy) } 38 | } 39 | 40 | extension Vector2: Vector2Representable { 41 | public var vector: Vector2 { 42 | self 43 | } 44 | } 45 | 46 | extension Vector2: Vector2Algebraic { 47 | public init(vector: Vector2) { 48 | self = vector 49 | } 50 | } 51 | 52 | extension Vector2: Vector2Transformable { 53 | public func repositioned(to point: some Vector2Representable) -> Vector2 { 54 | point.vector 55 | } 56 | } 57 | 58 | extension Vector2: VectorArithmetic { 59 | public mutating func scale(by rhs: Double) { 60 | dx *= rhs 61 | dy *= rhs 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Vector2/Vector2Representable+Array.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Vector2Representable+Array.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-03. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension Array where Element: Vector2Representable { 11 | /// A Vector2 array. 12 | var vectors: [Vector2] { 13 | map { $0.vector } 14 | } 15 | 16 | /// A CGPoint array. 17 | var points: [CGPoint] { 18 | map { $0.point } 19 | } 20 | 21 | /// Returns an array of corners matching the positions of the points with an applied corner style. 22 | /// - Parameter style: Style applied to all corners. 23 | /// - Returns: An array of corners matching the positions of the points with an applied corner style. 24 | func corners(_ style: CornerStyle? = nil) -> [Corner] { 25 | map({ $0.corner(style) }) 26 | } 27 | 28 | /// Returns an array of corners matching the positions of the points 29 | /// 30 | /// Nil corner style values apply default styles. Styles array can be smaller than the point array. If it's larger extra values will be ignored. 31 | /// - Parameter styles: Styles applied to each point in order. 32 | /// - Returns: description 33 | func corners(_ styles: [CornerStyle?]) -> [Corner] { 34 | corners() 35 | .applyingStyles(styles) 36 | } 37 | 38 | /// A bounding frame containing all the points in the array. 39 | /// 40 | /// This frame only takes into account the points and not any corner shapes so the shape itself might be inset in the frame. 41 | var bounds: CGRect { 42 | guard !isEmpty else { 43 | return .zero 44 | } 45 | 46 | let xArray = self.map { $0.vector.dx } 47 | let yArray = self.map { $0.vector.dy } 48 | let minX = xArray.min() ?? .zero 49 | let minY = yArray.min() ?? .zero 50 | let maxX = xArray.max() ?? .zero 51 | let maxY = yArray.max() ?? .zero 52 | 53 | return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) 54 | } 55 | 56 | /// Creates a point in the location of an anchor on the bounds. 57 | /// - Parameter anchor: Bounds anchor where the point is located. 58 | /// - Returns: The point where the bounds anchor is located. 59 | func anchorPoint(_ anchor: RectAnchor) -> CGPoint { 60 | bounds.point(anchor) 61 | } 62 | 63 | /// Center point of the bounds rectangle containing all points in the array. 64 | var center: CGPoint { 65 | anchorPoint(.center) 66 | } 67 | 68 | /// An array of angles occuring at each point assuming points are connected in a closed shape. 69 | var angles: [Angle] { 70 | guard self.count >= 3 else { 71 | return [] 72 | } 73 | 74 | return self 75 | .enumerated() 76 | .map { 77 | let previousPoint = $0.offset == 0 ? self.last! : self[$0.offset - 1] 78 | let nextPoint = $0.offset == self.count - 1 ? self.first! : self[$0.offset + 1] 79 | return Angle.threePoint(nextPoint, $0.element, previousPoint) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/ShapeUp/Vector2/Vector2Representable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Vector2Representable.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-03. 6 | // 7 | 8 | import simd 9 | import SwiftUI 10 | 11 | /// A type that has x and y values and can therefore be represented by a Vector2. 12 | /// 13 | /// Used for ``Vector2`` and CGPoint. 14 | /// Required for ``Vector2Transformable`` and ``Vector2Algebraic`` 15 | public protocol Vector2Representable { 16 | /// A Vector2 representation. 17 | var vector: Vector2 { get } 18 | } 19 | 20 | public extension Vector2Representable { 21 | /// A CGPoint representation 22 | /// 23 | /// An easy way to access a CGPoint representation without needing to know the object type. 24 | var point: CGPoint { 25 | self as? CGPoint ?? CGPoint(x: vector.dx, y: vector.dy) 26 | } 27 | 28 | /// Returns a corner at the same position with the applied style 29 | /// - Parameter style: Corner style to use. Default is .point. 30 | /// - Returns: Corner with the provided style and the same position as the point. 31 | func corner(_ style: CornerStyle? = nil) -> Corner { 32 | Corner(style ?? .point, point: point) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/ShapeUp/_Experimental/RelativeCornerShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RelativeCornerShape.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2023-09-06. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A CornerShape that stores corners based on relative positions. This would be useful for creating shapes with an array of animated corner positions. 11 | internal struct RelativeCornerShape: CornerShape { 12 | public let closed: Bool 13 | public var insetAmount: CGFloat = 0 14 | 15 | public var animatableData: CGFloat { 16 | get { insetAmount } 17 | set { insetAmount = newValue } 18 | } 19 | 20 | let relativeFrame: CGRect = .one 21 | public var relativeCorners: [Corner] 22 | 23 | /// Creates a custom insettable shape out of corners. 24 | /// - Parameters: 25 | /// - closed: A boolean determining if the shape should be closed. Default is true. 26 | /// - corners: Corners used to draw a single closed shape. 27 | public init(closed: Bool = true, _ corners: (CGRect) -> [Corner]) { 28 | self.closed = closed 29 | self.relativeCorners = corners(relativeFrame) 30 | } 31 | 32 | public func corners(in rect: CGRect) -> [Corner] { 33 | relativeCorners.repositioned(from: relativeFrame, to: rect) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/ShapeUp/_Extension-Internal/Angle+extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Angle+AngleRepresentable.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-02-08. 6 | // 7 | 8 | import SwiftUI 9 | 10 | fileprivate struct _Angle: AngleRepresentable { 11 | let radians: Double 12 | } 13 | 14 | /// Extension is internal so that Swift Package users have the option of adding protocol conformance themselves. 15 | internal extension Angle { 16 | fileprivate var _angle: _Angle { 17 | _Angle(radians: radians) 18 | } 19 | 20 | var type: AngleType { 21 | _angle.type 22 | } 23 | 24 | var positive: Angle { 25 | _angle.positive 26 | } 27 | 28 | var complementary: Angle { 29 | _angle.complementary 30 | } 31 | 32 | var supplementary: Angle { 33 | _angle.supplementary 34 | } 35 | 36 | var explementary: Angle { 37 | _angle.explementary 38 | } 39 | 40 | var minPositiveCoterminal: Angle { 41 | _angle.minPositiveCoterminal 42 | } 43 | 44 | func minRotation(from angle: Angle) -> Angle { 45 | _angle.minRotation(from: angle) 46 | } 47 | 48 | func maxRotation(from angle: Angle) -> Angle { 49 | _angle.maxRotation(from: angle) 50 | } 51 | 52 | var reflexCoterminal: Angle { 53 | _angle.reflexCoterminal 54 | } 55 | 56 | var nonReflexCoterminal: Angle { 57 | _angle.nonReflexCoterminal 58 | } 59 | 60 | var halved: Angle { 61 | _angle.halved 62 | } 63 | 64 | static func threePoint(_ initialPoint: some Vector2Representable, _ anchor: some Vector2Representable, _ terminalPoint: some Vector2Representable) -> Angle { 65 | _Angle.threePoint(initialPoint, anchor, terminalPoint) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/ShapeUp/_Extension-Internal/CGRect+extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRect+extensions.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2023-09-07. 6 | // 7 | 8 | import Foundation 9 | 10 | extension CGRect { 11 | /// A unit rectangle with origin at zero and size of 1 12 | static let one = CGRect(x: 0, y: 0, width: 1, height: 1) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ShapeUp/_Extension-Internal/CGSize+extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize+extensions.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2023-09-06. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension CGSize { 11 | /// A size with width and height of 1 12 | static let one = CGSize(width: 1, height: 1) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ShapeUp/_Extension-Public/CGPoint+publicExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPoint+publicExtensions.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2020-09-22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension CGPoint: Vector2Transformable { 11 | public var vector: Vector2 { 12 | Vector2(dx: x, dy: y) 13 | } 14 | 15 | /// Creates a point based on the supplied vector. 16 | /// - Parameter vector: Vector placed at zero and used to determine point location. 17 | public init(vector: Vector2) { 18 | self = vector.point 19 | } 20 | 21 | public func repositioned(to point: some Vector2Representable) -> Self { 22 | /// This function is required for Vector2Transformable conformance. Other types (like Corner) have to pass on their other properties but CGPoint only has point information. 23 | point.point 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Sources/ShapeUp/_Extension-Public/CGRect+publicExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRect+PublicExtensions.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-01-23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension CGRect { 11 | /// Creates an array of points from the 4 corners of the rectangle starting with the top left and going clockwise. 12 | var points: [CGPoint] { 13 | RectAnchor.vertexAnchors.points(in: self) 14 | } 15 | 16 | /// Creates a point in the location of an anchor. 17 | /// - Parameter anchor: Anchor where the point is located 18 | /// - Returns: A point where the anchor is located. 19 | func point(_ anchor: RectAnchor) -> CGPoint { 20 | anchor.point(in: self) 21 | } 22 | 23 | /// Creates an array of points in the locations of the supplied anchors. 24 | /// - Parameter anchors: Anchors defining point locations in order. 25 | /// - Returns: An array of points in the location and order of the supplied anchors. 26 | func points(_ anchors: [RectAnchor]) -> [CGPoint] { 27 | anchors.points(in: self) 28 | } 29 | 30 | /// Creates an array of points in the locations of the supplied anchors. 31 | /// - Parameter anchors: Anchors defining point locations in order. 32 | /// - Returns: An array of points in the location and order of the supplied anchors. 33 | func points(_ anchors: RectAnchor...) -> [CGPoint] { 34 | points(anchors) 35 | } 36 | 37 | /// Returns a point at the relative location inside this CGRect. 38 | /// 39 | /// Relative x values are multiplied by the width and positioned that distance from minX. 40 | /// Relative y values are multiplied by the height and positioned that distance from minY. 41 | /// - Parameter relativeLocation: A tuple with relative x and y coordinates respectively. 42 | /// - Returns: A point at the relative location inside this CGRect. 43 | func point(relativeLocation: (CGFloat, CGFloat)) -> CGPoint { 44 | CGPoint(x: minX + (relativeLocation.0 * width), y: minY + (relativeLocation.1 * height)) 45 | } 46 | 47 | /// Returns an array of points at the relative locations inside this CGRect. 48 | /// 49 | /// Relative x values are multiplied by the width and positioned that distance from minX. 50 | /// Relative y values are multiplied by the height and positioned that distance from minY. 51 | /// - Parameter relativeLocations: An array of tuples with relative x and y coordinates respectively. 52 | /// - Returns: A an array of points at the relative locations inside this CGRect. 53 | func points(relativeLocations: [(CGFloat, CGFloat)]) -> [CGPoint] { 54 | relativeLocations.map { point(relativeLocation: $0) } 55 | } 56 | 57 | /// Returns an array of points at the relative locations inside this CGRect. 58 | /// 59 | /// Relative x values are multiplied by the width and positioned that distance from minX. 60 | /// Relative y values are multiplied by the height and positioned that distance from minY. 61 | /// - Parameter relativeLocations: An array of tuples with relative x and y coordinates respectively. 62 | /// - Returns: A an array of points at the relative locations inside this CGRect. 63 | func points(relativeLocations: (CGFloat, CGFloat)...) -> [CGPoint] { 64 | points(relativeLocations: relativeLocations) 65 | } 66 | 67 | /// Creates an array of corners from the rectangle. 68 | /// - Parameter style: Corner style used for all corners. 69 | /// - Returns: An array of 4 corners, with the provided style, starting with the top left and going clockwise. 70 | func corners(_ style: CornerStyle = .point) -> [Corner] { 71 | points.corners(style) 72 | } 73 | 74 | /// Creates an array of corners from the rectangle. 75 | /// - Parameter styles: Array of corner styles starting with the top left and going clockwise. Nil values will use "point" 76 | /// - Returns: An array of 4 corners, with the provided styles, starting with the top left and going clockwise. 77 | func corners(_ styles: [CornerStyle?]) -> [Corner] { 78 | points.corners(styles) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/ShapeUp/_Extension-Public/InsettableShape+publicExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsettableShape+publicExtensions.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-16. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension InsettableShape { 11 | /// Returns a view with embossed edges that matches the parent shape. 12 | /// 13 | /// Using a light angle between 180 and 360 degrees will likely look debossed. 14 | /// - Parameters: 15 | /// - size: Size of the embossed effect. 16 | /// - angle: Light angle. Default is 45 degrees (down and to the right). 17 | /// - opacity: Opacity of the embossed effect. 18 | /// - backgroundColor: Color of the base shape. Default is clear. 19 | /// - Returns: A view with an embossed effect. 20 | func embossEdges(size: CGFloat, angle: Angle? = nil, opacity: Double? = nil, backgroundColor: Color = .clear) -> some View { 21 | let opacity = opacity ?? 1.0 22 | let angle = angle ?? .degrees(45) 23 | let offsetX = size * 1.5 * CGFloat(cos(angle.radians)) 24 | let offsetY = size * 1.5 * CGFloat(sin(angle.radians)) 25 | 26 | var inset: CGFloat { 27 | -max(1, size * 1.5) 28 | } 29 | 30 | var lineWidth: CGFloat { 31 | max(1, size) 32 | } 33 | 34 | return self 35 | .fill(backgroundColor) 36 | .overlay( 37 | ZStack { 38 | self 39 | .inset(by: inset) 40 | .stroke(style: .init(lineWidth: lineWidth)) 41 | .shadow(color: Color.black.opacity(opacity), radius: size, x: -offsetX, y: -offsetY) 42 | 43 | self 44 | .inset(by: inset) 45 | .stroke(style: .init(lineWidth: lineWidth)) 46 | .shadow(color: Color.white.opacity(opacity), radius: size, x: offsetX, y: offsetY) 47 | } 48 | .accessibilityIgnoresInvertColors() 49 | .clipShape(self) 50 | .blendMode(.overlay) 51 | ) 52 | } 53 | 54 | /// Returns a view with debossed edges that matches the parent shape. 55 | /// 56 | /// Deboss is simply an emboss effect with the light turned 180 degrees. Using a light angle between 180 and 360 degrees will likely look embossed. 57 | /// - Parameters: 58 | /// - size: Size of the debossed effect. 59 | /// - angle: Light angle. Default is 45 degrees (down and to the right). 60 | /// - opacity: Opacity of the debossed effect. 61 | /// - backgroundColor: Color of the base shape. Default is clear. 62 | /// - Returns: A view with a debossed effect. 63 | func debossEdges(size: CGFloat, angle: Angle? = nil, opacity: Double? = nil, backgroundColor: Color = .clear) -> some View { 64 | embossEdges(size: size, angle: (angle ?? .degrees(45)) + .degrees(180), opacity: opacity, backgroundColor: backgroundColor) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/ShapeUp/_Extension-Public/Path+publicExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Path+publicExtensions.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-01-25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension Path { 11 | /// Adds the shape described by an array of corners to a path. 12 | /// - Parameters: 13 | /// - corners: Array of corners that define the shape to add. 14 | /// - previousPoint: Previous point in the path used to determine the look of the first corner. Default is the last corner point. 15 | /// - nextPoint: Next point in the path used to determine the look of the last corner. Default is the first corner point. 16 | /// - moveToStart: A boolean value determining if the first point should be moved to. If this value is false a line will be added from wherever the path currrently is to the first corner. 17 | mutating func addOpenCornerShape(_ corners: [Corner], previousPoint: CGPoint? = nil, nextPoint: CGPoint? = nil, moveToStart: Bool = true) { 18 | corners 19 | .dimensions(previousPoint: previousPoint, nextPoint: nextPoint) 20 | .addOpenCornerShape(to: &self, moveToStart: moveToStart) 21 | } 22 | 23 | /// Adds a closed shape descrived by an array of corners to a path. 24 | /// 25 | /// Moves to the start of the shape and then draws to the end 26 | /// - Parameters: 27 | /// - corners: Array of corners that define the shape to add. 28 | mutating func addClosedCornerShape(_ corners: [Corner]) { 29 | corners.addCornerShape(to: &self) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ShapeUp/_Extension-Public/Rectangle+publicExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rectangle+publicExtensions.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2022-03-09. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension Rectangle { 11 | /// Creates a CornerRectangle applying the provided style to specified corners. 12 | /// - Parameters: 13 | /// - style: Style to apply to specified shape corners. 14 | /// - shapeCorners: Shape corners on which to apply the specified style. Missing values will keep current style. 15 | /// - Returns: A copy of this shape changing the style of specified corners to the provided style. 16 | func applyingStyle(_ style: CornerStyle, shapeCorners: Set = Set(CornerRectangle.ShapeCorner.allCases)) -> CornerRectangle { 17 | CornerRectangle() 18 | .applyingStyle(style, shapeCorners: shapeCorners) 19 | } 20 | 21 | /// Creates a CornerRectangle applying the styles of specified corners. 22 | /// - Parameters: 23 | /// - styles: Styles to apply to each specified shape corner. Nil or missing values will keep current style. 24 | /// - Returns: A copy of this shape changing the styles of specified corners. 25 | func applyingStyles(_ styles: [CornerRectangle.ShapeCorner: CornerStyle?]) -> CornerRectangle { 26 | CornerRectangle() 27 | .applyingStyles(styles) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/ShapeUp/_Extension-Public/Shape+publicExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Shape+publicExtensions.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-01-11. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension Shape { 11 | /// Scales a shape to fit a specified aspect ratio inside a specified frame. 12 | /// - Parameters: 13 | /// - frame: Frame to fit. 14 | /// - aspectRatio: Aspect ratio of the shape. 15 | /// - Returns: A shape scaled to fit a specified aspect ratio inside a specified frame. 16 | func scaleToFit(_ frame: CGSize, aspectRatio: CGFloat) -> some Shape { 17 | let frameRatio = frame.width / frame.height 18 | 19 | return self 20 | .scale(x: aspectRatio > frameRatio ? 1 : frameRatio * aspectRatio, y: aspectRatio > frameRatio ? frameRatio / aspectRatio : 1, anchor: .center) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ShapeUp/_Extension-Public/View+publicExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+publicExtensions.swift 3 | // ShapeUp 4 | // 5 | // Created by Ryan Lintott on 2021-09-16. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | /// Returns the same view with an overlayed embossed effect using edges of a supplied shape. 12 | /// 13 | /// This works best when the view itself has the same clip shape applied. 14 | /// 15 | /// Using a light angle between 180 and 360 degrees will likely look debossed. 16 | /// - Parameters: 17 | /// - shape: Shape edges will be used for embossed effect. 18 | /// - size: Size of the embossed effect. 19 | /// - angle: Light angle. Default is 45 degrees (down and to the right). 20 | /// - opacity: Opacity of the embossed effect. 21 | /// - Returns: The same view with an overlayed embossed effect using edges of a supplied shape. 22 | func emboss(using shape: S, size: CGFloat, angle: Angle? = nil, opacity: Double? = nil) -> some View where S : InsettableShape { 23 | self 24 | .overlay( 25 | shape 26 | .embossEdges(size: size, angle: angle, opacity: opacity) 27 | .allowsHitTesting(false) 28 | ) 29 | } 30 | 31 | /// Returns the same view with an overlayed debossed effect using edges of a supplied shape. 32 | /// 33 | /// This works best when the view itself has the same clip shape applied. 34 | /// 35 | /// Deboss is simply an emboss effect with the light turned 180 degrees. Using a light angle between 180 and 360 degrees will likely look embossed. 36 | /// - Parameters: 37 | /// - shape: Shape edges will be used for debossed effect. 38 | /// - size: Size of the debossed effect. 39 | /// - angle: Light angle. Default is 45 degrees (down and to the right). 40 | /// - opacity: Opacity of the debossed effect. 41 | /// - Returns: The same view with an overlayed debossed effect using edges of a supplied shape. 42 | func deboss(using shape: S, size: CGFloat, angle: Angle? = nil, opacity: Double? = nil) -> some View where S : InsettableShape { 43 | self 44 | .overlay( 45 | shape 46 | .debossEdges(size: size, angle: angle, opacity: opacity) 47 | .allowsHitTesting(false) 48 | ) 49 | } 50 | 51 | /// Returns the same view with an overlayed embossed effect. 52 | /// 53 | /// Embossed effect is created by offsetting the view instead of insetting. Thin parts of a view may look strage with larger amount values. 54 | /// - Parameters: 55 | /// - baseColor: Base colour applied to the whole view. Default is clear. 56 | /// - amount: Amount of offset used in embossed effect. 57 | /// - blur: Amount of blur used in embossed effect. 58 | /// - angle: Light angle. Default is 45 degrees (down and to the right). 59 | /// - opacity: Opacity of the embossed effect. 60 | /// - Returns: The same view with an overlayed embossed effect. 61 | func emboss(baseColor: Color? = nil, amount: CGFloat, blur: CGFloat? = nil, angle: Angle? = nil, opacity: Double? = nil) -> some View { 62 | self.modifier(EmbossViewModifier(baseColor: baseColor, amount: amount, blur: blur, angle: angle, opacity: opacity)) 63 | } 64 | 65 | /// Returns the same view with an overlayed debossed effect. 66 | /// 67 | /// Debossed effect is created by offsetting the view instead of insetting. Thin parts of a view may look strage with larger amount values. 68 | /// - Parameters: 69 | /// - baseColor: Base colour applied to the whole view. Default is clear. 70 | /// - amount: Amount of offset used in debossed effect. 71 | /// - blur: Amount of blur used in debossed effect. 72 | /// - angle: Light angle. Default is 45 degrees (down and to the right). 73 | /// - opacity: Opacity of the debossed effect. 74 | /// - Returns: The same view with an overlayed debossed effect. 75 | func deboss(baseColor: Color? = nil, amount: CGFloat, blur: CGFloat? = nil, angle: Angle? = nil, opacity: Double? = nil) -> some View { 76 | self.modifier(EmbossViewModifier(baseColor: baseColor, amount: amount, blur: blur, angle: angle, opacity: opacity, deboss: true)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/ShapeUpTests/AngleTypeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AngleTypeTests.swift 3 | // 4 | // 5 | // Created by Ryan Lintott on 2022-02-12. 6 | // 7 | 8 | @testable import ShapeUp 9 | import SwiftUI 10 | import XCTest 11 | 12 | class AngleTypeTests: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testAngleTypeIsComparableAndInOrder() throws { 23 | // given 24 | let angleTypes = AngleType.allCases 25 | 26 | // when 27 | let sorted = angleTypes.shuffled().sorted() 28 | 29 | // then 30 | XCTAssertEqual(angleTypes, sorted) 31 | } 32 | 33 | static let angleTypeTestValues: [(Angle, AngleType)] = [ 34 | (0.0, AngleType.zero), 35 | (45, .acute), 36 | (-45, .acute), 37 | (90, .rightAngle), 38 | (-90, .rightAngle), 39 | (100, .obtuse), 40 | (-100, .obtuse), 41 | (180, .straight), 42 | (-180, .straight), 43 | (350, .reflex), 44 | (-350, .reflex), 45 | (360, .fullRotation), 46 | (-360, .fullRotation), 47 | (370, .over360), 48 | (-370, .over360) 49 | ].map { (.degrees($0.0), $0.1) } 50 | 51 | func testTypeOfAngle() throws { 52 | // given 53 | let testValues = Self.angleTypeTestValues 54 | 55 | testValues.forEach { (angle, resultType) in 56 | // when 57 | let type = AngleType.type(of: angle) 58 | 59 | // then 60 | XCTAssertEqual(type, resultType) 61 | } 62 | } 63 | 64 | func testInitFromDegrees() throws { 65 | // given 66 | let testValues = Self.angleTypeTestValues 67 | 68 | testValues.forEach { (angle, resultType) in 69 | // when 70 | let type = AngleType(degrees: angle.degrees) 71 | 72 | // then 73 | XCTAssertEqual(type, resultType) 74 | } 75 | } 76 | 77 | func testInitFromRadians() throws { 78 | // given 79 | let testValues = Self.angleTypeTestValues 80 | 81 | testValues.forEach { (angle, resultType) in 82 | // when 83 | let type = AngleType(radians: angle.radians) 84 | 85 | // then 86 | XCTAssertEqual(type, resultType) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/ShapeUpTests/GeoMathTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeoMathTests.swift 3 | // 4 | // 5 | // Created by Ryan Lintott on 2022-02-28. 6 | // 7 | 8 | @testable import ShapeUp 9 | import SwiftUI 10 | import XCTest 11 | 12 | class GeoMathTests: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testLineIntersectsCircleTwice() throws { 23 | // Given 24 | let point1 = CGPoint(x: 2, y: 1) 25 | let point2 = CGPoint(x: 3, y: 2) 26 | let center = CGPoint(x: 4, y: 0) 27 | let radius = 3.0 28 | 29 | // When 30 | let intersectionPoints = GeoMath.intersectionPoints(line: (point1: point1, point2: point2), circle: (center: center, radius: radius)) 31 | 32 | // Then 33 | XCTAssertTrue(intersectionPoints.count == 2) 34 | XCTAssertTrue(intersectionPoints.contains(where: { $0 == CGPoint(x: 1, y: 0)})) 35 | XCTAssertTrue(intersectionPoints.contains(where: { $0 == CGPoint(x: 4, y: 3)})) 36 | } 37 | 38 | func testHorizontalLineIntersectsCircleOnce() throws { 39 | // Given 40 | let point1 = CGPoint(x: 6, y: 3) 41 | let point2 = CGPoint(x: 2, y: 3) 42 | let center = CGPoint(x: 2, y: 1) 43 | let radius = 2.0 44 | 45 | // When 46 | let intersectionPoints = GeoMath.intersectionPoints(line: (point1: point1, point2: point2), circle: (center: center, radius: radius)) 47 | 48 | // Then 49 | XCTAssertEqual(intersectionPoints, [CGPoint(x: 2, y: 3)]) 50 | } 51 | 52 | func testVerticalLineIntersectsCircleOnce() throws { 53 | // Given 54 | let point1 = CGPoint(x: 3, y: 6) 55 | let point2 = CGPoint(x: 3, y: 1) 56 | let center = CGPoint(x: 1, y: 3) 57 | let radius = 2.0 58 | 59 | // When 60 | let intersectionPoints = GeoMath.intersectionPoints(line: (point1: point1, point2: point2), circle: (center: center, radius: radius)) 61 | 62 | // Then 63 | XCTAssertEqual(intersectionPoints, [CGPoint(x: 3, y: 3)]) 64 | } 65 | 66 | func testNoInstersections() throws { 67 | // Given 68 | let point1 = CGPoint(x: 1, y: 1) 69 | let point2 = CGPoint(x: 2, y: 3) 70 | let center = CGPoint(x: 4, y: 1) 71 | let radius = 2.5 72 | 73 | // When 74 | let intersectionPoints = GeoMath.intersectionPoints(line: (point1: point1, point2: point2), circle: (center: center, radius: radius)) 75 | 76 | // Then 77 | XCTAssertEqual(intersectionPoints, []) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/ShapeUpTests/RectAnchorArrayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectAnchorArrayTests.swift 3 | // 4 | // 5 | // Created by Ryan Lintott on 2022-02-03. 6 | // 7 | 8 | @testable import ShapeUp 9 | import SwiftUI 10 | import XCTest 11 | 12 | class RectAnchorArrayTests: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testRectAnchorArrayPointsMatchRectAnchorPoints() throws { 23 | // given 24 | let rect = CGRect(x: -10, y: -20, width: 20, height: 40) 25 | let rectAnchors = RectAnchor.allCases 26 | 27 | // when 28 | let pointArray = rectAnchors.points(in: rect) 29 | 30 | // then 31 | // Check that both arrays have the same count 32 | XCTAssertEqual(pointArray.count, rectAnchors.count) 33 | 34 | if pointArray.count == rectAnchors.count { 35 | for i in 0..