Skip to content

Commit

Permalink
Fix follow path 6070
Browse files Browse the repository at this point in the history
fixes #6070

thank you @philter for sorting out the path mess, i got myself extra confused with `addToRawPath` (i thought we were simply passing the variables along and one of them got corrupted, i didnt notice that it was just down to a bad static cast when we were a path!)

vid of this behaving in dart & cpp

Added a rive-flutter test to this.

one interesting thing, essentially follow path currently follows the "first" path in a shape.

https://github.com/rive-app/rive/assets/1216025/d59026f6-c901-439c-aff2-2214a021ee75

Diffs=
ef8a4e7f7 Fix follow path 6070 (#6182)
3927ea695 add support for rendering static scene (#6192)

Co-authored-by: Maxwell Talbot <[email protected]>
Co-authored-by: Phil Chung <[email protected]>
  • Loading branch information
3 people committed Nov 3, 2023
1 parent 276dc20 commit 73731ff
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 60 deletions.
2 changes: 1 addition & 1 deletion .rive_head
Original file line number Diff line number Diff line change
@@ -1 +1 @@
351838fb575bb2533ef60fa4caab0071a1f50771
ef8a4e7f711aef6b2d468a885a79f919118bae70
132 changes: 74 additions & 58 deletions lib/src/rive_core/constraints/follow_path_constraint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:ui' as ui;
import 'package:rive/src/generated/constraints/follow_path_constraint_base.dart';
import 'package:rive/src/rive_core/component.dart';
import 'package:rive/src/rive_core/constraints/constraint.dart';
import 'package:rive/src/rive_core/shapes/path.dart';
import 'package:rive/src/rive_core/shapes/shape.dart';
import 'package:rive/src/rive_core/transform_component.dart';
import 'package:rive/src/rive_core/transform_space.dart';
Expand All @@ -17,61 +18,63 @@ class FollowPathConstraint extends FollowPathConstraintBase {
final ui.Path _worldPath = ui.Path();

Mat2D get targetTransform {
if (target is! Shape) {
return target!.worldTransform;
}
var metrics = _worldPath.computeMetrics().toList(growable: false);
if (metrics.isEmpty) {
return Mat2D();
}
double totalLength = 0.0;
for (final metric in metrics) {
totalLength += metric.length;
}
// Normalize distance value to 0-1 since we need to support values
// <0 and >1
// Negative values follow path in reverse direction
var actualDistance = distance % 1;
// This handles the case where distance is any whole number other than 0.
// In those cases we want actualDistance to return 1
if (distance != 0 && actualDistance == 0) {
actualDistance = 1;
}
double distanceUnits = totalLength * actualDistance.clamp(0, 1);
var itr = metrics.iterator;

// We already checked it wasn't empty.
itr.moveNext();
ui.PathMetric metric;
do {
metric = itr.current;
if (distanceUnits <= metric.length) {
break;
if (target is Shape || target is Path) {
var metrics = _worldPath.computeMetrics().toList(growable: false);
if (metrics.isEmpty) {
return Mat2D();
}
double totalLength = 0.0;
for (final metric in metrics) {
totalLength += metric.length;
}
// Normalize distance value to 0-1 since we need to support values
// <0 and >1
// Negative values follow path in reverse direction
var actualDistance = distance % 1;
// This handles the case where distance is any whole number other than 0.
// In those cases we want actualDistance to return 1
if (distance != 0 && actualDistance == 0) {
actualDistance = 1;
}
double distanceUnits = totalLength * actualDistance.clamp(0, 1);
var itr = metrics.iterator;

distanceUnits -= metric.length;
} while (itr.moveNext());
// We already checked it wasn't empty.
itr.moveNext();
ui.PathMetric metric;
do {
metric = itr.current;
if (distanceUnits <= metric.length) {
break;
}

var tangent = metric.getTangentForOffset(distanceUnits);
distanceUnits -= metric.length;
} while (itr.moveNext());

if (tangent == null) {
return Mat2D();
}
var tangent = metric.getTangentForOffset(distanceUnits);

Vec2D position = Vec2D.fromValues(tangent.position.dx, tangent.position.dy);
Mat2D transformB = Mat2D.clone(target!.worldTransform);
if (tangent == null) {
return Mat2D();
}

if (orient) {
Mat2D.fromRotation(
transformB, atan2(tangent.vector.dy, tangent.vector.dx));
Vec2D position =
Vec2D.fromValues(tangent.position.dx, tangent.position.dy);
Mat2D transformB = Mat2D.clone(target!.worldTransform);

if (orient) {
Mat2D.fromRotation(
transformB, atan2(tangent.vector.dy, tangent.vector.dx));
}
final offsetPosition = offset
? Vec2D.fromValues(constrainedComponent!.transform[4],
constrainedComponent!.transform[5])
: Vec2D();
transformB[4] = position.x + offsetPosition.x;
transformB[5] = position.y + offsetPosition.y;
return transformB;
} else {
return target!.worldTransform;
}
final offsetPosition = offset
? Vec2D.fromValues(constrainedComponent!.transform[4],
constrainedComponent!.transform[5])
: Vec2D();
transformB[4] = position.x + offsetPosition.x;
transformB[5] = position.y + offsetPosition.y;
return transformB;
}

@override
Expand Down Expand Up @@ -125,29 +128,41 @@ class FollowPathConstraint extends FollowPathConstraintBase {
var shape = target as Shape;
// Follow path should update after the target's path composer
shape.pathComposer.addDependent(this);
} else if (target is Path) {
var path = target as Path;
path.addDependent(this);
}

if (constrainedComponent != null) {
// The constrained component should update after follow path
addDependent(constrainedComponent!, via: this);
}
}

@override
void update(int dirt) {
if (target is! Shape) {
return;
}
var shape = target as Shape;
void _resetWorldPath(Set<Path> paths) {
_worldPath.reset();
for (final path in shape.paths) {
for (final path in paths) {
_worldPath.addPath(path.uiPath, ui.Offset.zero,
matrix4: path.pathTransform.mat4);
}
}

@override
Component? get targetDependencyParent =>
target != null ? (target as Shape) : null;
void update(int dirt) {
final _target = target;
if (_target is Shape) {
_resetWorldPath(_target.paths);
} else if (_target is Path) {
_resetWorldPath({_target});
}
}

@override
Component? get targetDependencyParent => target is Shape
? (target as Shape)
: target is Path
? (target as Path)
: null;

@override
void distanceChanged(double from, double to) => markConstraintDirty();
Expand All @@ -159,5 +174,6 @@ class FollowPathConstraint extends FollowPathConstraintBase {
void offsetChanged(bool from, bool to) => markConstraintDirty();

@override
bool validate() => super.validate() && (target == null || target is Shape);
bool validate() =>
super.validate() && (target == null || target is Shape || target is Path);
}
3 changes: 3 additions & 0 deletions test/assets/follow_path_path.riv
Git LFS file not shown
43 changes: 42 additions & 1 deletion test/goldens/follow_path/golden_follow_path_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import '../golden_comparator.dart';

void main() {
group('Golden - follow path tests', () {
testWidgets('Follow path over time', (WidgetTester tester) async {
testWidgets('Follow path - shape over time', (WidgetTester tester) async {
final riveBytes = loadFile('assets/follow_path_shapes.riv');
final file = RiveFile.import(riveBytes);
late Artboard artboard;
Expand Down Expand Up @@ -55,5 +55,46 @@ void main() {
reason: 'Follow path should work as animation advances',
);
});

testWidgets('Follow path - path over time', (WidgetTester tester) async {
final riveBytes = loadFile('assets/follow_path_path.riv');
final file = RiveFile.import(riveBytes);
late Artboard artboard;

final widget = RiveAnimation.direct(
file,
stateMachines: const ['State Machine 1'],
onInit: (a) {
artboard = a;
},
);
await tester.pumpWidget(widget);

await tester.pump();

await expectGoldenMatches(
find.byType(RiveAnimation),
'follow_path_path_01.png',
reason: 'Follow path should work as animation advances',
);

artboard.advance(0.5, nested: true);
await tester.pump();

await expectGoldenMatches(
find.byType(RiveAnimation),
'follow_path_path_02.png',
reason: 'Follow path should work as animation advances',
);

artboard.advance(2.5, nested: true);
await tester.pump();

await expectGoldenMatches(
find.byType(RiveAnimation),
'follow_path_path_03.png',
reason: 'Follow path should work as animation advances',
);
});
});
}
3 changes: 3 additions & 0 deletions test/goldens/follow_path/images/follow_path_path_01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions test/goldens/follow_path/images/follow_path_path_02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions test/goldens/follow_path/images/follow_path_path_03.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 73731ff

Please sign in to comment.