{"id":19093059,"url":"https://github.com/ryanlintott/shapeup","last_synced_at":"2025-04-05T20:05:20.364Z","repository":{"id":62930957,"uuid":"395778651","full_name":"ryanlintott/ShapeUp","owner":"ryanlintott","description":"Make shapes and cut corners in SwiftUI","archived":false,"fork":false,"pushed_at":"2024-11-12T15:55:42.000Z","size":1198,"stargazers_count":142,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-29T19:02:07.587Z","etag":null,"topics":["emboss","geometry","shapes","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ryanlintott.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-08-13T19:56:19.000Z","updated_at":"2025-03-23T20:04:57.000Z","dependencies_parsed_at":"2024-06-18T23:44:02.728Z","dependency_job_id":"de5a7d5b-4063-4a2e-9729-59985b5c01a4","html_url":"https://github.com/ryanlintott/ShapeUp","commit_stats":{"total_commits":40,"total_committers":2,"mean_commits":20.0,"dds":0.25,"last_synced_commit":"681046516f6d2669a73ba13694474c70c292dad2"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlintott%2FShapeUp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlintott%2FShapeUp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlintott%2FShapeUp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlintott%2FShapeUp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ryanlintott","download_url":"https://codeload.github.com/ryanlintott/ShapeUp/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247393568,"owners_count":20931812,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["emboss","geometry","shapes","swiftui"],"created_at":"2024-11-09T03:23:11.629Z","updated_at":"2025-04-05T20:05:20.341Z","avatar_url":"https://github.com/ryanlintott.png","language":"Swift","funding_links":["https://ko-fi.com/X7X04PU6T"],"categories":[],"sub_categories":[],"readme":"\u003cimg width=\"456\" alt=\"ShapeUp Logo\" src=\"https://user-images.githubusercontent.com/2143656/157464613-38fd35cc-7802-4cb7-914b-4da480a0411e.png\"\u003e\n\n[![Swift Compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fryanlintott%2FShapeUp%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/ryanlintott/ShapeUp)\n[![Platform Compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fryanlintott%2FShapeUp%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/ryanlintott/ShapeUp)\n![License - MIT](https://img.shields.io/github/license/ryanlintott/ShapeUp)\n![Version](https://img.shields.io/github/v/tag/ryanlintott/ShapeUp?label=version)\n![GitHub last commit](https://img.shields.io/github/last-commit/ryanlintott/ShapeUp)\n[![Mastodon](https://img.shields.io/badge/mastodon-@ryanlintott-5c4ee4.svg?style=flat)](http://mastodon.social/@ryanlintott)\n[![Twitter](https://img.shields.io/badge/twitter-@ryanlintott-blue.svg?style=flat)](http://twitter.com/ryanlintott)\n\n# Overview\nA Swift Package that makes SwiftUI shapes easier to make by redefining them as an array of styled corners. (The logo above was created in 100 lines + SwiftUI Text)\n\nFeatures:\n- [`Corner`](#corner), a `CGPoint` with `style`.\n- [`CornerStyle`](#cornerstyle) options: `.point`, `.rounded`, `.straight`, `.cutout`, and `.concave`\n- Basic shapes like [`CornerRectangle`](#basic-shapes), [`CornerTriangle`](#basic-shapes), and [`CornerPentagon`](#basic-shapes) with stylable corners.\n- [`CornerShape`](#cornershape), a protocol for making your own open or closed shapes out of an array of Corners.\n- [`CornerCustom`](#cornercustom), for building corner shapes inline without making a new type.\n- Add a [`Notch`](#notch) of any `NotchStyle` inbetween two corners.\n- [`.addOpenCornerShape()`](#add-cornershape) or [`.addClosedCornerShape()`](#add-cornershape) for adding a few corners to a SwiftUI `Path`\n- [`Vector2`](#vector2), a new type similar to `CGPoint` but used to do vector math.\n- [`Vector2Representable`](#vector2representable) protocol that adds a `.vector` property needed to conform to other Vector2-related protocols.\n- [`Vector2Algebraic`](#vector2algebraic) protocol used to add vector algebra capabilities to `Vector2`\n- [`Vector2Transformable`](#vector2transformable) protocol with methods for transforming arrays of points.\n- [`RectAnchor`](#rectanchor), an enum for all major anchor points on a rectangle. Used for transform functions.\n- [`RelatableValue`](#relatablevalue), an enum used to store `.relative` or `.absolute` values.\n- [`SketchyLine`](#sketchyline), an animatable line `Shape` that aligns to frame edges and can extend beyond the frame.\n- [`.emboss()` or `.deboss()`](#emboss-or-deboss) any SwiftUI `Shape` or `View`.\n- [`AnimatablePack`](#animatablepack) as an alternative to `AnimatablePair` that takes any number of properties.\n\n\n# Demo App\nThe `Example` folder has an app that demonstrates the features of this package.\n\n# Installation and Usage\nThis package is compatible with iOS 14+, macOS 11+, watchOS 7+, tvOS 14+, and visionOS.\n\n1. In Xcode go to `File -\u003e Add Packages`\n2. Paste in the repo's url: `https://github.com/ryanlintott/ShapeUp` and select by version.\n3. Import the package using `import ShapeUp`\n\n# Is this Production-Ready?\nReally it's up to you. I currently use this package in my own [Old English Wordhord app](https://oldenglishwordhord.com/app).\n\nAdditionally, if you find a bug or want a new feature add an issue and I will get back to you about it.\n\n# Support This Project\nShapeUp is open source and free but if you like using it, please consider supporting my work.\n\n[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/X7X04PU6T)\n\nOr you can buy a t-shirt with the ShapeUp logo\n\n\u003ca href=\"https://cottonbureau.com/p/JBYGB7/shirt/shapeup#/20149802\"\u003e\u003cimg width=\"256\" alt=\"ShapeUp T-Shirt\" src=\"https://cottonbureau.com/mockup?vid=20149802\u0026hash=6UJM\u0026w=512\"\u003e\u003c/a\u003e\n- - -\n# Features\n## Corner\nA point with a specified `CornerStyle` used to draw paths and create shapes.\n\n```swift\nCorner(.rounded(radius: 5), x: 0, y: 10)\n```\n\nCorners store no information about their orientation or where the previous and next points are located. When they're put into an array their order is assumed to be their drawing order. This means you can generate a `Path` from this array. By default this path is assumed to be closed with the last point connecting back to the first.\n\n```swift\n[\n    Corner(.rounded(radius: 10), x: 0, y: 0),\n    Corner(.cutout(radius: 5), x: 10, y: 0),\n    Corner(.straight(radius: 5), x: 5, y: 10)\n].path()\n```\n\n## CornerStyle\nAn enum storing style information for a `Corner`. In all cases, the radius is a [`RelatableValue`](#relatablevalue) that can either be an absolue value or relative to the length of the shortest line from that corner.\n\n\u003cimg width=\"50\" alt=\"Pink triangle with a point corner\" src=\"https://user-images.githubusercontent.com/2143656/157761591-2341d07c-5f0e-4434-ad19-22873f7357d9.svg\"\u003e `.point`\nA simple point corner with no properties.\n\n\u003cimg width=\"50\" alt=\"Pink triangle with a rounded corner\" src=\"https://user-images.githubusercontent.com/2143656/157762280-630dddf9-4cd4-4779-84e6-43f2f834e6b0.svg\"\u003e `.rounded(radius: RelatableValue)`\nA rounded corner with a radius.\n\n\u003cimg width=\"50\" alt=\"Pink triangle with a concave cut corner\" src=\"https://user-images.githubusercontent.com/2143656/157762293-ac45ea61-6427-4def-b560-060944ac2c1a.svg\"\u003e `.concave(radius: RelatableValue, radiusOffset: CGFloat)`\nA concave corner where the radius determines the start and end points of the cut and the radius offeset is the difference between the concave radius and the radius. The radiusOffset is mainly used when insetting a concave corner and is often left with the default value of zero.\n\n\u003cimg width=\"50\" alt=\"Pink triangle with a straight cut corner\" src=\"https://user-images.githubusercontent.com/2143656/157762299-437bcec4-2fc8-475b-bbbb-ed810d86ca7f.svg\"\u003e `.straight(radius: RelatableValue, cornerStyles: [CornerStyle] = [])`\nA straight chamfer corner where the radius determines the start and end points of the cut. Additional cornerstyles can be used on the two resulting corners of the chamfer. (You can continue nesting recursively.)\n\n\u003cimg width=\"50\" alt=\"Pink triangle with a cutout corner\" src=\"https://user-images.githubusercontent.com/2143656/157762313-c4015f99-7c53-4571-93b5-a8b476c9f5da.svg\"\u003e `.cutout(radius: RelatableValue, cornerStyles: [CornerStyle] = [])`\nA cutout corner where the radius determines the start and end points of the cut. Additional cornerstyles can be used on the three resulting corners of the cut. (Again, you can continue nesting recursively.)\n\n## Basic Shapes\n`CornerRectangle`, `CornerTriangle`, and `CornerPentagon` are pre-built shapes where you can customize the style of any corner.\n\nExamples:\n```swift\nCornerRectangle([\n    .topLeft: .straight(radius: 60),\n    .topRight: .cutout(radius: .relative(0.2)),\n    .bottomRight: .rounded(radius: .relative(0.8)),\n    .bottomLeft: .concave(radius: .relative(0.2))\n])\n.fill()\n\nCornerTriangle(\n    topPoint: .relative(0.6),\n    styles: [\n        .top: .straight(radius: 10),\n        .bottomRight: .rounded(radius: .relative(0.3)),\n        .bottomLeft: .concave(radius: .relative(0.2))\n    ]\n)\n.stroke()\n    \nCornerPentagon(\n    pointHeight: .relative(0.3),\n    topTaper: .relative(0.1),\n    bottomTaper: .relative(0.3),\n    styles: [\n        .topRight: .concave(radius: 30),\n        .bottomLeft: .straight(radius: .relative(0.3))\n    ]\n)\n.fill()\n```\n\n## CornerShape\nA protocol for creating shapes built from an array of `Corner`s. The path and inset functions needed to conform to SwiftUI InsettableShape are already implemented.\n\n### How to build a CornerShape\n- Set insetAbount zero (this property is used if the shape inset).\n- Set the closed property to define if your shape should be closed or left open.\n- Write a function that returns an array of corners.\n```swift\npublic struct MyShape: CornerShape {\n    public var insetAmount: CGFloat = .zero\n    public let closed = true\n   \n    public func corners(in rect: CGRect) -\u003e [Corner] {\n        [\n            Corner(x: rect.midX, y: rect.minY),\n            Corner(.rounded(radius: 5), x: rect.maxX, y: rect.maxY),\n            Corner(.rounded(radius: 5), x: rect.minX, y: rect.maxY)\n        ]\n    }\n}\n```\n\n### Using A CornerShape\nA `CornerShape` can be used in SwiftUI Views the same way as `RoundedRectangle` or similar.\n```swift\nMyShape()\n    .fill()\n```\n\nThe corners can also be accessed directly for use in a more complex shape\n```swift\npublic func corners(in rect: CGRect) -\u003e [Corner] {\n    MyShape()\n        .corners(in: rect)\n        .inset(by: 10)\n        .addingNotch(Notch(.rectangle, depth: 5), afterCornerIndex: 0)\n}\n```\n\n## CornerCustom\nSometimes you might want to make a shape inline without defining a new struct. `CornerCustom` is a `CornerShape` that takes a closure that returns an array of `Corner`s. The closure itself needs to be `Sendable` so that it can be used to generate a path for a SwiftUI `Shape`.\n\n```swift\nCornerCustom { rect in\n    [\n        Corner(x: rect.midX, y: rect.minY),\n        Corner(.rounded(radius: 10), x: rect.maxX, y: rect.maxY),\n        Corner(.rounded(radius: 10), x: rect.minX, y: rect.maxY)\n    ]\n}\n.strokeBorder(lineWidth: 5)\n```\n\n## Notch\nSometimes you want to cut a notch in the side of a shape. This can be tricky to do when the line is at an odd angle but `Notch` makes it easy. A `Notch` has a `NotchStyle`, position, length and depth.\n\nThe following code adds a rectangular notch between the second and third corner. The `addingNotch()` function makes all the necessary calculations to add the corners representing that notch into the `Corner` array.\n\n```swift\nlet notch = Notch(.rectangle, position: .relative(0.5), length: .relative(0.2), depth: .relative(0.1))\n\nlet corners = corners.addingNotch(notch, afterCornerIndex: 1)\n```\n### NotchStyle\nTwo basic styles are `.triangle` and `.rectangle` and both allow customization of the corner styles for the 2 or 3 resulting notch corners.\n```swift\n/// Specify styles for each corner\nlet notch1 = .triangle(cornerStyles: [.rounded(radius: 10), .point, .straight(radius: 5)])\n/// Or specify one style for all\nlet notch2 = .rectangle(cornerStyle: .rounded(radius: .relative(0.2))\n```\n\nThere is also a `.custom` notch style that takes a closure that returns an array of `Corner`s based on a `CGRect` that matches the size and orientation of the notch.\n```swift\nlet notch = .custom { rect in\n    [\n        Corner(x: rect.minX, y: rect.minY),\n        Corner(x: rect.minX, y: rect.midY),\n        Corner(.rounded(radius: 15), x: rect.midX, y: rect.maxY),\n        Corner(x: rect.maxX, y: rect.midY),\n        Corner(x: rect.maxX, y: rect.minY)\n    ]\n}\n```\n\n## Add CornerShape\nShapes made completely with corners have their limitations. Only straight lines and arcs are possible. If you want to use corners to draw only a portion of your shape you can do that too with `.addOpenCornerShape()` and `.addClosedCornerShape()` functions added to `Path`\n\n## Vector2\nA vector type used as an alternative to CGPoint that conforms to all the Vector2 protocols.\n\n## Vector2Representable\nA protocol that adds the `vector: Vector2` property. `Vector2`, `CGPoint`, and `Corner` all conform to this and it's required for any other Vector2 protocols.\n\nOther properties and methods:\n```swift\npoint: CGPoint\ncorner(_ style:) -\u003e Corner\n```\n\nArray extensions:\n```swift\npoints: [CGPoint]\nvectors: [Vector2]\ncorners(_ style: CornerStyle?) -\u003e [Corner]\ncorners(_ styles: [CornerStyle?]) -\u003e [Corner]\nbounds: CGRect\ncenter: CGPoint\nanchorPoint(_ anchor: RectAnchor) -\u003e CGPoint\nangles: [Angle]\n```\n\n## Vector2Algebraic\nA protocol that adds vector math. Only applied to `Vector2` by default but can be added to any other `Vector2Representable` if need be.\n\nFunctions include: magnitude, direction, normalized, addition, subtraction, and multiplication or division with scalars.\n\n## Vector2Transformable\nA protocol that adds transformation functions (move, rotate, flip, inset) to any `Vector2Representable` or array of that type. Applied to `Vector2`, `CGPoint`, and `Corner`.\n\n## RectAnchor\nAn enum to indicate one of 9 anchor locations on a rectangle. It's primarily used to quickly get `CGPoint` values from `CGRect`\n\n```swift\n// Current method\nlet point = CGPoint(x: rect.minX, y: rect.minY)\n// ShapeUp method\nlet point = rect.point(.topLeft)\n```\n\nThis is especially helpful when getting an array of points\n\n```swift\n// Current method\nlet points = [\n    CGPoint(x: rect.minX, y: rect.midY),\n    CGPoint(x: rect.midX, y: rect.midY),\n    CGPoint(x: rect.maxX, y: rect.maxY)\n]\n// ShapeUp method\nlet points = rect.points(.left, .center, .bottomRight)\n```\n\n## RelatableValue\nA handy enum that represents either a relative or absolute value. This is used in lots of situations throughout ShapeUp to give flexibility when defining parameters.\n\nWhen setting a corner radius you might want a fixed value like 20 or you might want a value that's 20% of the maximum so that it will scale proportionally. `RelatableValue` gives you both of those options.\n```swift\nlet absolute = RelatableValue.absolute(20)\nlet relative = RelatableValue.relative(0.2)\n```\n\nLater, the value is determined by running the `value(using total:)` function. Absolute values will always be the same but any relative values will be calculated. In the case of a corner radius, the total would be the maximum radius that would fit that corner given the length of the two lines and the angle of the corner.\n```swift\nlet radius = relatableRadius.value(using: maxRadius)\n```\n\nFor ease of use, `RelatableValue` conforms to `ExpressibleByIntegerLiteral` and `ExpressibleByFloatLiteral`. This means in many cases you can omit `.absolute()` when writing absolute values.\n```swift\n// Both are the same\nlet cornerStyle = .rounded(radius: .absolute(5))\nlet cornerStyle = .rounded(radius: 5)\n```\n## SketchyLine\nA animatable line Shape with ends that can extend and a position that can offset perpendicular to its direction.\n\n\u003cimg width=\"195\" alt=\"image\" src=\"https://user-images.githubusercontent.com/2143656/157765981-3f48e2bb-50c8-46ba-b2b3-80d7491f1473.png\"\u003e\n\n```swift\nText(\"Hello World\")\n    .alignmentGuide(.bottom) { d in\n        // moves bottom alignment to text baseline\n        return d[.firstTextBaseline]\n    }\n    .background(\n        SketchyLines(lines: [\n            .leading(startExtension: -2, endExtension: 10),\n            .bottom(startExtension: 5, endExtension: 5, offset: .relative(0.05))\n        ], drawAmount: 1)\n            .stroke(Color.red)\n        , alignment: .bottom\n    )\n```\n\n## Emboss or Deboss\nExtensions for `InsettableShape` and `View` that create an embossed or debossed effect.\n\n\u003cimg width=\"205\" alt=\"image\" src=\"https://user-images.githubusercontent.com/2143656/157765787-a8bcdee3-fec3-40f8-8414-1c66ca073db6.png\"\u003e\n\n## AnimatablePack\n*\\*Xcode 16+, iOS 17+, macOS 14+, watchOS 10+, tvOS 17+*\n\nAnimate lots of properties in a `Shape` using `AnimatablePack` instead of nesting `AnimatablePair` types\n\nHere is an example of animatableData using AnimatablePair:\n ```swift\n struct MyShape: Animatable {\n     var animatableData: AnimatablePair\u003cCGFloat, AnimatablePair\u003cRelatableValue, Double\u003e\u003e {\n         get { AnimatablePair(insetAmount, AnimatablePair(cornerRadius, rotation)) }\n         set {\n             insetAmount = newValue.first\n             cornerRadius = newValue.second.first\n             rotation = newValue.second.second\n         }\n     }\n }\n ```\n You can see how it would get quite large once you start adding more than a few properties.\n Here's how to use AnimatablePack instead:\n ```swift\n struct MyShape: Animatable {\n     var animatableData: AnimatablePack\u003cCGFloat, RelatableValue, Double\u003e {\n         get { AnimatablePack(insetAmount, cornerRadius, rotation) }\n         set { (insetAmount, cornerRadius, rotation) = newValue() }\n     }\n }\n ```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryanlintott%2Fshapeup","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fryanlintott%2Fshapeup","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryanlintott%2Fshapeup/lists"}