wake-ios/wake/SharedUI/Graphics/Parallelogram.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