diff --git a/example/lib/interactive_example.dart b/example/lib/interactive_example.dart new file mode 100644 index 00000000..399a403d --- /dev/null +++ b/example/lib/interactive_example.dart @@ -0,0 +1,536 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:lottie/lottie.dart'; + +void main() async { + runApp(const App()); +} + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: ChameleonLottieDrawer(), + ), + ); + } +} + +class ChameleonLottieDrawer extends StatefulWidget { + const ChameleonLottieDrawer({super.key}); + + @override + State createState() => _ChameleonLottieDrawerWidgetState(); +} + +class _ChameleonLottieDrawerWidgetState extends State { + int? _frameCallbackId; + final ValueNotifier _repaint = ValueNotifier(0); + double _lastFrameTime = 0.0; + _Painter? _painter; + + Future _requireLottieComposition() async { + var network = NetworkLottie( + 'https://labs.nearpod.com/bodymovin/demo/chameleon/chameleon2.json'); + return network.load(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _requireLottieComposition(), + builder: (context, AsyncSnapshot snapshot) { + /*表示数据成功返回*/ + if (snapshot.hasData) { + _painter = _Painter(snapshot.data!, repaint: _repaint); + return Listener( + onPointerMove: (detail) { + final offset = detail.position; + _painter?.updateTouchPoint(offset); + _fireRepaintCommand(); + }, + onPointerHover: (detail) { + final offset = detail.position; + _painter?.updateTouchPoint(offset); + _fireRepaintCommand(); + }, + child: CustomPaint( + painter: _painter!, size: const Size(400, 400))); + } else { + return const Center( + child: SizedBox( + height: 100, + width: 100, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.blue), + strokeWidth: 8.0), + )); + } + }); + } + + void _fireRepaintCommand() { + _repaint.value = _repaint.value + 1.0; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _frameCallbackId = + SchedulerBinding.instance.scheduleFrameCallback(beginFrame); + } + + @override + void dispose() { + if (null != _frameCallbackId) { + SchedulerBinding.instance.cancelFrameCallbackWithId(_frameCallbackId!); + _frameCallbackId = null; + } + super.dispose(); + } + + void beginFrame(Duration timeStamp) { + final t = + timeStamp.inMicroseconds / Duration.microsecondsPerMillisecond / 1000.0; + if (_lastFrameTime == 0.0) { + _lastFrameTime = t; + _frameCallbackId = + SchedulerBinding.instance.scheduleFrameCallback(beginFrame); + return; + } + + var elapsed = t - _lastFrameTime; + _lastFrameTime = t; + _painter?.setElapsedTime(elapsed); + _fireRepaintCommand(); + _frameCallbackId = + SchedulerBinding.instance.scheduleFrameCallback(beginFrame); + } +} + +class _Layer { + final String name; + final double radius; + final double divisor; + + _Layer({required this.name, required this.radius, required this.divisor}); +} + +class _EyeData { + Offset current; + double distance; + double eyeAngle; + + _EyeData( + {required this.current, required this.distance, required this.eyeAngle}); +} + +class _ChameleonAlphaColor extends ValueNotifier { + Color? _newColor; + int _targetAlpha = 255; + + _ChameleonAlphaColor(super.value) : _targetAlpha = value.alpha; + + void changeColor(final Color color) { + _newColor = color; + _targetAlpha = color.alpha; + } + + void stepAlpha(int stepVal) { + var alpha = super.value.alpha; + if (null == _newColor) { + if (alpha < _targetAlpha) { + alpha = alpha + stepVal; + if (alpha > _targetAlpha) { + alpha = _targetAlpha; + } + super.value = super.value.withAlpha(alpha); + } + } else { + if (alpha <= 0) { + super.value = _newColor!.withAlpha(0); + _newColor = null; + } else { + alpha = alpha - stepVal; + if (alpha < 0) { + alpha = 0; + } + super.value = super.value.withAlpha(alpha); + } + } + } +} + +class _Painter extends CustomPainter { + late final LottieApi api; + late final LottieDrawable drawable; + final LottieComposition composition; + double _frameTime = 0.0; + ValueNotifier isActive = ValueNotifier(false); + double _elapsed = 0; + + final double degToRads = pi / 180; + + //鼠标或者点击位置的坐标 + ValueNotifier touchPoint = ValueNotifier(const Offset(100, 500)); + ValueNotifier mouseChanged = ValueNotifier(false); + ValueNotifier distanceToMouse = ValueNotifier(0.0); + ValueNotifier minTongueRadius = ValueNotifier(405); + ValueNotifier maxTongueRadius = ValueNotifier(415); + ValueNotifier angle = ValueNotifier(0.0); + + // 变色龙的颜色 默认 rgba(158, 231, 152, 1); + _ChameleonAlphaColor chameleonColor = + _ChameleonAlphaColor(const Color.fromARGB(1, 158, 231, 152)); + + // 滑过或者点击的叶片 + String hitLeaf = ''; + + // 变色龙变色的定时器 + Timer? camouflageTimeout; + + var leftEyeCircles = [ + _Layer(name: 'Group 12', radius: 27, divisor: 20), + _Layer(name: 'Group 13', radius: 27, divisor: 20), + _Layer(name: 'Group 14', radius: 27, divisor: 20), + _Layer(name: 'Group 15', radius: 23, divisor: 20), + _Layer(name: 'Group 16', radius: 21, divisor: 35), + _Layer(name: 'Group 17', radius: 19, divisor: 50), + _Layer(name: 'Group 18', radius: 17, divisor: 65), + _Layer(name: 'Group 19', radius: 15, divisor: 80), + _Layer(name: 'Group 20', radius: 13, divisor: 95), + _Layer(name: 'Group 21', radius: 5, divisor: 75) + ]; + var rightEyeCircles = [ + _Layer(name: 'Group 1', radius: 27, divisor: 20), + _Layer(name: 'Group 2', radius: 27, divisor: 20), + _Layer(name: 'Group 3', radius: 27, divisor: 20), + _Layer(name: 'Group 4', radius: 23, divisor: 20), + _Layer(name: 'Group 5', radius: 21, divisor: 35), + _Layer(name: 'Group 6', radius: 19, divisor: 50), + _Layer(name: 'Group 7', radius: 17, divisor: 65), + _Layer(name: 'Group 8', radius: 15, divisor: 80), + _Layer(name: 'Group 9', radius: 13, divisor: 95), + _Layer(name: 'Group 10', radius: 5, divisor: 75) + ]; + + var leaves = ['leaf_1', 'leaf_2', 'leaf_3', 'leaf_4']; + + _Painter(this.composition, {super.repaint}) { + drawable = LottieDrawable(composition); + api = LottieApi(drawable); + drawable.delegates = _requireLottieDelegates(); + var timeout = const Duration(milliseconds: 1000); + camouflageTimeout = Timer(timeout, () { + chameleonColor.value = const Color.fromARGB(1, 240, 237, 231); + camouflageTimeout = null; + }); + } + + void _leavesHitTest() { + for (var i = 0; i < leaves.length; i++) { + var value = leaves[i]; + final keyPath = ['#$value', 'color_group', 'fill_prop']; + var hit = api.hitTest(keyPath, touchPoint.value); + if (hit) { + if ((hitLeaf != value) && (hitLeaf != 'hit$value')) { + hitLeaf = value; + if (null != camouflageTimeout) { + camouflageTimeout?.cancel(); + } + var timeout = const Duration(milliseconds: 15000); + camouflageTimeout = Timer(timeout, () { + hitLeaf = ''; + camouflageTimeout = null; + }); + break; + } + } + } + } + + void updateTouchPoint(Offset value) { + mouseChanged.value = true; + touchPoint.value = value; + _leavesHitTest(); + } + + LottieDelegates _requireLottieDelegates() { + final delegates = []; + final chameleon = _requireChameleonColorValueDelegates(); + delegates.addAll(chameleon); + final eyes = _requireEyeCircleDelegates(); + delegates.addAll(eyes); + final leafs = _requireLeafColorValueDelegates(); + delegates.addAll(leafs); + final mouthProps = _requireMouthPropertiesDelegates(); + delegates.addAll(mouthProps); + final arrowProps = _requireArrowPropertiesDelegates(); + delegates.addAll(arrowProps); + final tongueProps = _requireTonguePropertiesDelegates(); + delegates.addAll(tongueProps); + return LottieDelegates( + values: delegates, + ); + } + + List _requireEyeCircleDelegates() { + final delegates = []; + int i, len = leftEyeCircles.length; + var cachedMouseEyeData = + _EyeData(current: const Offset(-1, -1), distance: 0, eyeAngle: 0); + for (i = 0; i < len; i += 1) { + delegates.add(_requireEyeCircleValueDelegates( + leftEyeCircles[i], 'left_eye', cachedMouseEyeData)); + } + len = rightEyeCircles.length; + cachedMouseEyeData = + _EyeData(current: const Offset(-1, -1), distance: 0, eyeAngle: 0); + for (i = 0; i < len; i += 1) { + delegates.add(_requireEyeCircleValueDelegates( + rightEyeCircles[i], 'right_eye', cachedMouseEyeData)); + } + return delegates; + } + + ValueDelegate _requireEyeCircleValueDelegates( + _Layer circleData, String eye, _EyeData cachedMouseEyeData) { + final keyPath = [ + 'Loop', + eye, + circleData.name, + ]; + + Offset? lastValue; + double eyeAngle; + return ValueDelegate.transformPosition(keyPath, callback: (position) { + var currentValue = position.endValue; + currentValue ??= position.startValue; + currentValue ??= const Offset(0, 0); + lastValue ??= currentValue; + if (!isActive.value) { + var point = api.toContainerPoint(touchPoint.value); + var lp = api.toKeyPathLayerPoint(keyPath, point); + if (lp.isNotEmpty) { + point = lp.first; + } + cachedMouseEyeData.distance = sqrt(pow(point.dx, 2) + pow(point.dy, 2)); + cachedMouseEyeData.eyeAngle = + atan2(0 - point.dy, 0 - point.dx) / degToRads + 179; + cachedMouseEyeData.current = point; + } + eyeAngle = cachedMouseEyeData.eyeAngle; + var distance = cachedMouseEyeData.distance; + distance = distance > circleData.radius ? circleData.radius : distance; + var newValueX = currentValue.dx + distance * cos(eyeAngle * degToRads); + var newValueY = currentValue.dy + distance * sin(eyeAngle * degToRads); + newValueX = + lastValue!.dx + (newValueX - lastValue!.dx) / circleData.divisor * 3; + newValueY = + lastValue!.dy + (newValueY - lastValue!.dy) / circleData.divisor * 3; + lastValue = Offset(newValueX, newValueY); + return lastValue!; + }); + } + + ValueDelegate _requireLeafColorValueDelegate(final String leaf) { + final keyPath = ['#$leaf', 'color_group', 'fill_prop']; + return ValueDelegate.color(keyPath, callback: (color) { + var value = const Color.fromARGB(255, 255, 255, 255); + if (null != color.endValue) { + value = color.endValue!; + } else if (null != color.startValue) { + value = color.startValue!; + } + // 点击坐标检查,如果当前点击坐标在此种颜色的叶子上,则设置颜色为此叶子的颜色 + if (hitLeaf.isEmpty) { + chameleonColor.changeColor(const Color.fromARGB(1, 240, 237, 231)); + chameleonColor.stepAlpha(5); + } else if (hitLeaf == leaf) { + chameleonColor.changeColor(value); + hitLeaf = 'hit$leaf'; + } else if ('hit$leaf' == hitLeaf) { + chameleonColor.stepAlpha(5); + } + return value; + }); + } + + List _requireLeafColorValueDelegates() { + final delegates = []; + var len = leaves.length; + for (var i = 0; i < len; i += 1) { + delegates.add(_requireLeafColorValueDelegate(leaves[i])); + } + return delegates; + } + + List _requireChameleonColorValueDelegates() { + var chameleonColorPaths = [ + // head + ['Loop', 'head', 'Group 1', '.chameleon_color'], + // body + ['Loop', 'Body Outlines', 'Group 1', '.chameleon_color'], + ['Loop', 'Body Outlines', 'Group 2', '.chameleon_color'], + // tail + ['Loop', 'Tail Outlines', 'Group 1', '.chameleon_color'], + // legs + ['Loop', 'LegsFront Outlines', 'Group 1', '.chameleon_color'], + ['Loop', 'LegsFront Outlines', 'Group 2', '.chameleon_color'], + ['Loop', 'LegsBack Outlines', 'Group 1', '.chameleon_color'], + ['Loop', 'LegsBack Outlines', 'Group 2', '.chameleon_color'], + // belly (腹部) + ['Loop', 'Belly Outlines', 'Group 1', '.chameleon_color'], + ['Loop', 'Belly Outlines', 'Group 2', '.chameleon_color'], + ['Loop', 'Belly Outlines', 'Group 3', '.chameleon_color'], + ['Loop', 'Belly Outlines', 'Group 4', '.chameleon_color'], + ['Loop', 'Belly Outlines', 'Group 5', '.chameleon_color'], + ['Loop', 'Belly Outlines', 'Group 6', '.chameleon_color'], + ]; + final delegates = []; + for (var i = 0; i < chameleonColorPaths.length; i++) { + var keyPath = chameleonColorPaths[i]; + var colors = api.requireColorValueDelegates(keyPath, chameleonColor); + delegates.addAll(colors); + var delegate = ValueDelegate.opacity(keyPath, callback: (cb) { + return (chameleonColor.value.alpha * 100 / 255.0).round(); + }); + delegates.add(delegate); + } + return delegates; + } + + List _requireMouthPropertiesDelegates() { + final delegates = []; + final keyPath = ['Mouth']; + var perc = 0.0; + var delegate = ValueDelegate.timeRemap(keyPath, callback: (currentValue) { + if (!isActive.value && mouseChanged.value) { + var point = api.toContainerPoint(touchPoint.value); + final keyPath = ['Mouth', 'ReferencePoint']; + var point2 = api.toKeyPathLayerPoint(keyPath, point); + if (point2.isNotEmpty) { + var p = point2[0]; + angle.value = atan2(0 - p.dy, 0 - p.dx) / degToRads + 170; + distanceToMouse.value = sqrt(pow(0 - p.dx, 2) + pow(0 - p.dy, 2)); + } + mouseChanged.value = false; + } + + if (distanceToMouse.value < minTongueRadius.value) { + perc = distanceToMouse.value / minTongueRadius.value; + return perc * 9 / 30; + } else if (distanceToMouse.value > maxTongueRadius.value) { + perc = 1 - + min( + 1, + (distanceToMouse.value - maxTongueRadius.value) / + (maxTongueRadius.value + 100)); + return perc * (9 / 30); + } else if (distanceToMouse.value >= minTongueRadius.value) { + return 9 / 30; + } + return 0; + }); + delegates.add(delegate); + return delegates; + } + + List _requireArrowPropertiesDelegates() { + final delegates = []; + var keyPath = ['Mouth', 'Tongue_Comp', '.default_arrow', 'Shape 1']; + var currentScale = -1.0; + var currentScaleValue = const Offset(-1.0, -1.0); + var scale = ValueDelegate.transformScale(keyPath, callback: (value) { + var currentValue = value.endValue!; + var scale = currentValue.dx; + if (currentScale != scale) { + currentScaleValue = currentScaleValue.scale(scale, scale); + currentScale = scale; + } + return currentScaleValue; + }); + + delegates.add(scale); + + var rotation = ValueDelegate.transformRotation(keyPath, callback: (value) { + return -angle.value; + }); + delegates.add(rotation); + return delegates; + } + + List _requireTonguePropertiesDelegates() { + final delegates = []; + var tongueInitialAnimationTime = 0.0; + var tongueCurrentTime = 0.0; + + void animateTongue() { + final now = DateTime.now(); + tongueInitialAnimationTime = now.millisecondsSinceEpoch - 1500 / 30; + isActive.value = true; + } + + void resetTongue() { + isActive.value = false; + } + + var keyPath = ['Mouth', 'Tongue_Comp']; + + var timeRemap = ValueDelegate.timeRemap(keyPath, callback: (currentValue) { + if (distanceToMouse.value > minTongueRadius.value && + distanceToMouse.value < maxTongueRadius.value && + !isActive.value) { + animateTongue(); + } + if (isActive.value) { + final now = DateTime.now(); + tongueCurrentTime = 2 * + (now.millisecondsSinceEpoch - tongueInitialAnimationTime) / + 1000; + } + if (tongueCurrentTime > 2) { + tongueCurrentTime = 0; + resetTongue(); + } + return tongueCurrentTime; + }); + + delegates.add(timeRemap); + + keyPath = ['Mouth', 'Tongue_Comp']; + var rotation = + ValueDelegate.transformRotation(keyPath, callback: (currentValue) { + return angle.value; + }); + delegates.add(rotation); + return delegates; + } + + void setElapsedTime(double elapsed) { + _elapsed = elapsed; + } + + @override + void paint(Canvas canvas, Size size) { + _frameTime += _elapsed; + if (_frameTime > composition.seconds) { + _frameTime = 0; + } + drawable + ..setProgress(_frameTime / composition.seconds) + ..draw(canvas, Rect.fromLTWH(0, 0, size.width, size.height)); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/lottie.dart b/lib/lottie.dart index 4ab7da93..bbfca852 100644 --- a/lib/lottie.dart +++ b/lib/lottie.dart @@ -1,3 +1,4 @@ +export 'src/api/lottie_api.dart' show LottieApi; export 'src/composition.dart' show LottieComposition; export 'src/frame_rate.dart' show FrameRate; export 'src/lottie.dart' show Lottie; diff --git a/lib/src/animation/content/content_group.dart b/lib/src/animation/content/content_group.dart index c3f77e21..5e24bc3b 100644 --- a/lib/src/animation/content/content_group.dart +++ b/lib/src/animation/content/content_group.dart @@ -7,6 +7,7 @@ import '../../model/content/shape_group.dart'; import '../../model/key_path.dart'; import '../../model/key_path_element.dart'; import '../../model/layer/base_layer.dart'; +import '../../model/lottie_observer.dart'; import '../../utils.dart'; import '../../utils/path_factory.dart'; import '../../value/lottie_value_callback.dart'; @@ -16,7 +17,8 @@ import 'drawing_content.dart'; import 'greedy_content.dart'; import 'path_content.dart'; -class ContentGroup implements DrawingContent, PathContent, KeyPathElement { +class ContentGroup + implements DrawingContent, PathContent, KeyPathElement, LottieObserver { final Paint _offScreenPaint = Paint(); static List contentsFromModels(LottieDrawable drawable, @@ -42,6 +44,7 @@ class ContentGroup implements DrawingContent, PathContent, KeyPathElement { } final Matrix4 _matrix = Matrix4.identity(); + Rect _drawBounds = Rect.zero; final Path _path = PathFactory.create(); @override @@ -83,6 +86,21 @@ class ContentGroup implements DrawingContent, PathContent, KeyPathElement { } } + @override + void applyToMatrix(final Matrix4 matrix) { + _transformAnimation?.applyToMatrix(matrix); + } + + @override + List requireMatrixHierarchy() { + return []; + } + + @override + bool hitTest(final Offset position) { + return _drawBounds.contains(position); + } + void onValueChanged() { _lottieDrawable.invalidateSelf(); } @@ -165,9 +183,12 @@ class ContentGroup implements DrawingContent, PathContent, KeyPathElement { hasTwoOrMoreDrawableContent() && layerAlpha != 255; if (isRenderingWithOffScreen) { - var offScreenRect = getBounds(_matrix, applyParents: true); + _drawBounds = getBounds(_matrix, applyParents: true); _offScreenPaint.setAlpha(layerAlpha); - canvas.saveLayer(offScreenRect, _offScreenPaint); + canvas.saveLayer(_drawBounds, _offScreenPaint); + } else { + var matrix = parentMatrix.clone(); + _drawBounds = getBounds(matrix, applyParents: true); } var childAlpha = isRenderingWithOffScreen ? 255 : layerAlpha; diff --git a/lib/src/animation/content/fill_content.dart b/lib/src/animation/content/fill_content.dart index 7f120e89..ad9380a6 100644 --- a/lib/src/animation/content/fill_content.dart +++ b/lib/src/animation/content/fill_content.dart @@ -7,6 +7,7 @@ import '../../model/content/drop_shadow_effect.dart'; import '../../model/content/shape_fill.dart'; import '../../model/key_path.dart'; import '../../model/layer/base_layer.dart'; +import '../../model/lottie_observer.dart'; import '../../utils.dart'; import '../../utils/misc.dart'; import '../../utils/path_factory.dart'; @@ -20,9 +21,11 @@ import 'drawing_content.dart'; import 'key_path_element_content.dart'; import 'path_content.dart'; -class FillContent implements DrawingContent, KeyPathElementContent { +class FillContent + implements DrawingContent, KeyPathElementContent, LottieObserver { final Path _path = PathFactory.create(); final Paint _paint = Paint(); + Rect _drawBounds = Rect.zero; final BaseLayer layer; @override final String? name; @@ -65,6 +68,19 @@ class FillContent implements DrawingContent, KeyPathElementContent { layer.addAnimation(_opacityAnimation); } + @override + void applyToMatrix(final Matrix4 matrix) {} + + @override + List requireMatrixHierarchy() { + return []; + } + + @override + bool hitTest(final Offset position) { + return _drawBounds.contains(position); + } + void onValueChanged() { lottieDrawable.invalidateSelf(); } @@ -134,10 +150,10 @@ class FillContent implements DrawingContent, KeyPathElementContent { _path.addPath(_paths[i].getPath(), Offset.zero, matrix4: parentMatrix.storage); } - var outBounds = _path.getBounds(); + _drawBounds = _path.getBounds(); // Add padding to account for rounding errors. - outBounds = outBounds.inflate(1); - return outBounds; + _drawBounds = _drawBounds.inflate(1); + return _drawBounds; } @override diff --git a/lib/src/animation/keyframe/base_keyframe_animation.dart b/lib/src/animation/keyframe/base_keyframe_animation.dart index 757f4ef9..f9cc63df 100644 --- a/lib/src/animation/keyframe/base_keyframe_animation.dart +++ b/lib/src/animation/keyframe/base_keyframe_animation.dart @@ -102,6 +102,10 @@ abstract class BaseKeyframeAnimation { return _cachedEndProgress; } + A get rawValue { + return getValue(getCurrentKeyframe(), progress, null); + } + A get value { A value; @@ -116,10 +120,10 @@ abstract class BaseKeyframeAnimation { var xProgress = keyframe.xInterpolator!.transform(linearProgress); var yProgress = keyframe.yInterpolator!.transform(linearProgress); value = getValueSplitDimension( - keyframe, linearProgress, xProgress, yProgress); + keyframe, linearProgress, xProgress, yProgress, valueCallback); } else { var progress = getInterpolatedCurrentKeyframeProgress(); - value = getValue(keyframe, progress); + value = getValue(keyframe, progress, valueCallback); } _cachedGetValue = value; @@ -148,10 +152,15 @@ abstract class BaseKeyframeAnimation { /// keyframeProgress will be [0, 1] unless the interpolator has overshoot in which case, this /// should be able to handle values outside of that range. - A getValue(Keyframe keyframe, double keyframeProgress); - - A getValueSplitDimension(Keyframe keyframe, double linearKeyframeProgress, - double xKeyframeProgress, double yKeyframeProgress) { + A getValue(Keyframe keyframe, double keyframeProgress, + LottieValueCallback? valueCallback); + + A getValueSplitDimension( + Keyframe keyframe, + double linearKeyframeProgress, + double xKeyframeProgress, + double yKeyframeProgress, + LottieValueCallback? valueCallback) { throw Exception('This animation does not support split dimensions!'); } diff --git a/lib/src/animation/keyframe/color_keyframe_animation.dart b/lib/src/animation/keyframe/color_keyframe_animation.dart index 38f3ad9e..0deb404b 100644 --- a/lib/src/animation/keyframe/color_keyframe_animation.dart +++ b/lib/src/animation/keyframe/color_keyframe_animation.dart @@ -1,13 +1,15 @@ import 'dart:ui'; import '../../utils/gamma_evaluator.dart'; import '../../value/keyframe.dart'; +import '../../value/lottie_value_callback.dart'; import 'keyframe_animation.dart'; class ColorKeyframeAnimation extends KeyframeAnimation { ColorKeyframeAnimation(super.keyframes); @override - Color getValue(Keyframe keyframe, double keyframeProgress) { + Color getValue(Keyframe keyframe, double keyframeProgress, + LottieValueCallback? valueCallback) { if (keyframe.startValue == null || keyframe.endValue == null) { throw Exception('Missing values for keyframe.'); } @@ -15,7 +17,7 @@ class ColorKeyframeAnimation extends KeyframeAnimation { var endColor = keyframe.endValue; if (valueCallback != null) { - var value = valueCallback!.getValueInternal( + var value = valueCallback.getValueInternal( keyframe.startFrame, keyframe.endFrame, startColor, diff --git a/lib/src/animation/keyframe/double_keyframe_animation.dart b/lib/src/animation/keyframe/double_keyframe_animation.dart index 5cbcf759..45408d77 100644 --- a/lib/src/animation/keyframe/double_keyframe_animation.dart +++ b/lib/src/animation/keyframe/double_keyframe_animation.dart @@ -1,18 +1,20 @@ import 'dart:ui'; import '../../value/keyframe.dart'; +import '../../value/lottie_value_callback.dart'; import 'keyframe_animation.dart'; class DoubleKeyframeAnimation extends KeyframeAnimation { DoubleKeyframeAnimation(super.keyframes); @override - double getValue(Keyframe keyframe, double keyframeProgress) { + double getValue(Keyframe keyframe, double keyframeProgress, + LottieValueCallback? valueCallback) { if (keyframe.startValue == null || keyframe.endValue == null) { throw Exception('Missing values for keyframe.'); } if (valueCallback != null) { - var value = valueCallback!.getValueInternal( + var value = valueCallback.getValueInternal( keyframe.startFrame, keyframe.endFrame, keyframe.startValue, diff --git a/lib/src/animation/keyframe/gradient_color_keyframe_animation.dart b/lib/src/animation/keyframe/gradient_color_keyframe_animation.dart index 5d3d55b1..f46f4a86 100644 --- a/lib/src/animation/keyframe/gradient_color_keyframe_animation.dart +++ b/lib/src/animation/keyframe/gradient_color_keyframe_animation.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import '../../model/content/gradient_color.dart'; import '../../value/keyframe.dart'; +import '../../value/lottie_value_callback.dart'; import 'keyframe_animation.dart'; class GradientColorKeyframeAnimation extends KeyframeAnimation { @@ -16,7 +17,9 @@ class GradientColorKeyframeAnimation extends KeyframeAnimation { @override GradientColor getValue( - Keyframe keyframe, double keyframeProgress) { + Keyframe keyframe, + double keyframeProgress, + LottieValueCallback? valueCallback) { _gradientColor.lerp( keyframe.startValue!, keyframe.endValue!, keyframeProgress); return _gradientColor; diff --git a/lib/src/animation/keyframe/integer_keyframe_animation.dart b/lib/src/animation/keyframe/integer_keyframe_animation.dart index 93858b90..359a17d0 100644 --- a/lib/src/animation/keyframe/integer_keyframe_animation.dart +++ b/lib/src/animation/keyframe/integer_keyframe_animation.dart @@ -1,18 +1,20 @@ import 'dart:ui'; import '../../value/keyframe.dart'; +import '../../value/lottie_value_callback.dart'; import 'keyframe_animation.dart'; class IntegerKeyframeAnimation extends KeyframeAnimation { IntegerKeyframeAnimation(super.keyframes); @override - int getValue(Keyframe keyframe, double keyframeProgress) { + int getValue(Keyframe keyframe, double keyframeProgress, + LottieValueCallback? valueCallback) { if (keyframe.startValue == null || keyframe.endValue == null) { throw Exception('Missing values for keyframe.'); } if (valueCallback != null) { - var value = valueCallback!.getValueInternal( + var value = valueCallback.getValueInternal( keyframe.startFrame, keyframe.endFrame, keyframe.startValue, diff --git a/lib/src/animation/keyframe/path_keyframe_animation.dart b/lib/src/animation/keyframe/path_keyframe_animation.dart index 1bc98666..4ad70aa4 100644 --- a/lib/src/animation/keyframe/path_keyframe_animation.dart +++ b/lib/src/animation/keyframe/path_keyframe_animation.dart @@ -1,5 +1,6 @@ import 'dart:ui'; import '../../value/keyframe.dart'; +import '../../value/lottie_value_callback.dart'; import 'keyframe_animation.dart'; import 'path_keyframe.dart'; @@ -10,7 +11,8 @@ class PathKeyframeAnimation extends KeyframeAnimation { PathKeyframeAnimation(super.keyframes); @override - Offset getValue(Keyframe keyframe, double keyframeProgress) { + Offset getValue(Keyframe keyframe, double keyframeProgress, + LottieValueCallback? valueCallback) { var pathKeyframe = keyframe as PathKeyframe; var path = pathKeyframe.getPath(); if (path == null) { @@ -18,7 +20,7 @@ class PathKeyframeAnimation extends KeyframeAnimation { } if (valueCallback != null) { - var value = valueCallback!.getValueInternal( + var value = valueCallback.getValueInternal( pathKeyframe.startFrame, pathKeyframe.endFrame, pathKeyframe.startValue, diff --git a/lib/src/animation/keyframe/point_keyframe_animation.dart b/lib/src/animation/keyframe/point_keyframe_animation.dart index 81a596ed..c9d2aa49 100644 --- a/lib/src/animation/keyframe/point_keyframe_animation.dart +++ b/lib/src/animation/keyframe/point_keyframe_animation.dart @@ -1,14 +1,16 @@ import 'dart:ui'; import '../../value/keyframe.dart'; +import '../../value/lottie_value_callback.dart'; import 'keyframe_animation.dart'; class PointKeyframeAnimation extends KeyframeAnimation { PointKeyframeAnimation(super.keyframes); @override - Offset getValue(Keyframe keyframe, double keyframeProgress) { - return getValueSplitDimension( - keyframe, keyframeProgress, keyframeProgress, keyframeProgress); + Offset getValue(Keyframe keyframe, double keyframeProgress, + LottieValueCallback? valueCallback) { + return getValueSplitDimension(keyframe, keyframeProgress, keyframeProgress, + keyframeProgress, valueCallback); } @override @@ -16,7 +18,8 @@ class PointKeyframeAnimation extends KeyframeAnimation { Keyframe keyframe, double linearKeyframeProgress, double xKeyframeProgress, - double yKeyframeProgress) { + double yKeyframeProgress, + LottieValueCallback? valueCallback) { if (keyframe.startValue == null || keyframe.endValue == null) { throw Exception('Missing values for keyframe.'); } @@ -25,7 +28,7 @@ class PointKeyframeAnimation extends KeyframeAnimation { var endPoint = keyframe.endValue!; if (valueCallback != null) { - var value = valueCallback!.getValueInternal( + var value = valueCallback.getValueInternal( keyframe.startFrame, keyframe.endFrame, startPoint, diff --git a/lib/src/animation/keyframe/shape_keyframe_animation.dart b/lib/src/animation/keyframe/shape_keyframe_animation.dart index c5c6c434..6ff8c9da 100644 --- a/lib/src/animation/keyframe/shape_keyframe_animation.dart +++ b/lib/src/animation/keyframe/shape_keyframe_animation.dart @@ -3,6 +3,7 @@ import '../../model/content/shape_data.dart'; import '../../utils/misc.dart'; import '../../utils/path_factory.dart'; import '../../value/keyframe.dart'; +import '../../value/lottie_value_callback.dart'; import '../content/shape_modifier_content.dart'; import 'base_keyframe_animation.dart'; @@ -14,7 +15,8 @@ class ShapeKeyframeAnimation extends BaseKeyframeAnimation { ShapeKeyframeAnimation(super.keyframes); @override - Path getValue(Keyframe keyframe, double keyframeProgress) { + Path getValue(Keyframe keyframe, double keyframeProgress, + LottieValueCallback? valueCallback) { var startShapeData = keyframe.startValue!; var endShapeData = keyframe.endValue!; diff --git a/lib/src/animation/keyframe/split_dimension_path_keyframe_animation.dart b/lib/src/animation/keyframe/split_dimension_path_keyframe_animation.dart index f91c7d60..67d4115e 100644 --- a/lib/src/animation/keyframe/split_dimension_path_keyframe_animation.dart +++ b/lib/src/animation/keyframe/split_dimension_path_keyframe_animation.dart @@ -1,5 +1,6 @@ import 'dart:ui'; import '../../value/keyframe.dart'; +import '../../value/lottie_value_callback.dart'; import 'base_keyframe_animation.dart'; class SplitDimensionPathKeyframeAnimation @@ -30,7 +31,8 @@ class SplitDimensionPathKeyframeAnimation } @override - Offset getValue(Keyframe keyframe, double keyframeProgress) { + Offset getValue(Keyframe keyframe, double keyframeProgress, + LottieValueCallback? valueCallback) { return _point; } } diff --git a/lib/src/animation/keyframe/text_keyframe_animation.dart b/lib/src/animation/keyframe/text_keyframe_animation.dart index 35fadb90..9fc116ea 100644 --- a/lib/src/animation/keyframe/text_keyframe_animation.dart +++ b/lib/src/animation/keyframe/text_keyframe_animation.dart @@ -9,8 +9,9 @@ class TextKeyframeAnimation extends KeyframeAnimation { @override DocumentData getValue( - Keyframe keyframe, double keyframeProgress) { - var valueCallback = this.valueCallback; + Keyframe keyframe, + double keyframeProgress, + LottieValueCallback? valueCallback) { if (valueCallback != null) { return valueCallback.getValueInternal( keyframe.startFrame, diff --git a/lib/src/animation/keyframe/transform_keyframe_animation.dart b/lib/src/animation/keyframe/transform_keyframe_animation.dart index 25820cc7..70420354 100644 --- a/lib/src/animation/keyframe/transform_keyframe_animation.dart +++ b/lib/src/animation/keyframe/transform_keyframe_animation.dart @@ -87,6 +87,106 @@ class TransformKeyframeAnimation { _skewAngle?.setProgress(progress); } + void applyToMatrix(Matrix4 matrix) { + if (null != _anchorPoint) { + final anchorPoint = _anchorPoint!.rawValue; + if (anchorPoint.dx != 0 || anchorPoint.dy != 0) { + matrix.translate(-anchorPoint.dx, -anchorPoint.dy); + } + } + + if (null != _scale) { + final scale = _scale!.rawValue; + if (scale.dx != 1 || scale.dy != 1) { + matrix.scale(scale.dx, scale.dy); + } + } + + if (null != _skew) { + final skew = _skew!.rawValue; + final angle = (null == _skewAngle) ? null : _skewAngle!.rawValue; + final mCos = (null == angle) ? 0.0 : cos(radians(-angle + 90)); + final mSin = (null == angle) ? 1.0 : sin(radians(-angle + 90)); + final aTan = tan(radians(-skew)); + + final skewMatrix1 = Matrix4( + mCos, + mSin, + 0, + 0, + -mSin, + mCos, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, // + ); + + final skewMatrix2 = Matrix4( + 1, + 0, + 0, + 0, + aTan, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, // + ); + + final skewMatrix3 = Matrix4( + mCos, + -mSin, + 0, + 0, + mSin, + mCos, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, // + ); + + skewMatrix2.multiply(skewMatrix1); + skewMatrix3.multiply(skewMatrix2); + matrix.multiply(skewMatrix3); + } + + if (null != _rotation) { + final rotation = _rotation!.rawValue; + if (rotation != 0) { + matrix.rotateZ(-rotation * pi / 180.0); + } + } + + if (null != _position) { + final position = _position!.rawValue; + if (position.dx != 0 || position.dy != 0) { + matrix.translate(position.dx, position.dy); + } + } + + } + Matrix4 getMatrix() { _matrix.reset(); diff --git a/lib/src/animation/keyframe/value_callback_keyframe_animation.dart b/lib/src/animation/keyframe/value_callback_keyframe_animation.dart index 88589c64..0fbb9cff 100644 --- a/lib/src/animation/keyframe/value_callback_keyframe_animation.dart +++ b/lib/src/animation/keyframe/value_callback_keyframe_animation.dart @@ -39,7 +39,8 @@ class ValueCallbackKeyframeAnimation } @override - A getValue(Keyframe keyframe, double keyframeProgress) { + A getValue(Keyframe keyframe, double keyframeProgress, + LottieValueCallback? valueCallback) { return value; } } diff --git a/lib/src/api/lottie_api.dart b/lib/src/api/lottie_api.dart new file mode 100644 index 00000000..b7ebbb84 --- /dev/null +++ b/lib/src/api/lottie_api.dart @@ -0,0 +1,135 @@ +import 'package:flutter/cupertino.dart'; + +import '../../lottie.dart'; +import '../animation/content/drawing_content.dart'; +import '../animation/content/stroke_content.dart'; +import '../model/key_path.dart'; +import '../model/lottie_observer.dart'; +import '../utils.dart'; + +class LottieApi { + final LottieDrawable drawable; + + LottieApi(this.drawable); + + List _resolveKeyPath(final List keyPath) { + final layer = drawable.compositionLayer; + var keyPaths = []; + layer.resolveKeyPath(KeyPath(keyPath), 0, keyPaths, KeyPath([])); + return keyPaths; + } + + /// converts points from global animation coordinates to property animation coordinates + /// + /// * [keyPath], Key Path array such as ["Loop","left_eye","Group 10"]. + /// * [points], Global coordinates. + /// + List _toKeyPathLayerPoints( + final List keyPath, final List points) { + var transPoints = []; + var keyPaths = _resolveKeyPath(keyPath); + for (var i = 0; i < keyPaths.length; i++) { + var kp = keyPaths[i]; + var element = kp.resolvedElement; + if (null != element) { + var matrix = Matrix4.identity(); + if (element is LottieObserver) { + var observer = element as LottieObserver; + observer.applyToMatrix(matrix); + var hierarchy = observer.requireMatrixHierarchy(); + for (var i = 0; i < hierarchy.length; i++) { + hierarchy[i].applyToMatrix(matrix); + } + } + for (var j = 0; j < points.length; j++) { + var point = points[j]; + var trans = matrix.inversePoint(point); + transPoints.add(trans); + } + } + } + return transPoints; + } + + /// converts a point from global animation coordinates to property animation coordinates + /// + /// * [keyPath], Key Path array such as ["Loop","left_eye","Group 10"]. + /// * [raw], Global coordinates. + /// + List toKeyPathLayerPoint( + final List keyPath, final Offset raw) { + if (keyPath.isEmpty) { + return []; + } + var points = [raw]; + for (var i = 1; i <= keyPath.length; i++) { + var lp = _toKeyPathLayerPoints(keyPath.sublist(0, i), points); + if (lp.isEmpty) { + points = []; + } + points = lp; + } + return points; + } + + /// converts a point from animation coordinates to global coordinates + /// + /// * [point], animation coordinates point. + /// + Offset toContainerPoint(final Offset point) { + var bounds = drawable.drawBounds; + var offset = drawable.topLeft; + var newPoint = Offset( + point.dx - bounds.left - offset.dx, point.dy - bounds.top - offset.dy); + var matrix = drawable.matrix; + var scale = matrix.getScale(); + newPoint = newPoint * (1.0 / scale); + return newPoint; + } + + List requireColorValueDelegates( + final List keyPath, final ValueNotifier color) { + var delegates = []; + var keyPaths = _resolveKeyPath(keyPath); + for (var i = 0; i < keyPaths.length; i++) { + var value = keyPaths[i]; + var element = value.resolvedElement; + if (element is StrokeContent) { + var delegate = ValueDelegate.strokeColor(keyPath, callback: (cb) { + return color.value; + }); + delegates.add(delegate); + } else if (element is DrawingContent) { + var delegate = ValueDelegate.color(keyPath, callback: (cb) { + return color.value; + }); + delegates.add(delegate); + } + } + return delegates; + } + + /// check if the hit on Key Path + /// * [keyPath], Key Path array such as ["Loop","left_eye","Group 10"]. + /// * [point], hit point. + /// + bool hitTest(final List keyPath, final Offset point) { + var bounds = drawable.drawBounds; + if (!bounds.contains(point)) { + return false; + } + var hitPoint = point - drawable.topLeft; + var keyPaths = _resolveKeyPath(keyPath); + for (var i = 0; i < keyPaths.length; i++) { + var value = keyPaths[i]; + var element = value.resolvedElement; + if (element is LottieObserver) { + var observer = element! as LottieObserver; + if (observer.hitTest(hitPoint)) { + return true; + } + } + } + return false; + } +} diff --git a/lib/src/lottie_drawable.dart b/lib/src/lottie_drawable.dart index 36387545..dafac980 100644 --- a/lib/src/lottie_drawable.dart +++ b/lib/src/lottie_drawable.dart @@ -19,6 +19,15 @@ class LottieDrawable { bool enableMergePaths = false; FilterQuality? filterQuality; + ui.Offset _topLeft = const ui.Offset(0, 0); + ui.Rect _drawBounds = ui.Rect.zero; + + ui.Rect get drawBounds => _drawBounds; + + Matrix4 get matrix => _matrix; + + ui.Offset get topLeft => _topLeft; + /// Gives a suggestion whether to paint with anti-aliasing, or not. Default is true. bool antiAliasingSuggested = true; @@ -147,6 +156,8 @@ class LottieDrawable { fit ??= BoxFit.scaleDown; alignment ??= Alignment.center; + _drawBounds = rect; + var outputSize = rect.size; var inputSize = size; var fittedSizes = applyBoxFit(fit, inputSize, outputSize); @@ -160,6 +171,8 @@ class LottieDrawable { var destinationRect = destinationPosition & destinationSize; var sourceRect = alignment.inscribe(sourceSize, Offset.zero & inputSize); + _topLeft = destinationPosition; + canvas.save(); canvas.translate(destinationRect.left, destinationRect.top); _matrix.setIdentity(); diff --git a/lib/src/model/layer/base_layer.dart b/lib/src/model/layer/base_layer.dart index d15e8480..96832c72 100644 --- a/lib/src/model/layer/base_layer.dart +++ b/lib/src/model/layer/base_layer.dart @@ -19,6 +19,7 @@ import '../content/mask.dart'; import '../content/shape_data.dart'; import '../key_path.dart'; import '../key_path_element.dart'; +import '../lottie_observer.dart'; import 'composition_layer.dart'; import 'image_layer.dart'; import 'layer.dart'; @@ -27,7 +28,8 @@ import 'shape_layer.dart'; import 'solid_layer.dart'; import 'text_layer.dart'; -abstract class BaseLayer implements DrawingContent, KeyPathElement { +abstract class BaseLayer + implements DrawingContent, KeyPathElement, LottieObserver { static BaseLayer? forModel( CompositionLayer compositionLayer, Layer layerModel, @@ -56,6 +58,7 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement { } final Matrix4 _matrix = Matrix4.identity(); + Rect _drawBounds = Rect.zero; final Paint _contentPaint = ui.Paint(); final Paint _dstInPaint = ui.Paint()..blendMode = ui.BlendMode.dstIn; final Paint _dstOutPaint = ui.Paint()..blendMode = ui.BlendMode.dstOut; @@ -110,6 +113,21 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement { _setupInOutAnimations(); } + @override + void applyToMatrix(final Matrix4 matrix) { + transform.applyToMatrix(matrix); + } + + @override + List requireMatrixHierarchy() { + return (null == _parentLayers) ? [] : _parentLayers!; + } + + @override + bool hitTest(final Offset position) { + return _drawBounds.contains(position); + } + void setMatteLayer(BaseLayer? matteLayer) { _matteLayer = matteLayer; } @@ -214,10 +232,11 @@ abstract class BaseLayer implements DrawingContent, KeyPathElement { _matrix.preConcat(transform.getMatrix()); bounds = _intersectBoundsWithMask(bounds, _matrix); - if (bounds - .intersect(Rect.fromLTWH(0, 0, canvasSize.width, canvasSize.height)) - .isEmpty) { + _drawBounds = bounds + .intersect(Rect.fromLTWH(0, 0, canvasSize.width, canvasSize.height)); + if (_drawBounds.isEmpty) { bounds = Rect.zero; + _drawBounds = Rect.zero; } L.endSection('Layer#computeBounds'); diff --git a/lib/src/model/lottie_observer.dart b/lib/src/model/lottie_observer.dart new file mode 100644 index 00000000..f7fdd526 --- /dev/null +++ b/lib/src/model/lottie_observer.dart @@ -0,0 +1,15 @@ +import 'dart:ui'; + +import 'package:vector_math/vector_math_64.dart'; + +abstract class LottieObserver { + void applyToMatrix(final Matrix4 matrix); + + List requireMatrixHierarchy(); + + /// Determines the objects located at the given position. + /// + /// Returns true, if this object absorbs the hit. + /// Returns false, if the hit can continue to other objects . + bool hitTest(final Offset position); +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 83fc275e..0117e22d 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -50,6 +50,14 @@ extension Matrix4Extension on Matrix4 { } } + Offset inversePoint(Offset point) { + var trans = Matrix4.tryInvert(this); + if (null != trans) { + return MatrixUtils.transformPoint(trans, point); + } + return point; + } + double getScale() { var p0 = Vector3(0, 0, 0)..applyMatrix4(this); var p1 = Vector3(1 / sqrt(2), 1 / sqrt(2), 0)..applyMatrix4(this);