175 lines
5.6 KiB
Swift
175 lines
5.6 KiB
Swift
import SwiftUI
|
|
|
|
/// A skewed rounded rectangle rendered via shearing a RoundedRectangle.
|
|
/// - Parameters:
|
|
/// - shear: Horizontal shear factor. Positive values lean to the right. Typical 0.15 ~ 0.35
|
|
/// - cornerRadius: Corner radius of the base rounded rectangle before shear.
|
|
struct ParallelogramShape: InsettableShape {
|
|
var shear: CGFloat = 0.25
|
|
var cornerRadius: CGFloat = 6
|
|
var insetAmount: CGFloat = 0
|
|
|
|
var animatableData: AnimatablePair<CGFloat, CGFloat> {
|
|
get { AnimatablePair(shear, cornerRadius) }
|
|
set {
|
|
shear = newValue.first
|
|
cornerRadius = newValue.second
|
|
}
|
|
}
|
|
|
|
func inset(by amount: CGFloat) -> ParallelogramShape {
|
|
var copy = self
|
|
copy.insetAmount += amount
|
|
return copy
|
|
}
|
|
|
|
func path(in rect: CGRect) -> Path {
|
|
// To keep the final shape inside `rect`, we draw a rounded rectangle inset by half of the shear expansion
|
|
// and apply a shear transform around the vertical center.
|
|
let h = rect.height
|
|
let expandX = abs(shear) * h // total width expansion after shearing around center
|
|
let insetX = expandX / 2 + insetAmount
|
|
let insetRect = rect.insetBy(dx: insetX, dy: insetAmount)
|
|
|
|
let rr = RoundedRectangle(cornerRadius: max(0, cornerRadius - insetAmount), style: .continuous)
|
|
var path = rr.path(in: insetRect)
|
|
|
|
// Transform: translate to center, shear, translate back
|
|
let toCenter = CGAffineTransform(translationX: 0, y: -rect.midY)
|
|
let shearTransform = CGAffineTransform(a: 1, b: 0, c: shear, d: 1, tx: 0, ty: 0)
|
|
let back = CGAffineTransform(translationX: 0, y: rect.midY)
|
|
path = path.applying(toCenter).applying(shearTransform).applying(back)
|
|
return path
|
|
}
|
|
}
|
|
|
|
/// A configurable parallelogram view.
|
|
/// Example:
|
|
/// ParallelogramView(width: 36, height: 18, shear: 0.3, cornerRadius: 5, color: .black)
|
|
public struct ParallelogramView: View {
|
|
public var width: CGFloat
|
|
public var height: CGFloat
|
|
public var shear: CGFloat
|
|
public var cornerRadius: CGFloat
|
|
public var color: Color
|
|
public var stroke: Color? = nil
|
|
public var lineWidth: CGFloat = 1
|
|
|
|
public init(
|
|
width: CGFloat,
|
|
height: CGFloat,
|
|
shear: CGFloat = 0.25,
|
|
cornerRadius: CGFloat = 6,
|
|
color: Color = .black,
|
|
stroke: Color? = nil,
|
|
lineWidth: CGFloat = 1
|
|
) {
|
|
self.width = width
|
|
self.height = height
|
|
self.shear = shear
|
|
self.cornerRadius = cornerRadius
|
|
self.color = color
|
|
self.stroke = stroke
|
|
self.lineWidth = lineWidth
|
|
}
|
|
|
|
public var body: some View {
|
|
let shape = ParallelogramShape(shear: shear, cornerRadius: cornerRadius)
|
|
Group {
|
|
if let stroke = stroke, lineWidth > 0 {
|
|
shape.fill(color)
|
|
.overlay(
|
|
shape.stroke(stroke, lineWidth: lineWidth)
|
|
)
|
|
} else {
|
|
shape.fill(color)
|
|
}
|
|
}
|
|
.frame(width: width, height: height)
|
|
.accessibilityHidden(true)
|
|
}
|
|
}
|
|
|
|
/// A horizontal row that renders a given number of parallelograms.
|
|
public struct ParallelogramRow: View {
|
|
public var count: Int
|
|
public var itemSize: CGSize
|
|
public var shear: CGFloat
|
|
public var cornerRadius: CGFloat
|
|
public var color: Color
|
|
public var spacing: CGFloat
|
|
|
|
public init(
|
|
count: Int,
|
|
itemSize: CGSize,
|
|
shear: CGFloat = 0.25,
|
|
cornerRadius: CGFloat = 6,
|
|
color: Color = .black,
|
|
spacing: CGFloat = 8
|
|
) {
|
|
self.count = count
|
|
self.itemSize = itemSize
|
|
self.shear = shear
|
|
self.cornerRadius = cornerRadius
|
|
self.color = color
|
|
self.spacing = spacing
|
|
}
|
|
|
|
public var body: some View {
|
|
HStack(spacing: spacing) {
|
|
ForEach(0..<(max(0, count)), id: \.self) { _ in
|
|
ParallelogramView(
|
|
width: itemSize.width,
|
|
height: itemSize.height,
|
|
shear: shear,
|
|
cornerRadius: cornerRadius,
|
|
color: color
|
|
)
|
|
}
|
|
}
|
|
.accessibilityElement(children: .ignore)
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
struct Parallelogram_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
Group {
|
|
// Single parallelogram preview
|
|
ParallelogramView(width: 20, height: 18, shear: -0.3, cornerRadius: 2, color: .black)
|
|
.padding()
|
|
.previewDisplayName("Single")
|
|
|
|
// Row preview (similar to the screenshot)
|
|
ZStack {
|
|
Color(white: 0.97)
|
|
HStack {
|
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
|
.fill(.white)
|
|
.shadow(color: Color.black.opacity(0.06), radius: 6, x: 0, y: 2)
|
|
.overlay(
|
|
HStack {
|
|
ParallelogramRow(
|
|
count: 5,
|
|
itemSize: CGSize(width: 18, height: 20),
|
|
shear: -0.3,
|
|
cornerRadius: 2,
|
|
color: .black,
|
|
spacing: 2
|
|
)
|
|
}
|
|
.padding(.horizontal, 18)
|
|
)
|
|
.frame(height: 64)
|
|
.padding()
|
|
}
|
|
}
|
|
.previewDisplayName("Row")
|
|
}
|
|
.previewLayout(.sizeThatFits)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|