I'm playing with spriteKit to create a little game and I'm getting blocked trying to draw a cross (like and X) at a point where i've touched the screen. As i want to draw an X I thought in rotate two rectangles, one Pi/4 and the other one -Pi/4.
I've the following code, i've been playing with node position adding 10 or 20px on both axis. But I cannot understand how the rects intersect on their center for create a cross (like an X).
That's the code I have :
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
guard let touch = touches.first
else
{
return
}
let location = touch.location(in: self)
let touchedNode = nodes(at: location)
_ = atPoint(location).name
print("Posición touch: \(touchedNode)")
// Draw the cross in the touched point
let redSprite1 = SKShapeNode(rect: CGRect(x: 0, y: 0, width: 10, height: 40))
redSprite1.fillColor = .green
redSprite1.position = CGPoint(x: location.x - 10.0, y: location.y)
redSprite1.zRotation = CGFloat(-Pi/4)
addChild(redSprite1)
// 2
let redSprite2 = SKShapeNode(rect: CGRect(x: 0, y: 0, width: 10, height: 40))
redSprite2.fillColor = .red
redSprite2.position = CGPoint(x: location.x + 10.0, y: location.y)
redSprite2.zRotation = CGFloat(Pi/4)
addChild(redSprite2)
}
I've noted that the rotation is made not from the center of the shape but in their bottom.
Is there any option to rotate a shape or tile from their center ?
Thanks to #bg2b for the solution.
Looking to the apple doc linked on #bg2b answer and applied give the following code.
// 1
let redSprite1 = SKShapeNode(rectOf: CGSize(width: 10.0, height: 40.0))
redSprite1.fillColor = .green
redSprite1.position = CGPoint(x: location.x, y: location.y)
redSprite1.zRotation = CGFloat(-Pi/4)
redSprite1.name = "cruz1"
addChild(redSprite1)
// 2
let redSprite2 = SKShapeNode(rectOf: CGSize(width: 10.0, height: 40.0))
redSprite2.fillColor = .red
redSprite2.position = CGPoint(x: location.x, y: location.y)
redSprite2.zRotation = CGFloat(Pi/4)
redSprite2.name = "cruz2"
addChild(redSprite2)
Related
I am trying to understand how to achieve kind of circle with more than ten control points like in this video, which can be adjusted to any shape and implemented in swift language.
I have found javascript similar effects, but I don’t know how to start. I also tried to use the Bezier path implementation, the code is as follows, but I don't know how to complete it.
class MyBezierPathView: UIView {
private var path: UIBezierPath?
// start point
var startP = CGPoint.zero
// end point
var endP = CGPoint.zero
// control point
var controlP = CGPoint.zero
var pathColor: UIColor?
var pathWidth: CGFloat = 0.0
// current touch point
private var currentTouchP = 0
// init
override init(frame: CGRect) {
super.init(frame: frame)
}
// draw BezierPath
override func draw(_ rect: CGRect) {
path = UIBezierPath()
path?.move(to: startP)
path?.addQuadCurve(to: endP, controlPoint: controlP)
path?.lineWidth = pathWidth
pathColor?.setStroke()
path?.stroke()
path = UIBezierPath()
path?.lineWidth = 1
UIColor.gray.setStroke()
let lengths: [CGFloat] = [5]
path?.setLineDash(lengths, count: 1, phase: 1)
path?.move(to: controlP)
path?.addLine(to: startP)
path?.stroke()
path?.move(to: controlP)
path?.addLine(to: endP)
path?.stroke()
path = UIBezierPath(arcCenter: startP, radius: 4, startAngle: 0, endAngle: .pi * 2, clockwise: true)
UIColor.black.setStroke()
path?.fill()
path = UIBezierPath(arcCenter: endP, radius: 4, startAngle: 0, endAngle: .pi * 2, clockwise: true)
UIColor.black.setStroke()
path?.fill()
path = UIBezierPath(arcCenter: controlP, radius: 3, startAngle: 0, endAngle: .pi * 2, clockwise: true)
path?.lineWidth = 2
UIColor.black.setStroke()
path?.stroke()
let startMsgRect = CGRect(x: startP.x + 8, y: startP.y - 7, width: 50, height: 20)
"start point".draw(in: startMsgRect, withAttributes: nil)
let endMsgRect = CGRect(x: endP.x + 8, y: endP.y - 7, width: 50, height: 20)
"end point".draw(in: endMsgRect, withAttributes: nil)
let control1MsgRect = CGRect(x: controlP.x + 8, y: controlP.y - 7, width: 50, height: 20)
"control point".draw(in: control1MsgRect, withAttributes: nil)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let startPoint = touches.first?.location(in: self)
let startR = CGRect(x: startP.x - 4, y: startP.y - 4, width: 80, height: 80)
let endR = CGRect(x: endP.x - 4, y: endP.y - 4, width: 80, height: 80)
let controlR = CGRect(x: controlP.x - 4, y: controlP.y - 4, width: 80, height: 80)
guard let startPoint = startPoint else {
print("startPoint is nil.")
return
}
if startR.contains(startPoint) {
currentTouchP = 1
} else if endR.contains(startPoint) {
currentTouchP = 2
} else if controlR.contains(startPoint) {
currentTouchP = 3
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
var touchPoint = touches.first?.location(in: self)
if touchPoint!.x < 0 {
touchPoint!.x = 0
}
if touchPoint!.x > bounds.size.width {
touchPoint!.x = bounds.size.width
}
if touchPoint!.y < 0 {
touchPoint!.y = 0
}
if touchPoint!.y > bounds.size.height {
touchPoint!.y = bounds.size.height
}
switch currentTouchP {
case 1:
startP = touchPoint!
case 2:
endP = touchPoint!
case 3:
controlP = touchPoint!
default:
break
}
setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
currentTouchP = 0
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
touchesEnded(touches, with: event)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let frame = CGRect(x: 0, y: 0, width: view.bounds.size.width, height: view.bounds.size.height)
let pathView = MyBezierPathView(frame: frame)
pathView.startP = CGPoint(x: 110, y: 150)
pathView.endP = CGPoint(x: 258.47, y: 211.53)
pathView.controlP = CGPoint(x: 196.94, y: 150)
pathView.pathColor = #colorLiteral(red: 1, green: 0.1491314173, blue: 0, alpha: 1)
pathView.pathWidth = 2
pathView.backgroundColor = UIColor.white
pathView.layer.borderWidth = 1
view.addSubview(pathView)
}
}
One way you could approach this problem
create a circle from several curve segments (using addQuadCurve() or addCurve() of UIBezierPath class)
addQuadCurve() adds a curve with one control point while addCurve() adds a curve with 2 control points (the video you showed seems using paths with 2 control points, so it would be better using addCurve())
Then user needs to be able to move any of start/end and control points of these curves.
For each these change, you have to redraw the curves
I have created a sample playground with this idea. In this playground, I have created a red circle (not a perfect circle) by four curves using addQuadCurve(). This circle has 8 points you could use to alter the shape. If you use 4 curves with addCurve(), then you will have 12 points to alter the shape.
Then I changed a single point of the red circle and added the updated shape in green color below the original red circle.
import UIKit
import PlaygroundSupport
let container = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 700))
let view1 = UIView(frame: CGRect(x: 50, y: 50, width: 500, height: 350))
let layer1 = CAShapeLayer()
layer1.fillColor = UIColor.clear.cgColor
layer1.lineWidth = 5
layer1.strokeColor = UIColor.red.cgColor
//create a circle wich has 8 points to change it's shape (4 control points and 4 start/end points of curves)
let originalPath = UIBezierPath()
originalPath.move(to: CGPoint(x: 100, y: 0))
originalPath.addQuadCurve(to: CGPoint(x: 200, y: 100), controlPoint: CGPoint(x: 190, y: 10))
originalPath.addQuadCurve(to: CGPoint(x: 100, y: 200), controlPoint: CGPoint(x: 190, y: 190))
originalPath.addQuadCurve(to: CGPoint(x: 0, y: 100), controlPoint: CGPoint(x: 10, y: 190))
originalPath.addQuadCurve(to: CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 10, y: 10))
//add this path to the layer1
layer1.path = originalPath.cgPath
//suppose user move the CGPoint(x: 200, y: 100) to CGPoint(x: 220, y: 100)
//then we can redraw the 4 curves again
let view2 = UIView(frame: CGRect(x: 50, y: 350, width: 500, height: 350))
let layer2 = CAShapeLayer()
layer2.fillColor = UIColor.clear.cgColor
layer2.lineWidth = 5
layer2.strokeColor = UIColor.green.cgColor
//changedPath is almost same as originalPath except CGPoint(x: 250, y: 100)
let changedPath = UIBezierPath()
changedPath.move(to: CGPoint(x: 100, y: 0))
changedPath.addQuadCurve(to: CGPoint(x: 250, y: 100), controlPoint: CGPoint(x: 190, y: 10)) // <---- user has moved point CGPoint(x: 200, y: 100) to CGPoint(x: 250, y: 100). So add this curve to the new point
changedPath.addQuadCurve(to: CGPoint(x: 100, y: 200), controlPoint: CGPoint(x: 190, y: 190))
changedPath.addQuadCurve(to: CGPoint(x: 0, y: 100), controlPoint: CGPoint(x: 10, y: 190))
changedPath.addQuadCurve(to: CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 10, y: 10))
//adding changed path to layer2
layer2.path = changedPath.cgPath
view1.layer.addSublayer(layer1)
view2.layer.addSublayer(layer2)
container.addSubview(view1)
container.addSubview(view2)
PlaygroundPage.current.liveView = container
I am creating and endless runner for iphones in landscape mode using SpriteKit. I set up the scene as such:
override func viewDidLayoutSubviews() {
let scene = GameScene(size: CGSize(width: 812, height: 375))
let skView = view as! SKView
scene.backgroundColor = UIColor.red
scene.scaleMode = .aspectFill
skView.presentScene(scene)
skView.showsFPS = true
skView.showsNodeCount = true
print("Screen Size: \(GlobalProperties.screenSize.width) x \(GlobalProperties.screenSize.height)")
print("Scene Size: \(scene.size.width) x \(scene.size.height)")
}
I would like to position the player so that there is the same amount of pixels between the edge of the player and the right edge of the screen regardless of aspect ratio. Is this a reasonable practice for maintaining difficulty between devices? I have the layout setup as such:
override func didMove(to view: SKView) {
let player = SKSpriteNode(texture: SKTexture(imageNamed: "CuteMelon"))
player.anchorPoint = CGPoint(x: 0.5, y: 0.5)
player.position = CGPoint(x: frame.width - 600, y: frame.midY)
self.addChild(player)
let rect = SKSpriteNode(color: .orange, size: CGSize(width: 550, height: 200))
rect.anchorPoint = CGPoint(x: 1, y: 0.5)
rect.position = CGPoint(x: frame.width-50, y: frame.height/2)
self.addChild(rect)
}
I added the rectangle to see if in both cases the player was 600 pixels from the right (leaving a 50px gap to ensure it wasnt running off the edge)
The result is as follows:
iPhone XR: https://imgur.com/y6OYHkK which is working as intended
iPhone 8: https://imgur.com/nkrx5By which is not placing the rectangle 50 pixels from the right bound of the frame.
What do I have to do to fix this issue or should I go about solving it a different way entirely? Thank you
Simply figure out the difference between the scene size and the screen size, and shift the camera over by half that distance. The formula abs((sceneWidth - screenWidth * sceneHeight/screenHeight)/2) will get you that. What this does is scale the screen to whatever height the scene is, then subtract the two differences from the width and return half that value.
override func didMove(to view: SKView) {
let widthPadding = abs((self.frame.width - (UIScreen.main.bounds.width * self.frame.height / UIScreen.main.bounds.height )) / 2)
self.camera = self.camera ?? SKCameraNode()
self.camera.position.x += widthPadding
let player = SKSpriteNode(texture: SKTexture(imageNamed: "CuteMelon"))
player.anchorPoint = CGPoint(x: 0.5, y: 0.5)
player.position = CGPoint(x: frame.width - 600, y: frame.midY)
self.addChild(player)
let rect = SKSpriteNode(color: .orange, size: CGSize(width: 550, height: 200))
rect.anchorPoint = CGPoint(x: 1, y: 0.5)
rect.position = CGPoint(x: frame.width-50, y: frame.height/2)
self.addChild(rect)
}
I'm trying to create a drawing app that sits on top of a UIImagePickerController after the picture is taken. The touch delegate functions are called correctly but do not leave any traces on the view as they are supposed to. What I have so far:
func createImageOptionsView() { //creates the view above the picker controller overlay view
let imgOptsView = UIView()
let scale = CGAffineTransform(scaleX: 4.0 / 3.0, y: 4.0 / 3.0)
imgOptsView.tag = 1
imgOptsView.frame = view.frame
let imageView = UIImageView()
imageView.frame = imgOptsView.frame
imageView.translatesAutoresizingMaskIntoConstraints = true
imageView.transform = scale //to account for the removal of the black bar in the camera
let useButton = UIButton()
imageView.tag = 2
imageView.contentMode = .scaleAspectFit
imageView.image = currentImage
useButton.setImage(#imageLiteral(resourceName: "check circle"), for: .normal)
useButton.translatesAutoresizingMaskIntoConstraints = true
useButton.frame = CGRect(x: view.frame.width / 2, y: view.frame.height / 2, width: 100, height: 100)
let cancelButton = UIButton()
colorSlider.previewEnabled = true
colorSlider.translatesAutoresizingMaskIntoConstraints = true
imgOptsView.addSubview(imageView)
imgOptsView.addSubview(useButton)
imgOptsView.addSubview(colorSlider)
imgOptsView.isUserInteractionEnabled = true
useButton.addTarget(self, action: #selector(ViewController.usePicture(sender:)), for: .touchUpInside)
picker.cameraOverlayView!.addSubview(imgOptsView)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
mouseSwiped = false //to check if the touch moved or was simply dotted
let touch: UITouch? = touches.first
lastPoint = touch?.location(in: view) //where to start image context from
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
mouseSwiped = true
let touch: UITouch? = touches.first
let currentPoint: CGPoint? = touch?.location(in: view)
UIGraphicsBeginImageContext(view.frame.size) //begin drawing
tempDrawImage.image?.draw(in: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(view.frame.size.width), height: CGFloat(view.frame.size.height)))
UIGraphicsGetCurrentContext()?.move(to: CGPoint(x: CGFloat(lastPoint.x), y: CGFloat(lastPoint.y)))
UIGraphicsGetCurrentContext()?.addLine(to: CGPoint(x: CGFloat((currentPoint?.x)!), y: CGFloat((currentPoint?.y)!)))
UIGraphicsGetCurrentContext()!.setLineCap(.round)
UIGraphicsGetCurrentContext()?.setLineWidth(1.0)
UIGraphicsGetCurrentContext()?.setStrokeColor(c)
UIGraphicsGetCurrentContext()!.setBlendMode(.normal)
UIGraphicsGetCurrentContext()?.strokePath() //move from lastPoint to currentPoint with these options
tempDrawImage.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
lastPoint = currentPoint
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if !mouseSwiped {
UIGraphicsBeginImageContext(view.frame.size)
tempDrawImage.image?.draw(in: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(view.frame.size.width), height: CGFloat(view.frame.size.height)))
UIGraphicsGetCurrentContext()!.setLineCap(.round)
UIGraphicsGetCurrentContext()?.setLineWidth(1.0)
UIGraphicsGetCurrentContext()?.setStrokeColor(c)
UIGraphicsGetCurrentContext()?.move(to: CGPoint(x: CGFloat(lastPoint.x), y: CGFloat(lastPoint.y)))
UIGraphicsGetCurrentContext()?.addLine(to: CGPoint(x: CGFloat(lastPoint.x), y: CGFloat(lastPoint.y)))
UIGraphicsGetCurrentContext()?.strokePath()
UIGraphicsGetCurrentContext()!.flush()
tempDrawImage.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
UIGraphicsBeginImageContext(mainImage.frame.size)
mainImage.image?.draw(in: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(view.frame.size.width), height: CGFloat(view.frame.size.height)), blendMode: .normal, alpha: 1.0)
tempDrawImage.setNeedsDisplay()
mainImage.image = UIGraphicsGetImageFromCurrentImageContext() //paste tempImage onto the main image and then start over
tempDrawImage.image = nil
UIGraphicsEndImageContext()
}
func changedColor(_ slider: ColorSlider) {
c = slider.color.cgColor
}
This is just a guess, but worth a shot. I had a similar problem but in a different context... I was working with AVPlayer when it happened.
In my case, it's because the zPosition wasn't set correctly, so the overlays I was trying to draw appeared behind the video. Assuming that's the case, the fix would be to set the zPosition = -1 which moves it further back like so:
AVPlayerView.layer?.zPosition = -1
This moved the base layer behind the overlay layer (negative numbers move further away from the user in 3D space.)
I would see if the view controlled by your UIImagePickerController allows you to set it's zPosition similar to what AVPlayer allows. If I recall correctly, any view should be able to set it's zPosition. Alternatively, you could try moving your overlay layer to a positive value like zPosition = 1. (I'm not certain, but I would imagine that the default is position is 0.)
I am making a game where there are two balls on the scene, but the user is going to tap on the correct ball. However, when user taps on the correct ball, I want to set a random position for my second ball that when the user taps on the correct ball, I want to also want the second ball to move randomly.
Here is my code for setting up the random positions of the ball:
let currentBall = SKShapeNode(circleOfRadius: CGFloat(size))
let shape = SKShapeNode()
currentBall.fillColor = pickColor()
//Rectangle
shape.path = UIBezierPath(roundedRect: CGRect(x: 64, y: 64, width: 160,height: 160), cornerRadius: 50).cgPath
shape.fillColor = pickColor()
self.addChild(shape)
func randomBallPosition() -> CGPoint {
let xPosition = view!.scene!.frame.midX - viewMidX + CGFloat(arc4random_uniform(UInt32(viewMidX*2)))
let yPosition = view!.scene!.frame.midY - viewMidY + CGFloat(arc4random_uniform(UInt32(viewMidY*2)))
return CGPoint(x: xPosition, y: yPosition)
}
func handleTap() {
currentBall.position = randomBallPosition()
shape.position = randomBallPosition()
}
The issue is that you're setting the exact same position, which you have used for the first ball. As far as I understand both ball should appear at random independent positions. You should create a function to return a new position and call it for each ball:
class MyScene: SKScene {
var currentBall: SKShapeNode!
var shape: SKShapeNode!
override func didMove(to view: SKView) {
super.didMove(to: view)
let currentBall = SKShapeNode(circleOfRadius: CGFloat(size))
currentBall.fillColor = pickColor()
let shape = SKShapeNode()
shape.path = UIBezierPath(roundedRect: CGRect(x: 64, y: 64, width: 160,height: 160), cornerRadius: 50).cgPath
shape.fillColor = pickColor()
self.addChild(shape)
}
func handleTap() {
//your code here, except that you don't create new nodes, just modify existing ones
//at some point you will change balls' positions like this:
currentBall.position = randomBallPosition()
secondBall.position = randomBallPosition()
}
func randomBallPosition() -> CGPoint {
let xPosition = CGFloat(arc4random_uniform(UInt32((view?.bounds.maxX)! + 1)))
let yPosition = CGFloat(arc4random_uniform(UInt32((view?.bounds.maxY)! + 1)))
return CGPoint(x: xPosition, y: yPosition)
}
}
Hope this helps
For example if you have an image of a trampoline, and a character jumping on it. Then you want to animate how the trampoline bends down in the center.
To do this I would have to take a bitmap and apply it to a densely tessellated OpenGL ES mesh. Then apply texture to it. Then deform the mesh.
Does SpriteKit support this or can it only display textures as-is?
With iOS 10, SKWarpGeometry has been added that allows you to deform sprites. Warps are defined using a source and destination grid containing eight control points, these points define the warp transform; Sprite Kit will take care of tessellation and other low level details.
You can set the wrap geometry for a sprite directly, or animate warping with SKActions.
SKAction.animate(withWarps: [SKWarpGeometry], times: [NSNumber]) -> SKAction?
SKAction.animate(withWarps: [SKWarpGeometry], times: [NSNumber], restore: Bool) -> SKAction?
SKAction.warp(to: SKWarpGeometry, duration: TimeInterval) -> SKAction?
UPDATE Nov 2016
On Xcode 8+, the following code snippet can be used in a playground: Drag the control points and see the resulting SKSpriteNode get deformed accordingly.
// SpriteKit warp geometry example
// Copy the following in an Xcode 8+ playground
// and make sure your Assistant View is open
//
import UIKit
import PlaygroundSupport
import SpriteKit
let view = SKView(frame: CGRect(x: 0, y: 0, width: 480, height: 320))
let scene = SKScene(size: view.frame.size)
view.showsFPS = true
view.presentScene(scene)
PlaygroundSupport.PlaygroundPage.current.liveView = view
func drawCanvas1(frame: CGRect = CGRect(x: 0, y: 0, width: 200, height: 200)) -> UIImage {
UIGraphicsBeginImageContext(frame.size)
//// Star Drawing
let starPath = UIBezierPath()
starPath.move(to: CGPoint(x: frame.minX + 100.5, y: frame.minY + 24))
starPath.addLine(to: CGPoint(x: frame.minX + 130.48, y: frame.minY + 67.74))
starPath.addLine(to: CGPoint(x: frame.minX + 181.34, y: frame.minY + 82.73))
starPath.addLine(to: CGPoint(x: frame.minX + 149, y: frame.minY + 124.76))
starPath.addLine(to: CGPoint(x: frame.minX + 150.46, y: frame.minY + 177.77))
starPath.addLine(to: CGPoint(x: frame.minX + 100.5, y: frame.minY + 160))
starPath.addLine(to: CGPoint(x: frame.minX + 50.54, y: frame.minY + 177.77))
starPath.addLine(to: CGPoint(x: frame.minX + 52, y: frame.minY + 124.76))
starPath.addLine(to: CGPoint(x: frame.minX + 19.66, y: frame.minY + 82.73))
starPath.addLine(to: CGPoint(x: frame.minX + 70.52, y: frame.minY + 67.74))
starPath.close()
UIColor.red.setFill()
starPath.fill()
UIColor.green.setStroke()
starPath.lineWidth = 10
starPath.lineCapStyle = .round
starPath.lineJoinStyle = .round
starPath.stroke()
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
class ControlNode : SKShapeNode {
override init() {
super.init()
self.path = CGPath(ellipseIn: CGRect.init(x: 0, y: 0, width: 10, height: 10), transform: nil)
self.fillColor = UIColor.red
self.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
typealias UpdateHandler = (ControlNode) -> ()
var positionUpdated: UpdateHandler? = nil
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.fillColor = UIColor.yellow
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, let parent = self.parent {
let pos = touch.location(in: parent).applying(CGAffineTransform(translationX: -5, y: -5))
self.position = pos
positionUpdated?(self)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.fillColor = UIColor.red
}
var warpPosition: vector_float2 {
if let parent = self.parent {
let size = parent.frame.size
let pos = self.position.applying(CGAffineTransform(translationX: -size.width/2 + 5, y: -size.height/2 + 5))
return vector_float2(
x: Float(1 + pos.x / size.width),
y: Float(1 + pos.y / size.height)
)
}
return vector_float2(x: 0, y: 0)
}
}
extension SKSpriteNode {
func addControlNode(x: CGFloat, y: CGFloat) -> ControlNode {
let node = ControlNode()
node.position = CGPoint(
x: x * frame.width - frame.width / 2 - 5,
y: y * frame.height - frame.height / 2 - 5
)
self.addChild(node)
return node
}
}
let texture = SKTexture(image: drawCanvas1())
let image = SKSpriteNode(texture: texture)
image.position = CGPoint(x: 200, y: 160)
scene.addChild(image)
let controlPoints: [(x:CGFloat, y:CGFloat)] = [
(0, 0),
(0.5, 0),
(1.0, 0),
(0, 0.5),
(0.5, 0.5),
(1.0, 0.5),
(0, 1.0),
(0.5, 1.0),
(1.0, 1.0),
]
let sourcePositions = controlPoints.map {
vector_float2.init(Float($0.x), Float($0.y))
}
var controlNodes: [ControlNode] = []
controlPoints.forEach { controlNodes.append(image.addControlNode(x: $0.x, y: $0.y)) }
for node in controlNodes {
node.positionUpdated = {
[weak image]
_ in
let destinationPoints = controlNodes.map {
$0.warpPosition
}
image?.warpGeometry = SKWarpGeometryGrid(columns: 2, rows: 2, sourcePositions: sourcePositions, destinationPositions: destinationPoints)
}
}
PS: I referred to the SKWarpGeometry documentation.
Edit: See #Benzi's answer for a nice option using the SKWarpGeometry API available in iOS 10 / tvOS 10 / macOS 10.12. Prior to those releases, Sprite Kit could only display textures as billboards — the remainder of this answer addresses that pre-2016 situation, which might still be relevant if you're targeting older devices.
You might be able to get the distortion effect you're going for by using SKEffectNode and one of the geometry distortion Core Image filters, though. (CIBumpDistortion, for example.) Keep a reference to the effect node or its filter property in your game logic code (in your SKScene subclass or wherever else you put it), and you can adjust the filter's parameters using setValue:forKey: for each frame of animation.