Skip to content

Some observations and requests #143

Description

@tantzygames

Thank you for making this amazing tool. I’ve had a chance to use it a few times since late last year, and thought I’d explain how I’ve used it and make some feature requests.

rive_trails

First I created Christmas lights, then Christmas presents and then Easter eggs using v6. For each of these I needed to adjust various colors, set rotations, and turn components on and off. I needed to import from the src dir to get to these, so thank you for exposing the components so we no longer have to. However it still involves a fair bit of searching and experimentation to figure out how to get the right components and then how to achieve the desired results.

I create many icons/images white and set their color in Flutter. I want to use Rive in a similar way, and every time I’ve used Rive I’ve needed to change colors. I’d love to have a simple way to find an element by name, and set it’s fill or stroke color (internally searching it’s children for fills or strokes). Something like:

Artboard.getComponentByName(‘Ball’).setFillColor(Colors.red);
Artboard.getComponentByName(‘Ball’).y = 40.5;
Here is what I am doing for the lights:

rive.artboard.forEachComponent((child){
        if (child.name == 'Lid') {
          riveNode.Node node = child;
          node.rotation = vMath.radians((num * 60).toDouble());
        }
        else if (child.name == 'Fill') {
          Fill fill = child;
          fill.paint.color = fillColor;
        }
        else if (child.name == 'Stroke') {
          Stroke stroke = child;
          stroke.paint.color = strokeColor;
        }
        else if (child.name == 'Glow') {
          if (!inTimes) {
            try {
              riveNode.Node node = child;
              node.opacity = 0;
            } catch (e) {}

          }
        }
      });

For each RiveFile I stored the bytes so I could create unique instances. Another request is for easy instances, which I see you’ve been working on.

I’ve used Rive recently to create a metronome and animated play button for a midi player.

RiveButtons06.mp4

The metronome took some figuring out. I first created a simple looping animation with a custom controller to adjust the speed, but it wasn’t totally accurate compared to the playback. Then I tried ping-pong, but that had the same problem. I needed a way to sync the metronome for each tick. I ended up with 2 oneShot animations, but I needed them to stay in their final position instead of resetting, so I removed the auto-reset.
Most of my other oneShot uses require holding the final frame, but resetting before playing again. So another request is to include options to reset before playing or reset after playing.

I created an animation for the metronome weight from slowest to fastest. I wanted to set the time for this animation based on the speed of the music. I couldn’t find a good way to do this so I ended up setting the Y position.
I think it would be really useful for a variety of use cases (sliders, scroll bars and more) to be able to set an animation position, and a time to get there from the current position. For example, to move from the current time/position to 0.25 position over half a second:
AnimController.setTime(0.25, 0.5);

Since then I’ve been able to set the hand animation position which would probably work for the weight as well, but relies on the speed variable:

  metronomeTick.speed = 0;
  metronomeTick.play();
  metronomeTick.time = 0.25;

For the play button, play and pause are oneShot animations with resets at the beginning instead of the end.
For the loading I created an animation that included the entry, a looping section, and an exit. Now, I could split that into 3 separate animations but that would make it difficult to edit because I’d need to split it every time I made a change.
Instead I created overrides for the start time, end time and loop type, and import the same animation 3 times with different values. To do this I had to create a custom LinearAnimation and controller, but it works really well, and I think it would be a useful feature to include as a default.
To play the loading animations, I created an onLoop callback which is triggered when _didLoop = true; to chain them together, but gosh it was tricky. There are so many moving parts, trying to figure out how to get them to behave, and what part was going wrong was a nightmare. Added to that was a real problem that sometimes animations don’t quite get to 100% (which you can see at the end of the video) and so the callbacks are never called. I think there should be a way for this to work, but I haven’t been able to get it to be rock solid.
For now what works every time is waiting on a Future.delayed between each part of the animation.

The play buttons on the first screen need color changes, but in this case changing the fill color didn’t work because it un-hid all my hidden layers (I guess you’re using color alpha to fade layers). I had to use this instead which retained my animated layer alphas:

if (c.coreType == 18) {
  SolidColorBase solidColorBase = c;
  solidColorBase.colorValue = color.value;
}

To recap, I’d really to easily set the color of a component without affecting the layer alpha, and a standard animation controller which can:

  • Set the speed
  • An OnLoop callback guaranteed to be triggered
  • Option to reset before and/or after playing
  • Custom start time, end time and loop type to manipulate different sections of an animation independently

The reason I ask instead of just doing it myself, is because I figure it will be much easier for you to do it since you know how it all works, and it makes sense to do it once rather than every user experimenting and figuring it out for themselves, and I think it will generally make working with Rive animations much more flexible and user friendly.

Here is the code for my controller and animation in case it’s useful:

class AnimController extends rive.RiveAnimationController<rive.RuntimeArtboard> {

  AnimInstance _instance;

  double _speedMultiplier;

  /// Animation name
  final String animationName;

  /// Pauses the animation when it's created
  final bool autoplay;

  /// Mix value for the animation, value between 0 and 1
  double _mix;

  /// Fires when the animation stops being active
  final VoidCallback onStop;

  /// Fires when the animation starts being active
  final VoidCallback onStart;

  /// Fires when a looping animation loops
  final VoidCallback onLoop;

  /// Overrides for animation start time, end time and loop type
  double startOverride = -1;
  double endOverride = -1;
  Loop loopOverride;

  /// reset after playing OneShot?
  final bool oneShotReset;

  // Controls the level of mix for the animation, clamped between 0 and 1
  AnimController(this.animationName, {
    double mix = 1,
    double speed = 1,
    double start = -1,
    double end = -1,
    Loop loop,
    this.autoplay = true,
    this.oneShotReset = true,
    this.onStop,
    this.onStart,
    this.onLoop,
  }) {
    _mix = mix.clamp(0, 1).toDouble();
    _speedMultiplier = speed;
    startOverride = start;
    endOverride = end;
    loopOverride = loop;
    isActive = autoplay;
    isActiveChanged.addListener(onActiveChanged);
  }

  AnimInstance get instance => _instance;
  double get mix => _mix;
  set mix(double value) => _mix = value.clamp(0, 1).toDouble();
  set speed(double value) => _speedMultiplier = value;
  set time(double value) => instance.time = value;
  bool get didLoop => instance.didLoop;

  @override
  bool init(rive.RuntimeArtboard artboard) {
    //_instance = artboard.animationByName(animationName);
    var animation = artboard.animations.firstWhere(
          (animation) => animation is LinearAnimation && animation.name == animationName,
      orElse: () => null,
    );
    if (animation != null) {
      _instance = AnimInstance(animation as LinearAnimation);
      if (startOverride > -1)
        _instance.setStartOverride(startOverride);
      if (endOverride > -1)
        _instance.setEndOverride(endOverride);
      _instance.loopOverride = loopOverride;
      if (onLoop != null)
        _instance.onLoop = onLoop;
    }
    isActive = autoplay;
    return _instance != null;
  }

  /// Dispose of any callback listeners
  @override
  void dispose() {
    super.dispose();
    isActiveChanged.removeListener(onActiveChanged);
  }

  @override
  void apply(rive.RuntimeArtboard artboard, double elapsedSeconds) {
    if (_instance == null) {
      isActive = false;
    }
    if (!_instance.keepGoing && oneShotReset) {
      isActive = false;
    }

    _instance
      ..animation.apply(_instance.time, coreContext: artboard, mix: mix)
      ..advance(elapsedSeconds * _speedMultiplier);

  }

  /// Resets the animation back to it's starting time position
  void reset() => _instance?.reset();

  /// Perform tasks when the animation's active state changes
  void onActiveChanged() {
    // If the animation stops and it is at the end of the one-shot, reset the
    // animation back to the starting time
    if (!isActive && oneShotReset) {
      reset();
    }
    // Fire any callbacks
    isActive
        ? onStart?.call()
    // onStop can fire while widgets are still drawing
        : WidgetsBinding.instance?.addPostFrameCallback((_) => onStop?.call());
  }

  void play()
  {
    isActive = true;
  }
  void stop()
  {
    isActive = false;
  }
  void reverse()
  {
    instance.direction = -instance.direction;
    reset();
  }
}

class AnimInstance {
  final LinearAnimation animation;
  double _time = 0;
  double _totalTime = 0;
  double _lastTotalTime = 0;
  int _direction = 1;
  bool _didLoop = false;
  bool get didLoop => _didLoop;
  double _spilledTime = 0;
  double get spilledTime => _spilledTime;

  double get totalTime => _totalTime;
  double get lastTotalTime => _lastTotalTime;

  /// Overrides
  Loop loopOverride;
  int startOverride = -1;
  int endOverride = -1;

  /// Fires when a looping animation loops
  VoidCallback onLoop;

  AnimInstance(this.animation)
      : _time =
      (animation.enableWorkArea ? animation.workStart : 0).toDouble() /
          animation.fps;

  /// Note that when time is set, the direction will be changed to 1
  set time(double value) {
    if (_time == value) {
      return;
    }
    // Make sure to keep last and total in relative lockstep so state machines
    // can track change even when setting time.
    var diff = _totalTime - _lastTotalTime;
    _time = _totalTime = value;
    _lastTotalTime = _totalTime - diff;
    _direction = 1;
  }

  /// Returns the current time position of the animation in seconds
  double get time => _time;

  /// Direction should only be +1 or -1
  set direction(int value) => _direction = value == -1 ? -1 : 1;

  /// Returns the animation's play direction: 1 for forwards, -1 for backwards
  int get direction => _direction;

  /// Returns the end time of the animation in seconds
  double get endTime {
      if (endOverride > -1)
        return endOverride / animation.fps;
      return (animation.enableWorkArea ? animation.workEnd : animation.duration).toDouble() / animation.fps;
  }

  /// Returns the start time of the animation in seconds
  double get startTime {
    if (startOverride > -1)
      return startOverride / animation.fps;
    return (animation.enableWorkArea ? animation.workStart : 0).toDouble() / animation.fps;
  }

  /// Set start and end time overrides
  void setStartOverride(double seconds) {
    startOverride = (seconds * animation.fps).toInt();
  }
  void setEndOverride(double seconds) {
    endOverride = (seconds * animation.fps).toInt();
  }

  double get progress => (_time - startTime) / (endTime - startTime);

  /// Resets the animation to the starting frame
  void reset() {
    _time = startTime;
    //_didLoop = false;
  }

  /// Whether the controller driving this animation should keep requesting
  /// frames be drawn.
  bool get keepGoing => animation.loop != Loop.oneShot || !_didLoop;

  bool advance(double elapsedSeconds) {
    var deltaSeconds = elapsedSeconds * animation.speed * _direction;
    _lastTotalTime = _totalTime;
    _totalTime += deltaSeconds;
    _time += deltaSeconds;

    double frames = _time * animation.fps;

    var start = animation.enableWorkArea ? animation.workStart : 0;
    var end = animation.enableWorkArea ? animation.workEnd : animation.duration;
    Loop loop = animation.loop;

    /// Check overrides
    if (startOverride > -1)
      start = startOverride;
    if (endOverride > -1)
      end = endOverride;
    if (loopOverride != null)
      loop = loopOverride;

    var range = end - start;

    bool keepGoing = true;
    _didLoop = false;
    _spilledTime = 0;

    switch (loop) {
      case Loop.oneShot:
        if (frames > end) {
          keepGoing = false;
          _spilledTime = (frames - end) / animation.fps;
          frames = end.toDouble();
          _time = frames / animation.fps;
          _didLoop = true;
          onLoop?.call();
        }
        break;
      case Loop.loop:
        if (frames >= end) {
          _spilledTime = (frames - end) / animation.fps;
          frames = _time * animation.fps;
          frames = start + (frames - start) % range;
          _time = frames / animation.fps;
          _didLoop = true;
          onLoop?.call();
        }
        break;
      case Loop.pingPong:
      // ignore: literal_only_boolean_expressions
        while (true) {
          if (_direction == 1 && frames >= end) {
            _spilledTime = (frames - end) / animation.fps;
            _direction = -1;
            frames = end + (end - frames);
            _time = frames / animation.fps;
            _didLoop = true;
            onLoop?.call();
          } else if (_direction == -1 && frames < start) {
            _spilledTime = (start - frames) / animation.fps;
            _direction = 1;
            frames = start + (start - frames);
            _time = frames / animation.fps;
            _didLoop = true;
            onLoop?.call();
          } else {
            // we're within the range, we can stop fixing. We do this in a
            // loop to fix conditions when time has advanced so far that we've
            // ping-ponged back and forth a few times in a single frame. We
            // want to accomodate for this in cases where animations are not
            // advanced on regular intervals.
            break;
          }
        }
        break;
    }
    return keepGoing;
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions