How to draw CATransform3D for this layer? - ios

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)
}
}

Related

Shrink The Shape Keep Margin in Swift

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.

How to draw a line at the corner of a square with CAShapeLayer in Swift

I am developing a face recognition application of Vision library, and I am having trouble drawing lines with CAShapeLayer
here is the code after getting camera output:
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: imageBuffer, orientation: .leftMirrored, options: [:])
let faceDetectionRequest = VNDetectFaceLandmarksRequest(completionHandler: { (request: VNRequest, error: Error?) in
DispatchQueue.main.async {
self.faceLayers.forEach({ drawing in drawing.removeFromSuperlayer() })
if let observations = request.results as? [VNFaceObservation] {
for observation in observations {
let faceRectConverted = self.videoPreviewLayer.layerRectConverted(fromMetadataOutputRect: observation.boundingBox)
let faceRectanglePath = CGPath(rect: faceRectConverted, transform: nil)
let faceLayer = CAShapeLayer()
faceLayer.path = faceRectanglePath
faceLayer.fillColor = UIColor.clear.cgColor
faceLayer.strokeColor = UIColor.systemPink.cgColor
self.faceLayers.append(faceLayer)
self.cameraView.layer.addSublayer(faceLayer)
}
}
}
})
do {
try imageRequestHandler.perform([faceDetectionRequest])
} catch {
print(error.localizedDescription)
}
}
result
the problem I am facing when I want to draw a short line at the corner of the square
Thanks for all the support!
You can use this:
let thinLayer = CAShapeLayer()
thinLayer.path = CGPath(rect: rect, transform: nil)
thinLayer.strokeColor = UIColor.purple.cgColor
thinLayer.fillColor = UIColor.clear.cgColor
thinLayer.lineWidth = 1.0
view.layer.addSublayer(thinLayer)
let cornerWidth: CGFloat = 20.0
let topLeftBezierPath = UIBezierPath()
topLeftBezierPath.move(to: CGPoint(x: rect.origin.x, y: rect.origin.y + cornerWidth))
topLeftBezierPath.addLine(to: CGPoint(x: rect.origin.x, y: rect.origin.y))
topLeftBezierPath.addLine(to: CGPoint(x: rect.origin.x + cornerWidth, y: rect.origin.y))
let topRightBezierPath = UIBezierPath()
topRightBezierPath.move(to: CGPoint(x: rect.maxX, y: rect.origin.y + cornerWidth))
topRightBezierPath.addLine(to: CGPoint(x: rect.maxX, y: rect.origin.y))
topRightBezierPath.addLine(to: CGPoint(x: rect.maxX - cornerWidth, y: rect.origin.y))
let bottomRightBezierPath = UIBezierPath()
bottomRightBezierPath.move(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerWidth))
bottomRightBezierPath.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
bottomRightBezierPath.addLine(to: CGPoint(x: rect.maxX - cornerWidth, y: rect.maxY))
let bottomLeftBezierPath = UIBezierPath()
bottomLeftBezierPath.move(to: CGPoint(x: rect.origin.x, y: rect.maxY - cornerWidth))
bottomLeftBezierPath.addLine(to: CGPoint(x: rect.origin.x, y: rect.maxY))
bottomLeftBezierPath.addLine(to: CGPoint(x: rect.origin.x + cornerWidth, y: rect.maxY))
func cornerLayer(with bezierPath: UIBezierPath) -> CAShapeLayer {
let shape = CAShapeLayer()
shape.path = bezierPath.cgPath
shape.lineWidth = 4.0
shape.strokeColor = UIColor.purple.cgColor
shape.fillColor = UIColor.clear.cgColor
shape.lineCap = .round
return shape
}
let topLeftShape = cornerLayer(with: topLeftBezierPath)
view.layer.addSublayer(topLeftShape)
let topRightShape = cornerLayer(with: topRightBezierPath)
view.layer.addSublayer(topRightShape)
let bottomRightShape = cornerLayer(with: bottomRightBezierPath)
view.layer.addSublayer(bottomRightShape)
let bottomLeftShape = cornerLayer(with: bottomLeftBezierPath)
view.layer.addSublayer(bottomLeftShape)
Where:
view is the UIView on which to add the layer, in your case it's cameraView.
rect is the full rect of the face, in your case it's faceRectConverted
customize to fulfill your needs (lineWidh, strokeColor, cornerWidth which might me proportional to the size of the rect?)
Sample in Playground:
func drawing() -> UIView {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
view.backgroundColor = .orange
let rect = CGRect(x: 50, y: 50, width: 200, height: 200)
let thinLayer = CAShapeLayer()
thinLayer.path = CGPath(rect: rect, transform: nil)
thinLayer.strokeColor = UIColor.purple.cgColor
thinLayer.fillColor = UIColor.clear.cgColor
thinLayer.lineWidth = 1.0
view.layer.addSublayer(thinLayer)
let cornerWidth: CGFloat = 20.0
let topLeftBezierPath = UIBezierPath()
topLeftBezierPath.move(to: CGPoint(x: rect.origin.x, y: rect.origin.y + cornerWidth))
topLeftBezierPath.addLine(to: CGPoint(x: rect.origin.x, y: rect.origin.y))
topLeftBezierPath.addLine(to: CGPoint(x: rect.origin.x + cornerWidth, y: rect.origin.y))
let topRightBezierPath = UIBezierPath()
topRightBezierPath.move(to: CGPoint(x: rect.maxX, y: rect.origin.y + cornerWidth))
topRightBezierPath.addLine(to: CGPoint(x: rect.maxX, y: rect.origin.y))
topRightBezierPath.addLine(to: CGPoint(x: rect.maxX - cornerWidth, y: rect.origin.y))
let bottomRightBezierPath = UIBezierPath()
bottomRightBezierPath.move(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerWidth))
bottomRightBezierPath.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
bottomRightBezierPath.addLine(to: CGPoint(x: rect.maxX - cornerWidth, y: rect.maxY))
let bottomLeftBezierPath = UIBezierPath()
bottomLeftBezierPath.move(to: CGPoint(x: rect.origin.x, y: rect.maxY - cornerWidth))
bottomLeftBezierPath.addLine(to: CGPoint(x: rect.origin.x, y: rect.maxY))
bottomLeftBezierPath.addLine(to: CGPoint(x: rect.origin.x + cornerWidth, y: rect.maxY))
func cornerLayer(with bezierPath: UIBezierPath) -> CAShapeLayer {
let shape = CAShapeLayer()
shape.path = bezierPath.cgPath
shape.lineWidth = 4.0
shape.strokeColor = UIColor.purple.cgColor
shape.fillColor = UIColor.clear.cgColor
shape.lineCap = .round
return shape
}
let topLeftShape = cornerLayer(with: topLeftBezierPath)
view.layer.addSublayer(topLeftShape)
let topRightShape = cornerLayer(with: topRightBezierPath)
view.layer.addSublayer(topRightShape)
let bottomRightShape = cornerLayer(with: bottomRightBezierPath)
view.layer.addSublayer(bottomRightShape)
let bottomLeftShape = cornerLayer(with: bottomLeftBezierPath)
view.layer.addSublayer(bottomLeftShape)
return view
}
let drawn = drawing()
drawn
Output:

Draw a diagonal line with side curves UIBezierPath

I want to draw diagonal line with side curves like the following
Code:
func drawCanvas1(frame: CGRect = CGRect(x: 0, y: 0, width: 240, height: 120)) {
//// Bezier Drawing
let bezierPath = UIBezierPath()
UIColor.black.setStroke()
bezierPath.lineWidth = 1
bezierPath.stroke()
//// Bezier 2 Drawing
let bezier2Path = UIBezierPath()
bezier2Path.move(to: CGPoint(x: frame.minX + 0.5, y: frame.minY + 103.42))
bezier2Path.addCurve(to: CGPoint(x: frame.minX + 12.17, y: frame.minY + 119.5), controlPoint1: CGPoint(x: frame.minX + 0.5, y: frame.minY + 103.42), controlPoint2: CGPoint(x: frame.minX + 0.5, y: frame.minY + 119.5))
bezier2Path.addCurve(to: CGPoint(x: frame.minX + 232.03, y: frame.minY + 26.23), controlPoint1: CGPoint(x: frame.minX + 23.85, y: frame.minY + 119.5), controlPoint2: CGPoint(x: frame.minX + 232.03, y: frame.minY + 26.23))
bezier2Path.addCurve(to: CGPoint(x: frame.minX + 237.87, y: frame.minY + 0.5), controlPoint1: CGPoint(x: frame.minX + 232.03, y: frame.minY + 26.23), controlPoint2: CGPoint(x: frame.minX + 243.7, y: frame.minY + 16.58))
UIColor.black.setStroke()
bezier2Path.lineWidth = 1
bezier2Path.lineCapStyle = .square
bezier2Path.stroke()
let layer1 = CAShapeLayer()
layer1.path = bezierPath.cgPath
let layer2 = CAShapeLayer()
layer2.path = bezier2Path.cgPath
imageCollectionView.layer.addSublayer(layer1)
imageCollectionView.layer.addSublayer(layer2)
}
output:
As you can see in output it is filling with black color whereas I've only given stroke. I'm not really good at bezier path. can anyone help me in getting the desired effect
Maybe using addArc is easier?
func drawCanvas1(frame: CGRect = CGRect(x: 0, y: 0, width: 240, height: 120)) {
let rhsY:CGFloat = frame.minY + 60 // Please adjust this value depending on your need
let cornerRadius:CGFloat = 25
let angle = atan2(frame.maxY - rhsY, frame.width)
// upper left corner
let p1 = CGPoint(x: frame.minX, y: frame.minY)
// left point touching bottom left corner
let p2 = CGPoint(x: frame.minX, y: frame.maxY - cornerRadius)
let a = cornerRadius * cos(angle)
let o = cornerRadius * sin(angle)
// Center of bottom left round corner
let c1 = CGPoint(x: p2.x + a, y: p2.y - o)
let c1Start = CGFloat.pi - angle
let c1End = c1Start - (CGFloat.pi / 2)
// Center of bottom right round corner
let c2 = CGPoint(x: frame.maxX - cornerRadius, y: rhsY)
// bottom point touching bottom left corner
// let p3 = CGPoint(x: c1.x + o, y: c1.y + a)
// bottom point touching bottom right corner
let p4 = CGPoint(x: c2.x + o, y: c2.y + a)
// right point touching bottom right corner
// let p5 = CGPoint(x: frame.maxX, y: rhsY)
// upper right corner
let p6 = CGPoint(x: frame.maxX, y: frame.minY)
let path = UIBezierPath()
path.move(to: p1)
path.addLine(to: p2)
path.addArc(withCenter: c1, radius: cornerRadius, startAngle: c1Start, endAngle: c1End, clockwise: false)
path.addLine(to: p4)
path.addArc(withCenter: c2, radius: cornerRadius, startAngle: c1End, endAngle: 0, clockwise: false)
path.addLine(to: p6)
path.addLine(to: p1)
// I am guessing you are trying to use a mask to the top level image?
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
imageCollectionView.layer.mask = maskLayer
}
Unless you provide your fill color, black is used by default to fill the path. If the path is not closed, fill color tries to fill using start and end points.

Draw male and female gender symbols in iOS

Does anyone know how to programmatically draw the male and female gender signs in iOS like the ones below?
An example for drawing the female symbol. First, make a Layer using UIBezierPath to draw it:
import UIKit
class FemaleLayer: CAShapeLayer {
override var frame: CGRect {
didSet{
self.draw()
}
}
private func draw() {
self.lineWidth = 20.0
self.fillColor = UIColor.clear.cgColor
self.strokeColor = UIColor.black.cgColor
let path = UIBezierPath()
let sideLength = fmin(self.frame.width, self.frame.height)
let circlesRadius = (sideLength / 2.0 - self.lineWidth) * 0.6
let circleCenterY = self.bounds.midY * 0.8
path.addArc(withCenter: CGPoint(x:self.bounds.midX, y:circleCenterY), radius: circlesRadius, startAngle: 0.0, endAngle: 2 * CGFloat.pi, clockwise: true)
let circleBottomY = circleCenterY + circlesRadius
path.move(to: CGPoint(x: self.bounds.midX, y: circleBottomY))
path.addLine(to: CGPoint(x: self.bounds.midX, y: circleBottomY + circlesRadius))
path.move(to: CGPoint(x: self.bounds.midX * 0.6, y: circleBottomY + circlesRadius * 0.5))
path.addLine(to: CGPoint(x: self.bounds.midX * 1.4, y: circleBottomY + circlesRadius * 0.5))
self.path = path.cgPath
}
}
Then use it by adding the layer to a UIView:
let femaleLayer = FemaleLayer()
femaleLayer.frame = self.view.bounds
self.view.layer.addSublayer(femaleLayer)
The final result:
Updated, for male:
class MaleLayer: CAShapeLayer {
override var frame: CGRect {
didSet{
self.draw()
}
}
private func draw() {
self.lineWidth = 20.0
self.fillColor = UIColor.clear.cgColor
self.strokeColor = UIColor.black.cgColor
let path = UIBezierPath()
let sideLength = fmin(self.frame.width, self.frame.height)
let circlesRadius = (sideLength / 2.0 - self.lineWidth) * 0.6
let circleCenterX = self.bounds.midX * 0.9
let circleCenterY = self.bounds.midY * 1.2
path.addArc(withCenter: CGPoint(x:circleCenterX, y:circleCenterY), radius: circlesRadius, startAngle: 0.0, endAngle: 2 * CGFloat.pi, clockwise: true)
let circleRightTopX = circleCenterX + circlesRadius * 0.686
let circleRightTopY = circleCenterY - circlesRadius * 0.686
let lineLength = circlesRadius * 0.7
path.move(to: CGPoint(x: circleRightTopX, y: circleRightTopY))
path.addLine(to: CGPoint(x: circleRightTopX + lineLength, y: circleRightTopY - lineLength))
path.move(to: CGPoint(x: circleRightTopX, y: circleRightTopY - lineLength))
path.addLine(to: CGPoint(x: circleRightTopX + lineLength + self.lineWidth / 2.0, y: circleRightTopY - lineLength))
path.move(to: CGPoint(x: circleRightTopX + lineLength, y: circleRightTopY - lineLength))
path.addLine(to: CGPoint(x: circleRightTopX + lineLength , y: circleRightTopY))
self.path = path.cgPath
}
}

UIBezierPath Rotation around a UIView's center

I'm creating a custom UIView, in which I implement its draw(rect:) method by drawing a circle with a large width using UIBezierPath, that draw a square on the top (as shown in the picture, don't consider the colors or the size). Then I try creating rotated copies of the square, to match a "settings" icon (picture 2, consider only the outer ring). To do that last thing, I need to rotate the square using a CGAffineTransform(rotationAngle:) but the problem is that this rotation's center is the origin of the frame, and not the center of the circle. How can I create a rotation around a certain point in my view?
As a demonstration of #DuncanC's answer (up voted), here is the drawing of a gear using CGAffineTransforms to rotate the gear tooth around the center of the circle:
class Gear: UIView {
var lineWidth: CGFloat = 16
let boxWidth: CGFloat = 20
let toothAngle: CGFloat = 45
override func draw(_ rect: CGRect) {
let radius = (min(bounds.width, bounds.height) - lineWidth) / 4.0
var path = UIBezierPath()
path.lineWidth = lineWidth
UIColor.white.set()
// Use the center of the bounds not the center of the frame to ensure
// this draws correctly no matter the location of the view
// (thanks #dulgan for pointing this out)
let center = CGPoint(x: bounds.maxX / 2, y: bounds.maxY / 2)
// Draw circle
path.move(to: CGPoint(x: center.x + radius, y: center.y))
path.addArc(withCenter: CGPoint(x: center.x, y: center.y), radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
path.stroke()
// Box for gear tooth
path = UIBezierPath()
let point = CGPoint(x: center.x - boxWidth / 2.0, y: center.y - radius)
path.move(to: point)
path.addLine(to: CGPoint(x: point.x, y: point.y - boxWidth))
path.addLine(to: CGPoint(x: point.x + boxWidth, y: point.y - boxWidth))
path.addLine(to: CGPoint(x: point.x + boxWidth, y: point.y))
path.close()
UIColor.red.set()
// Draw a tooth every toothAngle degrees
for _ in stride(from: toothAngle, through: 360, by: toothAngle) {
// Move origin to center of the circle
path.apply(CGAffineTransform(translationX: -center.x, y: -center.y))
// Rotate
path.apply(CGAffineTransform(rotationAngle: toothAngle * .pi / 180))
// Move origin back to original location
path.apply(CGAffineTransform(translationX: center.x, y: center.y))
// Draw the tooth
path.fill()
}
}
}
let view = Gear(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
Here it is running in a Playground:
Shift the origin of your transform,
Rotate,
Shift back
Apply your transform
Maybe would help someone. Source
extension UIBezierPath {
func rotate(degree: CGFloat) {
let bounds: CGRect = self.cgPath.boundingBox
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radians = degree / 180.0 * .pi
var transform: CGAffineTransform = .identity
transform = transform.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: radians)
transform = transform.translatedBy(x: -center.x, y: -center.y)
self.apply(transform)
}
}
Example:
let progressLayerPath = UIBezierPath(ovalIn: CGRect(x: 0,
y: 0,
width: 70,
height: 70))
progressLayerPath.rotate(degree: -90) // <-------
progressLayer.path = progressLayerPath.cgPath
progressLayer.strokeColor = progressColor.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineWidth = lineWidth
layer.addSublayer(progressLayer)
If you want a quick n dirty version using UIViews instead:
UIView * dialView = [UIView new];
dialView.frame = CGRectMake(0, localY, w, 300);
dialView.backgroundColor = [UIColor whiteColor];
[analyticsView addSubview:dialView];
float lineWidth = 6;
float lineHeight = 20.0f;
int numberOfLines = 36;
for (int n = 0; n < numberOfLines; n++){
UIView * lineView = [UIView new];
lineView.frame = CGRectMake((w-lineWidth)/2, (300-lineHeight)/2, lineHeight, lineWidth);
lineView.backgroundColor = [UIColor redColor];
[dialView addSubview:lineView];
lineView.transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(360/numberOfLines*n));
lineView.transform = CGAffineTransformTranslate(lineView.transform, (150-lineHeight), 0);
}
Giving:
Where w is a holder width, and radians are calculated by:
#define DEGREES_TO_RADIANS(degrees)((M_PI * degrees)/180)

Resources