I draw a shape, I want shrink it with a specific value, I use transform scale with an anchor point but it isn't the result that I'm expected. I want space between the edges of the original shape and new shape have the same value.
here is the code:
func drawShape(){
let w = self.frame.width
let h = self.frame.height
let corner : CGFloat = 0
let center = CGPoint(x: w / 2, y: h / 2)
let disW = w / 3
let disH = h / 3
let point1 = CGPoint(x: 0, y: 2 * disH)
let point2 = center
let point3 = CGPoint(x: 2 * disW, y: h)
let point4 = CGPoint(x: 0, y: h)
let path = CGMutablePath()
path.move(to: point1)
path.addArc(tangent1End: point2, tangent2End: point3, radius: corner)
path.addArc(tangent1End: point3, tangent2End: point4, radius: corner)
path.addArc(tangent1End: point4, tangent2End: point1, radius: corner)
path.addArc(tangent1End: point1, tangent2End: point2, radius: corner)
let layer = CAShapeLayer()
layer.strokeColor = UIColor.black.cgColor
layer.fillColor = UIColor.lightGray.cgColor
layer.lineWidth = 1
layer.path = path
layer.frame = self.frame
layer.anchorPoint = CGPoint(x: 0, y: 1)
layer.frame = self.frame
layer.transform = CATransform3DMakeScale(0.9, 0.9, 1)
self.layer.addSublayer(layer)
}
Finally, I found a solution. Instead of use transform scale, I changed its corner points.
I am trying to achieve following shape using coregraphics.
I am able to create a rounded rect
func createRoundedRect() {
let path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 15.0)
// Specify the point that the path should start get drawn.
path.move(to: CGPoint(x: 0.0, y: 0.0))
// Create a line between the starting point and the bottom-left side of the view.
path.addLine(to: CGPoint(x: 0.0, y: self.frame.size.height))
// Create the bottom line (bottom-left to bottom-right).
path.addLine(to: CGPoint(x: self.frame.size.width, y: self.frame.size.height))
// Create the vertical line from the bottom-right to the top-right side.
path.addLine(to: CGPoint(x: self.frame.size.width, y: 0.0))
// Close the path. This will create the last line automatically.
path.close()
}
But I am not sure how to make a view of above shape. Any help or idea is appreciated.
You render this with just two arcs, one for the top and one for the bottom. Just use a fat lineWidth and set the strokeColor to be the same as the fillColor to achieve the desired corner radius.
For example:
#IBDesignable
class TvView: UIView {
override class var layerClass: AnyClass { CAShapeLayer.self }
var shapeLayer: CAShapeLayer { return layer as! CAShapeLayer}
#IBInspectable var curveHeight: CGFloat = 10 { didSet { setNeedsLayout() } }
#IBInspectable var cornerRadius: CGFloat = 10 { didSet { setNeedsLayout() } }
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.path = path()?.cgPath
shapeLayer.lineWidth = cornerRadius * 2
shapeLayer.lineJoin = .round
}
func path() -> UIBezierPath? {
let rect = bounds.insetBy(dx: cornerRadius, dy: cornerRadius)
guard
rect.height > 2 * curveHeight,
rect.width > 0,
curveHeight > 0
else {
return nil
}
let angle: CGFloat = 2 * (atan2(curveHeight, rect.width / 2))
let radius = rect.width / 2 / sin(angle)
let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.minY + radius), radius: radius, startAngle: .pi * 3 / 2 - angle, endAngle: .pi * 3 / 2 + angle, clockwise: true)
path.addArc(withCenter: CGPoint(x: rect.midX, y: rect.maxY - radius), radius: radius, startAngle: .pi / 2 - angle, endAngle: .pi / 2 + angle, clockwise: true)
path.close()
return path
}
}
Using the same color for stroke and fill, that yields:
Or, so you can see what’s going on, here it is with the stroke rendered in a different color:
I'm using CATransform3D and CAShapeLayer to create a layer like below
Here is my code.
let path = CGMutablePath()
let startPoint = CGPoint(x: center.x - width / 2, y: center.y - height / 2)
path.move(to: startPoint)
path.addLine(to: CGPoint(x: startPoint.x + width, y: startPoint.y))
path.addLine(to: CGPoint(x: startPoint.x + width, y: startPoint.y + height))
path.addLine(to: CGPoint(x: startPoint.x, y: startPoint.y + height))
path.closeSubpath()
let backgroundLayer = CAShapeLayer()
backgroundLayer.path = path
backgroundLayer.fillColor = UIColor.clear.cgColor
backgroundLayer.strokeColor = boarderColor.cgColor
var transform = CATransform3DIdentity
transform.m34 = -1 / 500
let angle = 45.toRadians
backgroundLayer.transform = CATransform3DRotate(transform, angle, 1, 0, 0)
The output is like below.
What is the reason for the difference of shape?
The backgroundLayer needs a frame and a position. If these are added, the result is as follows:
Source
Here a slightly modified version of your code that gives the result shown in the screenshot.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let boarderColor = UIColor.red
let height: CGFloat = 400
let width: CGFloat = 250
let center = CGPoint(x: width / 2.0, y: height / 2)
let path = CGMutablePath()
let startPoint = CGPoint(x: center.x - width / 2, y: center.y - height / 2)
path.move(to: startPoint)
path.addLine(to: CGPoint(x: startPoint.x + width, y: startPoint.y))
path.addLine(to: CGPoint(x: startPoint.x + width, y: startPoint.y + height))
path.addLine(to: CGPoint(x: startPoint.x, y: startPoint.y + height))
path.closeSubpath()
let backgroundLayer = CAShapeLayer()
backgroundLayer.path = path
backgroundLayer.fillColor = UIColor.clear.cgColor
backgroundLayer.strokeColor = boarderColor.cgColor
//these two lines are missing
backgroundLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
backgroundLayer.position = CGPoint(x: self.view.bounds.width / 2.0, y: self.view.bounds.height / 2)
var transform = CATransform3DIdentity
transform.m34 = -1 / 500
let angle = CGFloat(45 * Double.pi / 180.0)
backgroundLayer.transform = CATransform3DRotate(transform, angle, 1, 0, 0)
self.view.layer.addSublayer(backgroundLayer)
}
}
I'm trying to draw an UIView with some 'curvy edges'.
Here's what it's supposed to look like:
here's what I got:
Notice how the top right (TR) corner is not symmetrical to the bottom right (BR) corner ? The BR corner is very similar to what I want to achieve but I can't get the TR corner to align correctly (played around with bunch of different start and end angles).
here's the code:
struct Constants {
static let cornerRadius: CGFloat = 15.0 // used for left-top and left-bottom curvature
static let rightTipWidth: CGFloat = 40.0 // the max. width for the right tip thingy
static let rightCornerRadius: CGFloat = 10.0 // the radius for the right tip
static let rightEdgeRadius: CGFloat = 10.0 // the radius for the top right and bottom right curvature
}
override func draw(_ rect: CGRect) {
super.draw(rect)
// Initialize the path.
let path = UIBezierPath()
// starting point
let startingPoint = CGPoint(x: Constants.cornerRadius, y: 0.0)
path.move(to: startingPoint)
// create a center point for the arc for the top left corner
let leftTopCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: Constants.cornerRadius)
path.addArc(withCenter: leftTopCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 270.degreesToRadians, endAngle: 180.degreesToRadians, clockwise: false)
// move the path to the bottom left corner
path.addLine(to: CGPoint(x: 0.0, y: frame.size.height - Constants.cornerRadius))
// add the arc to bottom left
let leftBottomCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: frame.size.height - Constants.cornerRadius)
path.addArc(withCenter: leftBottomCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 180.degreesToRadians, endAngle: 90.degreesToRadians, clockwise: false)
// move along the bottom to the right edge - rightTipWidth
let maxXRightEdge = frame.size.width - Constants.rightTipWidth
path.addLine(to: CGPoint(x: maxXRightEdge, y: frame.size.height))
// add a curve at the bottom before tipping up at 45 degrees
let bottomRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: frame.size.height - Constants.rightEdgeRadius)
path.addArc(withCenter: bottomRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 90.degreesToRadians, endAngle: 45.degreesToRadians, clockwise: false)
// figure out the center for the right side curvature
let rightMidPointY = frame.size.height / 2.0
let halfRadius = (Constants.rightCornerRadius / 2.0)
// move up till the mid point corner radius
path.addLine(to: CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY + halfRadius)))
// the destination for the curve (end point of the curve)
let rightEndPoint = CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY - halfRadius))
// figure out the right side tip's control point (See: https://developer.apple.com/documentation/uikit/uibezierpath/1624351-addquadcurve)
let rightControlPoint = CGPoint(x: frame.size.width - halfRadius, y: rightMidPointY)
// add the curve for the right side tip
path.addQuadCurve(to: rightEndPoint, controlPoint: rightControlPoint)
// move up at 45 degrees
path.addLine(to: CGPoint(x: maxXRightEdge + Constants.rightEdgeRadius, y: Constants.rightEdgeRadius))
let topRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: Constants.rightEdgeRadius)
path.addArc(withCenter: topRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 315.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: false) // straight
path.close()
// Specify the fill color and apply it to the path.
UIColor.orange.setFill()
path.fill()
// Specify a border (stroke) color.
UIColor.orange.setStroke()
path.stroke()
}
extension BinaryInteger {
var degreesToRadians: CGFloat { return CGFloat(Int(self)) * .pi / 180 }
}
Just a quick summary of my thought process:
Create a bezierPath and move it to the startingPoint
Add the LT (left-top) curve and move the line downards
Move the line along the left edge and add the LB (left-bottom) curve
and the move line along the bottom to the right edge
Move the line till frame.size.width - Constants.rightTipWidth
Add an arc with a center point at x = currentPoint and y = height- rightEdgeRadius
Move the line up until y = (height / 2.0) +
(Constants.rightCornerRadius / 2.0)
Add the QuadCurve with an end point of y = (height / 2.0) -
(Constants.rightCornerRadius / 2.0)
Move the line up till x = maxXRightEdge + Constants.rightEdgeRadius
Add the top right (TR) curve ---> resulting in a non-symmetrical
curvature
Here is another rendition:
#IBDesignable
open class PointerView: UIView {
/// The left-top and left-bottom curvature
#IBInspectable var cornerRadius: CGFloat = 15 { didSet { updatePath() } }
/// The radius for the right tip
#IBInspectable var rightCornerRadius: CGFloat = 10 { didSet { updatePath() } }
/// The radius for the top right and bottom right curvature
#IBInspectable var rightEdgeRadius: CGFloat = 10 { didSet { updatePath() } }
/// The fill color
#IBInspectable var fillColor: UIColor = .blue { didSet { shapeLayer.fillColor = fillColor.cgColor } }
/// The stroke color
#IBInspectable var strokeColor: UIColor = .clear { didSet { shapeLayer.strokeColor = strokeColor.cgColor } }
/// The angle of the tip
#IBInspectable var angle: CGFloat = 90 { didSet { updatePath() } }
/// The line width
#IBInspectable var lineWidth: CGFloat = 0 { didSet { updatePath() } }
/// The shape layer for the pointer
private lazy var shapeLayer: CAShapeLayer = {
let _shapeLayer = CAShapeLayer()
_shapeLayer.fillColor = fillColor.cgColor
_shapeLayer.strokeColor = strokeColor.cgColor
_shapeLayer.lineWidth = lineWidth
return _shapeLayer
}()
public override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
layer.addSublayer(shapeLayer)
}
open override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
private func updatePath() {
let path = UIBezierPath()
let offset = lineWidth / 2
let boundingRect = bounds.insetBy(dx: offset, dy: offset)
let arrowTop = CGPoint(x: boundingRect.maxX - boundingRect.height / 2 / tan(angle * .pi / 180 / 2), y: boundingRect.minY)
let arrowRight = CGPoint(x: boundingRect.maxX, y: boundingRect.midY)
let arrowBottom = CGPoint(x: boundingRect.maxX - boundingRect.height / 2 / tan(angle * .pi / 180 / 2), y: boundingRect.maxY)
let start = CGPoint(x: boundingRect.minX + cornerRadius, y: boundingRect.minY)
// top left
path.move(to: start)
path.addQuadCurve(to: CGPoint(x: boundingRect.minX, y: boundingRect.minY + cornerRadius), controlPoint: CGPoint(x: boundingRect.minX, y: boundingRect.minY))
// left
path.addLine(to: CGPoint(x: boundingRect.minX, y: boundingRect.maxY - cornerRadius))
// lower left
path.addQuadCurve(to: CGPoint(x: boundingRect.minX + cornerRadius, y: boundingRect.maxY), controlPoint: CGPoint(x: boundingRect.minX, y: boundingRect.maxY))
// bottom
path.addLine(to: calculate(from: path.currentPoint, to: arrowBottom, less: rightEdgeRadius))
// bottom right (before tip)
path.addQuadCurve(to: calculate(from: arrowRight, to: arrowBottom, less: rightEdgeRadius), controlPoint: arrowBottom)
// bottom edge of tip
path.addLine(to: calculate(from: path.currentPoint, to: arrowRight, less: rightCornerRadius))
// tip
path.addQuadCurve(to: calculate(from: arrowTop, to: arrowRight, less: rightCornerRadius), controlPoint: arrowRight)
// top edge of tip
path.addLine(to: calculate(from: path.currentPoint, to: arrowTop, less: rightEdgeRadius))
// top right (after tip)
path.addQuadCurve(to: calculate(from: start, to: arrowTop, less: rightEdgeRadius), controlPoint: arrowTop)
path.close()
shapeLayer.lineWidth = lineWidth
shapeLayer.path = path.cgPath
}
/// Calculate some point between `startPoint` and `endPoint`, but `distance` from `endPoint
///
/// - Parameters:
/// - startPoint: The starting point.
/// - endPoint: The ending point.
/// - distance: Distance from the ending point
/// - Returns: Returns the point that is `distance` from the `endPoint` as you travel from `startPoint` to `endPoint`.
private func calculate(from startPoint: CGPoint, to endPoint: CGPoint, less distance: CGFloat) -> CGPoint {
let angle = atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x)
let totalDistance = hypot(endPoint.y - startPoint.y, endPoint.x - startPoint.x) - distance
return CGPoint(x: startPoint.x + totalDistance * cos(angle),
y: startPoint.y + totalDistance * sin(angle))
}
}
And because that is #IBDesignable, I can put it in a separate framework target and then optionally use it (and customize it) right in Interface Builder:
The only change I made in parameters was to not use the width of the tip, but rather the angle of the tip. That way, if the size changes as constraints (or whatever) change, it preserves the desired shape.
I also changed this to use a CAShapeLayer rather that a custom draw(_:) method to enjoy any efficiencies that Apple has built in to shape layers.
I don't know your implementation but I think it will be easy if you implemented it like that , that way you cam achieve symmetric shape perfectly
to draw a triangle , just tweak the positions of triangle points
class TriangleView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.beginPath()
context.move(to: CGPoint(x: rect.minX, y: rect.maxY))
context.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
context.addLine(to: CGPoint(x: (rect.maxX / 2.0), y: rect.minY))
context.closePath()
context.setFillColor(red: 1.0, green: 0.5, blue: 0.0, alpha: 0.60)
context.fillPath()
}
}
Here, you forgot halfRadius
// move up at 45 degrees
path.addLine(to: CGPoint(x: maxXRightEdge + Constants.rightEdgeRadius, y: Constants.rightEdgeRadius - halfRadius))
Full code:
override func draw(_ rect: CGRect) {
super.draw(rect)
// Initialize the path.
let path = UIBezierPath()
// starting point
let startingPoint = CGPoint(x: Constants.cornerRadius, y: 0.0)
path.move(to: startingPoint)
// create a center point for the arc for the top left corner
let leftTopCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: Constants.cornerRadius)
path.addArc(withCenter: leftTopCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 270.degreesToRadians, endAngle: 180.degreesToRadians, clockwise: false)
// move the path to the bottom left corner
path.addLine(to: CGPoint(x: 0.0, y: frame.size.height - Constants.cornerRadius))
// add the arc to bottom left
let leftBottomCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: frame.size.height - Constants.cornerRadius)
path.addArc(withCenter: leftBottomCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 180.degreesToRadians, endAngle: 90.degreesToRadians, clockwise: false)
// move along the bottom to the right edge - rightTipWidth
let maxXRightEdge = frame.size.width - Constants.rightTipWidth
path.addLine(to: CGPoint(x: maxXRightEdge, y: frame.size.height))
// add a curve at the bottom before tipping up at 45 degrees
let bottomRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: frame.size.height - Constants.rightEdgeRadius)
path.addArc(withCenter: bottomRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 90.degreesToRadians, endAngle: 45.degreesToRadians, clockwise: false)
// figure out the center for the right side curvature
let rightMidPointY = frame.size.height / 2.0
let halfRadius = (Constants.rightCornerRadius / 2.0)
// move up till the mid point corner radius
path.addLine(to: CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY + halfRadius)))
// the destination for the curve (end point of the curve)
let rightEndPoint = CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY - halfRadius))
// figure out the right side tip's control point (See: https://developer.apple.com/documentation/uikit/uibezierpath/1624351-addquadcurve)
let rightControlPoint = CGPoint(x: frame.size.width - halfRadius, y: rightMidPointY)
// add the curve for the right side tip
path.addQuadCurve(to: rightEndPoint, controlPoint: rightControlPoint)
// move up at 45 degrees
path.addLine(to: CGPoint(x: maxXRightEdge + Constants.rightEdgeRadius, y: Constants.rightEdgeRadius - halfRadius))
let topRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: Constants.rightEdgeRadius)
path.addArc(withCenter: topRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 315.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: false) // straight
path.close()
// Specify the fill color and apply it to the path.
UIColor.orange.setFill()
path.fill()
// Specify a border (stroke) color.
UIColor.orange.setStroke()
path.stroke()
}
I try to make UIView to show zig-zag bottom edge. Something like http://www.shutterstock.com/pic-373176370/stock-vector-receipt-vector-icon-invoice-flat-illustration-cheque-shadow-bill-with-total-cost-amount-and-dollar-symbol-abstract-text-receipt-paper-isolated-on-green.html?src=zMGBKj_5etMCcRB3cKmCoA-1-2
I have method that create a path and set as mask, but it show as 1/4 of the view. Do I need to set something else? Look like a retina problem or coordinate problem, but don't sure which one.
func layoutZigZag(bounds: CGRect) -> CALayer {
let maskLayer = CAShapeLayer()
maskLayer.bounds = bounds
let path = UIBezierPath()
let width = bounds.size.width
let height = bounds.size.height
let topRight = CGPoint(x: width , y: height)
let topLeft = CGPoint(x: 0 , y: height)
let bottomRight = CGPoint(x: width , y: 0)
let bottomLeft = CGPoint(x: 0 , y: 0)
let zigzagHeight: CGFloat = 10
let numberOfZigZag = Int(floor(width / 23.0))
let zigzagWidth = width / CGFloat(numberOfZigZag)
path.move(to: topLeft)
path.addLine(to: bottomLeft)
// zigzag
var currentX = bottomLeft.x
var currentY = bottomLeft.y
for i in 0..<numberOfZigZag {
let upper = CGPoint(x: currentX + zigzagWidth / 2, y: currentY + zigzagHeight)
let lower = CGPoint(x: currentX + zigzagWidth, y: currentY)
path.addLine(to: upper)
path.addLine(to: lower)
currentX += zigzagWidth
}
path.addLine(to: topRight)
path.close()
maskLayer.path = path.cgPath
return maskLayer
}
and
let rect = CGRect(x: 0, y: 0, width: 320, height: 400)
let view = UIView(frame: rect)
view.backgroundColor = UIColor.red
let zigzag = layoutZigZag(bounds: rect)
view.layer.mask = zigzag
Path look correct
Result is 1/4 of the view
Change maskLayer.bounds = bounds to maskLayer.frame = bounds
Update:
Upside down is because of difference between the UI and CG, we are creating the path in UIBezierPath and converting that path as a CGPath (maskLayer.path = path.cgPath). First we have to know the difference, where CGPath is Quartz 2D and origin is at the bottom left while in UIBezierPath is UIKit origin is at the top-left. As per your code, applied coordinates are as per the top-left ie UIBezierPath when we transform to CGPath (origin at bottom left) it becomes upside down. so change the code as below to get the desired effect.
let topRight = CGPoint(x: width , y: 0)
let topLeft = CGPoint(x: 0 , y: 0)
let bottomLeft = CGPoint(x: 0 , y: (height - zigzagHeight))
Quartz 2D Coordinate Systems
UIBezierPath Coordinate Systems