1

I have a .flr animation of a minion. Is it possible to change colors of his body, pants, eyes, etc dynamicaly and separately in flutter app?

enter image description here

PS Minion is just an example i found on rive.app .There will be another character with lots of different parts.

PPS Maybe there is a better way to make a simple animated character in flutter? For now, i have a stack with positioned colorfilterd images, but i guess it should be easier with rive.

Dmitry Podbolotov
  • 153
  • 1
  • 2
  • 7

4 Answers4

2

Yes you can. There is an example in the Flare github: https://github.com/2d-inc/Flare-Flutter/tree/master/example/change_color

import 'package:flare_flutter/flare_controller.dart';
import 'package:flare_flutter/flare.dart';
import 'package:flare_dart/math/mat2d.dart';
import 'package:flare_flutter/flare_actor.dart';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

List<Color> exampleColors = <Color>[Colors.red, Colors.green, Colors.blue];

class _MyHomePageState extends State<MyHomePage> with FlareController {
  FlutterColorFill _fill;
  void initialize(FlutterActorArtboard artboard) {
    // Find our "Num 2" shape and get its fill so we can change it programmatically.
    FlutterActorShape shape = artboard.getNode("Num 2");
    _fill = shape?.fill as FlutterColorFill;
  }

  void setViewTransform(Mat2D viewTransform) {}

  bool advance(FlutterActorArtboard artboard, double elapsed) {
    // advance is called whenever the flare artboard is about to update (before it draws).
    Color nextColor = exampleColors[_counter % exampleColors.length];
    if (_fill != null) {
      _fill.uiColor = nextColor;
    }
    // Return false as we don't need to be called again. You'd return true if you wanted to manually animate some property.
    return false;
  }

  // We're going to use the counter to iterate the color.
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      // This call to setState tells the Flutter framework that something has
      // changed in this State, which causes it to rerun the build method below
      // so that the display can reflect the updated values. If we changed
      // _counter without calling setState(), then the build method would not be
      // called again, and so nothing would appear to happen.
      _counter++;
      // advance the controller
      isActive.value = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: FlareActor("assets/change_color_example.flr", // You can find the example project here: https://www.2dimensions.com/a/castor/files/flare/change-color-example
          fit: BoxFit.contain, alignment: Alignment.center, controller: this),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

If you're using the beta of Rive it's a bit different. I'm not sure if it's the best approach but I'm doing the following:

artboard.forEachComponent((child){
    if (child.name == 'Fill') {
      Fill fill = child;
      fill.paint.color = Colors.red.withOpacity(0.25);
    }
    else if (child.name == 'Stroke') {
      Stroke stroke = child;
      stroke.paint.color = Colors.red;
    }
  });
Kretin
  • 237
  • 2
  • 10
2

As Kretin already outlined with the Flutter rive package it is possible even though it's beta as of writing.

To get this to work, you have to know thinks about your animation, for example the name of the shape you want to change (first), and the fill color you want to change.

Let's assume, I have an animation with a shape which name is 'Button1'.
This shape has one fill color that I'd like to change, then my code looks like this:

artboard.forEachComponent((child) {
      if (child is Shape && child.name == 'Button1') {
        final Shape shape = child;
        shape.fills.first.paint.color = Colors.red;
      }
    });
noobloser
  • 41
  • 4
1

I have tried the forEachComponent method that the other answers here mention, but it didn't work exactly like I expected. It iterated the components over and over again, spending a lot of process power and also because I was testing merging colors, it ended up merging again and again.

So I ended up checking the properties of the classes to find how to get the colors precisely and changing.

In my code, I'm changing a part of a color of linear gradients. But to get a solid color you can change

shape.fills.first.paintMutator.children.[first/last].color

to

shape.fills.first.paint.color

My code is like this:

final rootBone =
        artboard.children.firstWhereOrNull((e) => e.name == 'Root Bone');
    final rootBoneChildren = rootBone?.artboard?.drawables;

    // Changing hair color
    final hair =
        rootBoneChildren?.firstWhereOrNull((e) => e.name == 'hair');
    if (hair is Shape) {
      ((hair.fills.first.paintMutator as dynamic)?.children.first
              as dynamic)
          .color = AppColors.accent;
    }

    // Changing head color
    final head =
        rootBoneChildren?.firstWhereOrNull((e) => e.name == 'head');
    if (head is Shape) {
      final colorParts =
          (head.fills.first.paintMutator as dynamic)?.children;
      const mergeColor = AppColors.brown;
      const timeline = 0.9;
      // center
      colorParts.first.color =
          Color.lerp(colorParts.first.color, mergeColor, timeline);
      // border
      colorParts.last.color =
          Color.lerp(colorParts.last.color, mergeColor, timeline);
    }

ps: I'm casting as dynamic because it is a class called LinearGradient, extended from the paintMutator original class, and the class LinearGradient from rive has the same name as LinearGradient from flutter and I didn't want to use aliases everywhere in the code just because of those lines.

0

It seems like most are doing this in the controller but this doesn't seem necessary in Rive. I'm doing this with the onInit callback in the RiveAnimation constructor, so it looks something like this.

In the widget file:

  @override
  Widget build(final BuildContext context) {
    // ...
      RiveAnimation.asset(
        "path/to/your/asset",
        alignment: Alignment.center,
        fit: BoxFit.fitHeight,
        // etc...
        onInit: _initializeArtboard,
      )
    // ...
  }

Then, somewhere (or inline as a lambda function) create the initialize method:

void _initializeArtboard(final Artboard artboard) {
  // If a custom onInit is specified, the animation must be manually started.
  // The following is what Rive uses to initialize the first animation as a
  // default. Feel free to replace this if you have already got your own logic
  // to begin animating. (Or leave this out to not start the Rive animation
  // immediately).
  artboard.addController(SimpleAnimation(artboard.animations.first.name));

  if (color != null) {
    // Replaces the fill color of the first shape called MyShape.
    artboard.component<Shape>('MyShape')?.fills.first.paint.color = color!;
  }
}

This assumes that MyShape refers to the shape (with the fills applied to it in the Inspector on the right):

screenshot of Rive showing MyShape screenshot of fill in the Rive inspector

NBTX
  • 581
  • 1
  • 8
  • 24
  • Also, if you want to do multiple shapes at once, you can do `artboard.forEachComponent`. – NBTX Jun 07 '23 at 01:35