Compare commits
2 Commits
ff31361867
...
e74040a444
| Author | SHA1 | Date | |
|---|---|---|---|
| e74040a444 | |||
| 62dbd2594c |
99
wake/SharedUI/Graphics/Circle.swift
Normal file
99
wake/SharedUI/Graphics/Circle.swift
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// 一个可配置的圆形视图封装,便于在项目中复用。
|
||||||
|
/// 使用示例:
|
||||||
|
/// CircleView(diameter: 24, color: .orange, stroke: .white, lineWidth: 2)
|
||||||
|
public struct CircleView: View {
|
||||||
|
public var diameter: CGFloat
|
||||||
|
public var color: Color
|
||||||
|
public var stroke: Color? = nil
|
||||||
|
public var lineWidth: CGFloat = 1
|
||||||
|
|
||||||
|
public init(
|
||||||
|
diameter: CGFloat,
|
||||||
|
color: Color = .black,
|
||||||
|
stroke: Color? = nil,
|
||||||
|
lineWidth: CGFloat = 1
|
||||||
|
) {
|
||||||
|
self.diameter = diameter
|
||||||
|
self.color = color
|
||||||
|
self.stroke = stroke
|
||||||
|
self.lineWidth = lineWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Group {
|
||||||
|
if let stroke = stroke, lineWidth > 0 {
|
||||||
|
Circle()
|
||||||
|
.fill(color)
|
||||||
|
.overlay(
|
||||||
|
Circle().stroke(stroke, lineWidth: lineWidth)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Circle().fill(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: diameter, height: diameter)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 水平渲染多个等尺寸圆形的小组件。
|
||||||
|
public struct CircleRow: View {
|
||||||
|
public var count: Int
|
||||||
|
public var diameter: CGFloat
|
||||||
|
public var color: Color
|
||||||
|
public var spacing: CGFloat
|
||||||
|
|
||||||
|
public init(
|
||||||
|
count: Int,
|
||||||
|
diameter: CGFloat,
|
||||||
|
color: Color = .black,
|
||||||
|
spacing: CGFloat = 8
|
||||||
|
) {
|
||||||
|
self.count = count
|
||||||
|
self.diameter = diameter
|
||||||
|
self.color = color
|
||||||
|
self.spacing = spacing
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
HStack(spacing: spacing) {
|
||||||
|
ForEach(0..<(max(0, count)), id: \.self) { _ in
|
||||||
|
CircleView(diameter: diameter, color: color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
struct CircleView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
CircleView(diameter: 40, color: .black, stroke: .white, lineWidth: 2)
|
||||||
|
.padding()
|
||||||
|
.previewDisplayName("Single Circle")
|
||||||
|
|
||||||
|
ZStack {
|
||||||
|
Color(.black)
|
||||||
|
HStack {
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.fill(.white)
|
||||||
|
.shadow(color: Color.black.opacity(0.06), radius: 6, x: 0, y: 2)
|
||||||
|
.overlay(
|
||||||
|
HStack {
|
||||||
|
CircleRow(count: 6, diameter: 10, color: .black, spacing: 6)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
)
|
||||||
|
.frame(height: 56)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.previewDisplayName("Circle Row")
|
||||||
|
}
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
174
wake/SharedUI/Graphics/Parallelogram.swift
Normal file
174
wake/SharedUI/Graphics/Parallelogram.swift
Normal file
@ -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<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
|
||||||
|
|
||||||
|
|
||||||
202
wake/SharedUI/Graphics/Triangle.swift
Normal file
202
wake/SharedUI/Graphics/Triangle.swift
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// 三角形方向
|
||||||
|
public enum TriangleDirection: Equatable {
|
||||||
|
case up
|
||||||
|
case down
|
||||||
|
case left
|
||||||
|
case right
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 一个可插入(inset)的三角形 Shape,支持四个方向。
|
||||||
|
struct TriangleShape: InsettableShape {
|
||||||
|
var direction: TriangleDirection = .up
|
||||||
|
var insetAmount: CGFloat = 0
|
||||||
|
|
||||||
|
var animatableData: CGFloat {
|
||||||
|
get { insetAmount }
|
||||||
|
set { insetAmount = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
func inset(by amount: CGFloat) -> TriangleShape {
|
||||||
|
var copy = self
|
||||||
|
copy.insetAmount += amount
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
|
||||||
|
func path(in rect: CGRect) -> Path {
|
||||||
|
let r = rect.insetBy(dx: insetAmount, dy: insetAmount)
|
||||||
|
|
||||||
|
// 顶点布局:根据方向决定三点位置
|
||||||
|
let p1: CGPoint
|
||||||
|
let p2: CGPoint
|
||||||
|
let p3: CGPoint
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case .up:
|
||||||
|
p1 = CGPoint(x: r.midX, y: r.minY)
|
||||||
|
p2 = CGPoint(x: r.maxX, y: r.maxY)
|
||||||
|
p3 = CGPoint(x: r.minX, y: r.maxY)
|
||||||
|
|
||||||
|
case .down:
|
||||||
|
p1 = CGPoint(x: r.midX, y: r.maxY)
|
||||||
|
p2 = CGPoint(x: r.minX, y: r.minY)
|
||||||
|
p3 = CGPoint(x: r.maxX, y: r.minY)
|
||||||
|
|
||||||
|
case .left:
|
||||||
|
p1 = CGPoint(x: r.minX, y: r.midY)
|
||||||
|
p2 = CGPoint(x: r.maxX, y: r.minY)
|
||||||
|
p3 = CGPoint(x: r.maxX, y: r.maxY)
|
||||||
|
|
||||||
|
case .right:
|
||||||
|
p1 = CGPoint(x: r.maxX, y: r.midY)
|
||||||
|
p2 = CGPoint(x: r.minX, y: r.maxY)
|
||||||
|
p3 = CGPoint(x: r.minX, y: r.minY)
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = Path()
|
||||||
|
path.move(to: p1)
|
||||||
|
path.addLine(to: p2)
|
||||||
|
path.addLine(to: p3)
|
||||||
|
path.closeSubpath()
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 可配置的三角形视图封装,便于在项目中复用。
|
||||||
|
/// 示例:
|
||||||
|
/// TriangleView(width: 20, height: 18, direction: .up, color: .black, stroke: .white, lineWidth: 2)
|
||||||
|
public struct TriangleView: View {
|
||||||
|
public var width: CGFloat
|
||||||
|
public var height: CGFloat
|
||||||
|
public var direction: TriangleDirection
|
||||||
|
public var color: Color
|
||||||
|
public var stroke: Color? = nil
|
||||||
|
public var lineWidth: CGFloat = 1
|
||||||
|
public var rotation: Angle = .degrees(0)
|
||||||
|
|
||||||
|
public init(
|
||||||
|
width: CGFloat,
|
||||||
|
height: CGFloat,
|
||||||
|
direction: TriangleDirection = .up,
|
||||||
|
color: Color = .black,
|
||||||
|
stroke: Color? = nil,
|
||||||
|
lineWidth: CGFloat = 1,
|
||||||
|
rotation: Angle = .degrees(0)
|
||||||
|
) {
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.direction = direction
|
||||||
|
self.color = color
|
||||||
|
self.stroke = stroke
|
||||||
|
self.lineWidth = lineWidth
|
||||||
|
self.rotation = rotation
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
let shape = TriangleShape(direction: direction)
|
||||||
|
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)
|
||||||
|
.rotationEffect(rotation)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 水平渲染多个等尺寸三角形的小组件。
|
||||||
|
public struct TriangleRow: View {
|
||||||
|
public var count: Int
|
||||||
|
public var itemSize: CGSize
|
||||||
|
public var direction: TriangleDirection
|
||||||
|
public var color: Color
|
||||||
|
public var spacing: CGFloat
|
||||||
|
public var rotation: Angle
|
||||||
|
|
||||||
|
public init(
|
||||||
|
count: Int,
|
||||||
|
itemSize: CGSize,
|
||||||
|
direction: TriangleDirection = .up,
|
||||||
|
color: Color = .black,
|
||||||
|
spacing: CGFloat = 8,
|
||||||
|
rotation: Angle = .degrees(0)
|
||||||
|
) {
|
||||||
|
self.count = count
|
||||||
|
self.itemSize = itemSize
|
||||||
|
self.direction = direction
|
||||||
|
self.color = color
|
||||||
|
self.spacing = spacing
|
||||||
|
self.rotation = rotation
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
HStack(spacing: spacing) {
|
||||||
|
ForEach(0..<(max(0, count)), id: \.self) { _ in
|
||||||
|
TriangleView(
|
||||||
|
width: itemSize.width,
|
||||||
|
height: itemSize.height,
|
||||||
|
direction: direction,
|
||||||
|
color: color,
|
||||||
|
rotation: rotation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.accessibilityElement(children: .ignore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
struct Triangle_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
// 单个三角形
|
||||||
|
TriangleView(width: 24, height: 20, direction: .up, color: .black, stroke: .white, lineWidth: 2)
|
||||||
|
.padding()
|
||||||
|
.previewDisplayName("Single Up")
|
||||||
|
|
||||||
|
TriangleView(width: 24, height: 20, direction: .right, color: .orange)
|
||||||
|
.padding()
|
||||||
|
.background(Color(.systemGroupedBackground))
|
||||||
|
.previewDisplayName("Single Right")
|
||||||
|
|
||||||
|
TriangleView(width: 24, height: 20, direction: .up, color: .blue, rotation: .degrees(132))
|
||||||
|
.padding()
|
||||||
|
.previewDisplayName("Rotated 132")
|
||||||
|
|
||||||
|
// 行展示
|
||||||
|
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 {
|
||||||
|
TriangleRow(
|
||||||
|
count: 6,
|
||||||
|
itemSize: CGSize(width: 16, height: 14),
|
||||||
|
direction: .up,
|
||||||
|
color: .black,
|
||||||
|
spacing: 6,
|
||||||
|
rotation: .degrees(180)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 18)
|
||||||
|
)
|
||||||
|
.frame(height: 64)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.previewDisplayName("Row")
|
||||||
|
}
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Loading…
x
Reference in New Issue
Block a user