diff --git a/wake/SharedUI/Graphics/Parallelogram.swift b/wake/SharedUI/Graphics/Parallelogram.swift new file mode 100644 index 0000000..6a9421e --- /dev/null +++ b/wake/SharedUI/Graphics/Parallelogram.swift @@ -0,0 +1,174 @@ +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 { + 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 + +