24

I'm working on a game with SpriteKit. I'm drawing a shape with SKShapeNode. Now I want to animate its colour change but the SKActions is not working for SKShapeNode. Is there any way to do this or I have to use a different approach?

Thank you.

EDIT:

Thanks to LearnCocos2D I was able to come up with this quick (and totally not perfect) solution.

int groundChangeInterval = 5;
SKColor *originalColor = [SKColor colorWithRed:0.92 green:0.87 blue:0.38 alpha:1.0];
SKColor *finalColor = [SKColor colorWithRed:0.29 green:0.89 blue:0.31 alpha:1.0];

CGFloat red1 = 0.0, green1 = 0.0, blue1 = 0.0, alpha1 = 0.0;
[originalColor getRed:&red1 green:&green1 blue:&blue1 alpha:&alpha1];

CGFloat red2 = 0.0, green2 = 0.0, blue2 = 0.0, alpha2 = 0.0;
[finalColor getRed:&red2 green:&green2 blue:&blue2 alpha:&alpha2];

SKAction *changeGroundColor = [SKAction customActionWithDuration:groundChangeInterval actionBlock:^(SKNode *node, CGFloat elapsedTime) {
    CGFloat step = elapsedTime/groundChangeInterval;

    CGFloat red3 = 0.0, green3 = 0.0, blue3 = 0.0;
    red3 = red1-(red1-red2)*step;
    green3 = green1-(green1-green2)*step;
    blue3 = blue1-(blue1-blue2)*step;

    [(SKShapeNode*)node setFillColor:[SKColor colorWithRed:red3 green:green3 blue:blue3 alpha:1.0]];
    [(SKShapeNode*)node setStrokeColor:[SKColor colorWithRed:red3 green:green3 blue:blue3 alpha:1.0]];
}];

I only needed to fade two specific colours so it is not a universal solution but it is enough for now.

Thanks

Community
  • 1
  • 1
adam
  • 807
  • 3
  • 11
  • 17

6 Answers6

34

Updated for Swift 4.2

This the common code that you need to place in and extension file or something of the sort.

func lerp(a : CGFloat, b : CGFloat, fraction : CGFloat) -> CGFloat
{
    return (b-a) * fraction + a
}

struct ColorComponents {
    var red = CGFloat(0)
    var green = CGFloat(0)
    var blue = CGFloat(0)
    var alpha = CGFloat(0)
}

extension UIColor {
    func toComponents() -> ColorComponents {
        var components = ColorComponents()
        getRed(&components.red, green: &components.green, blue: &components.blue, alpha: &components.alpha)
        return components
    }
}

extension SKAction {
    static func colorTransitionAction(fromColor : UIColor, toColor : UIColor, duration : Double = 0.4) -> SKAction
    {
        return SKAction.customAction(withDuration: duration, actionBlock: { (node : SKNode!, elapsedTime : CGFloat) -> Void in
            let fraction = CGFloat(elapsedTime / CGFloat(duration))
            let startColorComponents = fromColor.toComponents()
            let endColorComponents = toColor.toComponents()
            let transColor = UIColor(red: lerp(a: startColorComponents.red, b: endColorComponents.red, fraction: fraction),
                                     green: lerp(a: startColorComponents.green, b: endColorComponents.green, fraction: fraction),
                                     blue: lerp(a: startColorComponents.blue, b: endColorComponents.blue, fraction: fraction),
                                     alpha: lerp(a: startColorComponents.alpha, b: endColorComponents.alpha, fraction: fraction))
            (node as? SKSpriteNode)?.color = transColor
        }
        )
    }
}

Usage:

redSKSpriteNodeThatBecomesBlue.run(SKAction.colorTransitionAction(fromColor: .red, toColor: .blue, duration: 5))

Please note that for SKShapeNode or other purposes you need to rewrite this line to suit your need:

(node as? SKSpriteNode)?.color = transColor

This solution is heavily inspired originally by Paddy Collins answer.

OwlOCR
  • 1,127
  • 11
  • 22
  • Results in "Simultaneous accesses to..." error in swift 4 unless `Exclusive Access to Memory` is set to `No Enforcement` in build settings. – Tim Nov 16 '18 at 06:34
  • I'll check in the evening, thanks for the heads up Tim. – OwlOCR Nov 16 '18 at 09:44
  • @Tim It seems the issue was that the old implementation used the same array to store all the inout color components. Please check updated version that uses a struct instead. Seems to be working alright now based on quick test. – OwlOCR Nov 16 '18 at 20:14
16

Use customActionWithDuration:block: and change the fillColor or strokeColor properties.

I suppose the colorize actions won't work because SKShapeNode has no color property. It's worth a try to add this property to the class in a subclass or category and redirect it to fillColor or strokeColor or both.

Aaron Brager
  • 65,323
  • 19
  • 161
  • 287
CodeSmile
  • 64,284
  • 20
  • 132
  • 217
  • @Aaron thanks for the edit ... adding syntax highlighting on the iPad is so damn annoying – CodeSmile Jan 01 '14 at 22:53
  • Yeah, I wish Apple would add a Markdown keyboard… never gonna happen though. – Aaron Brager Jan 01 '14 at 23:00
  • If only I could set the selection popup (copy, paste etc) to appear UNDER the selection rather than above. It usually blocks me from tapping any of the markdown icons in the edit box, and that's why I rarely format properly when on the ipad. – CodeSmile Jan 01 '14 at 23:32
  • 2
    This solution does not animate the color change, it just happens instantly. – bpn Aug 13 '14 at 12:02
  • bpn, you could need to set the duration for something like 3.0 (seconds) and then set the value in the block as a percentage of the color transition based on the timing value passed into the block – bshirley Oct 29 '14 at 23:16
8

Here is my implementation. I think a little easier to read.

-(SKAction*)getColorFadeActionFrom:(SKColor*)col1 toColor:(SKColor*)col2 {

    // get the Color components of col1 and col2
    CGFloat r1 = 0.0, g1 = 0.0, b1 = 0.0, a1 =0.0;
    CGFloat r2 = 0.0, g2 = 0.0, b2 = 0.0, a2 =0.0;
    [col1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
    [col2 getRed:&r2 green:&g2 blue:&b2 alpha:&a2];

    // return a color fading on the fill color
    CGFloat timeToRun = 0.3;

    return [SKAction customActionWithDuration:timeToRun actionBlock:^(SKNode *node, CGFloat elapsedTime) {

        CGFloat fraction = elapsedTime / timeToRun;

        SKColor *col3 = [SKColor colorWithRed:lerp(r1,r2,fraction)
                                        green:lerp(g1,g2,fraction)
                                         blue:lerp(b1,b2,fraction)
                                        alpha:lerp(a1,a2,fraction)];

        [(SKShapeNode*)node setFillColor:col3];
        [(SKShapeNode*)node setStrokeColor:col3];
    }];
}

double lerp(double a, double b, double fraction) {
    return (b-a)*fraction + a;
}
Patrick Collins
  • 4,046
  • 3
  • 26
  • 29
  • Very helpful. I converted this to a category on `SKAction` and included a `duration` parameter in the method signature. – Paulw11 Mar 04 '15 at 00:19
  • 1
    [Here's a category version](https://gist.github.com/warpling/fde6a753d6c3ef35b22b00aff4698a4b) (using a `lerp` macro). – Warpling Mar 09 '17 at 02:44
3

I wanted a continuous throbbing glow on my polygon. I used runBlock: instead of customActionWithDuration:block: but could have just run infinitely with that one.

SKShapeNode *shape = [SKShapeNode shapeNodeWithPoints:points count:indices.count];
shape.fillColor = [[SKColor yellowColor] colorWithAlphaComponent:0.2];
shape.strokeColor = [SKColor clearColor];
[self addChild:shape];
shape.userData = @{@"minAlpha": @0, @"maxAlpha": @0.5, @"deltaAlpha": @.025}.mutableCopy;
SKAction *a = [SKAction runBlock:^{
  SKColor *color = shape.fillColor;
  CGFloat delta = [shape.userData[@"deltaAlpha"] floatValue];
  CGFloat w, alpha; [color getWhite:&w alpha:&alpha];
  shape.fillColor = [color colorWithAlphaComponent:alpha + delta];
  if ((delta < 0 && alpha <= [shape.userData[@"minAlpha"] floatValue]) ||
      (delta > 0 && alpha >= [shape.userData[@"maxAlpha"] floatValue])) {
    shape.userData[@"deltaAlpha"] = @(-delta);
  }
}];
SKAction *slice = [SKAction sequence:@[a, [SKAction waitForDuration:0.05]]];
SKAction *glow = [SKAction repeatActionForever:slice];
[shape runAction:glow];
bshirley
  • 8,217
  • 1
  • 37
  • 43
3

Adding to mogelbuster's answer, which is adding to GOR's answer, which is adding to Patrick Collins' answer.

Swift 4

func shapeColorChangeAction(from fromColor: UIColor, to toColor: UIColor, withDuration duration: TimeInterval) -> SKAction {

    func components(for color: UIColor) -> [CGFloat] {
        var comp = color.cgColor.components!
        // converts [white, alpha] to [red, green, blue, alpha]
        if comp.count < 4 {
            comp.insert(comp[0], at: 0)
            comp.insert(comp[0], at: 0)
        }
        return comp
    }
    func lerp(a: CGFloat, b: CGFloat, fraction: CGFloat) -> CGFloat {
        return (b-a) * fraction + a
    }

    let fromComp = components(for: fromColor)
    let toComp = components(for: toColor)
    let durationCGFloat = CGFloat(duration)
    return SKAction.customAction(withDuration: duration, actionBlock: { (node, elapsedTime) -> Void in
        let fraction = elapsedTime / durationCGFloat
        let transColor = UIColor(red: lerp(a: fromComp[0], b: toComp[0], fraction: fraction),
                                 green: lerp(a: fromComp[1], b: toComp[1], fraction: fraction),
                                 blue: lerp(a: fromComp[2], b: toComp[2], fraction: fraction),
                                 alpha: lerp(a: fromComp[3], b: toComp[3], fraction: fraction))
        (node as! SKShapeNode).fillColor = transColor
    })
}
Jayden Irwin
  • 921
  • 9
  • 14
2

I found GOR's answer above helpful, who found Paddy Collin's answer above helpful, so I extended it to animate through an array of colors.

extension SKAction {

func multipleColorTransitionAction(colors:[SKColor], duration:Double) -> SKAction {
    guard colors.count > 1 else { return SKAction.colorize(withColorBlendFactor: 1, duration: 0) }
    var colorActions:[SKAction] = []
    for i in 1..<colors.count {
        colorActions.append( colorTransitionAction(fromColor: colors[i-1] , toColor: colors[i], duration: duration/Double(colors.count)) )
    }
    colorActions.append(colorTransitionAction(fromColor: colors.last!, toColor: colors.first!, duration: duration/Double(colors.count)))
    return SKAction.sequence(colorActions)
}

func colorTransitionAction(fromColor : SKColor, toColor : SKColor, duration : Double = 0.4) -> SKAction {
    func lerp(_ a : CGFloat, b : CGFloat, fraction : CGFloat) -> CGFloat { return (b-a) * fraction + a }
    var frgba:[CGFloat] = [0,0,0,0]
    var trgba:[CGFloat] = [0,0,0,0]
    fromColor.getRed(&frgba[0], green: &frgba[1], blue: &frgba[2], alpha: &frgba[3])
    toColor.getRed(&trgba[0], green: &trgba[1], blue: &trgba[2], alpha: &trgba[3])

    return SKAction.customAction(withDuration: duration, actionBlock: { (node : SKNode!, elapsedTime : CGFloat) -> Void in
        let fraction = CGFloat(elapsedTime / CGFloat(duration))
        let transColor = UIColor(red:   lerp(frgba[0], b: trgba[0], fraction: fraction),
                                 green: lerp(frgba[1], b: trgba[1], fraction: fraction),
                                 blue:  lerp(frgba[2], b: trgba[2], fraction: fraction),
                                 alpha: lerp(frgba[3], b: trgba[3], fraction: fraction))
        (node as! SKShapeNode).fillColor = transColor
    })
}
}

I changed a few things about GOR's original function to accommodate for multiple colors, mainly encapsulating frgba, trgba, and lerp so the block doesn't need a reference to self, and to allow frgba and trgba to have multiple instances captured by multiple blocks. Here is an example usage:

let theRainbow:[SKColor] = [.red,.orange,.yellow,.green,.cyan,.blue,.purple,.magenta]
let rainbowSequenceAction = SKAction.multipleColorTransitionAction(colors: theRainbow, duration: 10)
star.run(SKAction.repeatForever(rainbowSequenceAction))

Where star is an SKShapeNode.

mogelbuster
  • 1,066
  • 9
  • 19