Reputation: 495
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:
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:
How can I improve the performance of stroke rendering in while using float-based distance calculations?
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
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