Swift : Animate SKSpriteNode Gradient

2019-09-01 04:54发布

问题:

What i want to do seems very simple to me, but I can't figure out a way to implement it given that I am pretty new to iOS.

I want to pretty much have my background a gradient that changes colors by fading in and fading out very slowly.

I found this on github and used it to create two gradient nodes with different colors, the only issue is that I can't seem to animate its alpha or have it fade in and out in any way. https://github.com/braindrizzlestudio/BDGradientNode Here is what I did:

//Blueish
    let color1 = UIColor(red: 0.965, green: 0.929, blue: 0.667, alpha: 1)
    let color2 = UIColor(red: 0.565, green: 0.71, blue: 0.588, alpha: 1)
    let color3 = UIColor(red: 0.259, green: 0.541, blue: 0.529, alpha: 1)
    let colors = [color1, color2, color3]

    //Redish
    let color4 = UIColor(red: 1, green: 0.518, blue: 0.769, alpha: 1)
    let color5 = UIColor(red: 0.859, green: 0.22, blue: 0.541, alpha: 1)
    let color6 = UIColor(red: 0.737, green: 0, blue: 0.314, alpha: 1)
    let colors2 = [color4, color5, color6]

    let blending : Float = 0.3

    let location1 : CGFloat = 0.5
    let locations : [CGFloat] = [location1]

    let startPoint = CGPoint(x: 0.3, y: 0.0)
    let endPoint = CGPoint(x: 0.6, y: 0.8)

    let size = CGSize(width: self.size.width, height: self.size.height)

    let texture = SKTexture(imageNamed: "White Background")


    myGradientNode = BDGradientNode(linearGradientWithTexture: texture, colors: colors, locations: nil, startPoint: startPoint, endPoint: endPoint, blending: blending, keepTextureShape: true, size: size)

    myGradientNode2 = BDGradientNode(linearGradientWithTexture: texture, colors: colors2, locations: nil, startPoint: startPoint, endPoint: endPoint, blending: blending, keepTextureShape: true, size: size)
    myGradientNode2?.blending = 0

    self.addChild(myGradientNode2!)
    self.addChild(myGradientNode!)

I feel like there is an easier way, maybe not using this. You see this in a lot of gaming apps now and it would help so much if someone could assist me with achieving this stunning design!

回答1:

Here is a possible complete solution (in Swift 2.2), which you can tweak to your heart's content (including error handling that makes sense for your app). I'll lay out the code in reverse order, starting with the goal of being able to write:

// assuming:
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)

if let bg = HorizontalGradientNode(
    size: scene.size,
    fadingFrom: [.AliceBlue, .BurlyWood],
    to: [.AliceBlue, .CornflowerBlue],
    wait: 1,
    duration: 2)
{
    scene.addChild(bg)
}

This will add a node to the scene encapsulating two child gradient sprite nodes, the top one of which will fade in and out forever. The HorizontalGradientNode.init takes two arrays of CGColor. They can contain two or more colours each! The named CGColors (AliceBlue, CornflowerBlue, etc.) can be constructed in the following way:

public extension CGColor {

    public static func withHex(hex: Int, alpha: CGFloat = 1) -> CGColor {
        let x = max(0, min(hex, 0xffffff))
        let r = CGFloat((x & 0xff0000) >> 16)   / 0xff
        let g = CGFloat((x & 0x00ff00) >> 8)    / 0xff
        let b = CGFloat( x & 0x0000ff)          / 0xff
        return CGColorCreateGenericRGB(r, g, b, alpha)
    }

    public static var AliceBlue: CGColor { return .withHex(0xF0F8FF) }
    public static var BurlyWood: CGColor { return .withHex(0xDEB887) }
    public static var CornflowerBlue: CGColor { return .withHex(0x6495ED) }
    // see named colours at: http://www.w3schools.com/colors/colors_names.asp
}

The simplest version of HorizontalGradientNode would be something like:

class HorizontalGradientNode : SKNode {

    init?(size: CGSize, fadingFrom colors1: [CGColor], to colors2: [CGColor], wait: NSTimeInterval, duration: NSTimeInterval) {
        guard
            let grad1 = CGImage.withHorizontalGradient(size: size, colors: colors1),
            let grad2 = CGImage.withHorizontalGradient(size: size, colors: colors2)
            else
        {
            return nil
        }
        let bg1 = SKSpriteNode(texture: SKTexture(CGImage: grad1))
        let bg2 = SKSpriteNode(texture: SKTexture(CGImage: grad2))
        bg2.alpha = 0

        super.init()
        addChild(bg1)
        addChild(bg2)

        bg2.runAction(
            .repeatActionForever(
                .sequence(
                    [
                        .waitForDuration(wait),
                        .fadeInWithDuration(duration),
                        .waitForDuration(wait),
                        .fadeOutWithDuration(duration)
                    ]
                )
            )
        )
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

This CGImage.withHorizontalGradient(size:, colors:) could be implemented as follows:

public extension CGImage {

    public static func withHorizontalGradient(size size: CGSize, colors: [CGColor]) -> CGImage? {
        guard colors.count >= 2 else { return nil }
        let locations: [CGFloat] = colors.indices.map{
            CGFloat($0) / CGFloat(colors.count - 1)
        }
        guard let gradient = CGGradientCreateWithColors(nil, colors, locations) else {
            return nil
        }
        let start = CGPoint(x: size.width / 2, y: size.height)
        let end = CGPoint(x: size.width / 2, y: 0)
        return CGContext.rgb(size)?
            .draw(linearGradient: gradient, start: start, end: end)
            .image
    }
}

Making use of dot syntax extensions of CGContext like:

public extension CGContext {

    public static func rgb(size: CGSize) -> CGContext? {
        let sp = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB)
        let bi = CGImageAlphaInfo.PremultipliedLast.rawValue
        return CGBitmapContextCreate(nil, Int(ceil(size.width)), Int(ceil(size.height)), 8, 0, sp, bi)
    }

    public var image: CGImage? { return CGBitmapContextCreateImage(self) }

    public func draw(linearGradient
        gradient: CGGradient?,
        start: CGPoint,
        end: CGPoint,
        options: CGGradientDrawingOptions = [.DrawsBeforeStartLocation, .DrawsAfterEndLocation]
        ) -> CGContext
    {
        CGContextDrawLinearGradient(self, gradient, start, end, options)
        return self
    }
}

If you want to try this in a playground you can paste all this in, prefixed by the following snippet (for OS X) making sure you have the playground's timeline in the Assistant editor:

import XCPlayground
import Cocoa
import SpriteKit

let scene = SKScene(size: CGSize(width: 400, height: 400))
let view = SKView(frame: CGRect(origin: .zero, size: scene.size))
XCPlaygroundPage.currentPage.liveView = view
view.presentScene(scene)


回答2:

In the comments following my initial answer @OriginalAlchemist's asked if the HorizontalGradientNode can be amended to cycle through more than two sets of colours. Here is one way to do it (assuming CGImage.withHorizontalGradient(size:colors:) from my first answer is available):

class HorizontalGradientNode : SKNode {

    init(size: CGSize, gradientColorSets: [[CGColor]], wait: NSTimeInterval, duration: NSTimeInterval) {
        super.init()

        var i = 0
        let setNextGradientTexture = SKAction.customActionWithDuration(0) { (node, _) in
            guard let sprite = node as? SKSpriteNode else { return }
            let colors = gradientColorSets[i]
            i = (i + 1) % gradientColorSets.count
            guard let image = CGImage.withHorizontalGradient(size: size, colors: colors) else { return }
            sprite.texture = SKTexture(CGImage: image)
            sprite.size = sprite.texture!.size()
        }

        let sprite1 = SKSpriteNode()
        let sprite2 = SKSpriteNode()

        sprite1.runAction(setNextGradientTexture)

        sprite2.alpha = 0
        sprite2.runAction(
            .repeatActionForever(
                .sequence(
                    [
                         setNextGradientTexture,
                        .waitForDuration(wait),
                        .fadeInWithDuration(duration),
                        .runBlock({ sprite1.runAction(setNextGradientTexture) }),
                        .waitForDuration(wait),
                        .fadeOutWithDuration(duration)
                    ]
                )
            )
        )

        addChild(sprite1)
        addChild(sprite2)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Usage:

// assuming
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)

let gradientColorSets: [[CGColor]] = [
    [.AliceBlue, .BurlyWood], // note that you can have more than two colours per set
    [.BurlyWood, .CornflowerBlue],
    [.CornflowerBlue, .ForestGreen],
    [.ForestGreen, .AliceBlue]
]

let bg = HorizontalGradientNode(size: scene.size, gradientColorSets: gradientColorSets, wait: 1, duration: 2)
scene.addChild(bg)

@OriginalAlchemist also asked for an iOS compatible version of the named CGColors:

public extension CGColor {

    public static func withHex(hex: Int, alpha: CGFloat = 1) -> CGColor {
        let x = max(0, min(hex, 0xffffff)) // allows force unwrapping of the return value
        let r = CGFloat((x & 0xff0000) >> 16)   / 0xff
        let g = CGFloat((x & 0x00ff00) >> 8)    / 0xff
        let b = CGFloat( x & 0x0000ff)          / 0xff
        let cp = CGColorSpaceCreateDeviceRGB()
        return CGColorCreate(cp, [r, g, b, alpha])!
    }

    public static var AliceBlue: CGColor { return .withHex(0xF0F8FF) }
    public static var BurlyWood: CGColor { return .withHex(0xDEB887) }
    public static var CornflowerBlue: CGColor { return .withHex(0x6495ED) }
    public static var ForestGreen: CGColor { return .withHex(0x228B22) }
    // see named colours at: http://www.w3schools.com/colors/colors_names.asp
}