There are numerous questions about creating an eraser tool in CoreGraphics. I cannot find one that matches "pixilated".
Here's the situation. I'm playing with a simple drawing project. The pen tools work fine. The eraser tool is horribly pixilated. Here's a screen shot of what I mean:
Here's the drawing code I'm using (UPDATED):
// DrawingView
//
//
// Created by David DelMonte on 12/9/16.
// Copyright © 2016 David DelMonte. All rights reserved.
//
import UIKit
public protocol DrawingViewDelegate {
func didBeginDrawing(view: DrawingView)
func isDrawing(view: DrawingView)
func didFinishDrawing(view: DrawingView)
func didCancelDrawing(view: DrawingView)
}
open class DrawingView: UIView {
//initial settings
public var lineColor: UIColor = UIColor.black
public var lineWidth: CGFloat = 10.0
public var lineOpacity: CGFloat = 1.0
//public var lineBlendMode: CGBlendMode = .normal
//used for zoom actions
public var drawingEnabled: Bool = true
public var delegate: DrawingViewDelegate?
private var currentPoint: CGPoint = CGPoint()
private var previousPoint: CGPoint = CGPoint()
private var previousPreviousPoint: CGPoint = CGPoint()
private var pathArray: [Line] = []
private var redoArray: [Line] = []
var toolType: Int = 0
let π = CGFloat(M_PI)
private let forceSensitivity: CGFloat = 4.0
private struct Line {
var path: CGMutablePath
var color: UIColor
var width: CGFloat
var opacity: CGFloat
//var blendMode: CGBlendMode
init(path : CGMutablePath, color: UIColor, width: CGFloat, opacity: CGFloat) {
self.path = path
self.color = color
self.width = width
self.opacity = opacity
//self.blendMode = blendMode
}
}
override public init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.backgroundColor = UIColor.clear
}
override open func draw(_ rect: CGRect) {
let context : CGContext = UIGraphicsGetCurrentContext()!
for line in pathArray {
context.setLineWidth(line.width)
context.setAlpha(line.opacity)
context.setLineCap(.round)
switch toolType {
case 0: //pen
context.setStrokeColor(line.color.cgColor)
context.addPath(line.path)
context.setBlendMode(.normal)
break
case 1: //eraser
context.setStrokeColor(UIColor.clear.cgColor)
context.addPath(line.path)
context.setBlendMode(.clear)
break
case 3: //multiply
context.setStrokeColor(line.color.cgColor)
context.addPath(line.path)
context.setBlendMode(.multiply)
break
default:
break
}
context.beginTransparencyLayer(auxiliaryInfo: nil)
context.strokePath()
context.endTransparencyLayer()
}
}
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard drawingEnabled == true else {
return
}
self.delegate?.didBeginDrawing(view: self)
if let touch = touches.first as UITouch! {
//setTouchPoints(touch, view: self)
previousPoint = touch.previousLocation(in: self)
previousPreviousPoint = touch.previousLocation(in: self)
currentPoint = touch.location(in: self)
let newLine = Line(path: CGMutablePath(), color: self.lineColor, width: self.lineWidth, opacity: self.lineOpacity)
newLine.path.addPath(createNewPath())
pathArray.append(newLine)
}
}
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard drawingEnabled == true else {
return
}
self.delegate?.isDrawing(view: self)
if let touch = touches.first as UITouch! {
//updateTouchPoints(touch, view: self)
previousPreviousPoint = previousPoint
previousPoint = touch.previousLocation(in: self)
currentPoint = touch.location(in: self)
let newLine = createNewPath()
if let currentPath = pathArray.last {
currentPath.path.addPath(newLine)
}
}
}
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard drawingEnabled == true else {
return
}
}
override open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
guard drawingEnabled == true else {
return
}
}
public func canUndo() -> Bool {
if pathArray.count > 0 {return true}
return false
}
public func canRedo() -> Bool {
return redoArray.count > 0
}
public func undo() {
if pathArray.count > 0 {
redoArray.append(pathArray.last!)
pathArray.removeLast()
}
setNeedsDisplay()
}
public func redo() {
if redoArray.count > 0 {
pathArray.append(redoArray.last!)
redoArray.removeLast()
}
setNeedsDisplay()
}
public func clearCanvas() {
pathArray = []
setNeedsDisplay()
}
private func createNewPath() -> CGMutablePath {
//print(#function)
let midPoints = getMidPoints()
let subPath = createSubPath(midPoints.0, mid2: midPoints.1)
let newPath = addSubPathToPath(subPath)
return newPath
}
private func calculateMidPoint(_ p1 : CGPoint, p2 : CGPoint) -> CGPoint {
//print(#function)
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5);
}
private func getMidPoints() -> (CGPoint, CGPoint) {
//print(#function)
let mid1 : CGPoint = calculateMidPoint(previousPoint, p2: previousPreviousPoint)
let mid2 : CGPoint = calculateMidPoint(currentPoint, p2: previousPoint)
return (mid1, mid2)
}
private func createSubPath(_ mid1: CGPoint, mid2: CGPoint) -> CGMutablePath {
//print(#function)
let subpath : CGMutablePath = CGMutablePath()
subpath.move(to: CGPoint(x: mid1.x, y: mid1.y))
subpath.addQuadCurve(to: CGPoint(x: mid2.x, y: mid2.y), control: CGPoint(x: previousPoint.x, y: previousPoint.y))
return subpath
}
private func addSubPathToPath(_ subpath: CGMutablePath) -> CGMutablePath {
//print(#function)
let bounds : CGRect = subpath.boundingBox
let drawBox : CGRect = bounds.insetBy(dx: -0.54 * lineWidth, dy: -0.54 * lineWidth)
self.setNeedsDisplay(drawBox)
return subpath
}
}
UPDATE:
I notice that each eraser touch is square. Please see the second image to show in more detail:
I then rewrote some code as suggested by Pranal Jaiswal:
override open func draw(_ rect: CGRect) {
print(#function)
let context : CGContext = UIGraphicsGetCurrentContext()!
if isEraserSelected {
for line in undoArray {
//context.beginTransparencyLayer(auxiliaryInfo: nil)
context.setLineWidth(line.width)
context.addPath(line.path)
context.setStrokeColor(UIColor.clear.cgColor)
context.setBlendMode(.clear)
context.setAlpha(line.opacity)
context.setLineCap(.round)
context.strokePath()
}
} else {
for line in undoArray {
context.setLineWidth(line.width)
context.setLineCap(.round)
context.addPath(line.path)
context.setStrokeColor(line.color.cgColor)
context.setBlendMode(.normal)
context.setAlpha(line.opacity)
context.strokePath()
}
}
}
I'm still getting the same result. I'd appreciate any more help.
I couldn't exactly look at ur code But I had done something similar in Swift 2.3 a while ago (I do understand u are looking at swift 3 but right now this is version that I have).
Here is how the drawing class works looks like.
import Foundation
import UIKit
import QuartzCore
class PRSignatureView: UIView
{
var drawingColor:CGColorRef = UIColor.blackColor().CGColor //Col
var drawingThickness:CGFloat = 0.5
var drawingAlpha:CGFloat = 1.0
var isEraserSelected: Bool
private var currentPoint:CGPoint?
private var previousPoint1:CGPoint?
private var previousPoint2:CGPoint?
private var path:CGMutablePathRef = CGPathCreateMutable()
var image:UIImage?
required init?(coder aDecoder: NSCoder) {
//self.backgroundColor = UIColor.clearColor()
self.isEraserSelected = false
super.init(coder: aDecoder)
self.backgroundColor = UIColor.clearColor()
}
override func drawRect(rect: CGRect)
{
self.isEraserSelected ? self.eraseMode() : self.drawingMode()
}
private func drawingMode()
{
if (self.image != nil)
{
self.image!.drawInRect(self.bounds)
}
let context:CGContextRef = UIGraphicsGetCurrentContext()!
CGContextAddPath(context, path)
CGContextSetLineCap(context, CGLineCap.Round)
CGContextSetLineWidth(context, self.drawingThickness)
CGContextSetStrokeColorWithColor(context, drawingColor)
CGContextSetBlendMode(context, CGBlendMode.Normal)
CGContextSetAlpha(context, self.drawingAlpha)
CGContextStrokePath(context);
}
private func eraseMode()
{
if (self.image != nil)
{
self.image!.drawInRect(self.bounds)
}
let context:CGContextRef = UIGraphicsGetCurrentContext()!
CGContextSaveGState(context)
CGContextAddPath(context, path);
CGContextSetLineCap(context, CGLineCap.Round)
CGContextSetLineWidth(context, self.drawingThickness)
CGContextSetBlendMode(context, CGBlendMode.Clear)
CGContextStrokePath(context)
CGContextRestoreGState(context)
}
private func midPoint (p1:CGPoint, p2:CGPoint)->CGPoint
{
return CGPointMake((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5)
}
private func finishDrawing()
{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0);
drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)
self.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
func clearSignature()
{
path = CGPathCreateMutable()
self.image = nil;
self.setNeedsDisplay();
}
// MARK: - Touch Delegates
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
path = CGPathCreateMutable()
let touch = touches.first!
previousPoint1 = touch.previousLocationInView(self)
currentPoint = touch.locationInView(self)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first!
previousPoint2 = previousPoint1
previousPoint1 = touch.previousLocationInView(self)
currentPoint = touch.locationInView(self)
let mid1 = midPoint(previousPoint2!, p2: previousPoint1!)
let mid2 = midPoint(currentPoint!, p2: previousPoint1!)
let subpath:CGMutablePathRef = CGPathCreateMutable()
CGPathMoveToPoint(subpath, nil, mid1.x, mid1.y)
CGPathAddQuadCurveToPoint(subpath, nil, previousPoint1!.x, previousPoint1!.y, mid2.x, mid2.y)
CGPathAddPath(path, nil, subpath);
self.setNeedsDisplay()
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.touchesMoved(touches, withEvent: event)
self.finishDrawing()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.touchesMoved(touches!, withEvent: event)
self.finishDrawing()
}
}
Source Code for test app I created using the above code
Edit: Converting few lines code to swift 3 as requested
subpath.move(to: CGPoint(x: mid1.x, y: mid1.y))
subpath.addQuadCurve(to:CGPoint(x: mid2.x, y: mid2.y) , control: CGPoint(x: previousPoint1!.x, y: previousPoint1!.y))
path.addPath(subpath)
Edit: In response to the updated Question
Here is the updated Drawing Class that must solve the issue for sure. https://drive.google.com/file/d/0B5nqEBSJjCriTU5oRXd5c2hRV28/view?usp=sharing
Issues addressed:
Line Struct did not hold the tool type associated. Whenever setNeedsDislpay() is called you redraw all the objects in pathArray and all Objects were getting redrawn with the current selected tool. I have added a new variable associatedTool to address the issue.
Use of function beginTransparencyLayer will set the blend mode to kCGBlendModeNormal. As this was common for all cases related to tooltype this was causing the mode to be set to normal. I have removed these two lines
//context.beginTransparencyLayer(auxiliaryInfo: nil)
//context.endTransparencyLayer()
Try this it has no error while erasing and it can be us for drawing erasing and clearing your screen. you can even increase or decrease ur size of the pencil and eraser. also u may change color accordingly.
hope this is helpfull for u.....
import UIKit
class DrawingView: UIView {
var lineColor:CGColor = UIColor.black.cgColor
var lineWidth:CGFloat = 5
var drawingAlpha:CGFloat = 1.0
var isEraserSelected: Bool
private var currentPoint:CGPoint?
private var previousPoint1:CGPoint?
private var previousPoint2:CGPoint?
private var path:CGMutablePath = CGMutablePath()
var image:UIImage?
required init?(coder aDecoder: NSCoder) {
//self.backgroundColor = UIColor.clearColor()
self.isEraserSelected = false
super.init(coder: aDecoder)
self.backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect)
{
self.isEraserSelected ? self.eraseMode() : self.drawingMode()
}
private func drawingMode()
{
if (self.image != nil)
{
self.image!.draw(in: self.bounds)
}
let context:CGContext = UIGraphicsGetCurrentContext()!
context.addPath(path)
context.setLineCap(CGLineCap.round)
context.setLineWidth(self.lineWidth)
context.setStrokeColor(lineColor)
context.setBlendMode(CGBlendMode.normal)
context.setAlpha(self.drawingAlpha)
context.strokePath();
}
private func eraseMode()
{
if (self.image != nil)
{
self.image!.draw(in: self.bounds)
}
let context:CGContext = UIGraphicsGetCurrentContext()!
context.saveGState()
context.addPath(path);
context.setLineCap(CGLineCap.round)
context.setLineWidth(self.lineWidth)
context.setBlendMode(CGBlendMode.clear)
context.strokePath()
context.restoreGState()
}
private func midPoint (p1:CGPoint, p2:CGPoint)->CGPoint
{
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5);
}
private func finishDrawing()
{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0);
drawHierarchy(in: self.bounds, afterScreenUpdates: true)
self.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
func clearSignature()
{
path = CGMutablePath()
self.image = nil;
self.setNeedsDisplay();
}
// MARK: - Touch Delegates
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
path = CGMutablePath()
let touch = touches.first!
previousPoint1 = touch.previousLocation(in: self)
currentPoint = touch.location(in: self)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
previousPoint2 = previousPoint1
previousPoint1 = touch.previousLocation(in: self)
currentPoint = touch.location(in: self)
let mid1 = midPoint(p1: previousPoint2!, p2: previousPoint1!)
let mid2 = midPoint(p1: currentPoint!, p2: previousPoint1!)
let subpath:CGMutablePath = CGMutablePath()
subpath.move(to: CGPoint(x: mid1.x, y: mid1.y), transform: .identity)
subpath.addQuadCurve(to: CGPoint(x: mid2.x, y: mid2.y), control: CGPoint(x: (previousPoint1?.x)!, y: (previousPoint1?.y)!))
path.addPath(subpath, transform: .identity)
self.setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.touchesMoved(touches, with: event)
self.finishDrawing()
}
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
self.touchesMoved(touches!, with: event)
self.finishDrawing()
}
}
Related
I have this behaviour with this code:
import UIKit
class Page: UIView {
var bezierMemory = [BezierRecord]()
var currentBezier: UIBezierPath = UIBezierPath()
var firstPoint: CGPoint = CGPoint()
var previousPoint: CGPoint = CGPoint()
var morePreviousPoint: CGPoint = CGPoint()
var previousCALayer: CALayer = CALayer()
var pointCounter = 0
var selectedPen: Pen = Pen(width: 3.0, strokeOpacity: 1, strokeColor: .red, fillColor: .init(gray: 0, alpha: 0.5), isPencil: true, connectsToStart: true, fillPencil: true)
enum StandardPageSizes {
case A4, LEGAL, LETTER
}
var firstCALayer = true
var pointsTotal = 0
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else { return }
let point = touch.location(in: self)
firstPoint = point
pointCounter = 1
currentBezier = UIBezierPath()
currentBezier.lineWidth = selectedPen.width
selectedPen.getStroke().setStroke()
currentBezier.move(to: point)
previousPoint = point
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let touch = touches.first else { return }
let point = touch.location(in: self)
pointCounter += 1
if (pointCounter == 3) {
let midpoint = CGPoint(x: (morePreviousPoint.x + point.x)/2.0, y: (morePreviousPoint.y + point.y)/2.0)
currentBezier.addQuadCurve(to: midpoint, controlPoint: morePreviousPoint)
let updatedCALayer = CAShapeLayer()
updatedCALayer.path = currentBezier.cgPath
updatedCALayer.lineWidth = selectedPen.width
updatedCALayer.opacity = selectedPen.strokeOpacity
updatedCALayer.strokeColor = selectedPen.getStroke().cgColor
updatedCALayer.fillColor = selectedPen.getFill()
if (firstCALayer) {
layer.addSublayer(updatedCALayer)
firstCALayer = false
} else {
layer.replaceSublayer(previousCALayer, with: updatedCALayer)
}
previousCALayer = updatedCALayer
pointCounter = 1
}
morePreviousPoint = previousPoint
previousPoint = point
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
guard let touch = touches.first else { return }
let point = touch.location(in: self)
if (pointCounter != 3) {
if (selectedPen.connectsToStart) {
currentBezier.addQuadCurve(to: firstPoint, controlPoint: previousPoint)
} else {
currentBezier.addQuadCurve(to: point, controlPoint: previousPoint)
}
let updatedCALayer = CAShapeLayer()
updatedCALayer.path = currentBezier.cgPath
updatedCALayer.lineWidth = selectedPen.width
updatedCALayer.opacity = selectedPen.strokeOpacity
updatedCALayer.strokeColor = selectedPen.getStroke().cgColor
updatedCALayer.fillColor = selectedPen.getFill()
if (firstCALayer) {
layer.addSublayer(updatedCALayer)
firstCALayer = false
} else {
// layer.setNeedsDisplay()
layer.replaceSublayer(previousCALayer, with: updatedCALayer)
}
}
firstCALayer = true
let bezierRecord = BezierRecord(bezier: currentBezier, strokeColor: selectedPen.getStroke(), fillColor: selectedPen.getFill(), strokeWidth: selectedPen.width)
bezierMemory.append(bezierRecord)
}
private func normPoint(point: CGPoint) -> CGPoint {
return CGPoint(x: point.x/frame.width, y: point.y/frame.height)
}
public class BezierRecord {
var bezier: UIBezierPath
var strokeColor: UIColor
var strokeWidth: CGFloat
var fillColor: CGColor
init(bezier: UIBezierPath, strokeColor: UIColor, fillColor: CGColor, strokeWidth: CGFloat) {
self.bezier = bezier
self.strokeColor = strokeColor
self.strokeWidth = strokeWidth
self.fillColor = fillColor
}
}
}
Really the only relevant parts are touchesMoved and touchesEnded, where CALayer's are dealt with. As you can see from the gif, I can draw outside the bounds of the Page (UIView) as long as I start drawing inside the bounds. I do not want this - what I would like is something where you can maintain a stroke outside of the bounds of the Page (as long as you start it on the Page), but the stroke wont appear outside of the Page. Any ideas?
EDIT: I should add that for these UIBezierCurves, (0, 0) is considered to be the top left of the Page (white), and not the entire view. Thus, for example, beziers that start on the page and continue on, are negative.
All you should need to do is set .clipsToBounds = true on the view.
One approach:
override init(frame: CGRect) {
super.init(frame: .zero)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
self.clipsToBounds = true
}
You could put self.clipsToBounds = true in both of the init funcs, but it is common practice (no pun intended) to add a "common init" func like this (it can be named whatever... this is how I do it). Frequently we have other "initial setup" code that we want called, and this avoids duplicating the code.
Hello I'm trying to draw some rectangles on a UIImageView but when I add the background image the rectangle drawing is not working anymore:
This is the code:
import UIKit
class DrawView: UIView {
var startPos: CGPoint?
var endPos: CGPoint?
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
if startPos != nil && endPos != nil {
print("both there")
drawRectangle(context: context, startPoint: startPos!, endPoint: endPos!, isFilled: false)
}
}
public func drawRectangle(context: CGContext, startPoint: CGPoint, endPoint: CGPoint, isFilled: Bool) {
let xx = startPoint
let xy = CGPoint(x:endPoint.x, y:startPoint.y)
let yx = CGPoint(x:startPoint.x, y:endPoint.y)
let yy = endPoint
print("draw rectangle")
context.move(to: xx)
context.addLine(to: xx)
context.addLine(to: xy)
context.addLine(to: yy)
context.addLine(to: yx)
context.addLine(to: xx)
context.setLineCap(.square)
context.setLineWidth(8)
if isFilled {
context.setFillColor(UIColor.purple.cgColor)
context.fillPath()
} else {
context.setStrokeColor(UIColor.red.cgColor)
context.strokePath()
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: self)
print(position)
startPos = position
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: self)
print(position)
endPos = position
setNeedsDisplay()
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: self)
print(position)
endPos = position
setNeedsDisplay()
}
}
func addBackground() {
// screen width and height:
let width = UIScreen.main.bounds.size.width
let height = UIScreen.main.bounds.size.height
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: width, height: height))
imageView.image = UIImage(named: "img1.jpg")
// you can change the content mode:
imageView.contentMode = UIView.ContentMode.scaleAspectFill
self.addSubview(imageView)
self.sendSubviewToBack(imageView)
}
}
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var drawView: DrawView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
drawView.addBackground()
}
}
Im suspecting that I'm not using the correct UIGraphicsGetCurrentContext after setting the image but I was not able to figure it out how to fix it.
I'm working on a drawing app prototype and what I want to achieve is perfect rounding lines. I've implemented drawing via curves but still, my lines look a little bit pixelated.
Here is my result:
and here is what I'm trying to achieve:
class ViewController: UIViewController {
private var currentPoint: CGPoint?
private var previousPoint1: CGPoint?
private var previousPoint2: CGPoint?
private var lineColor = UIColor.black
private var lineWidth = CGFloat(10)
private var lineAlpha = CGFloat(1)
#IBOutlet private weak var imageView: UIImageView!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
previousPoint1 = touch.previousLocation(in: self.view)
currentPoint = touch.location(in: self.view)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
previousPoint2 = previousPoint1
previousPoint1 = touch.previousLocation(in: self.view)
currentPoint = touch.location(in: self.view)
let mid1 = middlePoint(previousPoint1!, previousPoint2: previousPoint2!)
let mid2 = middlePoint(currentPoint!, previousPoint2: previousPoint1!)
draw(move: CGPoint(x: mid1.x, y: mid1.y), to: CGPoint(x: mid2.x, y: mid2.y), control: CGPoint(x: previousPoint1!.x, y: previousPoint1!.y))
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
touchesMoved(touches, with: event)
}
private func middlePoint(_ previousPoint1: CGPoint, previousPoint2: CGPoint) -> CGPoint {
return CGPoint(x: (previousPoint1.x + previousPoint2.x) * 0.5, y: (previousPoint1.y + previousPoint2.y) * 0.5)
}
func draw(move: CGPoint, to: CGPoint, control: CGPoint) {
UIGraphicsBeginImageContext(view.frame.size)
guard let ctx = UIGraphicsGetCurrentContext() else { return }
imageView.image?.draw(in: view.bounds)
ctx.move(to: move)
ctx.addQuadCurve(to: to, control: control)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
ctx.setLineWidth(lineWidth)
ctx.setStrokeColor(lineColor.cgColor)
ctx.setBlendMode(.normal)
ctx.setAlpha(lineAlpha)
ctx.strokePath()
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
imageView.image = finalImage
UIGraphicsEndImageContext()
}
}
P.S. I want to draw via context, not using drawRect
The problem here is that the scale of imageContext you are creating is not correct. Rather than creating your context with UIGraphicsBeginImageContext try to create it with UIGraphicsBeginImageContextWithOptions and give scale value as 0. When you specify a scale value of 0, the scale factor is set to the scale factor of the device’s main screen. Updated code snippet would be like the one follows.
func draw(move: CGPoint, to: CGPoint, control: CGPoint) {
UIGraphicsBeginImageContextWithOptions(view.frame.size, false, 0)
guard let ctx = UIGraphicsGetCurrentContext() else { return }
imageView.image?.draw(in: view.bounds)
ctx.move(to: move)
ctx.addQuadCurve(to: to, control: control)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
ctx.setLineWidth(lineWidth)
ctx.setStrokeColor(lineColor.cgColor)
ctx.setBlendMode(.normal)
ctx.setAlpha(lineAlpha)
ctx.strokePath()
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
imageView.image = finalImage
UIGraphicsEndImageContext()
}
I want to add calligraphy brush effect just like shown in below Image.for drawing I am using SwiftyDrawView.
Following is code snippets from SwiftyDraw
/// Overriding draw(rect:) to stroke paths
override open func draw(_ rect: CGRect) {
super.draw(rect)
guard let context: CGContext = UIGraphicsGetCurrentContext() else { return }
for line in lines {
context.setLineCap(.round)
context.setLineJoin(.round)
context.setLineWidth(line.brush.width)
// set blend mode so an eraser actually erases stuff
context.setBlendMode(line.brush.blendMode)
context.setAlpha(line.brush.opacity)
context.setStrokeColor(line.brush.color.cgColor)
context.addPath(line.path)
context.strokePath()
}
}
// TouchBegan
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isEnabled, let touch = touches.first else { return }
if #available(iOS 9.1, *) {
guard allowedTouchTypes.flatMap({ $0.uiTouchTypes }).contains(touch.type) else { return }
}
guard delegate?.swiftyDraw(shouldBeginDrawingIn: self, using: touch) ?? true else { return }
delegate?.swiftyDraw(didBeginDrawingIn: self, using: touch)
setTouchPoints(touch, view: self)
let newLine = Line(path: CGMutablePath(),
brush: Brush(color: brush.color, width: brush.width, opacity: brush.opacity, blendMode: brush.blendMode))
newLine.path.addPath(createNewPath())
lines.append(newLine)
drawingHistory = lines // adding a new line should also update history
}
and touchesMoved
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isEnabled, let touch = touches.first else { return }
if #available(iOS 9.1, *) {
guard allowedTouchTypes.flatMap({ $0.uiTouchTypes }).contains(touch.type) else { return }
}
delegate?.swiftyDraw(isDrawingIn: self, using: touch)
updateTouchPoints(for: touch, in: self)
let newPath = createNewPath()
if let currentPath = lines.last {
currentPath.path.addPath(newPath)
}
}
Touch ended
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isEnabled, let touch = touches.first else { return }
delegate?.swiftyDraw(didFinishDrawingIn: self, using: touch)
}
and Touch cancel
override open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isEnabled, let touch = touches.first else { return }
delegate?.swiftyDraw(didCancelDrawingIn: self, using: touch)
}
// and set TouchPint
private func setTouchPoints(_ touch: UITouch,view: UIView) {
previousPoint = touch.previousLocation(in: view)
previousPreviousPoint = touch.previousLocation(in: view)
currentPoint = touch.location(in: view)
}
// and updateTouchPoints
private func updateTouchPoints(for touch: UITouch,in view: UIView) {
previousPreviousPoint = previousPoint
previousPoint = touch.previousLocation(in: view)
currentPoint = touch.location(in: view)
}
and createNewPath
private func createNewPath() -> CGMutablePath {
let midPoints = getMidPoints()
let subPath = createSubPath(midPoints.0, mid2: midPoints.1)
let newPath = addSubPathToPath(subPath)
return newPath
}
and calculateMidPoint
private func calculateMidPoint(_ p1 : CGPoint, p2 : CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5);
}
and getMidPoints
private func getMidPoints() -> (CGPoint, CGPoint) {
let mid1 : CGPoint = calculateMidPoint(previousPoint, p2: previousPreviousPoint)
let mid2 : CGPoint = calculateMidPoint(currentPoint, p2: previousPoint)
return (mid1, mid2)
}
and createSubPath
private func createSubPath(_ mid1: CGPoint, mid2: CGPoint) -> CGMutablePath {
let subpath : CGMutablePath = CGMutablePath()
subpath.move(to: CGPoint(x: mid1.x, y: mid1.y))
subpath.addQuadCurve(to: CGPoint(x: mid2.x, y: mid2.y), control: CGPoint(x: previousPoint.x, y: previousPoint.y))
return subpath
}
and addSubPathToPath
private func addSubPathToPath(_ subpath: CGMutablePath) -> CGMutablePath {
let bounds : CGRect = subpath.boundingBox
let drawBox : CGRect = bounds.insetBy(dx: -2.0 * brush.width, dy: -2.0 * brush.width)
self.setNeedsDisplay(drawBox)
return subpath
}
I Found simple Solution :
private func createSubPath(_ mid1: CGPoint, mid2: CGPoint) -> CGMutablePath {
let subpath : CGMutablePath = CGMutablePath()
subpath.move(to: CGPoint(x: mid1.x, y: mid1.y))
subpath.addQuadCurve(to: CGPoint(x: mid2.x, y: mid2.y), control: CGPoint(x: previousPoint.x, y: previousPoint.y))
for i in 1 ..< 6 {
subpath.addLines(between: [CGPoint(x: mid1.x + CGFloat(i), y: mid1.y + CGFloat(i)),CGPoint(x: mid2.x+CGFloat(i), y: mid2.y + CGFloat(i))])
}
return subpath
}
Using Swift to draw lines, it keeps moving up the longer I continue to draw. Or maybe it's rotating back, like it's on a ball? The pictures show me starting out with a line, then as I draw, you can see that it reaches the top of the square. It's so weird! Why is that happening? How can I stop it? Code below.
var context: CGContext?
var adjustedImageViewRect: CGRect?
var adjustedImageView: UIImageView?
var lastPoint = CGPoint.zero
var firstPoint = CGPoint.zero
var brushWidth: CGFloat = 10.0
var opacity: CGFloat = 1.0
var swiped = false
var undermineArray: [CGPoint] = []
#IBOutlet weak var drawLineImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
drawLineImageView.image = UIImage(named: "layer")
}
override func viewDidAppear(_ animated: Bool) {
let rect = AVMakeRect(aspectRatio: drawLineImageView.image!.size, insideRect: drawLineImageView.bounds)
adjustedImageViewRect = rect
adjustedImageView = UIImageView(frame: rect)
self.view.addSubview(adjustedImageView!)
self.view.bringSubview(toFront: adjustedImageView!)
}
func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint) {
if let adjImgView = adjustedImageView {
if let adjImgViewRect = adjustedImageViewRect {
UIGraphicsBeginImageContextWithOptions(adjImgViewRect.size, false, 0)
adjImgView.image?.draw(in: adjImgView.bounds)
context = UIGraphicsGetCurrentContext()
context?.move(to: fromPoint)
context?.addLine(to: toPoint)
context?.setLineCap(CGLineCap.round)
context?.setLineWidth(brushWidth)
context?.setStrokeColor(UIColor.red.cgColor)
context?.setBlendMode(CGBlendMode.normal)
context?.strokePath()
adjImgView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//clear out previous line
firstPoint = CGPoint.zero
lastPoint = CGPoint.zero
undermineArray.removeAll()
adjustedImageView?.image = UIImage()
swiped = false
if let touch = touches.first {
if let rect = adjustedImageViewRect {
if (touch.location(in: self.adjustedImageView).y < 0
|| touch.location(in: self.adjustedImageView).y > rect.height) {
adjustedImageView?.image = UIImage()
return
}
}
firstPoint = touch.location(in: self.adjustedImageView)
lastPoint = touch.location(in: self.adjustedImageView)
undermineArray.append(firstPoint)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
swiped = true
if let touch = touches.first {
var currentPoint = touch.location(in: adjustedImageView)
if let rect = adjustedImageViewRect {
if (touch.location(in: self.adjustedImageView).y < 0) {
currentPoint.y = 0
}
if (touch.location(in: self.adjustedImageView).y > rect.height) {
currentPoint.y = rect.height
}
}
undermineArray.append(currentPoint)
drawLine(from: lastPoint, to: currentPoint)
lastPoint = currentPoint
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let rect = adjustedImageViewRect {
if (firstPoint.y < 0 || firstPoint.y > rect.height) {
adjustedImageView?.image = UIImage()
return
}
}
if let touch = touches.first {
if let rect = adjustedImageViewRect {
if (touch.location(in: self.adjustedImageView).y < 0) {
lastPoint.y = 0
}
if (touch.location(in: self.adjustedImageView).y > rect.height) {
lastPoint.y = rect.height
}
}
}
if !swiped {
self.drawLine(from: lastPoint, to: lastPoint)
}
}
AH I got it. It turns out that UIImage's draw() function draws the image, and unbeknownst to me until now, "scales it as needed to fit."
So, we'll use something else. We'll just use drawAsPattern().
Change
adjImgView.image?.draw(in: adjImgView.bounds)
to
adjImgView.image?.drawAsPattern(in: adjImgView.bounds)
and voilà, no crazy scaling/spinning/shrinking going on.