Cut a hole on SKNode and update its physicsBody in Swift - swift

I'm creating an app which will contains some holes on the image.
Since the code is very big I've created this simple code which has the same idea.
This code has an image with it's physicsBody set already.
What I would like is in the touchesBegan function to draw some transparent circles at the touched location and update the image physicsBody (making a hole on the image).
I've found several codes in objective C and UIImage, can someone help with Swift and SKSpriteNode?
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let texture = SKTexture(imageNamed: "Icon.png")
let node = SKSpriteNode(texture: texture)
node.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
addChild(node)
node.physicsBody = SKPhysicsBody(rectangleOfSize: texture.size())
node.physicsBody?.dynamic = false
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
}
}

One option is to apply a mask and render that to an image and update your SKSpriteNode's texture. Then use that texture to determine the physics body. This process however will not be very performant.
For example in touchesBegan you can say something like:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
guard let touch = touches.first else { return }
if let node = self.nodeAtPoint(touch.locationInNode(self.scene!)) as? SKSpriteNode{
let layer = self.layerFor(touch, node: node) // We'll use a helper function to create the layer
let image = self.snapShotLayer(layer) // Helper function to snapshot the layer
let texture = SKTexture(image:image)
node.texture = texture
// This will map the physical bounds of the body to alpha channel of texture. Hit in performance
node.physicsBody = SKPhysicsBody(texture: node.texture!, size: node.size)
}
}
Here we just get the node and look at the first touch, you could generalize to allow multi touch, but we then create a layer with the new image with transparency, then create a texture out of it, then update the node's physics body.
For creating the layer you can say something like:
func layerFor(touch: UITouch, node: SKSpriteNode) -> CALayer
{
let touchDiameter:CGFloat = 20.0
let layer = CALayer()
layer.frame = CGRect(origin: CGPointZero, size: node.size)
layer.contents = node.texture?.CGImage()
let locationInNode = touch.locationInNode(node)
// Convert touch to layer coordinate system from node coordinates
let touchInLayerX = locationInNode.x + node.size.width * 0.5 - touchDiameter * 0.5
let touchInLayerY = node.size.height - (locationInNode.y + node.size.height * 0.5) - touchDiameter * 0.5
let circleRect = CGRect(x: touchInLayerX, y: touchInLayerY, width: touchDiameter, height: touchDiameter)
let circle = UIBezierPath(ovalInRect: circleRect)
let shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: node.size.width, height: node.size.height)
let path = UIBezierPath(rect: shapeLayer.frame)
path.appendPath(circle)
shapeLayer.path = path.CGPath
shapeLayer.fillRule = kCAFillRuleEvenOdd
layer.mask = shapeLayer
return layer
}
Here we just set the current texture to the contents of a CALayer then we create a CAShapeLayer for the mask we want to create. We want the mask to be opaque for most of the layer but we want a transparent circle, so we create a path of a rectangle then add a circle to it. We set the fillRule to kCAFillRuleEvenOdd to fill everything but our circle.
Lastly, we render that layer to a UIImage that we can use to update our texture.
func snapShotLayer(layer: CALayer) -> UIImage
{
UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()
layer.renderInContext(context!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
Maybe someone will provide a more performant way of accomplishing this, but I think this will work for a lot of cases.

Related

Crop quadrilateral image from image to swiftUI [duplicate]

I would like to clip a bezier path from an image. For some reason, the image remains the unclipped. And how do I position the path so it would be properly cut?
extension UIImage {
func imageByApplyingMaskingBezierPath(_ path: UIBezierPath, _ pathFrame: CGFrame) -> UIImage {
UIGraphicsBeginImageContext(self.size)
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
path.addClip()
draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height))
let maskedImage = UIGraphicsGetImageFromCurrentImageContext()!
context.restoreGState()
UIGraphicsEndImageContext()
return maskedImage
}
}
You need to add your path.cgPath to your current context, also you need to remove context.saveGState() and context.restoreGState()
Use this code
func imageByApplyingMaskingBezierPath(_ path: UIBezierPath, _ pathFrame: CGRect) -> UIImage {
UIGraphicsBeginImageContext(self.size)
let context = UIGraphicsGetCurrentContext()!
context.addPath(path.cgPath)
context.clip()
draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height))
let maskedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return maskedImage
}
Using it
let testPath = UIBezierPath()
testPath.move(to: CGPoint(x: self.imageView.frame.width / 2, y: self.imageView.frame.height))
testPath.addLine(to: CGPoint(x: 0, y: 0))
testPath.addLine(to: CGPoint(x: self.imageView.frame.width, y: 0))
testPath.close()
self.imageView.image = UIImage(named:"Image")?.imageByApplyingMaskingBezierPath(testPath, self.imageView.frame)
Result
You can try like this.
var path = UIBezierPath()
var shapeLayer = CAShapeLayer()
var cropImage = UIImage()
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch?{
let touchPoint = touch.location(in: self.YourimageView)
print("touch begin to : \(touchPoint)")
path.move(to: touchPoint)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch?{
let touchPoint = touch.location(in: self.YourimageView)
print("touch moved to : \(touchPoint)")
path.addLine(to: touchPoint)
addNewPathToImage()
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch?{
let touchPoint = touch.location(in: self.YourimageView)
print("touch ended at : \(touchPoint)")
path.addLine(to: touchPoint)
addNewPathToImage()
path.close()
}
}
func addNewPathToImage(){
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = lineWidth
YourimageView.layer.addSublayer(shapeLayer)
}
func cropImage(){
UIGraphicsBeginImageContextWithOptions(YourimageView.bounds.size, false, 1)
tempImageView.layer.render(in: UIGraphicsGetCurrentContext()!)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.cropImage = newImage!
}
#IBAction func btnCropImage(_ sender: Any) {
cropImage()
}
Once you draw a path on particular button action just call your imageByApplyingMaskingBezierPath
Here is Swift code to get clips from an image based on a UIBezierPath and it is very quickly implemented. This method works if the image is already being shown on the screen, which will most often be the case. The resultant image will have a transparent background, which is what most people want when they clip part of a photo image. You can use the resultant clipped image locally because you will have it in a UIImage object that I called imageWithTransparentBackground. This very simple code also shows you how to save the image to the camera roll, and how to also put it right into the pasteboard so a user can paste that image directly into a text message, paste it into Notes, an email, etc. Note that in order to write the image to the camera roll, you need to edit the info.plist and provide a reason for “Privacy - Photo Library Usage Description”
import Photos // Needed if you save to the camera roll
Provide a UIBezierPath for clipping. Here is my declaration for one.
let clipPath = UIBezierPath()
Populate the clipPath with some logic of your own using some combination of commands. Below are a few I used in my drawing logic. Provide CGPoint equivalents for aPointOnScreen, etc Build your path relative to the main screen as self.view is this apps ViewController (for this code), and self.view.layer is rendered through the clipPath.
clipPath.move(to: aPointOnScreen)
clipPath.addLine(to: otherPointOnScreen)
clipPath.addLine(to: someOtherPointOnScreen)
clipPath.close()
This logic uses all of the devices screen as the context size. A CGSize is declared for that. fullScreenX and fullScreenY are my variables where I have already captured the devices width and height. It is nice if the photo you are clipping from is already zoomed into and is an adequate size as shown on the whole of the screen. What you see, is what you get.
let mainScreenSize = CGSize(width: fullScreenX, height: fullScreenY)
// Get an empty context
UIGraphicsBeginImageContext(mainScreenSize)
// Specify the clip path
clipPath.addClip()
// Render through the clip path from the whole of the screen.
self.view.layer.render(in: UIGraphicsGetCurrentContext()!)
// Get the clipped image from the context
let image : UIImage = UIGraphicsGetImageFromCurrentImageContext()!
// Done with the context, so end it.
UIGraphicsEndImageContext()
// The PNG data has the alpha channel for the transparent background
let imageData = image.pngData()
// Below is the local UIImage to use within your code
let imageWithTransparentBackground = UIImage.init(data: imageData!)
// Make the image available to the pasteboard.
UIPasteboard.general.image = imageWithTransparentBackground
// Save the image to the camera roll.
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: imageWithTransparentBackground!)
}, completionHandler: { success, error in
if success {
//
}
else if let error = error {
//
}
else {
//
}
})

Swift: ARKit place a rectangle based on 2 nodes

My idea is that i want to place 2 sphere nodes at selected locations. From that point i basically want to draw a rectangle that will adjust the height with a slider. Basically that means that the 2 spheres will represent all 4 corners in the beginning. But when testing the code i use 1 meter height as a test.
The problem i have is that i cant seem to place the rectangle at the correct location as illustrated in the image below:
the rectangle in the image has a higher y-point than the points, it's rotated slightly and its center point is above node 2 and not in between the nodes. I don't want to use node.rotation as it wont work dynamically.
This is the code i use to place the 2 nodes and draw + add the rectangle.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let location = touches.first?.location(in: sceneView) else {return}
let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])
guard let result = hitTest.last else {return}
// Converts the matrix_float4x4 to an SCNMatrix4 to be used with SceneKit
let transform = SCNMatrix4.init(result.worldTransform)
// Creates an SCNVector3 with certain indexes in the matrix
let vector = SCNVector3Make(transform.m41, transform.m42, transform.m43)
let node = addSphere(withPosition: vector)
nodeArray.append(node)
sceneView.scene.rootNode.addChildNode(node)
if nodeArray.count == 2 {
let node1 = sceneView.scene.rootNode.childNodes[0]
let node2 = sceneView.scene.rootNode.childNodes[1]
let bezeierPath = UIBezierPath()
bezeierPath.lineWidth = 0.01
bezeierPath.move(to: CGPoint(x: CGFloat(node1.position.x), y: CGFloat(node1.position.y)))
bezeierPath.addLine(to: CGPoint(x: CGFloat(node2.position.x), y: CGFloat(node2.position.y)))
bezeierPath.addLine(to: CGPoint(x: CGFloat(node2.position.x), y: CGFloat(node2.position.y+1.0)))
bezeierPath.addLine(to: CGPoint(x: CGFloat(node1.position.x), y: CGFloat(node1.position.y+1.0)))
bezeierPath.close()
bezeierPath.fill()
let shape = SCNShape(path: bezeierPath, extrusionDepth: 0.02)
shape.firstMaterial?.diffuse.contents = UIColor.red.withAlphaComponent(0.8)
let node = SCNNode.init(geometry: shape)
node.position = SCNVector3(CGFloat(abs(node1.position.x-node2.position.x)/2), CGFloat(abs((node1.position.y)-(node2.position.y))/2), CGFloat(node1.position.z))
sceneView.scene.rootNode.addChildNode(node)
}
}
Also note that this is not my final code. It will be refactored once i get everything working :).
This is currently working. I used the wrong calculation to set it in the middle of the 2 points.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check if a window is placed already and fetch location in sceneView
guard debugMode, let location = touches.first?.location(in: sceneView) else {return}
// Fetch targets at the current location
let hitTest = sceneView.hitTest(location, types: [ARHitTestResult.ResultType.featurePoint])
guard let result = hitTest.last else {return}
// Create sphere are position. getPosition() is an extension
createSphere(withPosition: result.getPosition())
// When 2 nodes has been placed, create a window and hide the spheres
if nodeArray.count == 2 {
let firstNode = nodeArray[nodeArray.count-1]
let secondNode = nodeArray[nodeArray.count-2]
firstNode.isHidden = true
secondNode.isHidden = true
createWindow(firstNode: firstNode, secondNode: secondNode)
sceneView.debugOptions = []
}
}
func createSphere(withPosition position: SCNVector3) {
// Create an geometry which you will apply materials to and convert to a node
let object = SCNSphere(radius: 0.01)
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
object.materials = [material]
let node = SCNNode(geometry: object)
node.position = position
nodeArray.append(node)
sceneView.scene.rootNode.addChildNode(node)
}
func createWindow(firstNode: SCNNode, secondNode: SCNNode) {
// Create an geometry which you will apply materials to and convert to a node
let shape = SCNBox(width: CGFloat(abs(firstNode.worldPosition.x-secondNode.worldPosition.x)), height: 0.01, length: 0.02, chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor.red.withAlphaComponent(0.8)
// Each side of a cube can have different materials
// https://stackoverflow.com/questions/27509092/scnbox-different-colour-or-texture-on-each-face
shape.materials = [material]
let node = SCNNode(geometry: shape)
let minX = min(firstNode.worldPosition.x, secondNode.worldPosition.x)
let maxX = max(firstNode.worldPosition.x, secondNode.worldPosition.x)
// The nodes pivot Y-axis should be split in half of the nodes height, this will cause the node to
// only scale in one direction, when scaling it
// https://stackoverflow.com/questions/42568420/scenekit-understanding-the-pivot-property-of-scnnode/42613002#42613002
node.position = SCNVector3(((maxX-minX)/2)+minX, firstNode.worldPosition.y, firstNode.worldPosition.z)
node.pivot = SCNMatrix4MakeTranslation(0, 0.005, 0)
node.scale = SCNVector3Make(1, -50, 1)
node.name = "window"
sceneView.scene.rootNode.addChildNode(node)
}
Also added:
#objc func resetSceneView() {
// Clear all the nodes
sceneView.scene.rootNode.enumerateHierarchy { (node, stop) in
node.childNodes.forEach({
$0.removeFromParentNode()
})
node.removeFromParentNode()
}
// Reset the session
sceneView.session.pause()
sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints]
sceneView.session.run(configuration, options: .resetTracking)
let matrix = sceneView.session.currentFrame!.camera.transform
sceneView.session.setWorldOrigin(relativeTransform: matrix)
nodeArray = []
}
To make sure that the world alignment is reset to your camera position

SpriteKit partial texture mapping

Is it possible to create a SKSpriteNode that displays only a part of a texture?
For example, can I create a square with size 100x100 displaying the specific region of a texture of size 720x720 like from x1=300 to x2=400 and y1=600 to y2=700?
Thanks for your help.
Try something like this:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
let visibleArea = SKSpriteNode(color: .black, size: CGSize(width:100,height:100))
let parentNode = SKSpriteNode(color: .white, size: CGSize(width:200, height:200))
override func didMove(to view: SKView) {
let cropNode = SKCropNode()
let texture = SKSpriteNode(imageNamed: "Spaceship")
visibleArea.position = CGPoint(x: 0, y: 100)
cropNode.maskNode = visibleArea
cropNode.addChild(texture)
addChild(cropNode)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let previousPosition = touch.previousLocation(in: self)
let translation = CGPoint(x: location.x - previousPosition.x , y: location.y - previousPosition.y )
visibleArea.position = CGPoint(x: visibleArea.position.x + translation.x , y: visibleArea.position.y + translation.y)
}
}
}
Overriden touchesMoved method is there just because of better example. What I did here, is:
created SKCropNode
added a texture to it which will be masked
defined visible area which is SKSpriteNode and assigned it to crop node's mask property, which actually does the magic
Here is the result:
If you want to break up a texture into smaller chunks of textures to be used as puzzle pieces, then you want to use SKTexture(rect: in texture:)
Here is an example of how to use it:
let texture = SKTexture(...) //How ever you plan on getting main texture
let subTextureRect = CGRect(x:0,y:0.width:10,height:10) // The actual location and size of where you want to grab the sub texture from main texture
let subTexture = SKTexture(rect:subTextureRect, in:texture);
You now have a chunk of the sub texture to use in other nodes.

How to detect collision without physics

I want to detect when two bodies contact without using didBeginContact() function
I try with CGRectIntersectsRect() without any effect.
class GameScene: SKScene, SKPhysicsContactDelegate {
// Player
var _player = SKNode()
var platform = SKNode()
let heroCategory: UInt32 = 1 << 0
let platformCategory: UInt32 = 1 << 1
override func didMoveToView(view: SKView) {
/* Setup your scene here */
// Setup
self.anchorPoint = CGPointMake(0.5, 0.5)
self.physicsWorld.gravity = CGVectorMake(0.0, -2.0);
self.physicsWorld.contactDelegate = self
// Start populating
_player = creatPlayer()
self.addChild(_player)
platform = creatPlatform(1, atPosition: CGPoint(x: 0, y: -200))
self.addChild(platform)
let platformTwo = creatPlatform(2, atPosition: CGPoint(x: 0, y: 200))
self.addChild(platformTwo)
}
func creatPlatform(type: Int, atPosition: CGPoint) -> PlatformNode {
let node = PlatformNode()
node.platformType = type
node.position = atPosition
let sprite = SKSpriteNode(color: UIColor.redColor(), size: CGSize(width: 200, height: 50))
node.addChild(sprite)
node.physicsBody = SKPhysicsBody(rectangleOfSize: sprite.size)
node.physicsBody?.dynamic = false;
node.physicsBody?.categoryBitMask = platformCategory
node.physicsBody?.contactTestBitMask = heroCategory
node.physicsBody?.collisionBitMask = 0;
return node
}
func creatPlayer() -> SKNode {
let playerNode = SKNode()
let sprite = SKSpriteNode(color: UIColor.whiteColor(), size: CGSize(width: 50, height: 50))
playerNode.addChild(sprite)
playerNode.physicsBody = SKPhysicsBody(rectangleOfSize: sprite.size)
playerNode.physicsBody?.dynamic = false
playerNode.physicsBody?.allowsRotation = false
playerNode.physicsBody?.restitution = 1.0
playerNode.physicsBody?.friction = 0.0
playerNode.physicsBody?.angularDamping = 0.0
playerNode.physicsBody?.linearDamping = 0.0
playerNode.physicsBody?.categoryBitMask = heroCategory
playerNode.physicsBody?.contactTestBitMask = platformCategory
playerNode.physicsBody?.collisionBitMask = 0;
return playerNode
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
_player.physicsBody?.dynamic = true
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if(CGRectIntersectsRect(platform.frame, _player.frame)){
println("Collision")
}
}
}
Maybe using CGRectIntersectsRect is a wrong approach, but I can be used instead?
The frame property
The frame property is a rectangle in the parent’s coordinate system
that contains the node’s content, ignoring the node’s children.
Also from docs:
The frame is non-empty if the node’s class draws content.
And because SKNode class does not perform any drawing of its own, and your platform is probably subclass of SKNode and player is SKNode the frame property is always (0,0,0,0).
You can access the real size of a frame in few ways:
You can make player and platform as subclasses of SKSpriteNode.
You can leave them as they are and access the child SKSpriteNode inside them.
You can leave as they are and use calcualteAccumulatedFrame() method.
About calcualteAccumulatedFrame() from docs:
A node’s accumulated frame, retrieved by calling the
calculateAccumulatedFrame method, is the largest rectangle that
includes the frame of the node and the frames of all its descendants.

How would I get my character image to change when there is a jump?

I have this code where it animates my 2d character to make it look like its running. In my touchesBegan I have this code where when theres a tap the character jumps and I want the image to change to this jump image I have for the character. Why when I tap the screen and the character jumps the image doesn't change? Thanks! Heres the code I have?
func addHero() {
let heroTextureOne = SKTexture(imageNamed: "heroup")
let heroTextureTwo = SKTexture(imageNamed: "herodown")
let anim = SKAction.animateWithTextures([heroTextureOne, heroTextureTwo], timePerFrame: 0.2)
let run = SKAction.repeatActionForever(anim)
theHero = SKSpriteNode(texture: heroTextureTwo)
theHero.runAction(run)
theHero.physicsBody = SKPhysicsBody(rectangleOfSize: theHero.size)
theHero.physicsBody?.affectedByGravity = true
theHero.position = CGPointMake(self.size.width / 3, self.size.height / 1.0)
theHero.zPosition = 15
addChild(theHero)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
var touch: UITouch = touches.first as! UITouch
var location = touch.locationInNode(self)
var node = self.nodeAtPoint(location)
if (theHero.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 250)) != nil) {
theHero.texture = SKTexture(imageNamed: "jumphero")
}
have you tried not using the if statement? I'm not sure that your statement is returning true, therefore not going into the block, try this, without if. Just apply impulse, then change texture:
theHero.physicsBody?.applyImpulse(CGVector(dx: 0, dy: 250))
theHero.texture = SKTexture(imageNamed: "jumphero")