SKShapeNode - Animate color change
Asked Answered
G

6

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

Grandsire answered 1/1, 2014 at 19:51 Comment(0)
P
35

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.

Pula answered 14/1, 2015 at 21:4 Comment(3)
Results in "Simultaneous accesses to..." error in swift 4 unless Exclusive Access to Memory is set to No Enforcement in build settings.Nevski
I'll check in the evening, thanks for the heads up Tim.Pula
@Nevski 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.Pula
R
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.

Reindeer answered 1/1, 2014 at 20:25 Comment(5)
@Aaron thanks for the edit ... adding syntax highlighting on the iPad is so damn annoyingReindeer
Yeah, I wish Apple would add a Markdown keyboard… never gonna happen though.Huddleston
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.Reindeer
This solution does not animate the color change, it just happens instantly.Burnell
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 blockUnintelligent
D
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;
}
Downthrow answered 7/1, 2015 at 17:23 Comment(2)
Very helpful. I converted this to a category on SKAction and included a duration parameter in the method signature.Omega
Here's a category version (using a lerp macro).Compensation
U
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];
Unintelligent answered 29/10, 2014 at 23:14 Comment(0)
P
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
    })
}
Palimpsest answered 8/3, 2018 at 6:22 Comment(0)
A
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.

Atheism answered 25/7, 2017 at 16:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.