How to make a fading eraser using CGMutablePath? - swift

I have a CGMutablePath of touch event handling. I can able to erase clean path by using below code. But I want to make a fading-out eraser that is shows on below image. I tried it to make this for some days but failed to do that. Fading-out means showing less alpha around touches area of eraser.
var path = CGMutablePath()
public override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.saveGState()
ctx.addPath(path)
ctx.setLineCap(.round)
ctx.setLineWidth(20.0)
ctx.setBlendMode(.clear)
ctx.strokePath()
ctx.restoreGState()
}

Related

custom overlay(renderer) is getting cut of by map tiles (in some cases)

I wrote a custom renderer to represent a min and a max Radius. In some cases the renderer is not working as expected. It looks like the overlay is getting cut of by the map tiles.
See the full video
Here is how I did it. Did I miss something?
class RadiusOverlayRenderer: MKOverlayRenderer {
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
guard let overlay = self.overlay as? RadiusOverlay else {
return
}
let maxRadiusRect = self.rect(for: overlay.boundingMapRect)
.offsetBy(
dx: CGFloat(-overlay.boundingMapRect.height)/2,
dy: CGFloat(-overlay.boundingMapRect.width)/2
)
let minRadiusRect = CGRect(
x: Double(maxRadiusRect.midX)-overlay.minRadRect.width/2,
y: Double(maxRadiusRect.midY)-overlay.minRadRect.height/2,
width: overlay.minRadRect.width,
height: overlay.minRadRect.height)
let aPath = CGMutablePath()
aPath.addEllipse(in: maxRadiusRect)
aPath.addEllipse(in: minRadiusRect)
aPath.closeSubpath()
context.setFillColor(overlay.color.cgColor)
context.setAlpha(overlay.alpha)
context.addPath(aPath)
context.drawPath(using: .eoFillStroke)
}
}
Notice that only the upper left parts are clipped?
with .offsetBy you are drawing outside of the .boundingMapRect.
Remove the .offsetBy...
If you want to draw your circle at a different place, then adjust coordinate and / or boundingMapRect of your MKOverlay.

Draw Trails in Xcode

I am working on a screensaver in Xcode. I have an array of points and am changing their position s every frame in the override func animateOneFrame(). How would I have the points create a fading trail similar to this: https://www.youtube.com/watch?v=fDSIRXmnVvk&t=117s&ab_channel=CodeParade
For reference, in my draw function I have this and it only displays the points, no trail. It effectively draws a black background every frame even though I don't tell it to:
override func draw(_ rect: NSRect) {
for point in 1..<PointArray.count {
drawPoint(p: PointArray[point])
}
}
private func drawPoint(p: point) {
let pixel = NSRect(x: p.position.x,
y: p.position.y,
width: 3,
height: 3)
p.color.setFill()
pixel.fill()
}

Drawing directly in a NSView without using the draw(_ updateRect: NSRect) function

I would like to draw CGImage pictures directly to a View and with the normal method using the draw func I only get 7 pictures in a second on a new Mac Book Pro. So I decided to use the updateLayer func instead. I have defined wantsUpdateLayer = true and my new updateLayer func is called as expected. But then starts my problem. When using the draw func, I get the current CGContext with "NSGraphicsContext.current?.cgContext" but in my updateLayer func the "NSGraphicsContext.current?.cgContext" is nil. So I do not know where to put my CGImage, that it will be displayed on my screen. Also "self.view?.window?.graphicsContext?.cgContext" and "self.window?.graphicsContext?.cgContext" are nil, too. There are no buttons or other elements in this view and in the window of the view, only one picture, filling the complete window. And this picture must change 30 times in a second. Generating the pictures is done by a separate thread and needs about 1 millisecond for a picture. I think that from "outside" the NSView class it is not possible to write the picture but my updateLayer func is inside the class.
Here is what the func looks like actually:
override func updateLayer ()
{
let updateRect: NSRect = NSRect(x: 0.0, y: 0.0, width: 1120.0, height: 768.0)
let context1 = self.view?.window?.graphicsContext?.cgContext
let context2 = self.window?.graphicsContext?.cgContext
let context3 = NSGraphicsContext.current?.cgContext
}
And all three contexts are nil in the time the function is called automatically after I set the needsDisplay flag.
Any ideas where to draw my CGImages?
The updateLayer func is called automatically by the user interface. I do not call it manually. It is called by the view. My problem is where inside this method to put my picture to be shown on the screen. Perhaps I have to add a layer or use a default layer of the view but I do not know how to do this.
Meanwhile I have found the solution with some tipps from a good friend:
override var wantsUpdateLayer: Bool
{
return (true)
}
override func updateLayer ()
{
let cgimage: CGImage? = picture // Here comes the picture
if cgimage != nil
{
let nsimage: NSImage? = NSImage(cgImage: cgimage!, size: NSZeroSize)
if nsimage != nil
{
let desiredScaleFactor: CGFloat? = self.window?.backingScaleFactor
if desiredScaleFactor != nil
{
let actualScaleFactor: CGFloat? = nsimage!.recommendedLayerContentsScale(desiredScaleFactor!)
if actualScaleFactor != nil
{
self.layer!.contents = nsimage!.layerContents(forContentsScale: actualScaleFactor!)
self.layer!.contentsScale = actualScaleFactor!
}
}
}
}
}
This is the way to directly write into the layer. Depending on the format (CGImage or NSImage) you first must convert it. As soon as the func wantsUpdateLayer returns a true, the func updateLayer() is used instead of the func draw(). Thats all.
For all who want to see my "Normal" draw function:
override func draw (_ updateRect: NSRect)
{
let cgimage: CGImage? = picture // Here comes the picture
if cgimage != nil
{
if #available(macOS 10.10, *)
{
NSGraphicsContext.current?.cgContext.draw(cgimage!, in: updateRect)
}
}
else
{
super.draw(updateRect)
}
}
The additional speed is 2 times or more, depending on what hardware you use. On a modern Mac Pro there is only a little bit more speed but on a modern Mac Book Pro you will get 10 times or more speed. This works with Mojave 10.14.6 and Catalina 10.15.6. I did not test it with older macOS versions. The "Normal" draw function works with 10.10.6 to 10.15.6.

How could I get mouse click coordinates using Swift on MacOS?

I try to find out about getting mouse click coordinates to draw a line. I would like to make two clicks (first and second dots) and a line'll be create.
I analysed a lot of codes, but they are huge (for example, I'm fond of this way https://stackoverflow.com/a/47496766/9058168).
I hope drawing line in Swift isn't difficult. Could I type another variable which has mouse click coordinates instead of numeral coordinates (look at code below)? If your answer is true, how to code it? Help me, please, to make it simplier.
import Cocoa
class DrawLine: NSView {
override func draw(_ dirtyRect: NSRect) {
NSBezierPath.strokeLine(from: CGPoint(x: 20, y: 20), to: CGPoint(x: 0, y: 100))
}
}
Listen for the mouse down event and set start location and end location using it to draw the path.
import Cocoa
class DrawLine: NSView {
var startPoint:NSPoint?
var endPoint:NSPoint?
override func mouseDown(with event: NSEvent){
if startPoint == nil || self.endPoint != nil{
self.startPoint = event.locationInWindow
} else {
self.endPoint = event.locationInWindow
self.needsDisplay = true
}
}
override func draw(_ dirtyRect: NSRect) {
if let theStart = startPoint, let theEnd = endPoint{
NSBezierPath.strokeLine(from: theStart, to: theEnd)
}
}
}

value of optional type "CGContext?" not unwrapped

I try to draw a line in the view, but my code fail to complier due to optional type error. I am new to swift and objective-c and spend many hours to search answer. The issue does not fix so far. So, Can anyone provide some clue to fix this issue?
Code:
import UIKit
class DrawLines: UIView {
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
//Drawing code
// context
let context = UIGraphicsGetCurrentContext()
CGContextSetLineWidth(context, 3.0)
CGContextSetStrokeColorWithColor(context, UIColor.purpleColor().cgColor)
//create a path
CGContextMoveToPoint(context,0,0)
CGContextAddLineToPoint(context,250,320)
}
}
Error:
UIGraphicsGetCurrentContext() returns optional, to use it in your example you need to call context!.
The best way to use it it wrap it in if-let:
if let context = UIGraphicsGetCurrentContext() {
// Use context here
}
or even better use guard let:
guard let context = UIGraphicsGetCurrentContext() else { return }
// Use context here
In this case the solution is to use ! when getting the context:
let context = UIGraphicsGetCurrentContext()!
The app will crash when there is no current context which means you have done something very wrong.
Just forced unwrap the context, it is 100% safe but solves only one issue.
From the documentation of UIGraphicsGetCurrentContext:
The current graphics context is nil by default. Prior to calling its drawRect: method, view objects push a valid context onto the stack, making it current.
In Swift 3 (assumed from the draw signature) the graphics syntax changed significantly:
class DrawLines: UIView {
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
context.setLineWidth(3.0)
context.setStrokeColor(UIColor.purple.cgColor)
//create a path
// context.beginPath()
context.move(to: CGPoint())
context.addLine(to: CGPoint(x:250, y:320))
// context.strokePath()
}
}
PS: But to draw the line you should uncomment the beginPath() and strokePath() lines.