admin管理员组文章数量:1289900
I've written a code to draw a "scribbling" effect with a rectangular shape.
Ofc shape of scribbling must not be changed on view refresh.
At least it works — and result looks pretty good too!
I wonder if it's possible to write a code that can generate "scribbling" with:
- Any shape
- Any angle of lines ?
As example I want to draw shapes:
- Circle
- Capsule
- Rounded Rectangle
- Diamond (Rhomboid)
- ect
I believe there must be a better way than writing separate code for each shape.
Here is my code to draw a rectangular shape:
import SwiftUI
struct FillScribbleRect: View {
var brushSize: CGFloat = 3
var color: Color = .green
var type: ScribbleType = .vertical
var outer = false
var body: some View {
ApproximateRect(distance: brushSize * (outer ? 0.6 : 0.5), type: type, outer: outer )
.stroke(style: StrokeStyle(lineWidth: brushSize, lineCap: .square, lineJoin: .bevel))
.foregroundColor(color)
}
}
fileprivate struct ApproximateRect: Shape {
let distance: Double
let type: ScribbleType
let outer: Bool
func path(in rect: CGRect) -> Path {
switch type {
case .vertical:
return vertical(in: rect)
case .horizontal:
return horizontal(in: rect)
}
}
}
extension ApproximateRect {
func distCalc(in rect: CGRect) -> ([UInt], [UInt]) {
var distToBorder: [UInt] = []
var distBetweenLines: [UInt] = []
var rnd = SeededRandom(seed: Int(distance) )
var count: Int
switch type {
case .vertical:
count = Int(rect.width/distance)
case .horizontal:
count = Int(rect.height/distance)
}
for _ in 1...count {
distToBorder.append( rnd.next(upperBound: UInt(8) ) )
distBetweenLines.append( rnd.next(upperBound: UInt(distance) ) )
}
return (distToBorder, distBetweenLines)
}
func vertical(in rect: CGRect) -> Path {
let (distToBorder, distBetweenLines) = distCalc(in: rect)
var path = Path()
//top line
path.move(to: CGPoint(x: rect.minX, y: rect.minY) )
var flag = true
for i in distToBorder.indices {
if flag {
let t = (outer ? 1 : -1 ) * Double(distToBorder[i])
path.addQuadCurve(
to: CGPoint(x: rect.minX + distance * Double(i+2) - Double(distBetweenLines[i]), y: rect.height + t ),
control: CGPoint(x: distance * Double(i), y: rect.height/2)
)
} else {
let t = (outer ? -1 : 1 ) * Double(distToBorder[i])
path.addQuadCurve(
to: CGPoint(x: rect.minX + distance * Double(i+2) - Double(distBetweenLines[i]), y: rect.minY + t ),
control: CGPoint(x: distance * Double(i), y: rect.height/2 )
)
}
flag.toggle()
}
return path
}
func horizontal(in rect: CGRect) -> Path {
let (distToBorder, distBetweenLines) = distCalc(in: rect)
var path = Path()
//top line
path.move(to: CGPoint(x: rect.minX, y: rect.minY) )
var flag = true
for i in distToBorder.indices {
if flag {
let t = (outer ? 1 : -1 ) * Double(distToBorder[i])
path.addQuadCurve(
to: CGPoint(x: rect.width + t, y: rect.minY + distance * Double(i+2) - Double(distBetweenLines[i]) ),
control: CGPoint(x: rect.width/2, y: distance * Double(i))
)
} else {
let t = (outer ? -1 : 1 ) * Double(distToBorder[i])
path.addQuadCurve(
to: CGPoint(x: rect.minX + t, y: rect.minY + distance * Double(i+2) - Double(distBetweenLines[i]) ),
control: CGPoint(x: rect.width/2, y: distance * Double(i) )
)
}
flag.toggle()
}
return path
}
}
fileprivate struct SeededRandom: RandomNumberGenerator {
init(seed: Int) { srand48(seed) }
func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) }
}
enum ScribbleType {
case vertical
case horizontal
}
/////////////////////
/// Preview
////////////////////
struct FillScribbleRect_Previews: PreviewProvider {
static var previews: some View {
let size: CGFloat = 5
return VStack (spacing: 30) {
FillScribbleRect(brushSize: size, type: .horizontal)
.frame(width: 50, height: 50)
FillScribbleRect(brushSize: 6, type: .vertical)
.frame(width: 200, height: 50)
FillScribbleRect(brushSize: size, type: .horizontal)
.frame(width: 50, height: 200)
}
.padding(150)
.background(Color.white)
}
}
I've written a code to draw a "scribbling" effect with a rectangular shape.
Ofc shape of scribbling must not be changed on view refresh.
At least it works — and result looks pretty good too!
I wonder if it's possible to write a code that can generate "scribbling" with:
- Any shape
- Any angle of lines ?
As example I want to draw shapes:
- Circle
- Capsule
- Rounded Rectangle
- Diamond (Rhomboid)
- ect
I believe there must be a better way than writing separate code for each shape.
Here is my code to draw a rectangular shape:
import SwiftUI
struct FillScribbleRect: View {
var brushSize: CGFloat = 3
var color: Color = .green
var type: ScribbleType = .vertical
var outer = false
var body: some View {
ApproximateRect(distance: brushSize * (outer ? 0.6 : 0.5), type: type, outer: outer )
.stroke(style: StrokeStyle(lineWidth: brushSize, lineCap: .square, lineJoin: .bevel))
.foregroundColor(color)
}
}
fileprivate struct ApproximateRect: Shape {
let distance: Double
let type: ScribbleType
let outer: Bool
func path(in rect: CGRect) -> Path {
switch type {
case .vertical:
return vertical(in: rect)
case .horizontal:
return horizontal(in: rect)
}
}
}
extension ApproximateRect {
func distCalc(in rect: CGRect) -> ([UInt], [UInt]) {
var distToBorder: [UInt] = []
var distBetweenLines: [UInt] = []
var rnd = SeededRandom(seed: Int(distance) )
var count: Int
switch type {
case .vertical:
count = Int(rect.width/distance)
case .horizontal:
count = Int(rect.height/distance)
}
for _ in 1...count {
distToBorder.append( rnd.next(upperBound: UInt(8) ) )
distBetweenLines.append( rnd.next(upperBound: UInt(distance) ) )
}
return (distToBorder, distBetweenLines)
}
func vertical(in rect: CGRect) -> Path {
let (distToBorder, distBetweenLines) = distCalc(in: rect)
var path = Path()
//top line
path.move(to: CGPoint(x: rect.minX, y: rect.minY) )
var flag = true
for i in distToBorder.indices {
if flag {
let t = (outer ? 1 : -1 ) * Double(distToBorder[i])
path.addQuadCurve(
to: CGPoint(x: rect.minX + distance * Double(i+2) - Double(distBetweenLines[i]), y: rect.height + t ),
control: CGPoint(x: distance * Double(i), y: rect.height/2)
)
} else {
let t = (outer ? -1 : 1 ) * Double(distToBorder[i])
path.addQuadCurve(
to: CGPoint(x: rect.minX + distance * Double(i+2) - Double(distBetweenLines[i]), y: rect.minY + t ),
control: CGPoint(x: distance * Double(i), y: rect.height/2 )
)
}
flag.toggle()
}
return path
}
func horizontal(in rect: CGRect) -> Path {
let (distToBorder, distBetweenLines) = distCalc(in: rect)
var path = Path()
//top line
path.move(to: CGPoint(x: rect.minX, y: rect.minY) )
var flag = true
for i in distToBorder.indices {
if flag {
let t = (outer ? 1 : -1 ) * Double(distToBorder[i])
path.addQuadCurve(
to: CGPoint(x: rect.width + t, y: rect.minY + distance * Double(i+2) - Double(distBetweenLines[i]) ),
control: CGPoint(x: rect.width/2, y: distance * Double(i))
)
} else {
let t = (outer ? -1 : 1 ) * Double(distToBorder[i])
path.addQuadCurve(
to: CGPoint(x: rect.minX + t, y: rect.minY + distance * Double(i+2) - Double(distBetweenLines[i]) ),
control: CGPoint(x: rect.width/2, y: distance * Double(i) )
)
}
flag.toggle()
}
return path
}
}
fileprivate struct SeededRandom: RandomNumberGenerator {
init(seed: Int) { srand48(seed) }
func next() -> UInt64 { return UInt64(drand48() * Double(UInt64.max)) }
}
enum ScribbleType {
case vertical
case horizontal
}
/////////////////////
/// Preview
////////////////////
struct FillScribbleRect_Previews: PreviewProvider {
static var previews: some View {
let size: CGFloat = 5
return VStack (spacing: 30) {
FillScribbleRect(brushSize: size, type: .horizontal)
.frame(width: 50, height: 50)
FillScribbleRect(brushSize: 6, type: .vertical)
.frame(width: 200, height: 50)
FillScribbleRect(brushSize: size, type: .horizontal)
.frame(width: 50, height: 200)
}
.padding(150)
.background(Color.white)
}
}
Share
Improve this question
edited Feb 20 at 1:26
Andrew
asked Feb 20 at 1:17
AndrewAndrew
11.4k9 gold badges84 silver badges117 bronze badges
0
1 Answer
Reset to default 2 +100Path
provides the function contains(_:eoFill:)
, so this can be used to scan across the painting area and test whether a point is inside or outside a shape. This works quite well for shapes that have a simple form. Shapes with bits "sticking out" (such as a star shape) will be more complicated to fill.
- One way to approach this kind of painting exercise would be to use a
Canvas
. This way, the brush size can be passed in as a parameter and used by theCanvas
to perform the stroke. However, aCanvas
gets clipped to its frame size, so this means insetting the shape to allow for the random scribble effect. - An easier approach is to create the scribbled form as a
Shape
. This way, the path can overflow the frame, which makes it simpler to fill the shape as supplied. Padding can be added afterwards, if you don't want it overflowing the frame by so much. - When implemented as a
Path
, the stroke needs to be performed afterwards, so care must be taken to use the same brush size as was passed as parameter. Alternatively, theShape
can be wrapped in aView
, so that the stroke is performed by theView
and the brush size is sure to be the same. This is in fact how you were already doing it.
Here is an example of how a scribbled path can be implemented using the point-inspection technique. It uses SeededRandom
from your example, so that the random effects are always the same:
struct ScribbledForm<S: Shape>: Shape {
let shape: S
let brushSize: CGFloat
var maxDivergence: CGFloat = 10
func path(in rect: CGRect) -> Path {
let shapePath = shape.path(in: rect)
let rnd = SeededRandom(seed: Int(brushSize))
return Path { path in
var x = 0.0
var y = 0.0
var xStep = 1.0
var lastPoint: CGPoint?
while y <= rect.size.height {
while x >= 0 && x <= rect.size.width {
if shapePath.contains(CGPoint(x: x, y: y)) {
let dx = randomOffset(randomVal: rnd.next(), magnitude: maxDivergence)
let dy = randomOffset(randomVal: rnd.next(), magnitude: brushSize / 2)
let point = CGPoint(x: x + dx, y: y + dy)
if let lastPoint {
let midX = lastPoint.x + ((point.x - lastPoint.x) / 2)
let midY = lastPoint.y + ((point.y - lastPoint.y) / 2)
path.addQuadCurve(
to: point,
control: CGPoint(x: midX, y: midY - (brushSize * 0.5))
)
} else {
path.move(to: point)
}
lastPoint = point
break
} else {
x += xStep
}
}
if xStep < 0 {
x = 0
xStep = 1
} else {
x = rect.size.width
xStep = -1
}
y += brushSize / 2
}
}
}
private func randomOffset(randomVal: UInt64, magnitude: Double) -> Double {
((Double(randomVal) / Double(UInt64.max)) * magnitude) - (magnitude / 2)
}
}
struct ScribbledShape<S: Shape>: View {
let shape: S
var brushSize: CGFloat = 3
var body: some View {
ScribbledForm(shape: shape, brushSize: brushSize)
.stroke(style: .init(lineWidth: brushSize, lineCap: .square, lineJoin: .bevel))
}
}
This implementation always works horizontally. If you want to fill using a vertical scribble, just rotate the result.
Example use:
VStack(spacing: 20) {
ScribbledShape(shape: Rectangle(), brushSize: 5)
.frame(width: 100, height: 200)
.foregroundStyle(.green)
.border(.red)
ScribbledShape(shape: Circle(), brushSize: 5)
.foregroundStyle(.green)
.frame(width: 200, height: 200)
.rotationEffect(.degrees(-30))
.border(.red)
Color.clear
.frame(width: 300, height: 200)
.overlay {
// DiamondShape: see https://stackoverflow/q/78496625/20386264
ScribbledShape(shape: DiamondShape(), brushSize: 5)
.frame(width: 200, height: 300)
.foregroundStyle(.green)
.rotationEffect(.degrees(-90))
}
.border(.red)
}
EDIT The scribble can be animated by applying .trim
to the path and animating the trim size. Important: the trim must be applied before the stroke!
Example adaption:
struct AnimatedScribbledShape<S: Shape>: View {
let shape: S
var brushSize: CGFloat = 10
var animationDuration = 5.0
var pauseDuration = 2.0
var withRepeat = false
@State private var trimTo = CGFloat.zero
var body: some View {
ScribbledForm(shape: shape, brushSize: brushSize)
.trim(from: 0, to: trimTo) // must come before stroke!
.stroke(style: .init(lineWidth: brushSize, lineCap: .square, lineJoin: .bevel))
.animation(.easeInOut(duration: trimTo == 0 ? 0 : animationDuration), value: trimTo)
.task(id: trimTo) {
if trimTo == 0 {
trimTo = 1
} else if withRepeat {
try? await Task.sleep(for: .seconds(animationDuration + pauseDuration))
trimTo = 0
}
}
}
}
// ScribbledShape(shape: DiamondShape(), brushSize: 5)
AnimatedScribbledShape(shape: DiamondShape(), withRepeat: true)
本文标签: What’s the best way to create a quotscribblingquot effect with any shape in SwiftUIStack Overflow
版权声明:本文标题:What’s the best way to create a "scribbling" effect with any shape in SwiftUI? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1741463015a2380149.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论