Search code examples
swiftcalayercatransform3d

CATransform3DRotate applied to layer doesnt work


Could you please advice why CATransform3DRotate doesnt work for layer in my case, its blue layer on the image.

enter image description here

I have custom view, where I draw nature view, I want to animate changing the moon phase that will be done inside the white circle layer as its mask. I suppose that it is good idea to apply here 3DRotation, but for some reason it doesn't work even without animation, could be please advice what I am doing wrong?

    func drawMoonPhase(inRect rect:CGRect, inContext context: CGContext) {

    let moonShape = UIBezierPath(ovalIn: rect)
    moonShape.lineWidth = 4.0
    UIColor.white.setStroke()
    moonShape.stroke()
    moonShape.close()

    let moonLayer = CAShapeLayer()
    moonLayer.path = moonShape.cgPath
    moonLayer.opacity = 0
    self.layer.addSublayer(moonLayer)

    let circlePath = UIBezierPath(ovalIn: rect)
    UIColor.blue.setFill()
    circlePath.fill()
    circlePath.close()

    let circleShape = CAShapeLayer()
    circleShape.path = circlePath.cgPath
    circleShape.opacity = 0

    var transform = CATransform3DIdentity
    transform.m34 = -1 / 500.0

    transform = CATransform3DRotate(transform, (CGFloat(Double.pi * 0.3)), 0, 1, 0)
    circleShape.transform = transform

    moonLayer.mask = circleShape

}

Thanks in advance

EDIT: Maybe I want clear , the effect i want is the below: enter image description here


Solution

  • The transform occurs around the layer's anchor point that by default is in the center. Therefore what it is happening there is that the shape rotates around itself causing no visible result. :)

    what you should do in this layout of layer is to use cos and sin math functions in order to determine the x and y position of your moon.

    Let me know if you need more insights I will be happy to help.

    also, please note that you don't need 2 shapeLayers in order to have the blue moon with the white stroke. CAShapeLayer has properties for both fill and stroke so you can simplify your code.

    Based on the new info here is my new answer:

    I was not able to get a nice effect by using the transform, so I decided to write the mask manually. this is the result:

    /**
     Draw a mask based on the moon phase progress.
    
     - parameter rect: The rect where the mon will be drawn
     - parameter progress: The progress of the moon phase. This value must be between 0 and 1
     */
    func moonMask(in rect: CGRect, forProgress progress: CGFloat)->CALayer {
        let path = CGMutablePath()
        let center = CGPoint(x: rect.midX, y: rect.midY)
        path.move(to: CGPoint(x: rect.midX, y: 0))
    
        let relativeProgress = (max(min(progress, 1), 0) - 0.5) * 2
        let radius = rect.width/2
    
        let tgX = rect.midX+(relativeProgress * (radius*4/3))
        path.addCurve(to: CGPoint(x: rect.midX, y: rect.maxY), control1: CGPoint(x: tgX, y: 0), control2: CGPoint(x: tgX, y: rect.maxY))
        path.addArc(center: center, radius: rect.width/2, startAngle: .pi/2, endAngle: .pi*3/2, clockwise: false)
        //path.closeSubpath()
    
        let mask = CAShapeLayer()
        mask.path = path
        mask.fillColor = UIColor.black.cgColor
    
        return mask
    }
    

    The function above draws a shapelier that can be used as mask for your moonLayer. This layer will be drawnin relation to a progress parameter that you will pass in the function where 1 is full moon and 0 is new moon.

    You can put everything together to have the desired effect, and you can extract the path creation code to make a nice animation if you want.

    This should answer your question I hope. To quick test I wrote this playground:

    import UIKit
    
    let rect = CGRect(origin: .zero, size: CGSize(width: 200, height: 200))
    
    let view = UIView(frame: rect)
    view.backgroundColor = .black
    let layer = view.layer
    
    /**
     Draw a mask based on the moon phase progress.
    
     - parameter rect: The rect where the mon will be drawn
     - parameter progress: The progress of the moon phase. This value must be between 0 and 1
     */
    func moonMask(in rect: CGRect, forProgress progress: CGFloat)->CALayer {
        let path = CGMutablePath()
        let center = CGPoint(x: rect.midX, y: rect.midY)
        path.move(to: CGPoint(x: rect.midX, y: 0))
    
        let relativeProgress = (max(min(progress, 1), 0) - 0.5) * 2
        let radius = rect.width/2
    
        let tgX = rect.midX+(relativeProgress * (radius*4/3))
        path.addCurve(to: CGPoint(x: rect.midX, y: rect.maxY), control1: CGPoint(x: tgX, y: 0), control2: CGPoint(x: tgX, y: rect.maxY))
        path.addArc(center: center, radius: rect.width/2, startAngle: .pi/2, endAngle: .pi*3/2, clockwise: false)
        //path.closeSubpath()
    
        let mask = CAShapeLayer()
        mask.path = path
        mask.fillColor = UIColor.white.cgColor
    
        return mask
    }
    
    let moonLayer = CAShapeLayer()
    moonLayer.path = UIBezierPath(ovalIn: rect).cgPath
    moonLayer.fillColor = UIColor.clear.cgColor
    moonLayer.strokeColor = UIColor.white.cgColor
    moonLayer.lineWidth = 2
    moonLayer.shadowColor = UIColor.white.cgColor
    moonLayer.shadowOpacity = 1
    moonLayer.shadowRadius = 10
    moonLayer.shadowPath = moonLayer.path
    moonLayer.shadowOffset = .zero
    
    layer.addSublayer(moonLayer)
    
    let moonPhase = moonMask(in: rect, forProgress: 0.3)
    moonPhase.shadowColor = UIColor.white.cgColor
    moonPhase.shadowOpacity = 1
    moonPhase.shadowRadius = 10
    moonPhase.shadowPath = moonLayer.path
    moonPhase.shadowOffset = .zero
    
    layer.addSublayer(moonPhase)
    
    view
    

    enter image description here