Swift: Trying to rotate a drawn shape? - ios

I'm having trouble trying to rotate the shape I have drawn in the following code: I tried using:
triangle.transform = triangle.transform.rotated(by: 3.14159)
but that gives me an error of "value of type 'CATransform3D' has no member 'rotated'". I'm not sure how to rotate the shape.
override func viewDidLoad() {
super.viewDidLoad()
let trianglePath = UIBezierPath()
trianglePath.move(to: CGPoint(x: UIScreen.main.bounds.size.width * 0.5, y: UIScreen.main.bounds.height * 0.5))
trianglePath.addLine(to: CGPoint(x: (UIScreen.main.bounds.size.width * 0.5) + 100, y: (UIScreen.main.bounds.height * 0.5) - 50))
trianglePath.addLine(to: CGPoint(x: (UIScreen.main.bounds.size.width * 0.5) + 100, y: (UIScreen.main.bounds.height * 0.5) + 50))
trianglePath.close()
let triangle = CAShapeLayer()
triangle.path = trianglePath.cgPath
triangle.fillColor = UIColor.gray.cgColor
self.view.layer.addSublayer(triangle)
}

Use this to rotate your triangle, you can use use CGAffineTransform
triangle.setAffineTransform(CGAffineTransform(rotationAngle: .pi))
or using CATransform3D, rotating in Z axis by pi
triangle.transform = CATransform3DMakeRotation(.pi, 0, 0, 1)
but to this work you need to adjust also your CAShapeLayer frame
triangle.frame = self.view.bounds //this do the trick
For animate after it shows
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.1) {
triangle.setAffineTransform(CGAffineTransform(rotationAngle: .pi)) //this
triangle.transform = CATransform3DMakeRotation(.pi, 0, 0, 1) //or this
}
}
Hope this helps

Related

Animate tab bar shape created using UIBezierPath() based on selected index

I have created a tab Bar shape using UIBezierPath(). Im new to creating shapes so in the end I got the desired shape closer to what I wanted, not perfect but it works. It's a shape which has a wave like top, so on the left side there is a crest and second half has a trough. This is the code that I used to create the shape:
func createPath() -> CGPath {
let height: CGFloat = 40.0 // Height of the wave-like curve
let extraHeight: CGFloat = -20.0 // Additional height for top left and top right corners
let path = UIBezierPath()
let width = self.frame.width
// Creating a wave-like top edge for tab bar starting from left side
path.move(to: CGPoint(x: 0, y: extraHeight)) // Start at top left corner with extra height
path.addQuadCurve(to: CGPoint(x: width/2, y: extraHeight), controlPoint: CGPoint(x: width/4, y: extraHeight - height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width*3/4, y: extraHeight + height))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
Above im using -20 so that shape stays above bounds of tab bar and second wave's trough stays above the icons of tab bar. Here is the desired result:
This was fine until I was asked to animate the shape on pressing tab bar items. So if I press second item, the crest should be above second item and if fourth, then it should be above fourth item. So I created a function called updateShape(with selectedIndex: Int) and called it in didSelect method of my TabBarController. In that im passing index of the selected tab and based on that creating new path and removing old and replacing with new one. Here is how im doing it:
func updateShape(with selectedIndex: Int) {
let shapeLayer = CAShapeLayer()
let height: CGFloat = 40.0
let extraHeight: CGFloat = -20.0
let width = self.frame.width
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: extraHeight))
if selectedIndex == 0 {
path.addQuadCurve(to: CGPoint(x: width / 2, y: extraHeight), controlPoint: CGPoint(x: width / 4, y: extraHeight - height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width / 2 + width / 4, y: extraHeight + height))
}
else if selectedIndex == 1 {
path.addQuadCurve(to: CGPoint(x: width / 2 + width / 4, y: extraHeight), controlPoint: CGPoint(x: width / 4 + width / 4, y: extraHeight - height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width * 3 / 4 + width / 4, y: extraHeight + height))
}
else if selectedIndex == 2 {
let xShift = width / 4
path.addQuadCurve(to: CGPoint(x: width / 2 + xShift, y: extraHeight), controlPoint: CGPoint(x: width / 8 + xShift, y: extraHeight + height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width * 7 / 8 + xShift, y: extraHeight - height))
}
else {
path.addQuadCurve(to: CGPoint(x: width / 2, y: extraHeight), controlPoint: CGPoint(x: width / 4, y: extraHeight + height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width / 2 + width / 4, y: extraHeight - height))
}
path.addLine(to: CGPoint(x: width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.secondarySystemBackground.cgColor
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
And calling it like this:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBar = tabBarController.tabBar as! CustomTabBar
guard let index = viewControllers?.firstIndex(of: viewController) else {
return
}
tabBar.updateShape(with: index)
}
This is working fine as you can see below but the problem is I learned creating shapes just now and creating that wave based on width of screen so the crest and trough are exactly half the width of frame so I was able to do it for FourthViewController too and got this:
But the problem arises for remaining 2 indices. Im not able to create same wave which looks like the crest is moving above second or third item instead I get something like this:
It doesn't look like other to waves showing the hump over third item. Also my code is strictly based on 4 items and was wondering if Im asked to add 1 more item so tab bar has 5 or 6 items, it would be trouble. Is there any way to update my function that creates new shapes based on index of tab bar or can anyone help me just create shapes for remaining two items? The shape should look same just the hump should exactly be over the selected item.
So you want something like this:
Here's how I did it. First, I made a WaveView that draws this curve:
Notice that the major peak exactly in the center. Here's the source code:
class WaveView: UIView {
var trough: CGFloat {
didSet { setNeedsDisplay() }
}
var color: UIColor {
didSet { setNeedsDisplay() }
}
init(trough: CGFloat, color: UIColor = .white) {
self.trough = trough
self.color = color
super.init(frame: .zero)
contentMode = .redraw
isOpaque = false
}
override func draw(_ rect: CGRect) {
guard let gc = UIGraphicsGetCurrentContext() else { return }
let bounds = self.bounds
let size = bounds.size
gc.translateBy(x: bounds.origin.x, y: bounds.origin.y)
gc.move(to: .init(x: 0, y: size.height))
gc.saveGState(); do {
// Transform the geometry so the bounding box of the curve
// is (-1, 0) to (+1, +1), with the y axis going up.
gc.translateBy(x: bounds.midX, y: trough)
gc.scaleBy(x: bounds.size.width * 0.5, y: -trough)
// Now draw the curve.
for x in stride(from: -1, through: 1, by: 2 / size.width) {
let y = (cos(2.5 * .pi * x) + 1) / 2 * (1 - x * x)
gc.addLine(to: .init(x: x, y: y))
}
}; gc.restoreGState()
// The geometry is restored.
gc.addLine(to: .init(x: size.width, y: size.height))
gc.closePath()
gc.setFillColor(UIColor.white.cgColor)
gc.fillPath()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
Then, I made a subclass of UITabBarController named WaveTabBarController that creates a WaveView and puts it behind the tab bar. It makes the WaveView exactly twice the width of the tab bar, and sets the x coordinate of the WaveView's frame such that the peak is above the selected tab item. Here's the source code:
class WaveTabBarController: UITabBarController {
let waveView = WaveView(trough: 20)
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
if superclass!.instancesRespond(to: #selector(UITabBarDelegate.tabBar(_:didSelect:))) {
super.tabBar(tabBar, didSelect: item)
}
view.setNeedsLayout()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let shouldAnimate: Bool
if waveView.superview != view {
view.insertSubview(waveView, at: 0)
shouldAnimate = false
} else {
shouldAnimate = true
}
let tabBar = self.tabBar
let w: CGFloat
if let selected = tabBar.selectedItem, let items = tabBar.items {
w = (CGFloat(items.firstIndex(of: selected) ?? 0) + 0.5) / CGFloat(items.count) - 1
} else {
w = -1
}
let trough = waveView.trough
let tabBarFrame = view.convert(tabBar.bounds, from: tabBar)
let waveFrame = CGRect(
x: tabBarFrame.origin.x + tabBarFrame.size.width * w,
y: tabBarFrame.origin.y - trough,
width: 2 * tabBarFrame.size.width,
height: tabBarFrame.size.height + trough
)
guard waveFrame != waveView.frame else {
return
}
if shouldAnimate {
// Don't animate during the layout pass.
DispatchQueue.main.async { [waveView] in
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
waveView.frame = waveFrame
}
}
} else {
waveView.frame = waveFrame
}
}
}

Animate UIView to "squiggle" its way to destination instead of going in straight line

I have an animation like this:
bubble.frame.origin = CGPoint(x: 75, y: -120)
UIView.animate(
withDuration: 2.5,
animations: {
self.bubble.transform = CGAffineTransform(translationX: 0, y: self.view.frame.height * -1.3)
}
)
However, the animation goes in a straight line. I want the animation to do a little back and forth action on its way to its destination. Like a bubble. Any ideas?
If you want to animate along a path you can use CAKeyframeAnimation. The only question is what sort of path do you want. A dampened sine curve might be sufficient:
func animate() {
let box = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
box.backgroundColor = .blue
box.center = bubblePoint(1)
view.addSubview(box)
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = bubblePath().cgPath
animation.duration = 5
box.layer.add(animation, forKey: nil)
}
where
private func bubblePath() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: bubblePoint(0))
for value in 1...100 {
path.addLine(to: bubblePoint(CGFloat(value) / 100))
}
return path
}
/// Point on curve at moment in time.
///
/// - Parameter time: A value between 0 and 1.
/// - Returns: The corresponding `CGPoint`.
private func bubblePoint(_ time: CGFloat) -> CGPoint {
let startY = view.bounds.maxY - 100
let endY = view.bounds.minY + 100
let rangeX = min(30, view.bounds.width * 0.4)
let midX = view.bounds.midX
let y = startY + (endY - startY) * time
let x = sin(time * 4 * .pi) * rangeX * (0.1 + time * 0.9) + midX
let point = CGPoint(x: x, y: y)
return point
}
Yielding:

CATransform3DMakeRotation anchorpoint and position in Swift

Hi I am trying to rotate a triangle shaped CAShapeLayer. The northPole CAShapeLayer is defined in a custom UIView class. I have two functions in the class one to setup the layer properties and one to animate the rotation. The rotation animate works fine but after the rotation completes it reverts back to the original position. I want the layer to stay at the angle given (positionTo = 90.0) after the rotation animation.
I am trying to set the position after the transformation to retain the correct position after the animation. I have also tried to get the position from the presentation() method but this has been unhelpful. I have read many articles on frame, bounds, anchorpoints but I still cannot seem to make sense of why this transformation rotation is not working.
private func setupNorthPole(shapeLayer: CAShapeLayer) {
shapeLayer.frame = CGRect(x: self.bounds.width / 2 - triangleWidth / 2, y: self.bounds.height / 2 - triangleHeight, width: triangleWidth, height: triangleHeight)//self.bounds
let polePath = UIBezierPath()
polePath.move(to: CGPoint(x: 0, y: triangleHeight))
polePath.addLine(to: CGPoint(x: triangleWidth / 2, y: 0))
polePath.addLine(to: CGPoint(x: triangleWidth, y: triangleHeight))
shapeLayer.path = polePath.cgPath
shapeLayer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
The animate function:
private func animate() {
let positionTo = CGFloat(DegreesToRadians(value: degrees))
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.fromValue = 0.0
rotate.toValue = positionTo //CGFloat(M_PI * 2.0)
rotate.duration = 0.5
northPole.add(rotate, forKey: nil)
let present = northPole.presentation()!
print("presentation position x " + "\(present.position.x)")
print("presentation position y " + "\(present.position.y)")
CATransaction.begin()
northPole.transform = CATransform3DMakeRotation(positionTo, 0.0, 0.0, 1.0)
northPole.position = CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2)
CATransaction.commit()
}
To fix the problem, in the custom UIView class I had to override layoutSubviews() with the following:
super.layoutSubviews()
let northPoleTransform: CATransform3D = northPole.transform
northPole.transform = CATransform3DIdentity
setupNorthPole(northPole)
northPole.transform = northPoleTransform

Creating a curly bracket curve from two points

I'm trying to create a curly bracket in Swift, from two points. The idea works fine, with a straight line, because it's currently not dynamic in anyway. My issue lies in finding the dynamic control points and center depending on the location of p1 and p2 points.
This is my current code:
override func viewDidLoad() {
super.viewDidLoad()
let path = UIBezierPath()
let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 100)
let c1 = CGPointMake(150, 80)
let c2 = CGPointMake(250, 80)
var midPoint = midPointForPoints(p1, p2: p2)
var midP1 = midPoint
midP1.x -= 10
var midP2 = midPoint
midP2.x += 10
midPoint.y -= 20
path.moveToPoint(p1)
path.addQuadCurveToPoint(midP1, controlPoint: c1)
path.addLineToPoint(midPoint)
path.addLineToPoint(midP2)
path.addQuadCurveToPoint(p2, controlPoint: c2)
let shape = CAShapeLayer()
shape.lineWidth = 5
shape.strokeColor = UIColor.redColor().CGColor
shape.fillColor = UIColor.clearColor().CGColor
shape.path = path.CGPath
self.view.layer.addSublayer(shape)
}
func midPointForPoints(p1: CGPoint, p2: CGPoint)->CGPoint{
let deltaX = (p1.x + p2.x)/2
let deltaY = (p1.y + p2.y)/2
let midPoint = CGPointMake(deltaX, deltaY)
return midPoint
}
This doesen't take the degrees of the points into account, so if I were to create the two points as:
let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 300)
It would not find the proper control points and midpoint.
Hope someone can help me in the right direction. The idea is of course in the end to just know the two points (p1, p2) and dynamically create every other points, I just typed in values for the moment, to make it easier for myself. I've added images of the issue to better show you.
First create a path for a brace that starts at (0, 0) and ends at (1, 0). Then apply an affine transformation that moves, scales, and rotates the path to span your designed endpoints. It needs to transform (0, 0) to your start point and (1, 0) to your end point. Creating the transformation efficiently requires some trigonometry, but I've done the homework for you:
extension UIBezierPath {
class func brace(from start: CGPoint, to end: CGPoint) -> UIBezierPath {
let path = self.init()
path.move(to: .zero)
path.addCurve(to: CGPoint(x: 0.5, y: -0.1), controlPoint1: CGPoint(x: 0, y: -0.2), controlPoint2: CGPoint(x: 0.5, y: 0.1))
path.addCurve(to: CGPoint(x: 1, y: 0), controlPoint1: CGPoint(x: 0.5, y: 0.1), controlPoint2: CGPoint(x: 1, y: -0.2))
let scaledCosine = end.x - start.x
let scaledSine = end.y - start.y
let transform = CGAffineTransform(a: scaledCosine, b: scaledSine, c: -scaledSine, d: scaledCosine, tx: start.x, ty: start.y)
path.apply(transform)
return path
}
}
Result:
Here's the entire Swift playground I used to make the demo:
import UIKit
import PlaygroundSupport
extension UIBezierPath {
class func brace(from start: CGPoint, to end: CGPoint) -> UIBezierPath {
let path = self.init()
path.move(to: .zero)
path.addCurve(to: CGPoint(x: 0.5, y: -0.1), controlPoint1: CGPoint(x: 0, y: -0.2), controlPoint2: CGPoint(x: 0.5, y: 0.1))
path.addCurve(to: CGPoint(x: 1, y: 0), controlPoint1: CGPoint(x: 0.5, y: 0.1), controlPoint2: CGPoint(x: 1, y: -0.2))
let scaledCosine = end.x - start.x
let scaledSine = end.y - start.y
let transform = CGAffineTransform(a: scaledCosine, b: scaledSine, c: -scaledSine, d: scaledCosine, tx: start.x, ty: start.y)
path.apply(transform)
return path
}
}
class ShapeView: UIView {
override class var layerClass: Swift.AnyClass { return CAShapeLayer.self }
lazy var shapeLayer: CAShapeLayer = { self.layer as! CAShapeLayer }()
}
class ViewController: UIViewController {
override func loadView() {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 600, height: 200))
view.backgroundColor = .white
for (i, handle) in handles.enumerated() {
handle.autoresizingMask = [ .flexibleTopMargin, .flexibleTopMargin, .flexibleBottomMargin, .flexibleRightMargin ]
let frame = CGRect(x: view.bounds.width * 0.1 + CGFloat(i) * view.bounds.width * 0.8 - 22, y: view.bounds.height / 2 - 22, width: 44, height: 44)
handle.frame = frame
handle.shapeLayer.path = CGPath(ellipseIn: handle.bounds, transform: nil)
handle.shapeLayer.lineWidth = 2
handle.shapeLayer.lineDashPattern = [2, 6]
handle.shapeLayer.lineCap = kCALineCapRound
handle.shapeLayer.strokeColor = UIColor.blue.cgColor
handle.shapeLayer.fillColor = nil
view.addSubview(handle)
let panner = UIPanGestureRecognizer(target: self, action: #selector(pannerDidFire(panner:)))
handle.addGestureRecognizer(panner)
}
brace.shapeLayer.lineWidth = 2
brace.shapeLayer.lineCap = kCALineCapRound
brace.shapeLayer.strokeColor = UIColor.black.cgColor
brace.shapeLayer.fillColor = nil
view.addSubview(brace)
setBracePath()
self.view = view
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setBracePath()
}
private let handles: [ShapeView] = [
ShapeView(),
ShapeView()
]
private let brace = ShapeView()
private func setBracePath() {
brace.shapeLayer.path = UIBezierPath.brace(from: handles[0].center, to: handles[1].center).cgPath
}
#objc private func pannerDidFire(panner: UIPanGestureRecognizer) {
let view = panner.view!
let offset = panner.translation(in: view)
panner.setTranslation(.zero, in: view)
var center = view.center
center.x += offset.x
center.y += offset.y
view.center = center
setBracePath()
}
}
let vc = ViewController()
PlaygroundPage.current.liveView = vc.view
The key to the problem is when the figure is rotated your base vectors will rotate. When your figure is axis-aligned your base vectors are u (1, 0) and v (0, 1).
So when you are performing midPoint.y -= 20 you can see it as the same as midPoint.x -= v.x * 20; midPoint.y -= v.y * 20 where v is (0, 1). The results are the same, check for yourself.
This implementation will do what your code does, only axis independent.
let path = UIBezierPath()
let p1 = CGPointMake(100, 100)
let p2 = CGPointMake(300, 100)
let o = p1.plus(p2).divide(2.0) // origo
let u = p2.minus(o) // base vector 1
let v = u.turn90() // base vector 2
let c1 = o.minus(u.times(0.5)).minus(v.times(0.2)) // CGPointMake(150, 80)
let c2 = o.plus(u.times(0.5)).minus(v.times(0.2)) // CGPointMake(250, 80)
var midPoint = o.minus(v.times(0.2))
var midP1 = o.minus(u.times(0.2))
var midP2 = o.plus(u.times(0.2))
Note: I set the factors to match the initial values in your implementation.
Also added this CGPoint extension for convenience. Hope it helps.
extension CGPoint {
public func plus(p: CGPoint) -> (CGPoint)
{
return CGPoint(x: self.x + p.x, y: self.y + p.y)
}
public func minus(p: CGPoint) -> (CGPoint)
{
return CGPoint(x: self.x - p.x, y: self.y - p.y)
}
public func times(f: CGFloat) -> (CGPoint)
{
return CGPoint(x: self.x * f, y: self.y * f)
}
public func divide(f: CGFloat) -> (CGPoint)
{
return self.times(1.0/f)
}
public func turn90() -> (CGPoint)
{
return CGPoint(x: -self.y, y: x)
}
}

Does SpriteKit support dense tessellation of a sprite / texture / shape so it can be warped freely?

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.

Resources