[SWIFT] Try drawing a cube with View and Layer

Following Last year's sphere drawing, I tried to draw a cube this year. This time, I was particular about drawing with genuine ** complete coding ** without using any photos or images. (Last time I used a cheat technique that distorts and fits some photos)

From the result, it looks like this. image.png

Does it look like a wooden square? I mainly use CoreAnimation`` CoreGraphics`` CoreImage`.

The project is up below. https://github.com/yumemi-ajike/Cube

Cube face

Define front, back, top, bottom, left side, and right side as CALayer, and add them as squares. The size was appropriately set to 200 on each side.

WireCubeView.swift


    let size: CGFloat = 200
    lazy var frontLayer: CALayer = {
        let transform = CATransform3DMakeTranslation(0, 0, size / 2)
        return createFaceLayer(with: transform)
    }()
    lazy var rightLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(size / 2, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 0, 1, 0)
        return createFaceLayer(with: transform)
    }()
    lazy var topLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(0, -size / 2, 0)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 1, 0, 0)
        return createFaceLayer(with: transform)
    }()
    lazy var leftLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(-size / 2, 0, 0)
        transform = CATransform3DRotate(transform, -CGFloat.pi / 2 , 0, 1, 0)
        return createFaceLayer(with: transform)
    }()
    lazy var bottomLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(0, size / 2, 0)
        transform = CATransform3DRotate(transform, -CGFloat.pi / 2 , 1, 0, 0)
        return createFaceLayer(with: transform)
    }()
    lazy var backLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(0, 0, -size / 2)
        transform = CATransform3DRotate(transform, CGFloat.pi , 0, 1, 0)
        return createFaceLayer(with: transform)
    }()
    
    func createFaceLayer(with transform: CATransform3D) -> CALayer {
        
        let layer = CALayer()
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: size)
        layer.borderColor = UIColor.white.cgColor
        layer.borderWidth = 1
        layer.transform = transform
        layer.allowsEdgeAntialiasing = true
        return layer
    }

WireCubeView.swift


    override func layoutSubviews() {
        super.layoutSubviews()
        ...
        baseLayer.addSublayer(frontLayer)
        baseLayer.addSublayer(rightLayer)
        baseLayer.addSublayer(topLayer)
        baseLayer.addSublayer(leftLayer)
        baseLayer.addSublayer(bottomLayer)
        baseLayer.addSublayer(backLayer)
        baseLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
        
        ...
        layer.addSublayer(baseLayer)
        ...
    }

The front moves 100 in the Z direction.

        let transform = CATransform3DMakeTranslation(0, 0, size / 2)

The right side moves 100 in the X direction, and the Y axis is rotated CGFloat.pi / 2, that is, 90 °.

        var transform = CATransform3DMakeTranslation(size / 2, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 0, 1, 0)

Move -100 in the Y direction and rotate the X axis 90 °.

        var transform = CATransform3DMakeTranslation(0, -size / 2, 0)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 1, 0, 0)

The left side moves -100 in the X direction, which is the opposite of the right side, and the Y axis is rotated by -90 °.

        var transform = CATransform3DMakeTranslation(-size / 2, 0, 0)
        transform = CATransform3DRotate(transform, -CGFloat.pi / 2 , 0, 1, 0)

The bottom surface moves 100 in the Y direction, which is the opposite of the top surface, and the X axis is rotated by -90 °.

        var transform = CATransform3DMakeTranslation(0, size / 2, 0)
        transform = CATransform3DRotate(transform, -CGFloat.pi / 2 , 1, 0, 0)

The back side moves -100 in the Z direction, and the Y axis is rotated CG Float.pi, that is, 180 °.

        var transform = CATransform3DMakeTranslation(0, 0, -size / 2)
        transform = CATransform3DRotate(transform, CGFloat.pi , 0, 1, 0)

If you execute the build at this point, only the front (= frontLayer) will be visible. figure1.png

Adjust the overall angle because it needs to look recognizable as a cube.

        override func layoutSubviews() {
        ...
        var transform = CATransform3DIdentity
        ...
        transform = CATransform3DRotate(transform, -30 * CGFloat.pi / 180, 0, 1, 0)
        transform = CATransform3DRotate(transform, -30 * CGFloat.pi / 180, 1, 0, 0)
        transform = CATransform3DRotate(transform, 15 * CGFloat.pi / 180, 0, 0, 1)
        baseLayer.transform = transform

Transform baseLayer, which is the parent of all 6 sides. If you tilt the Y-axis by -30 °, the X-axis by -30 °, and the Z-axis by 15 °, it looks like a cube. By the way, if you tilt it, the line will be drawn at a delicate angle and it will be jagged and dirty, so apply antialiasing to each layer.

layer.allowsEdgeAntialiasing = true

figure2.png

At this rate, the perspective is not working, so the depth cannot be felt and it is not possible to determine which direction it is in.

        transform.m34 = -1.0 / 1000

By entering a value in CATransform3D.m34, it will be parsed and you will be able to see in what direction it exists. The value is appropriate. figure3.png

Now it looks like a cube. The balance that can be seen on each side is also good.

Make a face

Express the subtle light and darkness of the surface with gradation.

Color the visible front, top, and right sides with CAGradientLayer.

CubeView.swift


    lazy var frontLayer: CAGradientLayer = {
        let transform = CATransform3DMakeTranslation(0, 0, size / 2)
        return createGradientFaceLayer(with: transform,
                                       colors: [UIColor(white: 0.4, alpha: 1.0),
                                                UIColor(white: 0.6, alpha: 1.0)])
    }()
    lazy var rightLayer: CAGradientLayer = {
        var transform = CATransform3DMakeTranslation(size / 2, 0, 0)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 0, 1, 0)
        return createGradientFaceLayer(with: transform,
                                       colors: [UIColor(white: 0.6, alpha: 1.0),
                                                UIColor(white: 0.8, alpha: 1.0)])
    }()
    lazy var topLayer: CAGradientLayer = {
        var transform = CATransform3DMakeTranslation(0, -size / 2, 0)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 1, 0, 0)
        return createGradientFaceLayer(with: transform,
                                       colors: [UIColor(white: 1.0, alpha: 1.0),
                                                UIColor(white: 0.8, alpha: 1.0)])
    }()

CubeView.swift


    func createGradientFaceLayer(with transform: CATransform3D, colors: [UIColor]) -> CAGradientLayer {
        
        let layer = CAGradientLayer()
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: size)
        layer.colors = colors.map { $0.cgColor }
        layer.transform = transform
        layer.allowsEdgeAntialiasing = true
        return layer
    }

Assuming that the light source is slightly on the upper right The front is 40% → 60% from top to bottom, The right side is 60% → 80% from top to bottom, The upper surface is 100% → 80% white from the back to the front with a gray gradation. figure4.png Brightening the front and right sides downward is the expression that the reflected light from the ground plane hits and brightens.

Make background/ground

Add GroundView after CubeView. The background/ground (= GroundView) is defined as a layer separate from the cube (= CubeView) to improve the reusability of the cube itself.

GroundView.swift


final class GroundView: UIView {
    
    lazy var groundLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        layer.colors = [UIColor(white: 1.0, alpha: 1.0).cgColor,
                        UIColor(white: 0.7, alpha: 1.0).cgColor]
        layer.locations = [0.5, 1.0]
        return layer
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        layer.addSublayer(groundLayer)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        groundLayer.frame = bounds
    }
}

figure5.png

Cast a shadow

Now that the background/ground is created, cast the shadow of the cube. There seems to be some controversy over whether shadows should be in the background/ground or cube View hierarchy. I want the shadow to have the same perspective as the cube, so I decided to add it to the baseLayer of CubeView, which seems to require only one description of transform.

I made a trial and error on how to express the shadow, but it was difficult to create a shadow from each side of the cube, so I decided to set up a completely separate side as a shadow.

Add a shadow base layer so that it spreads toward you.

CubeView.swift


    lazy var shadowLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(size / 2, size / 2, size / 2)
        transform = CATransform3DRotate(transform, CGFloat.pi / 2 , 1, 0, 0)
        let layer = CALayer()
        layer.frame = CGRect(x: -size, y: -size, width: size * 2, height: size * 2)
        layer.transform = transform
        layer.allowsEdgeAntialiasing = true
        return layer
    }()

CubeView.swift


    override func layoutSubviews() {
        super.layoutSubviews()
        ...
        baseLayer.addSublayer(shadowLayer)
        ...
    }

figure6.png

Drops the shadow gradation.

CubeView.swift


    lazy var shadowGradientLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        layer.frame = CGRect(x: 0, y: 0, width: size * 2, height: size * 2)
        layer.colors = [UIColor(white: 0, alpha: 0.4), .clear].map { $0.cgColor }
        layer.allowsEdgeAntialiasing = true
        return layer
    }()

CubeView.swift


    override func layoutSubviews() {
        super.layoutSubviews()
        
        shadowLayer.addSublayer(shadowGradientLayer)
        ...
    }

figure7.png

Clip the shape of the shadow with a path.

CubeView.swift


    lazy var shadowShapeLayer: CAShapeLayer = {
        let layer = CAShapeLayer()
        layer.frame = CGRect(x: 0, y: 0, width: size * 2, height: size * 2)
        layer.fillColor = UIColor.black.cgColor
        let path = CGMutablePath()
        path.move(to: CGPoint(x: 0, y: size))
        path.addLine(to: CGPoint(x: size, y: size))
        path.addLine(to: CGPoint(x: size, y: 0))
        path.addLine(to: CGPoint(x: size * 1.5, y: size))
        path.addLine(to: CGPoint(x: size / 2 * 3, y: size * 2))
        path.addLine(to: CGPoint(x: size / 2, y: size * 2))
        path.addLine(to: CGPoint(x: 0, y: size))
        path.closeSubpath()
        layer.path = path
        layer.allowsEdgeAntialiasing = true
        return layer
    }()

CubeView.swift


    override func layoutSubviews() {
        super.layoutSubviews()
        
        ...
        shadowGradientLayer.mask = shadowShapeLayer
        ...

figure9.png

By clipping the gradation with such a path, it looks like a shadow. figure8.png

I don't know what kind of cube it is, but I can't deny the feeling that the CG is exposed because it's realistic, so I'll make details.

Creating a shape

By pursuing details such as the shape of the cube, we will increase the persuasive power of the picture.

Drop the corner

The part where each surface touches at a right angle is so sharp that it cannot be a real object. At this point, the material has not been clarified yet, but if it is a real thing, metal, wood, or plastic should always have a slight rounded corner, so I will drop the corner.

Since you want to round the corners and clip the content, set CAGradientLayer to a sublayer of CALayer and set cornerRadius.

    let cornerRadius: CGFloat = 2.0
    ...

    func createGradientFaceLayer(with transform: CATransform3D, colors: [UIColor]) -> CALayer {
        
        let layer = CALayer()
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame = CGRect(x: 0, y: 0, width: size, height: size)
        gradientLayer.colors = colors.map { $0.cgColor }
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: size)
        layer.cornerRadius = cornerRadius
        layer.masksToBounds = true
        layer.transform = transform
        layer.allowsEdgeAntialiasing = true
        layer.addSublayer(gradientLayer)
        return layer
    }

Next, place a white → transparent gradation as a highlight on the part where each of the front/top/right side touches.

    lazy var frontTopLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: cornerRadius)
        layer.colors = [UIColor(white: 1, alpha: 0.8),
                        UIColor(white: 1, alpha: 0)].map { $0.cgColor }
        layer.transform = CATransform3DMakeTranslation(size / 2, size / 2, 0)
        layer.allowsEdgeAntialiasing = true
        return layer
    }()
    lazy var frontLeftLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        let transform = CATransform3DMakeTranslation(size / 2, size / 2, 0)
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: cornerRadius, height: size)
        layer.colors = [UIColor(white: 1, alpha: 0.3),
                        UIColor(white: 1, alpha: 0)].map { $0.cgColor }
        layer.startPoint = CGPoint(x: 0, y: 0.5)
        layer.endPoint = CGPoint(x: 1, y: 0.5)
        layer.transform = transform
        layer.allowsEdgeAntialiasing = true
        return layer
    }()
    lazy var frontRightLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        let transform = CATransform3DMakeTranslation(size * 1.5 - cornerRadius, size / 2, 0)
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: cornerRadius, height: size)
        layer.colors = [UIColor(white: 1, alpha: 0),
                        UIColor(white: 1, alpha: 0.3)].map { $0.cgColor }
        layer.startPoint = CGPoint(x: 0, y: 0.5)
        layer.endPoint = CGPoint(x: 1, y: 0.5)
        layer.transform = transform
        layer.allowsEdgeAntialiasing = true
        return layer
    }()
    lazy var frontBottomLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: cornerRadius)
        layer.colors = [UIColor(white: 1, alpha: 0),
                        UIColor(white: 1, alpha: 0.2)].map { $0.cgColor }
        layer.transform = CATransform3DMakeTranslation(size / 2, size * 1.5 - cornerRadius, 0)
        layer.allowsEdgeAntialiasing = true
        return layer
    }()
    override func layoutSubviews() {
        ...
        frontLayer.addSublayer(frontTopLayer)
        frontLayer.addSublayer(frontRightLayer)
        frontLayer.addSublayer(frontLeftLayer)
        frontLayer.addSublayer(frontBottomLayer)
        rightLayer.addSublayer(rightTopLayer)
        rightLayer.addSublayer(rightLeftLayer)
        rightLayer.addSublayer(rightRightLayer)
        rightLayer.addSublayer(rightBottomLayer)
        topLayer.addSublayer(topTopLayer)
        topLayer.addSublayer(topBottomLayer)
        topLayer.addSublayer(topRightLayer)
        topLayer.addSublayer(topLeftLayer)
        ...
    }

If you set cornerRadius to an exaggerated value, it will look like a dice like this. figure10.png

Since the shape of the cube does not match the shadow, modify the path of shadowShapeLayer to the shape of the shadow that matches the cube.

    lazy var shadowShapeLayer: CAShapeLayer = {
        let layer = CAShapeLayer()
        layer.frame = CGRect(x: 0, y: 0, width: size * 2, height: size * 2)
        layer.fillColor = UIColor.black.cgColor
        let path = CGMutablePath()
        path.move(to: CGPoint(x: cornerRadius, y: size))
        path.addLine(to: CGPoint(x: size - cornerRadius, y: size))
        // curve
        path.addCurve(to: CGPoint(x: size, y: size - cornerRadius), control1: CGPoint(x: size, y: size), control2: CGPoint(x: size, y: size - cornerRadius))
        path.addLine(to: CGPoint(x: size, y: cornerRadius))
        // curve
        path.addCurve(to: CGPoint(x: size + cornerRadius, y: cornerRadius * 2), control1: CGPoint(x: size, y: 0), control2: CGPoint(x: size + cornerRadius, y: cornerRadius * 2))
        path.addLine(to: CGPoint(x: size * 1.5, y: size))
        path.addLine(to: CGPoint(x: size / 2 * 3, y: size * 2))
        path.addLine(to: CGPoint(x: size / 2, y: size * 2))
        path.addLine(to: CGPoint(x: cornerRadius, y: size + cornerRadius * 2))
        // curve
        path.addCurve(to: CGPoint(x: cornerRadius, y: size), control1: CGPoint(x: 0, y: size), control2: CGPoint(x: cornerRadius, y: size))
        path.closeSubpath()
        layer.path = path
        layer.allowsEdgeAntialiasing = true
        return layer
    }()

figure11.png

Make the rounded corners smaller to make them less noticeable. figure12.png

Shadow of ground plane

By drawing the shadow of the ground plane of the cube firmly, it is possible to express that the object is in contact with the ground surface, and the image can be tightened.

Let's realize it by setting shadow on bottomLayer which is the bottom of the cube. .. .. figure13.png

At first glance, it doesn't look strange, but if you look closely, the shadow on the left edge of the screen is strange. Another problem arises that the shadows are too dark even though the corners should be rounded. figure13-1.png

When shadow is set to bottomLayer by adding the surface shadowBaseLayer that is the target of shadow to the sublayer of bottomLayer and adding the background color, the shadow will be cast only on this shadowBaseLayer.

    lazy var bottomLayer: CALayer = {
        var transform = CATransform3DMakeTranslation(0, size / 2, 0)
        transform = CATransform3DRotate(transform, -CGFloat.pi / 2 , 1, 0, 0)
        let layer = createFaceLayer(with: transform, color: .clear)
        let shadowRadius: CGFloat = 3
        let shadowBaseLayer = CALayer()
        shadowBaseLayer.frame = CGRect(x: cornerRadius, y: cornerRadius, width: size - cornerRadius * 2, height: size - cornerRadius * 2)
        shadowBaseLayer.backgroundColor = UIColor.white.cgColor
        layer.addSublayer(shadowBaseLayer)
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowRadius = shadowRadius
        layer.shadowOpacity = 1
        layer.shadowOffset = CGSize(width: 1, height: -2)
        return layer
    }()

shadowBaseLayer expresses that the circumference of the bottom surface is not grounded by laying out only the rounded corners inside the rectangle of bottomLayer. It's the same as the area around the dice placed on the table is not exactly grounded. figure14.png figure15-2.png

Comparison before and after making

highlight/No shadow highlight/With shadow
figure15-1.png figure15-2.png

The realism of the picture has improved a little. The shape and condition have become easier to convey, but I don't know the material or texture of what it is made of, so it's an NG step as a drawing.

Material and texture

By drawing the material of the cube, the texture is expressed and the persuasive power is further increased.

If it is made of glass, it is necessary to express the reflection of the surrounding landscape where the cube is placed, and it seems difficult to express this with only the code. Also, I'm likely to use photos like last time. The surface of the plastic product is also smooth, so it is necessary to express the reflection and it is not tasteful. The same applies to metal. It is necessary to express the scratches and dirt on the surface of plaster, and it is harsh to express this with a code.

How about a tree? Since the annual rings have some regularity, I decided to draw the grain of the square lumber because it might be possible to express it.

Preparation

First, prepare to put a texture on each side. Change the color specification of the gradation added to the sublayer of each surface from grayscale to white alpha expression, Allow CGImage to be included in CALayer.contents on each side.

CubeView.swift


    lazy var frontLayer: CALayer = {
        ...
        return createGradientFaceLayer(with: transform,
                                       colors: [UIColor(white: 0, alpha: 0.6),
                                                UIColor(white: 0, alpha: 0.4)])
    }()

Creating textures

Added LumberTexture class. An object that manages the color of trees, the spacing between annual rings, the width, the darkness, and the color, and finally spits out CGImage. Dimension to set the spit Image on each layer.

By design The angle and shading of the cube are managed by CubeView, and LumberTexture manages only the texture information, so it should be an object that generates images of all 6 sides, but due to time constraints. I made only the three visible sides.

LumberTexture.swift


final class LumberTexture {
    //Annual rings
    struct Ring {
        let distance: CGFloat          //interval
        let width: CGFloat             //width
        let depth: CGFloat             //Darkness
        let colorComponents: [CGFloat] //color
    }
    //Length of one side
    let side: CGFloat
    //Wood color
    let baseColorComponents: [CGFloat] = [(226 / 255), (193 / 255), (146 / 255)]
    //Fine annual ring color RGB
    let smoothRingColorComponents: [CGFloat] = [(199 / 255), (173 / 255), (122 / 255)]
    //Rough annual ring color RGB
    let roughRingColorComponents: [[CGFloat]] = [
        [(176 / 255), (106 / 255), (71 / 255)],
        [(194 / 255), (158 / 255), (96 / 255)],
    ]
    //Detailed annual ring information
    private var roughRings: [Ring] = []
    //Rough annual ring information
    private var smoothRings: [Ring] = []
}

Information generation of annual rings

Fine annual rings are randomly prepared for the length of the diagonal line with a narrow feeling/narrow width. The color specifies a color that is less noticeable than the color of wood. The color is RGB sampled from the cypress wood that came out by googled appropriately.

LumberTexture.swift


    private func createSmoothRings() -> [Ring] {
        
        var smoothRings: [Ring] = []
        var pointer: CGFloat = 0
        
        repeat {
            let distance = CGFloat(Float.random(in: 2 ... 3))
            let width = CGFloat(Float.random(in: 0.5 ... 2))
            let depth = CGFloat(Float.random(in: 0.8 ... 1.0))
            let colorComponents = smoothRingColorComponents
            if (pointer + distance + width / 2) < (side * sqrt(2)) {
                smoothRings.append(Ring(distance: distance, width: width, depth: depth, colorComponents: colorComponents))
                pointer += distance
            } else {
                break
            }
            
        } while(pointer < (side * sqrt(2)))
        
        return smoothRings
    }

Rough annual rings have a wide spacing/wide width to specify a prominent color for the wood color.

LumberTexture.swift


    private func createRoughRings() -> [Ring] {
        
        var roughRings: [Ring] = []
        var pointer: CGFloat = 0
        
        repeat {
            let distance = CGFloat(Float.random(in: 5 ... 30))
            let width = CGFloat(Float.random(in: 2 ... 12))
            let depth = CGFloat(Float.random(in: 0.4 ... 0.6))
            let colorComponents = roughRingColorComponents[Int.random(in: 0 ... 1)]
            if (pointer + distance + width / 2) < (side * sqrt(2)) {
                roughRings.append(Ring(distance: distance, width: width, depth: depth, colorComponents: colorComponents))
                pointer += distance
            } else {
                break
            }
            
        } while(pointer < (side * sqrt(2)))
        
        return roughRings
    }

The top and side sides have different images, but if they do not have the same annual ring width, the Tsuji 褄 will not match, so the information on the annual rings is retained. Get an image Prepare a func and draw an annual ring with a pass like this.

Top image

LumberTexture.swift


func lumberTopImage() -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side, height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        // Draw base color
        context.setFillColor(UIColor(red: baseColorComponents[0],
                                     green: baseColorComponents[1],
                                     blue: baseColorComponents[2],
                                     alpha: 1).cgColor)
        context.fill(CGRect(x: 0, y: 0, width: side, height: side))
        
        // Draw annual tree rings
        [smoothRings, roughRings].forEach { rings in
            var pointer: CGFloat = 0
            rings.forEach { ring in
                pointer += ring.distance
                
                context.setLineWidth(ring.width)
                let startPoint = CGPoint(x: pointer, y: side)
                let endPoint = CGPoint(x: 0, y: side - pointer)
                context.move(to: startPoint)
                context.addCurve(to: endPoint,
                                  control1: CGPoint(x: pointer, y: side - pointer),
                                  control2: endPoint)
                let components: [CGFloat] = ring.colorComponents
                context.setStrokeColor(UIColor(red: components[0],
                                               green: components[1],
                                               blue: components[2],
                                               alpha: ring.depth).cgColor)
                context.strokePath()
            }
        }
        
        return context.makeImage()
    }

Since it is randomly generated, the pattern will be different each time, but the image on the top will look like this. Baumkuchen. top_image.png

Front image

The grain of wood that comes to the front is a shape in which this part of the upper surface is stretched and falls vertically. figure17-1.png

Draw the grain of the front from the information of the annual rings that you hold.

LumberTexture.swift


    func lumberSideImage() -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side * sqrt(2), height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        // Draw base color
        context.setFillColor(UIColor(red: baseColorComponents[0],
                                     green: baseColorComponents[1],
                                     blue: baseColorComponents[2],
                                     alpha: 1).cgColor)
        context.fill(CGRect(x: 0, y: 0, width: side * sqrt(2), height: side))
        
        // Draw smooth annual tree rings
        var pointer: CGFloat = 0
        smoothRings.forEach { ring in
            pointer += ring.distance
            
            context.setLineWidth(ring.width)
            let startPoint = CGPoint(x: pointer, y: 0)
            let endPoint = CGPoint(x: pointer, y: side)
            context.move(to: startPoint)
            context.addLine(to: endPoint)
            let components: [CGFloat] = ring.colorComponents
            context.setStrokeColor(UIColor(red: components[0],
                                           green: components[1],
                                           blue: components[2],
                                           alpha: ring.depth).cgColor)
            context.strokePath()
        }
        
        // Draw rough annual tree rings
        pointer = 0
        roughRings.forEach { ring in
            pointer += ring.distance
            
            context.setLineWidth(ring.width)
            let startPoint = CGPoint(x: pointer, y: 0)
            let endPoint = CGPoint(x: pointer, y: side)
            context.move(to: startPoint)
            context.addLine(to: endPoint)
            let components: [CGFloat] = ring.colorComponents
            context.setStrokeColor(UIColor(red: components[0],
                                           green: components[1],
                                           blue: components[2],
                                           alpha: ring.depth).cgColor)
            context.strokePath()
        }
        
        ...
    }

Draw from top to bottom with the same annual ring information as the top surface. figure17-2.png

However, it is unnatural that all annual rings are drawn in parallel/vertical like an artificial object, so Use CITwirlDistortion of CIFilter to distort a little.

LumberTexture.swift


    func lumberSideImage() -> CGImage? {
        
        ...
        
        // Distort the pattern
        if let image = context.makeImage() {
            
            let ciimage = CIImage(cgImage: image)
            let filter = CIFilter(name: "CITwirlDistortion")
            filter?.setValue(ciimage, forKey: kCIInputImageKey)
            filter?.setValue(CIVector(x: side * 1.2, y: -side / 3), forKey: kCIInputCenterKey)
            filter?.setValue(side * 1.3, forKey: kCIInputRadiusKey)
            filter?.setValue(CGFloat.pi / 8, forKey: kCIInputAngleKey)
            
            if let outputImage = filter?.outputImage {
                let cicontext = CIContext(options: nil)
                return cicontext.createCGImage(outputImage, from: CGRect(x: 0, y: 0, width: side, height: side))
            }
            
            return image
        }
        return nil
    }

side_image.png

Try to fit the image on the top and front. figure17-3.png

Image of the right side

The expression here is very difficult. .. Since it is necessary to express the cross sections of the annual rings on the upper surface and the front surface at the same time, and the distortion is also provided on the front surface, it seems that a considerably complicated calculation is required if all are expressed by paths. I didn't have any knowledge or man-hours, so I managed to make a mistake by trial and error.

First, draw the annual ring, which is the cross section of the upper surface. The pattern that continues from here must be generated. figure18-1.png

Tiling from the right edge 1px of the top image to create an image rotated 90 °.

LumberTexture.swift


    private func topTilingImage(topImage: CGImage) -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side, height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        if let cropImage = topImage.cropping(to: CGRect(x: side - 1, y: 0, width: 1, height: side)) {
            
            context.saveGState()
            context.rotate(by: -CGFloat.pi / 2)
            context.draw(cropImage, in: CGRect(x: 0, y: 0, width: side, height: side), byTiling: true)
            context.restoreGState()
            
            return context.makeImage()
        }
        return nil
    }

figure18-2.png

Next, draw the annual ring, which is the cross section of the front surface. It is necessary to generate a pattern that continues from here, but it is a little complicated due to the distorted effect. figure18-3.png

This also creates an image by tiling from the right end 1px of the image.

LumberTexture.swift


    private func sideTilingImage(sideImage: CGImage) -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side, height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        if let cropImage = sideImage.cropping(to: CGRect(x: side - 1, y: 0, width: 1, height: side)) {
            
            context.saveGState()
            context.draw(cropImage, in: CGRect(x: 0, y: 0, width: side, height: side), byTiling: true)
            context.restoreGState()
            return context.makeImage()
        }
        return nil
    }

figure18-4.png

According to the upper cross section According to the front cross section
figure18-5.png figure18-6.png
It looks good at first glance, but it doesn't match the distorted part on the front. .. The part that continues from the distortion on the front seems to be good here. ..

From a 3D perspective, the front cross section is a little, and the rest should be a basic upper cross section pattern? Like this? It's a little unnatural, but is it okay? stretch_image.png

The front section image with gradation mask is combined with the upper section image.

LumberTexture.swift


    func lumberStrechImage(topImage: CGImage?, sideImage: CGImage?) -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side, height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        if let topImage = topImage,
           let tilingImage = topTilingImage(topImage: topImage) {
            
            context.saveGState()
            context.draw(tilingImage, in: CGRect(x: 0, y: 0, width: side, height: side))
            context.restoreGState()
        }
        
        if let sideImage = sideImage,
            let tilingImage = sideTilingImage(sideImage: sideImage),
            let maskedImage = gradientMaskedImage(image: tilingImage) {
            
            context.saveGState()
            context.draw(maskedImage, in: CGRect(x: 0, y: 0, width: side, height: side))
            context.restoreGState()
        }
        
        return context.makeImage()
    }

LumberTexture.swift


    private func gradientMaskedImage(image: CGImage) -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side, height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        if let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceGray(),
                                     colors: [UIColor.black.cgColor,
                                              UIColor.white.cgColor] as CFArray,
                                     locations: [0.8, 1.0]) {

            context.saveGState()
            context.drawLinearGradient(gradient,
                                       start: CGPoint(x: 0, y: 0),
                                       end: CGPoint(x: side / 4, y: side / 8),
                                       options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
            context.restoreGState()
            if let maskImage = context.makeImage(),
               let mask = CGImage(maskWidth: maskImage.width,
                                  height: maskImage.height,
                                  bitsPerComponent: maskImage.bitsPerComponent,
                                  bitsPerPixel: maskImage.bitsPerPixel,
                                  bytesPerRow: maskImage.bytesPerRow,
                                  provider: maskImage.dataProvider!,
                                  decode: nil,
                                  shouldInterpolate: false) {
                
                return image.masking(mask)
            }
        }
        return nil
    }

I'm not so confident, but I cheated like this. figure18-7.png

Preservation of wood grain

Each time the wood grain is drawn randomly, it will have a different width and color thickness. If you can continue the grain you like, it will help you to compare when you make it.

Make the Ring structure Codable.

LumberTexture.swift


final class LumberTexture {
    struct Ring: Codable {
        let distance: CGFloat
        let width: CGFloat
        let depth: CGFloat
        let colorComponents: [CGFloat]
    }
    ...

Add a save function to the part that holds the information of annual rings. Since Ring is set to Codable, read/write to UserDefaults as it is using JSONDecoder/JSONEncoder.

LumberTexture.swift


    private func createSmoothRings() -> [Ring] {
        
        // Restore saved rings from UserDefaults
        if let data = UserDefaults.standard.data(forKey: "SmoothRings"),
              let rings = try? JSONDecoder().decode([Ring].self, from: data) {
            
            return rings
        }
        
        //Generation process
        
        // Save rings to UserDefaults
        UserDefaults.standard.setValue(try? JSONEncoder().encode(smoothRings), forKeyPath: "SmoothRings")
        
        return smoothRings
    }

Recreate the grain only when you tap it.

CubeView.swift


    override func layoutSubviews() {
        super.layoutSubviews()
        
        ...
        
        updateTexture()
        addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(updateTextureAction)))
    }

CubeView.swift


    private func updateTexture() {
        
        let topImage = texture.lumberTopImage()
        let sideImage = texture.lumberSideImage()
        topLayer.contents = topImage
        frontLayer.contents = sideImage
        rightLayer.contents = texture.lumberStrechImage(topImage: topImage, sideImage: sideImage)
    }
    
    @objc private func updateTextureAction() {
        
        texture.updateRings()
        updateTexture()
    }

LumberTexture.swift


final class LumberTexture {

    ...
    
    func updateRings() {
        
        UserDefaults.standard.removeObject(forKey: "SmoothRings")
        UserDefaults.standard.removeObject(forKey: "RoughRings")
        self.smoothRings = createSmoothRings()
        self.roughRings = createRoughRings()
    }
}

With this, the grain will not be updated unless you tap it.

Highlight adjustment

figure19-0.png Adjust the brightness because the highlight of the part in contact with the upper surface is too strong.

--- a/Cube/CubeView.swift
+++ b/Cube/CubeView.swift
@@ -65,7 +65,7 @@ final class CubeView: UIView {
     lazy var frontTopLayer: CAGradientLayer = {
         let layer = CAGradientLayer()
         layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: cornerRadius)
-        layer.colors = [UIColor(white: 1, alpha: 0.8),
+        layer.colors = [UIColor(white: 1, alpha: 0.3),
                         UIColor(white: 1, alpha: 0)].map { $0.cgColor }
         layer.transform = CATransform3DMakeTranslation(size / 2, size / 2, 0)
         layer.allowsEdgeAntialiasing = true
@@ -107,7 +107,7 @@ final class CubeView: UIView {
     lazy var rightTopLayer: CAGradientLayer = {
         let layer = CAGradientLayer()
         layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: cornerRadius)
-        layer.colors = [UIColor(white: 1, alpha: 0.8),
+        layer.colors = [UIColor(white: 1, alpha: 0.3),
                         UIColor(white: 1, alpha: 0)].map { $0.cgColor }
         layer.transform = CATransform3DMakeTranslation(size / 2, size / 2, 0)
         layer.allowsEdgeAntialiasing = true
@@ -172,7 +172,7 @@ final class CubeView: UIView {
         let transform = CATransform3DMakeTranslation(size * 1.5 - cornerRadius, size / 2, 0)
         layer.frame = CGRect(x: -size / 2, y: -size / 2, width: cornerRadius, height: size)
         layer.colors = [UIColor(white: 1, alpha: 0),
-                        UIColor(white: 1, alpha: 1)].map { $0.cgColor }
+                        UIColor(white: 1, alpha: 0.3)].map { $0.cgColor }
         layer.startPoint = CGPoint(x: 0, y: 0.5)
         layer.endPoint = CGPoint(x: 1, y: 0.5)
         layer.transform = transform
@@ -183,7 +183,7 @@ final class CubeView: UIView {
         let layer = CAGradientLayer()
         layer.frame = CGRect(x: -size / 2, y: -size / 2, width: size, height: cornerRadius)
         layer.colors = [UIColor(white: 1, alpha: 0),
-                        UIColor(white: 1, alpha: 1)].map { $0.cgColor }
+                        UIColor(white: 1, alpha: 0.3)].map { $0.cgColor }
         layer.transform = CATransform3DMakeTranslation(size / 2, size * 1.5 - cornerRadius, 0)
         layer.allowsEdgeAntialiasing = true
         return layer

figure19-1.png

Check it in grayscale.

grayscale.diff


diff --git a/Cube/LumberTexture.swift b/Cube/LumberTexture.swift
index a5cdbb4..1df3b2c 100644
--- a/Cube/LumberTexture.swift
+++ b/Cube/LumberTexture.swift
@@ -116,7 +116,7 @@ extension LumberTexture {
         context.setFillColor(UIColor(red: baseColorComponents[0],
                                      green: baseColorComponents[1],
                                      blue: baseColorComponents[2],
-                                     alpha: 1).cgColor)
+                                     alpha: 1).convertToGrayScaleColor().cgColor)
         context.fill(CGRect(x: 0, y: 0, width: side, height: side))
         
         // Draw annual tree rings
@@ -136,7 +136,7 @@ extension LumberTexture {
                 context.setStrokeColor(UIColor(red: components[0],
                                                green: components[1],
                                                blue: components[2],
-                                               alpha: ring.depth).cgColor)
+                                               alpha: ring.depth).convertToGrayScaleColor().cgColor)
                 context.strokePath()
             }
         }
@@ -153,7 +153,7 @@ extension LumberTexture {
         context.setFillColor(UIColor(red: baseColorComponents[0],
                                      green: baseColorComponents[1],
                                      blue: baseColorComponents[2],
-                                     alpha: 1).cgColor)
+                                     alpha: 1).convertToGrayScaleColor().cgColor)
         context.fill(CGRect(x: 0, y: 0, width: side * sqrt(2), height: side))
         
         // Draw smooth annual tree rings
@@ -170,7 +170,7 @@ extension LumberTexture {
             context.setStrokeColor(UIColor(red: components[0],
                                            green: components[1],
                                            blue: components[2],
-                                           alpha: ring.depth).cgColor)
+                                           alpha: ring.depth).convertToGrayScaleColor().cgColor)
             context.strokePath()
         }
         
@@ -188,7 +188,7 @@ extension LumberTexture {
             context.setStrokeColor(UIColor(red: components[0],
                                            green: components[1],
                                            blue: components[2],
-                                           alpha: ring.depth).cgColor)
+                                           alpha: ring.depth).convertToGrayScaleColor().cgColor)
             context.strokePath()
         }
         
@@ -301,3 +301,12 @@ extension LumberTexture {
         return nil
     }
 }
+
+extension UIColor {
+    func convertToGrayScaleColor() -> UIColor {
+        var grayscale: CGFloat = 0
+        var alpha: CGFloat = 0
+        self.getWhite(&grayscale, alpha: &alpha)
+        return UIColor(white: grayscale, alpha: alpha)
+    }
+}

grayscale.png There seems to be no problem with brightness. I want to make the wood grain a little more realistic.

Improve the realism of wood grain

Since the width of the color that is the base of the wood grain is monochromatic, it gives a monotonous impression, and the color seems a little dark, so increase the number of colors.

LumberTexture.swift


final class LumberTexture {
    ...
    let baseColorComponents: [CGFloat] = [(255 / 255), (227 / 255), (220 / 255)]
    let centerBaseColorComponents: [[CGFloat]] = [
        [(205 / 255), (175 / 255), (131 / 255)],
        [(201 / 255), (138 / 255), (40 / 255)],
    ]
    ...

Add a two-color gradation with a width of 80% to the base drawing part of the annual ring drawing part on the upper surface to make it look natural.

LumberTexture.swift


    func lumberTopImage() -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side, height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        // Draw base color
        ...
        
        if let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
                                     colors: [
                                        UIColor(red: centerBaseColorComponents[0][0],
                                                green: centerBaseColorComponents[0][1],
                                                blue: centerBaseColorComponents[0][2],
                                                alpha: 0.3).cgColor,
                                        UIColor(red: centerBaseColorComponents[1][0],
                                                green: centerBaseColorComponents[1][1],
                                                blue: centerBaseColorComponents[1][2],
                                                alpha: 1).cgColor] as CFArray,
                                     locations: [0.7, 1.0]) {
            
            context.drawRadialGradient(gradient,
                                       startCenter: CGPoint(x: 0, y: side),
                                       startRadius: 0,
                                       endCenter: CGPoint(x: 0, y: side),
                                       endRadius: side * 0.8,
                                       options: [.drawsBeforeStartLocation])
        }
        ...
    }

The drawing on the front is also changed to follow the change on the top.

LumberTexture.swift


    func lumberSideImage() -> CGImage? {
        
        UIGraphicsBeginImageContext(CGSize(width: side * sqrt(2), height: side))
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        
        // Draw base color
        ...
        
        if let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
                                     colors: [
                                        UIColor(red: centerBaseColorComponents[0][0],
                                                green: centerBaseColorComponents[0][1],
                                                blue: centerBaseColorComponents[0][2],
                                                alpha: 0.3).cgColor,
                                        UIColor(red: centerBaseColorComponents[1][0],
                                                green: centerBaseColorComponents[1][1],
                                                blue: centerBaseColorComponents[1][2],
                                                alpha: 1).cgColor] as CFArray,
                                     locations: [0.7, 1.0]) {
            
            context.drawLinearGradient(gradient,
                                       start: CGPoint.zero,
                                       end: CGPoint(x: side * 0.8, y: 0),
                                       options: [.drawsBeforeStartLocation])
        }
        ...
    }

Adjust the color because some colors of thick annual rings are too conspicuous

LumberTexture.swift


    let roughRingColorComponents: [[CGFloat]] = [
        [(176 / 255), (130 / 255), (71 / 255)], <-
        [(194 / 255), (158 / 255), (96 / 255)],
    ]
Top surface Front right side
figure20-1.png figure20-2.png figure20-3.png
Before After
figure20-5.png figure20-4.png

Cutting marks

If you look closely at the wood, you may find that there are some scratches left on it when it is processed differently from the wood fibers. It seems that the rest is left by cutting in the direction against the fibers of the tree. Draw slightly artificially at regular intervals.

LumberTexture.swift


    func lumberTopImage() -> CGImage? {
        
        ...
        
        // Draw scratch
        var pointer: CGFloat = 0
        repeat {
            
            context.setLineWidth(1)
            let startPoint = CGPoint(x: 0, y: pointer * sqrt(2))
            let endPoint = CGPoint(x: pointer * sqrt(2), y: 0)
            context.move(to: startPoint)
            context.addLine(to: endPoint)
            let alpha = (1 - pointer / side * sqrt(2))
            context.setStrokeColor(UIColor(white: 1, alpha: alpha).cgColor)
            context.strokePath()
            
            pointer += 6
        } while(pointer < side * sqrt(2))
        
        return context.makeImage()
    }

Draw so that the effect gradually disappears from the back to the front of the top surface. It looks like this when drawn in red for easy understanding.

figure20-7.png

Before After
figure20-4.png figure20-6.png

The effect is not high, but a color width and a new direction have been added to the upper surface.

play

Tap to switch grain

movie.gif

Separate the definition of color information

Make it customizable by allowing color information to be entered from the outside. Allows you to define and specify LumberColorSet as a structure.

LumberColorSet.swift


struct LumberColorSet {
    let baseColorComponents: [CGFloat]
    let centerBaseColorComponents: [[CGFloat]]
    let smoothRingColorComponents: [CGFloat]
    let roughRingColorComponents: [[CGFloat]]
    
    static var `default`: LumberColorSet {
        return .init(baseColorComponents: [
                        (255 / CGFloat(255)), (227 / CGFloat(255)), (220 / CGFloat(255))
                     ],
                     centerBaseColorComponents: [
                        [(205 / CGFloat(255)), (175 / CGFloat(255)), (131 / CGFloat(255))],
                        [(201 / CGFloat(255)), (138 / CGFloat(255)), (40 / CGFloat(255))],
                     ],
                     smoothRingColorComponents: [
                        (199 / CGFloat(255)), (173 / CGFloat(255)), (122 / CGFloat(255))
                     ],
                     roughRingColorComponents: [
                        [(176 / CGFloat(255)), (130 / CGFloat(255)), (71 / CGFloat(255))],
                        [(194 / CGFloat(255)), (158 / CGFloat(255)), (96 / CGFloat(255))],
                     ])
    }
}

LumberTexture.swift


final class LumberTexture {
    ...
    private var colorSet: LumberColorSet = LumberColorSet.default
    
    init(side: CGFloat, colorSet: LumberColorSet = LumberColorSet.default) {
        self.side = side
        self.base = createBase()
        self.smoothRings = createSmoothRings()
        self.roughRings = createRoughRings()
    }

Christmas color

Let's define the Christmas color.

LumberColorSet.swift


struct LumberColorSet {
    ...
    static var xmas: LumberColorSet {
        return .init(baseColorComponents: [
                        (255 / CGFloat(255)), (245 / CGFloat(255)), (193 / CGFloat(255))
                     ],
                     centerBaseColorComponents: [
                        [(105 / CGFloat(255)), (58 / CGFloat(255)), (24 / CGFloat(255))],
                        [(223 / CGFloat(255)), (176 / CGFloat(255)), (39 / CGFloat(255))],
                     ],
                     smoothRingColorComponents: [
                        (0 / CGFloat(255)), (162 / CGFloat(255)), (95 / CGFloat(255))
                     ],
                     roughRingColorComponents: [
                        [(160 / CGFloat(255)), (28 / CGFloat(255)), (34 / CGFloat(255))],
                        [(255 / CGFloat(255)), (0 / CGFloat(255)), (0 / CGFloat(255))],
                     ])
    }
    ...
}

#FFF5C1 #693A18 #DFB027 #00A25F #A01C22 #FF0000 Repeat tapping until you get a nice pattern. It might be a little scary. It looks like a watermelon. figure21-1.png

Extract colors from images

Since it is troublesome to define RGB, I wrote a code to extract typical colors from an image.

LumberColorSet.swift


    init?(image: UIImage) {
        
        guard let components = image.cgImage?.getPixelColors(count: 6),
              components.count == 6 else {
            return nil
        }
        
        self.init(baseColorComponents: [
                    components[0][0], components[0][1], components[0][2]
                  ],
                  centerBaseColorComponents: [
                    [components[1][0], components[1][1], components[1][2]],
                    [components[2][0], components[2][1], components[2][2]]
                  ],
                  smoothRingColorComponents: [
                    components[3][0], components[3][1], components[3][2]
                  ],
                  roughRingColorComponents: [
                    [components[4][0], components[4][1], components[4][2]],
                    [components[5][0], components[5][1], components[5][2]]
                  ])
    }

Try to extract the representative color of the image using CIKMeans of CIFilter added from iOS14. It seems to be an algorithm that extracts representative colors by the k-means method.

Since 6 colors are required, give " inputMeans " 6 typical colors and make it an attribute so that filter.outputImage can be obtained by 6 x 1. For the attributes, I referred to this URL.

LumberColorSet.swift


    private func colorSampleImage(count: Int) -> CGImage? {
        
        let inputImage = CIImage(cgImage: self)

        guard let filter = CIFilter(name: "CIKMeans") else { return nil }
        filter.setDefaults()
        filter.setValue(inputImage, forKey: kCIInputImageKey)
        filter.setValue(inputImage.extent, forKey: kCIInputExtentKey)
        filter.setValue(64, forKey: "inputCount")
        filter.setValue(10, forKey: "inputPasses")
        let seeds = [CIColor(red: 0, green: 0, blue: 0),    // black
                     CIColor(red: 1, green: 0, blue: 0),    // red
                     CIColor(red: 0, green: 1, blue: 0),    // green
                     CIColor(red: 1, green: 1, blue: 0),    // yellow
                     CIColor(red: 0, green: 0, blue: 1),    // blue
                     CIColor(red: 1, green: 1, blue: 1)]    // white
        filter.setValue(seeds, forKey: "inputMeans")
        guard let outputImage = filter.outputImage else { return nil }

        let context = CIContext(options: nil)
        return context.createCGImage(outputImage, from: CGRect(origin: .zero, size: outputImage.extent.size))
    }

Get RGB pixel by pixel from the 6 x 1 image obtained above.

LumberColorSet.swift


    private func getPixelColors(count: Int) -> [[CGFloat]] {
        
        var components: [[CGFloat]] = []
        guard let importantColorImage = colorSampleImage(count: count) else { return components }
        
        (0...count).forEach { index in
            
            let scale: CGFloat = 1
            let rect = CGRect(x: CGFloat(index) * scale, y: 0, width: scale, height: scale)
            if let cropImage = importantColorImage.cropping(to: rect),
               let color = cropImage.averageColor() {
                
                var r: CGFloat = 0
                var g: CGFloat = 0
                var b: CGFloat = 0
                var a: CGFloat = 0
                color.getRed(&r, green: &g, blue: &b, alpha: &a)
                components.append([r, g, b])
            }
        }
        return components
    }

Extract the average color from a 1 x 1 image.

    private func averageColor() -> UIColor? {

        let inputImage = CIImage(cgImage: self)

        guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: inputImage.extent]) else { return nil }
        guard let outputImage = filter.outputImage else { return nil }

        var bitmap = [UInt8](repeating:0,count:4)
        let context = CIContext(options: nil)
        context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)

        return UIColor(red: CGFloat(bitmap[0]) / 255, green: CGFloat(bitmap[1]) / 255, blue: CGFloat(bitmap[2]) / 255, alpha: CGFloat(bitmap[3]) / 255)
    }

Results extracted from the Christmas tree

The extracted results have the following colors. #063902 #241410 #070707 #080808 #050505 #797979

As mentioned in the referenced site, the extracted colors are not as intended, but by the k-means method? There seems to be a correction, so let's adjust the saturation and brightness below.

let color = cropImage.averageColor()?.color(mimimumBrightness: 0.5).color(mimimumSaturation: 0.5)
extension UIColor {
    
    func color(mimimumBrightness: CGFloat) -> UIColor {
        
        var h: CGFloat = 0
        var s: CGFloat = 0
        var b: CGFloat = 0
        var a: CGFloat = 0
        getHue(&h, saturation: &s, brightness: &b, alpha: &a)
        
        if b < mimimumBrightness {
            return UIColor(hue: h, saturation: s, brightness: mimimumBrightness, alpha: a)
        }
        return self
    }
    
    func color(mimimumSaturation: CGFloat) -> UIColor {
        
        var h: CGFloat = 0
        var s: CGFloat = 0
        var b: CGFloat = 0
        var a: CGFloat = 0
        getHue(&h, saturation: &s, brightness: &b, alpha: &a)
        
        if s < mimimumSaturation {
            return UIColor(hue: h, saturation: mimimumSaturation, brightness: b, alpha: a)
        }
        return self
    }
}

The result of adjusting only the brightness #0D8004 #804739 #808080 #808080 #808080 #808080

The result of adjusting both brightness and saturation #0D8004 #804739 #804040 #804040 #804040 #804040

Hmmm ... I didn't get much expected value. figure22-1.png

I will try it with an image that seems to be clear. Raster color. rasta.jpg

#010101 #560200 #035500 #545400 #000000 #000000 Colors that play with lightness and saturation #804040 #800300 #058000 #808000 #804040 #804040

Somehow I got a similar color, but after all unintended correction is applied. figure22-2.png

I sampled colors from other wood grain images such as cedar and red pine and applied them. I didn't get very good results, so I will omit it. ..

Impressions

I never thought of drawing an organic thing called wood grain with code. It was fun to make trial and error as to what kind of irregularity should be added to make it more realistic, as there were discoveries that could be incorporated into the code by finding regularity. I wanted to put the knots of the tree somewhere, but I gave up because it was difficult.

Recommended Posts

Try drawing a cube with View and Layer
View Slider with GWT and SmartGWT
Try running MySql and Blazor with docker-compose
Let's try WebSocket with Java and javascript!
Draw a graph with Sinatra and Chartkick
Try writing "Hello, World" with a combination of various languages and libraries
Try to link Ruby and Java with Dapr
Transition to a view controller with Swift WebKit
Prepare a scraping environment with Docker and Java
A simple rock-paper-scissors game with JavaFX and SceneBuilder
Try debugging a Java program with VS Code
Try using DI container with Laravel and Spring Boot
NLP4J [005-1] Try Twitter analysis with Twitter4J and NLP4J (data collection)
Design and implement a breakout game with a clean architecture
Design and implement a breakout game with a clean architecture
[Ruby] I made a crawler with anemone and nokogiri.
Run Mosquitto with Docker and try WebSocket communication with MQTT
Create a JAVA WEB application and try OMC APM
Try building Java into a native module with GraalVM