Rezwan Khan chowdhury
Rezwan Khan chowdhury

Reputation: 495

Performance slowdown and inaccuracies in stroke rendering due to float-based distance calculation in drawing

Description:

I'm currently working on a Drawing application and facing challenges with stroke rendering due to the use of float-based distance calculations. While attempting to convert integer distances to float values for more precise stroke placement, I encountered significant performance slowdowns and inaccuracies in the stroke rendering process.

Details:

In my CustomStrokeDrawingView class, I've implemented stroke rendering logic that relies on calculating distances between consecutive points using float values. However, the conversion from integer distances to float values has introduced performance issues and inaccuracies in the stroke rendering behavior. See the following image: enter image description here

Here’s what I have done so far :


import UIKit
import AVKit

class CustomStrokeDrawingView: UIView {
    
    var shape: UIImage!
    var shape2: UIImage!
    var strockColor: UIColor = .black
    var pts: [CGPoint] = []
    var pts2: [CGPoint] = []
    var shapeSize: CGSize = CGSize(width: 40, height: 40)
    var pixelsPerIteration: CGFloat = 1.0
    var space: Int = 1
    var brushAlpha: CGFloat = 1.0
    var blendMode: CGBlendMode = .normal
    var loopIndex = 0
    var angle:Bool = true
    
    
    var dynamicOpacity:Float = 0
    var dynamicScale:Float = 0
    var degree:Float = 0
    
    var isColorActive = true
    let angles:[CGFloat] = [10,20,30,40,50]
    
    var filter = compositingFilterStrings[0]
    var layerarray:[CALayer] = []
    
    var previousPoint: CGPoint!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        // crash if we can't load the "brush" image
        guard let img =  UIImage(named:"shape-8") else {
            fatalError("Could not load shape image!")
        }
        shape = img.maskWithColor(color: .red)
        
    }
    
    func setShape(image:UIImage, setting:Setting){
        shape = image
        if isColorActive{
            shape = image.maskWithColor(color: strockColor)
            
            updateShape(setting: setting)
        }
        
        
    }
    
    func updateShape(setting:Setting){
        let newSize = AVMakeRect(
             aspectRatio: shape.size,
             insideRect: CGRect(
                 origin: .zero,
                 size: CGSize(
                     width: Int(setting.size),
                     height: Int(setting.size)))).size
        self.shapeSize = newSize
    }
    
    
    func setstrockColor(color:UIColor){
        strockColor = color
        if isColorActive{
            shape = shape.maskWithColor(color: color)
        }
        
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        print("kb touch")
        
        for touch in touches
        {
            let pressure = touch.force
            print("pressure is: \(String(describing: pressure))")
        }
        
        
        
        
        
        let touch = touches.first
        if let startPoint = touch?.location(in: self) {
            // reset points array to only the starting point
            //print("Start:", startPoint)
            loopIndex = 0
            pts = [startPoint]
            pts2 = [startPoint]
            layerarray =  []
            previousPoint = nil
            setNeedsDisplay()
        }
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        if let touchPoint = touch?.location(in: self) {
            let x = Int(touchPoint.x)
            let y = Int(touchPoint.y)
            
            pts.append(CGPoint(x: x, y: y))
            pts2 = interpolatePoints(from: pts)
            
            setNeedsDisplay()
        }
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        guard pts2.count > 0 else { return }
        
        for idx in loopIndex..<(pts2.count) {
            let startx: CGFloat = pts2[idx].x
            let starty: CGFloat = pts2[idx].y
            let dstRect: CGRect = .init(x: 0.0, y: 0.0, width: shapeSize.width, height: shapeSize.height)
            
            let x: CGFloat = startx
            let y: CGFloat = starty 
            
            
            
            if isAdd(p: pts2[idx], space: getSpace()){
                previousPoint = pts2[idx]
                let imageLayer = CALayer()
            //    imageLayer.backgroundColor = UIColor.red.cgColor
                if angle{
                    imageLayer.rotate(angle: angles.randomElement()!)
                }else{
                    imageLayer.rotate(angle: CGFloat(self.degree))
                }
                
                let maxScale:CGFloat = 1.0
                
                
                let s = 1.0 - dynamicScale
                let scale:CGFloat = CGFloat(s)+(CGFloat(idx)/400.0) <= maxScale ? CGFloat(s)+(CGFloat(idx)/400.0) : maxScale
                
                
                // DispatchQueue.main.asyncAfter(deadline: .now()+0.02) {
                imageLayer.transform = CATransform3DScale(imageLayer.transform, scale, scale, 1)
                //  }
                
                
                
                
                let minopacity:CGFloat = CGFloat(1.0-dynamicOpacity)
                let dunamicOpacity:CGFloat = minopacity+(CGFloat(idx)/300.0) <= brushAlpha ? minopacity+(CGFloat(idx)/300.0) : brushAlpha
                
                
                
                imageLayer.opacity = Float(dunamicOpacity)
                
                
                imageLayer.bounds = dstRect
                imageLayer.position = CGPoint(x:x ,y:y)
                imageLayer.contents = shape?.cgImage
                imageLayer.name = "Brush"
                imageLayer.compositingFilter = filter
                imageLayer.minificationFilterBias = 10
                // layerarray.append(imageLayer)
                self.layer.addSublayer(imageLayer)
                
            }
            
            
            loopIndex = idx
            
        }
        
        
    }
    
    
    func isAdd(p:CGPoint, space:Int)->Bool{
        
        if let previousPoint = previousPoint{
            let a = max((abs(previousPoint.x - p.x)),(abs(previousPoint.y - p.y)))
            
            if a >= (CGFloat(space)){
                return  true
            }else{
                //print(a)
                return  false
            }
        }else{
            return  true
            
        }
        
    }
    
    
    
    func generateArray(count: Int, max: Double, d: Double) -> [Double] {
        var a1 = [Double]()
        var current = 1.0
        var increasing = true
        
        for _ in 0..<count {
            a1.append(current)
            
            if increasing {
                current += d
                if current >= max {
                    increasing = false
                }
            } else {
                current -= d
                if current <= 1 {
                    increasing = true
                }
            }
        }
        
        return a1
    }
    
    
    
    func interpolatePoints(from array: [CGPoint]) -> [CGPoint] {
        var result: [CGPoint] = []
        var prevPoint: CGPoint?
        
        for point in array {
            if let prev = prevPoint {
                let diffX = point.x - prev.x
                let diffY = point.y - prev.y
                
                
                if abs(diffX) != 0 || abs(diffY) != 0 {
                    let steps = max(abs(diffX), abs(diffY))
                    
                    for i in 0..<Int(steps ){
                        let newX = prev.x + (diffX * CGFloat(i) / CGFloat(steps))
                        let newY = prev.y + (diffY * CGFloat(i) / CGFloat(steps))
                        
                        if (Int(prev.x) == Int(newX) && Int(prev.y) == Int(newY)){
                            
                        }else{
                            
                            let p = CGPoint(x: Double(Int(newX)), y: Double(Int(newY)))
                            result.append(p)
                            
                            
                        }
                        
                    }
                }
            }
            
            if let p = prevPoint{
                if (Int(p.x) == Int(point.x) && Int(p.y) == Int(point.y)){
                }else{
                    result.append(CGPoint(x: Int(point.x), y: Int(point.y)))
                    
                }
            }
            
            prevPoint = point
        }
        if (result.count > 0){
            result.remove(at: 0)
        }
        return result
    }
    
    
}


extension CustomStrokeDrawingView{
    
    func getSpace()->Int{
//        let width = Int(shapeSize.width)
//        
//        let space =  Int((width)/2) + space
//        print(space)
      //  return Int((width)/2) + space
        return space
    }
    
    func updateSetting(setting:Setting){
       let newSize = AVMakeRect(
            aspectRatio: shape.size,
            insideRect: CGRect(
                origin: .zero,
                size: CGSize(
                    width: Int(setting.size),
                    height: Int(setting.size)))).size
        
        
        self.shapeSize = newSize
       // print("----->>>ppp \(self.shapeSize) -----> \(shape.size)")
        self.brushAlpha = CGFloat(setting.opacity)
        self.blendMode = setting.blendMode
        self.space = Int(setting.distance)
        self.angle = setting.angle
        self.filter = setting.filter
        self.dynamicOpacity = setting.dynamicOpacity
        self.dynamicScale = setting.dynamicScale
        self.degree = setting.degree
        
    }
    
    func clearDraw(){
        if let layer = self.layer.sublayers{
            for i in layer{
                if i.name == "Brush"{
                    i.removeFromSuperlayer()
                }
            }
        }
        
    }
}


In this method, I attempt to calculate the squared distance between consecutive points using float arithmetic to ensure precise stroke placement.

    func isAdd(p:CGPoint, space:Int)->Bool{
        
        if let previousPoint = previousPoint{
            let a = max((abs(previousPoint.x - p.x)),(abs(previousPoint.y - p.y)))
            
            if a >= (CGFloat(space)){
                return  true
            }else{
                //print(a)
                return  false
            }
        }else{
            return  true
            
        }
        
    }
    
    
    
    func generateArray(count: Int, max: Double, d: Double) -> [Double] {
        var a1 = [Double]()
        var current = 1.0
        var increasing = true
        
        for _ in 0..<count {
            a1.append(current)
            
            if increasing {
                current += d
                if current >= max {
                    increasing = false
                }
            } else {
                current -= d
                if current <= 1 {
                    increasing = true
                }
            }
        }
        
        return a1
    }

Question:

  1. How can I improve the performance of stroke rendering in while using float-based distance calculations?

  2. Are there alternative approaches or optimizations that can ensure accurate stroke placement without compromising performance?

Any insights or suggestions on this would be greatly appreciated!

Upvotes: 1

Views: 68

Answers (1)

jrturton
jrturton

Reputation: 119292

Are you 100% sure where your performance issues are coming from? Have you profiled on a device, in release mode, using instruments? I don’t understand why you’re moving between Int and Float representations of the touched points - leave them as they are otherwise you are going to lose accuracy.

Creating a huge amount of new layer objects in every call of draw(_ rect:) is a huge red flag for me, as is setting a bunch of implicitly animatable properties in there. If you want to repeat an image with various transformations applied to it, you might want to look at CAReplicatorLayer. A more traditional approach to a drawing-type app is to use CAShapeLayer(s) with paths described by the user’s touches. But in any case, you should only create and add sub layers that aren’t already there, otherwise after a small number of calls to the draw method you will have stacks of redundant layers that will definitely kill your performance.

Upvotes: 1

Related Questions