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

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)
}
}
}

Related

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()
}

NSTrackingArea doesn't always update cursor

I am having a hard time using NSTrackingArea to update the cursor to show where my custom draggable NSView can be resized. Here's an example:
The NSRect for the handle:
var handleBottomLeft: NSRect {
NSRect(x: self.bounds.origin.x,
y: self.bounds.origin.y,
width: handleMargin + borderWidth,
height: handleMargin + borderWidth)
}
The tracking area:
var bottomLeftTrackingArea: NSTrackingArea?
override func updateTrackingAreas() {
if let area = bottomLeftTrackingArea {
self.removeTrackingArea(area)
}
bottomLeftTrackingArea = nil
bottomLeftTrackingArea = NSTrackingArea(rect: handleBottomLeft, options: [.activeInKeyWindow, .cursorUpdate], owner: self, userInfo: nil)
addTrackingArea(bottomLeftTrackingArea!)
super.updateTrackingAreas()
}
And finally, the cursor update:
override func cursorUpdate(with event: NSEvent) {
switch event.trackingArea {
case bottomLeftTrackingArea:
print("pointing hand")
NSCursor.pointingHand.set()
default:
print("arrow")
super.cursorUpdate(with: event)
}
}
The cursor should change when it enters or leaves the blue box, but it only happens 50-75% of the time. Here's an example of the cursor not changing when it enters, but changing when it leaves.
https://i.imgur.com/fbqqzGy.mp4
Resizing my tracking areas to not overlap with each other helped resolve the issue for me. I can't tell from your sample code if you have this issue, but figured I'd share just in case!

Detect object proximity to walls

I am trying to write something really simple - and use SpriteKit on this instance.
The way I did it on other platforms, is by having an invisible child "stick" sticking out a bit. By detecting collision between the invisible "sticks" I can tell wherether the object is close to the wall or not.
I am trying to replicate the same thing using SpriteKit. Of course, I'd prefer to have an invisible "beam" coming out of the object and giving me distance - but that's probably too much hassle.
I'd appriciate any ways to improve on my silly project I got so far.
My Project so far
Thanks.
Here is what I came up with that doesn't involve physics...
Drag the mouse to move the car, and the label in the center updates telling you the distance to the closest wall. Release mouse to reset car.
Very simple example, can be updated to give more accurate measurements.
class GameScene: SKScene {
let car = SKSpriteNode(color: .blue, size: CGSize(width: 50, height: 100))
let label = SKLabelNode(text: "")
func findNearestWall() -> CGFloat {
// you can make this more advanced by using the CGPoint of the frame borders of the car, or even 6+ points for more accuracy
let closestX: CGFloat = {
if car.position.x < 0 { // left wall
return abs(frame.minX - car.position.x)
} else { // right wall
return abs(frame.maxX - car.position.x)
}
}()
let closestY: CGFloat = {
if car.position.y < 0 { // bottom wall
return abs(frame.minY - car.position.y)
} else { // top wall
return abs(frame.maxY - car.position.y)
}
}()
if closestX < closestY {
return closestX.rounded() // as closest wall distance
} else {
return closestY.rounded() // as closest wall distance
}
}
override func didMove(to view: SKView) {
removeAllChildren()
label.fontSize *= 2
addChild(car)
addChild(label)
}
override func mouseDown(with event: NSEvent) {
}
override func mouseDragged(with event: NSEvent) {
let location = event.location(in: self)
car.position = location
}
override func mouseUp(with event: NSEvent) {
car.position = CGPoint.zero
}
override func didEvaluateActions() {
label.text = String(describing: findNearestWall())
}
}

Swift not managing to call draw(GRect) more than once

I am trying to learn swift bu it seems I am already stuck.
Can someone point out why the { didSet { setNeedsDisplay() } } does not seem to work?!
The goal is to draw a small circle where the user taps
Here is my Controller:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var display: UIView!{
didSet{
display.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(addCirclePoint(byReactingTo:))))
}
}
let pointsAndLines = PointsAndLinesView()
func addCirclePoint(byReactingTo tapRecognizer: UITapGestureRecognizer){
let point = tapRecognizer.location(in: display)
pointsAndLines.point = point
}
}
And here is my View:
import UIKit
class PointsAndLinesView: UIView {
var point = CGPoint(){ didSet{ setNeedsDisplay() } }
private func addCirclePoint()->UIBezierPath{
let circlePoint = UIBezierPath(arcCenter: point, radius: 5.0, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
circlePoint.lineWidth = 4
print("Did I run?!")
return circlePoint
}
override func draw(_ rect: CGRect) {
UIColor.black.set()
addCirclePoint().stroke()
}
}
Looking forward to your responses.
EDIT:
For future reference in case someone needs it the mistake was,
that display is a subclass of PointsAndLinesView and NOT of UIView
and display.point should be set.
Looking at the code you have never added the pointsAndLines control to a view so you will never see it. You can add it as a subview of the main view and then set it's frame manually or use auto layout to position it.

(Swift) pan & zoom constrained to a certain size image

I am trying to pan and zoom across an image background in spritekit, I have managed to get the zoom working ok and manually entered some restrictions on how far you can pan the image, however the problem is when you pan the screen right to the edge of the image and then zoom out the background shows.
I want the camera to restrict only to the image on screen and not any blank background. Any ideas on how I should do this or any better solutions?
Here is what I got so far
class GameScene:SKScene{
var cam: SKCameraNode!
var scaleNum:CGFloat=1
override func didMove(to view: SKView){
cam=SKCameraNode()
cam.setScale(CGFloat(scaleNum))
self.camera=cam
self.addChild(cam)
let gesture=UIPinchGestureRecognizer(target: self, action: #selector(zoomIn(recognizer:)))
self.view!.addGestureRecognizer(gesture)
}
func zoomIn(recognizer: UIPinchGestureRecognizer){
if recognizer.state == .changed{
cam.setScale(recognizer.scale)
scaleNum=recognizer.scale
if cam.xScale<1 || cam.yScale<1{
cam.setScale(1)
}
if cam.xScale>3 || cam.yScale > 3{
cam.setScale(3)
}
// recognizer.scale=1
test()
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let firstTouch=touches.first
let location=(firstTouch?.location(in: self))!
let previousLocation=firstTouch?.previousLocation(in: self)
cam?.position.x -= location.x - (previousLocation?.x)!
cam?.position.y -= location.y - (previousLocation?.y)!
test()
}
func test(){
if cam.position.x < 1000*scaleNum{
cam.position.x=1000*scaleNum
}
if cam.position.x > 9200*scaleNum{
cam.position.x=9200*scaleNum
}
if cam.position.y<617*scaleNum{
cam.position.y=617*scaleNum
}
if cam.position.y>4476*scaleNum{
cam.position.y=4476*scaleNum
}
}
}
First of all, I would change your zoomIn function to this:
func zoomIn(recognizer: UIPinchGestureRecognizer){
if recognizer.state == .changed {
scaleNum = recognizer.scale
if scaleNum < 1 { scaleNum = 1 }
if scaleNum > 3 { scaleNum = 3 }
cam.setScale(scaleNum)
test()
}
}
It is easier to understand, you're not setting the camera scale twice, and most importantly, when you clamp the camera scale, scaleNum reflects that clamped value. This was not the case before, and in fact, that small change might be your entire problem.
Now I don't have much experience with UIPinchGestureRecognizer but I think the reason your zoom gesture works "ok" is because you are assigning directly from recognizer.scale to cam scale. Correct me if I'm wrong, but I think UIGestureRecognizer always starts with a scale of 1 for each new gesture, but your camera scale maintains its last value.
As an example, imagine your camera is at a scale of 1. A user zooms in to a scale of 2, the scene zooms in perfectly. The user lifts their fingers ending the gesture. Then the user tries to zoom in more, so they begin a new gesture, starting with a scale of 1, but your scene is still at a scale of 2. You can't assign the gesture scale directly or the image scale will 'jump' back to 1 for each new gesture. You have to convert from the gesture scale space to the camera scale space.
How exactly you do this is a design and feel choice. With no experience, my advice would be to change the line in my zoomIn function from
`scaleNum = recognizer.scale'
to
`scaleNum *= recognizer.scale`
Try both versions, and let me know how they work. If there is still a problem, then it most likely resides in your test() function. If so, I will try and help out with that as needed.
Thanks for the answer above, I managed to get it working, code below. Still needs a bit of tweaking but you can pan and zoom anywhere on the background image but the view should be constrained within the background image and not move into empty space beyond the image
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var cam: SKCameraNode!
var scaleNum: CGFloat=1
var background: SKSpriteNode!
var playableRect: CGRect!
override func didMove(to view: SKView) {
background=self.childNode(withName: "clouds") as! SKSpriteNode
cam=SKCameraNode()
cam.setScale(CGFloat(scaleNum))
self.camera=cam
self.addChild(cam)
self.isUserInteractionEnabled=true
let gesture=UIPinchGestureRecognizer(target: self, action: #selector(zoomIn(recognizer:)))
self.view!.addGestureRecognizer(gesture)
let maxAspectRatio:CGFloat=16.0/9.0
let playableHeight=size.width/maxAspectRatio
let playableMargin=(size.height-playableHeight)/2.0
playableRect=CGRect(x:0, y: playableMargin, width: size.width, height: playableHeight)
}
func zoomIn(recognizer: UIPinchGestureRecognizer){
if recognizer.state == .changed{
let savedScale=scaleNum
scaleNum=recognizer.scale
if scaleNum<1{
scaleNum=1
}
else if scaleNum>3{
scaleNum=3
}
if testcamera(posX: cam.position.x, posY: cam.position.y){
cam.setScale(scaleNum)
}
else{
scaleNum=savedScale
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let firstTouch=touches.first
let location=(firstTouch?.location(in: self))!
var posX=cam.position.x
var posY=cam.position.y
let previousLocation=firstTouch?.previousLocation(in: self)
posX -= location.x - (previousLocation?.x)!
posY -= location.y - (previousLocation?.y)!
if testcamera(posX: posX, posY: posY){
cam.position.x=posX
cam.position.y=posY
}
}
func testcamera(posX: CGFloat, posY: CGFloat)->Bool{
var cameraRect : CGRect {
let xx = posX - size.width/2*scaleNum
let yy = posY - playableRect.height/2*scaleNum
return CGRect(x: xx, y: yy, width: size.width*scaleNum, height: playableRect.height*scaleNum)
}
let backGroundRect=CGRect(x: background.position.x-background.frame.width/2, y: background.position.y-background.frame.height/2, width: background.frame.width, height: background.frame.height)
return backGroundRect.contains(cameraRect)
}
}