-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1e42719
commit 774a189
Showing
4 changed files
with
154 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// | ||
// RoundedPath.swift | ||
// Three-Column-Layout | ||
// | ||
// Created by Mark Onyschuk on 10/11/23. | ||
// | ||
|
||
import SwiftUI | ||
|
||
extension Path { | ||
|
||
/// Creates a path from the union of `rects` and rounds its edges with a radius of up to `roundness` pixels. | ||
/// | ||
/// Since the path is formed from the union of `rects`, abutting rectangles are grouped into a single rectangle. | ||
/// - Parameters: | ||
/// - rects: a collection of rects to round | ||
/// - roundness: a maximum roundness in pixels | ||
/// - Returns: a `Path` | ||
public static func rects( | ||
_ rects: some Collection<CGRect>, roundness: CGFloat | ||
) -> Self? { | ||
let rects = rects.filter { !$0.isEmpty } | ||
|
||
if let first = rects.first { | ||
let union = rects.dropFirst().reduce(CGPath(rect: first, transform: nil)) { | ||
path, next in path.union(CGPath(rect: next, transform: nil)) | ||
} | ||
|
||
var polys: [[CGPoint]] = [] | ||
|
||
union.applyWithBlock { | ||
let elt = $0.pointee | ||
|
||
switch elt.type { | ||
case .moveToPoint: | ||
polys = polys.dropLast() | ||
polys.append([elt.points[0]]) | ||
case .addLineToPoint: | ||
polys[polys.count - 1].append(elt.points[0]) | ||
case .closeSubpath: | ||
polys.append([.zero]) | ||
|
||
default: | ||
break | ||
} | ||
} | ||
|
||
return Path { | ||
path in for pts in polys { | ||
var first: CGPoint? = nil | ||
for i in 1...pts.count { | ||
// fetch three points - the control point, its predecessor and successor points | ||
let cp = pts[i % pts.count] | ||
|
||
let prev = pts[i-1] | ||
let next = pts[(i + 1) % pts.count] | ||
|
||
// get normals between `cp` and both predecessor and successor points | ||
let n1 = (cp - prev).normal | ||
let n2 = (next - cp).normal | ||
|
||
// get half-lengths between `cp` and both predecessor and successor points | ||
let l1 = (cp - prev).length / 2 | ||
let l2 = (next - cp).length / 2 | ||
|
||
// calculate a rounding radius no greater than the half-lengths | ||
let r = min(roundness, l1, l2) | ||
|
||
// project start and end points along normals from `cp` | ||
let p1 = cp - (n1 * r) | ||
let p2 = cp + (n2 * r) | ||
|
||
if first == nil { | ||
first = p1 | ||
path.move(to: p1) | ||
} else { | ||
path.addLine(to: p1) | ||
} | ||
|
||
// quad-curve from start and end points, with `cp` as the control point | ||
path.addQuadCurve(to: p2, control: cp) | ||
} | ||
|
||
if first != nil { | ||
path.closeSubpath() | ||
} | ||
} | ||
} | ||
} else { | ||
return nil | ||
} | ||
} | ||
} | ||
|
51 changes: 51 additions & 0 deletions
51
Sources/Geometry/SwiftUI View Modifiers/FrameListModifier.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// | ||
// FrameListReader.swift | ||
// Geometry | ||
// | ||
// Created by Mark Onyschuk on 9/18/24. | ||
// Copyright © 2024 Dimension North Inc. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
|
||
private struct FrameList<ID>: PreferenceKey where ID: Hashable { | ||
static var defaultValue: [ID: CGRect] { | ||
[:] | ||
} | ||
static func reduce(value: inout [ID: CGRect], nextValue: () -> [ID: CGRect]) { | ||
value.merge(nextValue(), uniquingKeysWith: { old, new in new }) | ||
} | ||
} | ||
|
||
private enum FrameListModifier<ID>: ViewModifier where ID: Hashable { | ||
case reader(onChange: ([ID: CGRect])-> ()) | ||
case writer(id: ID, coordinates: CoordinateSpace) | ||
|
||
public func body(content: Content) -> some View { | ||
switch self { | ||
case let .reader(onChange): | ||
content.onPreferenceChange(FrameList<ID>.self, perform: onChange) | ||
case let .writer(id, coordinates): | ||
content.overlay(GeometryReader { | ||
geom in Color.clear.preference( | ||
key: FrameList<ID>.self, | ||
value: [id: geom.frame(in: coordinates)] | ||
) | ||
}) | ||
} | ||
} | ||
} | ||
|
||
extension View { | ||
func frames<ID>(_ value: Binding<[ID: CGRect]>) -> some View where ID: Hashable { | ||
readFrames { value.wrappedValue = $0 } | ||
} | ||
|
||
func writeFrame<ID>(id: ID, coordinates: CoordinateSpace) -> some View where ID: Hashable { | ||
self.modifier(FrameListModifier<ID>.writer(id: id, coordinates: coordinates)) | ||
} | ||
|
||
func readFrames<ID>(onChange: @escaping ([ID: CGRect])-> ()) -> some View where ID: Hashable { | ||
self.modifier(FrameListModifier<ID>.reader(onChange: onChange)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters