Extend Path to match SwiftUI (#172)
* Initial Path element and subpath support * Fix ambiguous .init * Add more path commands * Fix line length * Fix macOS build * Add missing file to xcodeproj
This commit is contained in:
parent
b9ade79df1
commit
f891edd928
|
@ -34,6 +34,18 @@ public struct CGPoint: Equatable {
|
|||
public static var zero: Self {
|
||||
.init(x: 0, y: 0)
|
||||
}
|
||||
|
||||
func rotate(_ angle: Angle, around origin: Self) -> Self {
|
||||
let cosAngle = cos(angle.radians)
|
||||
let sinAngle = sin(angle.radians)
|
||||
return .init(x: cosAngle * (x - origin.x) - sinAngle * (y - origin.y) + origin.x,
|
||||
y: sinAngle * (x - origin.x) + cosAngle * (y - origin.y) + origin.y)
|
||||
}
|
||||
|
||||
func offset(by offset: Self) -> Self {
|
||||
.init(x: x + offset.x,
|
||||
y: y + offset.y)
|
||||
}
|
||||
}
|
||||
|
||||
public struct CGSize: Equatable {
|
||||
|
@ -54,13 +66,13 @@ public struct CGRect: Equatable {
|
|||
public let origin: CGPoint
|
||||
public let size: CGSize
|
||||
|
||||
public init(_ origin: CGPoint, _ size: CGSize) {
|
||||
public init(origin: CGPoint, size: CGSize) {
|
||||
self.origin = origin
|
||||
self.size = size
|
||||
}
|
||||
|
||||
public static var zero: Self {
|
||||
.init(.zero, .zero)
|
||||
.init(origin: .zero, size: .zero)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,7 +125,8 @@ public struct CGAffineTransform: Equatable {
|
|||
/// Returns an affine transformation matrix constructed from a rotation value you provide.
|
||||
/// - Parameters:
|
||||
/// - angle: The angle, in radians, by which this matrix rotates the coordinate system axes.
|
||||
/// A positive value specifies clockwise rotation and a negative value specifies counterclockwise rotation.
|
||||
/// A positive value specifies clockwise rotation and anegative value specifies
|
||||
/// counterclockwise rotation.
|
||||
public init(rotationAngle angle: CGFloat) {
|
||||
self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0)
|
||||
}
|
||||
|
@ -146,7 +159,8 @@ public struct CGAffineTransform: Equatable {
|
|||
y: (b * point.x) + (d * point.y) + ty)
|
||||
}
|
||||
|
||||
/// Returns an affine transformation matrix constructed by combining two existing affine transforms.
|
||||
/// Returns an affine transformation matrix constructed by combining two existing affine
|
||||
/// transforms.
|
||||
/// - Parameters:
|
||||
/// - t2: The affine transform to concatenate to this affine transform.
|
||||
/// - Returns: A new affine transformation matrix. That is, `t’ = t1*t2`.
|
||||
|
@ -178,12 +192,15 @@ public struct CGAffineTransform: Equatable {
|
|||
|
||||
/// Returns an affine transformation matrix constructed by rotating an existing affine transform.
|
||||
/// - Parameters:
|
||||
/// - angle: The angle, in radians, by which to rotate the affine transform. A positive value specifies clockwise rotation and a negative value specifies counterclockwise rotation.
|
||||
/// - angle: The angle, in radians, by which to rotate the affine transform.
|
||||
/// A positive value specifies clockwise rotation and a negative value specifies
|
||||
/// counterclockwise rotation.
|
||||
public func rotated(by angle: CGFloat) -> Self {
|
||||
Self(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0)
|
||||
}
|
||||
|
||||
/// Returns an affine transformation matrix constructed by translating an existing affine transform.
|
||||
/// Returns an affine transformation matrix constructed by translating an existing
|
||||
/// affine transform.
|
||||
/// - Parameters:
|
||||
/// - tx: The value by which to move x values with the affine transform.
|
||||
/// - ty: The value by which to move y values with the affine transform.
|
||||
|
|
|
@ -15,14 +15,31 @@
|
|||
// Created by Carson Katri on 06/28/2020.
|
||||
//
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#endif
|
||||
|
||||
/// The outline of a 2D shape.
|
||||
public struct Path: Equatable, LosslessStringConvertible {
|
||||
public var description: String {
|
||||
"""
|
||||
\(storage)
|
||||
\(elements)
|
||||
\(transform)
|
||||
"""
|
||||
var pathString = [String]()
|
||||
for element in elements {
|
||||
switch element {
|
||||
case let .move(to: pos):
|
||||
pathString.append("\(pos.x) \(pos.y) m")
|
||||
case let .line(to: pos):
|
||||
pathString.append("\(pos.x) \(pos.y) l")
|
||||
case let .curve(to: pos, control1: c1, control2: c2):
|
||||
pathString.append("\(c1.x) \(c1.y) \(c2.x) \(c2.y) \(pos.x) \(pos.y) c")
|
||||
case let .quadCurve(to: pos, control: c):
|
||||
pathString.append("\(c.x) \(c.y) \(pos.x) \(pos.y) q")
|
||||
case .closeSubpath:
|
||||
pathString.append("h")
|
||||
}
|
||||
}
|
||||
return pathString.joined(separator: " ")
|
||||
}
|
||||
|
||||
public enum Storage: Equatable {
|
||||
|
@ -48,8 +65,8 @@ public struct Path: Equatable, LosslessStringConvertible {
|
|||
public var transform: CGAffineTransform = .identity
|
||||
|
||||
public struct _SubPath: Equatable {
|
||||
let path: Path
|
||||
let transform: CGAffineTransform
|
||||
public let path: Path
|
||||
public let transform: CGAffineTransform
|
||||
}
|
||||
|
||||
public var subpaths: [_SubPath] = []
|
||||
|
@ -78,7 +95,8 @@ public struct Path: Equatable, LosslessStringConvertible {
|
|||
cornerRadius: CGFloat,
|
||||
style: RoundedCornerStyle = .circular) {
|
||||
storage = .roundedRect(FixedRoundedRect(rect: rect,
|
||||
cornerSize: CGSize(width: cornerRadius, height: cornerRadius),
|
||||
cornerSize: CGSize(width: cornerRadius,
|
||||
height: cornerRadius),
|
||||
style: style))
|
||||
}
|
||||
|
||||
|
@ -215,21 +233,56 @@ extension Path {
|
|||
}
|
||||
|
||||
public mutating func addRect(_ rect: CGRect, transform: CGAffineTransform = .identity) {
|
||||
subpaths.append(.init(path: .init(rect), transform: transform))
|
||||
move(to: rect.origin)
|
||||
addLine(to: CGPoint(x: rect.size.width, y: 0)
|
||||
.offset(by: rect.origin))
|
||||
addLine(to: CGPoint(x: rect.size.width, y: rect.size.height)
|
||||
.offset(by: rect.origin))
|
||||
addLine(to: CGPoint(x: 0, y: rect.size.height)
|
||||
.offset(by: rect.origin))
|
||||
closeSubpath()
|
||||
}
|
||||
|
||||
public mutating func addRoundedRect(in rect: CGRect,
|
||||
cornerSize: CGSize,
|
||||
style: RoundedCornerStyle = .circular,
|
||||
transform: CGAffineTransform = .identity) {
|
||||
subpaths.append(.init(path: .init(roundedRect: rect,
|
||||
cornerSize: cornerSize,
|
||||
style: style),
|
||||
transform: transform))
|
||||
move(to: CGPoint(x: rect.size.width, y: rect.size.height / 2)
|
||||
.offset(by: rect.origin))
|
||||
addLine(to: CGPoint(x: rect.size.width, y: rect.size.height - cornerSize.height)
|
||||
.offset(by: rect.origin))
|
||||
addQuadCurve(to: CGPoint(x: rect.size.width - cornerSize.width, y: rect.size.height)
|
||||
.offset(by: rect.origin),
|
||||
control: CGPoint(x: rect.size.width, y: rect.size.height)
|
||||
.offset(by: rect.origin))
|
||||
addLine(to: CGPoint(x: cornerSize.width, y: rect.size.height)
|
||||
.offset(by: rect.origin))
|
||||
addQuadCurve(to: CGPoint(x: 0, y: rect.size.height - cornerSize.height)
|
||||
.offset(by: rect.origin),
|
||||
control: CGPoint(x: 0, y: rect.size.height)
|
||||
.offset(by: rect.origin))
|
||||
addLine(to: CGPoint(x: 0, y: cornerSize.height)
|
||||
.offset(by: rect.origin))
|
||||
addQuadCurve(to: CGPoint(x: cornerSize.width, y: 0)
|
||||
.offset(by: rect.origin),
|
||||
control: CGPoint.zero
|
||||
.offset(by: rect.origin))
|
||||
addLine(to: CGPoint(x: rect.size.width - cornerSize.width, y: 0)
|
||||
.offset(by: rect.origin))
|
||||
addQuadCurve(to: CGPoint(x: rect.size.width, y: cornerSize.height)
|
||||
.offset(by: rect.origin),
|
||||
control: CGPoint(x: rect.size.width, y: 0)
|
||||
.offset(by: rect.origin))
|
||||
closeSubpath()
|
||||
}
|
||||
|
||||
public mutating func addEllipse(in rect: CGRect, transform: CGAffineTransform = .identity) {
|
||||
subpaths.append(.init(path: .init(ellipseIn: rect), transform: transform))
|
||||
subpaths.append(.init(path: .init(ellipseIn: .init(origin: rect.origin
|
||||
.offset(by: .init(x: rect.size.width / 2,
|
||||
y: rect.size.height / 2)),
|
||||
size: .init(width: rect.size.width / 2,
|
||||
height: rect.size.height / 2))),
|
||||
transform: transform))
|
||||
}
|
||||
|
||||
public mutating func addRects(_ rects: [CGRect], transform: CGAffineTransform = .identity) {
|
||||
|
@ -245,31 +298,79 @@ extension Path {
|
|||
startAngle: Angle,
|
||||
delta: Angle,
|
||||
transform: CGAffineTransform = .identity) {
|
||||
// I don't know how to do this without sin/cos
|
||||
addArc(center: center,
|
||||
radius: radius,
|
||||
startAngle: startAngle,
|
||||
endAngle: startAngle + delta,
|
||||
clockwise: false)
|
||||
}
|
||||
|
||||
// There's a great article on bezier curves here:
|
||||
// https://pomax.github.io/bezierinfo
|
||||
// FIXME: Handle negative delta
|
||||
public mutating func addArc(center: CGPoint,
|
||||
radius: CGFloat,
|
||||
startAngle: Angle,
|
||||
endAngle: Angle,
|
||||
clockwise: Bool,
|
||||
transform: CGAffineTransform = .identity) {
|
||||
// I don't know how to do this without sin/cos
|
||||
if clockwise {
|
||||
addArc(center: center,
|
||||
radius: radius,
|
||||
startAngle: endAngle,
|
||||
endAngle: endAngle + (.radians(.pi * 2) - endAngle) + startAngle,
|
||||
clockwise: false)
|
||||
} else {
|
||||
let angle = abs(startAngle.radians - endAngle.radians)
|
||||
if angle > .pi / 2 {
|
||||
// Split the angle into 90º chunks
|
||||
let chunk1 = Angle.radians(startAngle.radians + (.pi / 2))
|
||||
addArc(center: center,
|
||||
radius: radius,
|
||||
startAngle: startAngle,
|
||||
endAngle: chunk1,
|
||||
clockwise: clockwise)
|
||||
addArc(center: center,
|
||||
radius: radius,
|
||||
startAngle: chunk1,
|
||||
endAngle: endAngle,
|
||||
clockwise: clockwise)
|
||||
} else {
|
||||
let startPoint = CGPoint(x: radius + center.x,
|
||||
y: center.y)
|
||||
let endPoint = CGPoint(x: (radius * cos(angle)) + center.x,
|
||||
y: (radius * sin(angle)) + center.y)
|
||||
let l = (4 / 3) * tan(angle / 4)
|
||||
let c1 = CGPoint(x: radius + center.x, y: (l * radius) + center.y)
|
||||
let c2 = CGPoint(x: ((cos(angle) + l * sin(angle)) * radius) + center.x,
|
||||
y: ((sin(angle) - l * cos(angle)) * radius) + center.y)
|
||||
|
||||
move(to: startPoint.rotate(startAngle, around: center))
|
||||
addCurve(to: endPoint.rotate(startAngle, around: center),
|
||||
control1: c1.rotate(startAngle, around: center),
|
||||
control2: c2.rotate(startAngle, around: center))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: How does this arc method work?
|
||||
public mutating func addArc(tangent1End p1: CGPoint,
|
||||
tangent2End p2: CGPoint,
|
||||
radius: CGFloat,
|
||||
transform: CGAffineTransform = .identity) {
|
||||
// I don't know how to do this without sin/cos
|
||||
}
|
||||
transform: CGAffineTransform = .identity) {}
|
||||
|
||||
public mutating func addPath(_ path: Path, transform: CGAffineTransform = .identity) {
|
||||
subpaths.append(.init(path: path, transform: transform))
|
||||
}
|
||||
|
||||
public var currentPoint: CGPoint? {
|
||||
nil
|
||||
switch elements.last {
|
||||
case let .move(to: point): return point
|
||||
case let .line(to: point): return point
|
||||
case let .curve(to: point, control1: _, control2: _): return point
|
||||
case let .quadCurve(to: point, control: _): return point
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func applying(_ transform: CGAffineTransform) -> Path {
|
||||
|
|
|
@ -52,9 +52,9 @@ extension Rectangle: InsettableShape {
|
|||
}
|
||||
|
||||
public func path(in rect: CGRect) -> Path {
|
||||
.init(CGRect(rect.origin,
|
||||
CGSize(width: rect.size.width - (amount / 2),
|
||||
height: rect.size.height - (amount / 2))))
|
||||
.init(CGRect(origin: rect.origin,
|
||||
size: CGSize(width: rect.size.width - (amount / 2),
|
||||
height: rect.size.height - (amount / 2))))
|
||||
}
|
||||
|
||||
public func inset(by amount: CGFloat) -> Rectangle._Inset {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
// Created by Max Desiatov on 06/28/2020.
|
||||
//
|
||||
|
||||
public struct Angle {
|
||||
public struct Angle: AdditiveArithmetic {
|
||||
public var radians: Double
|
||||
public var degrees: Double {
|
||||
get { radians * (180.0 / .pi) }
|
||||
|
@ -41,4 +41,30 @@ public struct Angle {
|
|||
public static func degrees(_ degrees: Double) -> Angle {
|
||||
Angle(degrees: degrees)
|
||||
}
|
||||
|
||||
public static let zero: Angle = .radians(0)
|
||||
|
||||
public static func + (lhs: Self, rhs: Self) -> Self {
|
||||
.radians(lhs.radians + rhs.radians)
|
||||
}
|
||||
|
||||
public static func += (lhs: inout Self, rhs: Self) {
|
||||
// swiftlint:disable:next shorthand_operator
|
||||
lhs = lhs + rhs
|
||||
}
|
||||
|
||||
public static func - (lhs: Self, rhs: Self) -> Self {
|
||||
.radians(lhs.radians - rhs.radians)
|
||||
}
|
||||
|
||||
public static func -= (lhs: inout Self, rhs: Self) {
|
||||
// swiftlint:disable:next shorthand_operator
|
||||
lhs = lhs - rhs
|
||||
}
|
||||
}
|
||||
|
||||
extension Angle: Hashable, Comparable {
|
||||
public static func < (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.radians < rhs.radians
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import TokamakCore
|
|||
public typealias Path = TokamakCore.Path
|
||||
|
||||
extension Path: ViewDeferredToRenderer {
|
||||
// TODO: Support transformations, subpaths, and read through elements to create a path
|
||||
// TODO: Support transformations
|
||||
func svgFrom(storage: Storage,
|
||||
strokeStyle: StrokeStyle = .init(lineWidth: 0,
|
||||
lineCap: .butt,
|
||||
|
@ -30,35 +30,86 @@ extension Path: ViewDeferredToRenderer {
|
|||
dashPhase: 0)) -> AnyView {
|
||||
let stroke = [
|
||||
"stroke-width": "\(strokeStyle.lineWidth)",
|
||||
"stroke": "black", // TODO: Use the environment variable "foregroundColor"
|
||||
]
|
||||
let uniqueKeys = { (first: String, _: String) in first }
|
||||
switch storage {
|
||||
case .empty:
|
||||
return AnyView(EmptyView())
|
||||
case let .rect(rect):
|
||||
return AnyView(AnyView(HTML("rect", ["width": "\(max(0, rect.size.width))",
|
||||
"height": "\(max(0, rect.size.height))"]
|
||||
.merging(stroke, uniquingKeysWith: uniqueKeys))))
|
||||
case .ellipse:
|
||||
return AnyView(HTML("ellipse", ["cx": "50%", "cy": "50%", "rx": "50%", "ry": "50%"]
|
||||
return AnyView(AnyView(HTML("rect", [
|
||||
"width": "\(max(0, rect.size.width))",
|
||||
"height": "\(max(0, rect.size.height))",
|
||||
"x": "\(rect.origin.x - (rect.size.width / 2))",
|
||||
"y": "\(rect.origin.y - (rect.size.height / 2))",
|
||||
].merging(stroke, uniquingKeysWith: uniqueKeys))))
|
||||
case let .ellipse(rect):
|
||||
return AnyView(HTML("ellipse", ["cx": "\(rect.origin.x)",
|
||||
"cy": "\(rect.origin.y)",
|
||||
"rx": "\(rect.size.width)",
|
||||
"ry": "\(rect.size.height)"]
|
||||
.merging(stroke, uniquingKeysWith: uniqueKeys)))
|
||||
case let .roundedRect(roundedRect):
|
||||
return AnyView(HTML("rect", [
|
||||
"width": "\(roundedRect.rect.size.width)",
|
||||
"height": "\(roundedRect.rect.size.height)",
|
||||
"rx": "\(roundedRect.cornerSize.width)",
|
||||
"ry": "\(roundedRect.style == .continuous ? roundedRect.cornerSize.width : roundedRect.cornerSize.height)",
|
||||
]
|
||||
.merging(stroke, uniquingKeysWith: uniqueKeys)))
|
||||
"ry": """
|
||||
\(roundedRect.style == .continuous ?
|
||||
roundedRect.cornerSize.width :
|
||||
roundedRect.cornerSize.height)
|
||||
""",
|
||||
"x": "\(roundedRect.rect.origin.x)",
|
||||
"y": "\(roundedRect.rect.origin.y)",
|
||||
].merging(stroke, uniquingKeysWith: uniqueKeys)))
|
||||
case let .stroked(stroked):
|
||||
return stroked.path.svgFrom(storage: stroked.path.storage, strokeStyle: stroked.style)
|
||||
return AnyView(stroked.path.svgBody(strokeStyle: stroked.style))
|
||||
case let .trimmed(trimmed):
|
||||
return trimmed.path.svgFrom(storage: trimmed.path.storage, strokeStyle: strokeStyle) // TODO: Trim the path
|
||||
return trimmed.path.svgFrom(storage: trimmed.path.storage,
|
||||
strokeStyle: strokeStyle) // TODO: Trim the path
|
||||
}
|
||||
}
|
||||
|
||||
var size: CGSize {
|
||||
func svgFrom(elements: [Element],
|
||||
strokeStyle: StrokeStyle = .init(lineWidth: 0,
|
||||
lineCap: .butt,
|
||||
lineJoin: .miter,
|
||||
miterLimit: 0,
|
||||
dash: [],
|
||||
dashPhase: 0)) -> AnyView {
|
||||
var d = [String]()
|
||||
for element in elements {
|
||||
switch element {
|
||||
case let .move(to: pos):
|
||||
d.append("M\(pos.x),\(pos.y)")
|
||||
case let .line(to: pos):
|
||||
d.append("L\(pos.x),\(pos.y)")
|
||||
case let .curve(to: pos, control1: c1, control2: c2):
|
||||
d.append("C\(c1.x),\(c1.y),\(c2.x),\(c2.y),\(pos.x),\(pos.y)")
|
||||
case let .quadCurve(to: pos, control: c1):
|
||||
d.append("Q\(c1.x),\(c1.y),\(pos.x),\(pos.y)")
|
||||
case .closeSubpath:
|
||||
d.append("Z")
|
||||
}
|
||||
}
|
||||
return AnyView(HTML("path", [
|
||||
"style": "stroke-width: \(strokeStyle.lineWidth);",
|
||||
"d": d.joined(separator: "\n"),
|
||||
]))
|
||||
}
|
||||
|
||||
func svgFrom(subpaths: [_SubPath],
|
||||
strokeStyle: StrokeStyle = .init(lineWidth: 0,
|
||||
lineCap: .butt,
|
||||
lineJoin: .miter,
|
||||
miterLimit: 0,
|
||||
dash: [],
|
||||
dashPhase: 0)) -> AnyView {
|
||||
AnyView(ForEach(Array(subpaths.enumerated()), id: \.offset) { _, path in
|
||||
path.path.svgBody(strokeStyle: strokeStyle)
|
||||
})
|
||||
}
|
||||
|
||||
var storageSize: CGSize {
|
||||
switch storage {
|
||||
case .empty:
|
||||
return .zero
|
||||
|
@ -73,12 +124,51 @@ extension Path: ViewDeferredToRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
var elementsSize: CGSize {
|
||||
// Curves may clip without an explicit size
|
||||
let positions = elements.compactMap { elem -> CGPoint? in
|
||||
switch elem {
|
||||
case let .move(to: pos): return pos
|
||||
case let .line(to: pos): return pos
|
||||
case let .curve(to: pos, control1: _, control2: _): return pos
|
||||
case let .quadCurve(to: pos, control: _): return pos
|
||||
case .closeSubpath: return nil
|
||||
}
|
||||
}
|
||||
let xPos = positions.map(\.x).sorted(by: <)
|
||||
let minX = xPos.first ?? 0
|
||||
let maxX = xPos.last ?? 0
|
||||
let yPos = positions.map(\.y).sorted(by: <)
|
||||
let minY = yPos.first ?? 0
|
||||
let maxY = yPos.last ?? 0
|
||||
|
||||
return CGSize(width: abs(maxX - min(0, minX)), height: abs(maxY - min(0, minY)))
|
||||
}
|
||||
|
||||
var size: CGSize {
|
||||
.init(width: max(storageSize.width, elementsSize.width),
|
||||
height: max(storageSize.height, elementsSize.height))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func svgBody(strokeStyle: StrokeStyle = .init(lineWidth: 0,
|
||||
lineCap: .butt,
|
||||
lineJoin: .miter,
|
||||
miterLimit: 0,
|
||||
dash: [],
|
||||
dashPhase: 0)) -> some View {
|
||||
svgFrom(storage: storage, strokeStyle: strokeStyle)
|
||||
svgFrom(elements: elements, strokeStyle: strokeStyle)
|
||||
svgFrom(subpaths: subpaths, strokeStyle: strokeStyle)
|
||||
}
|
||||
|
||||
public var deferredBody: AnyView {
|
||||
AnyView(HTML("svg", ["style": """
|
||||
width: \(max(0, size.width));
|
||||
height: \(max(0, size.height));
|
||||
overflow: visible;
|
||||
"""]) {
|
||||
svgFrom(storage: storage)
|
||||
svgBody()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
import TokamakCore
|
||||
|
||||
public typealias Shape = TokamakCore.Shape
|
||||
|
||||
// Border modifier
|
||||
extension _OverlayModifier: DOMViewModifier
|
||||
where Overlay == _ShapeView<_StrokedShape<TokamakCore.Rectangle._Inset>, Color> {
|
||||
|
|
|
@ -17,11 +17,27 @@
|
|||
|
||||
import TokamakCore
|
||||
|
||||
protocol ShapeAttributes {
|
||||
func attributes(_ style: ShapeStyle) -> [String: String]
|
||||
}
|
||||
|
||||
extension _StrokedShape: ShapeAttributes {
|
||||
func attributes(_ style: ShapeStyle) -> [String: String] {
|
||||
if let color = style as? Color {
|
||||
return ["style": "stroke: \(color); fill: none;"]
|
||||
} else {
|
||||
return ["style": "stroke: black; fill: none;"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension _ShapeView: ViewDeferredToRenderer {
|
||||
public var deferredBody: AnyView {
|
||||
let path = shape.path(in: .zero).deferredBody
|
||||
if let fillColor = style as? Color {
|
||||
return AnyView(HTML("div", ["style": "fill: \(fillColor.description)"]) { path })
|
||||
if let shapeAttributes = shape as? ShapeAttributes {
|
||||
return AnyView(HTML("div", shapeAttributes.attributes(style)) { path })
|
||||
} else if let color = style as? Color {
|
||||
return AnyView(HTML("div", ["style": "fill: \(color);"]) { path })
|
||||
} else {
|
||||
return path
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#if canImport(SwiftUI)
|
||||
import SwiftUI
|
||||
#else
|
||||
import TokamakDOM
|
||||
#endif
|
||||
|
||||
struct Star: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
Path { path in
|
||||
path.move(to: .init(x: 40, y: 0))
|
||||
path.addLine(to: .init(x: 20, y: 76))
|
||||
path.addLine(to: .init(x: 80, y: 30.4))
|
||||
path.addLine(to: .init(x: 0, y: 30.4))
|
||||
path.addLine(to: .init(x: 64, y: 76))
|
||||
path.addLine(to: .init(x: 40, y: 0))
|
||||
print(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PathDemo: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Star()
|
||||
.fill(Color(red: 1, green: 0.75, blue: 0.1, opacity: 1))
|
||||
Path { path in
|
||||
path.addRect(.init(origin: .zero, size: .init(width: 20, height: 20)))
|
||||
path.addEllipse(in: .init(origin: .init(x: 25, y: 0),
|
||||
size: .init(width: 20, height: 20)))
|
||||
path.addRoundedRect(in: .init(origin: .init(x: 50, y: 0),
|
||||
size: .init(width: 20, height: 20)),
|
||||
cornerSize: .init(width: 4, height: 4))
|
||||
path.addArc(center: .init(x: 85, y: 10),
|
||||
radius: 10,
|
||||
startAngle: .degrees(90),
|
||||
endAngle: .degrees(180),
|
||||
clockwise: true)
|
||||
}
|
||||
.stroke(Color(red: 1, green: 0.75, blue: 0.1, opacity: 1), lineWidth: 4)
|
||||
.padding(.vertical)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
// Copyright 2020 Tokamak contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import TokamakDOM
|
||||
|
||||
struct SVGCircle: View {
|
||||
var body: some View {
|
||||
HTML("svg", ["width": "100%", "height": "100%"]) {
|
||||
HTML("circle", [
|
||||
"cx": "50%", "cy": "50%", "r": "40%",
|
||||
"stroke": "green", "stroke-width": "4", "fill": "yellow",
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
|
@ -43,10 +43,7 @@ struct TokamakDemoView: View {
|
|||
}
|
||||
ForEachDemo()
|
||||
TextDemo()
|
||||
#if canImport(TokamakDOM)
|
||||
SVGCircle()
|
||||
.frame(width: 25, height: 25)
|
||||
#endif
|
||||
PathDemo()
|
||||
TextFieldDemo()
|
||||
SpacerDemo()
|
||||
EnvironmentDemo()
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */; };
|
||||
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */; };
|
||||
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */; };
|
||||
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; };
|
||||
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; };
|
||||
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
|
||||
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
|
||||
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
|
||||
|
@ -47,6 +49,7 @@
|
|||
85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentDemo.swift; sourceTree = "<group>"; };
|
||||
85ED18BD24AD46340085DFA0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
85ED18BF24AD464B0085DFA0 /* iOS Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOS Info.plist"; sourceTree = "<group>"; };
|
||||
B51F214F24B920B400CF2583 /* PathDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathDemo.swift; sourceTree = "<group>"; };
|
||||
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; };
|
||||
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
@ -104,6 +107,7 @@
|
|||
85ED189E24AD425E0085DFA0 /* Counter.swift */,
|
||||
85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */,
|
||||
85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */,
|
||||
B51F214F24B920B400CF2583 /* PathDemo.swift */,
|
||||
);
|
||||
name = TokamakDemo;
|
||||
path = ../Sources/TokamakDemo;
|
||||
|
@ -210,6 +214,7 @@
|
|||
files = (
|
||||
85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */,
|
||||
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */,
|
||||
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */,
|
||||
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
|
||||
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
|
||||
D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */,
|
||||
|
@ -227,6 +232,7 @@
|
|||
files = (
|
||||
85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
|
||||
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */,
|
||||
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */,
|
||||
85ED18A424AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
|
||||
85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
|
||||
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue