diff --git a/spine-flutter/example/lib/animation_state_events.dart b/spine-flutter/example/lib/animation_state_events.dart index 770cdf077..cc5933adc 100644 --- a/spine-flutter/example/lib/animation_state_events.dart +++ b/spine-flutter/example/lib/animation_state_events.dart @@ -7,7 +7,7 @@ class AnimationStateEvents extends StatelessWidget { @override Widget build(BuildContext context) { reportLeaks(); - final controller = SpineWidgetController((controller) { + final controller = SpineWidgetController(onInitialized: (controller) { for (final bone in controller.skeleton.getBones()) { print(bone); } diff --git a/spine-flutter/example/lib/dress_up.dart b/spine-flutter/example/lib/dress_up.dart index 006bb4182..c075c2a85 100644 --- a/spine-flutter/example/lib/dress_up.dart +++ b/spine-flutter/example/lib/dress_up.dart @@ -54,7 +54,7 @@ class DressUpState extends State { _selectedSkins[skin.getName()] = false; } _drawable = drawable; - _controller = SpineWidgetController((controller) { + _controller = SpineWidgetController(onInitialized: (controller) { controller.animationState.setAnimationByName(0, "dance", true); }); setState(() { diff --git a/spine-flutter/example/lib/ik_following.dart b/spine-flutter/example/lib/ik_following.dart new file mode 100644 index 000000000..61dc7ff7f --- /dev/null +++ b/spine-flutter/example/lib/ik_following.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:spine_flutter/spine_flutter.dart'; + +class IkFollowing extends StatefulWidget { + const IkFollowing({Key? key}) : super(key: key); + + @override + IkFollowingState createState() => IkFollowingState(); +} + +class IkFollowingState extends State { + late SpineWidgetController controller; + Offset? crossHairPosition; + + @override + void initState() { + super.initState(); + + controller = SpineWidgetController(onInitialized: (controller) { + // Set the walk animation on track 0, let it loop + controller.animationState.setAnimationByName(0, "walk", true); + controller.animationState.setAnimationByName(1, "aim", true); + }, onAfterUpdateWorldTransforms: (controller) { + var worldPosition = crossHairPosition; + if (worldPosition == null) return; + var bone = controller.skeleton.findBone("crosshair"); + if (bone == null) return; + var position = bone.getParent()?.worldToLocal(worldPosition.dx, worldPosition.dy) ?? Vector2(0, 0); + bone.setX(position.x); + bone.setY(position.y); + }); + } + + void _updateBonePosition(Offset position) { + crossHairPosition = controller.toSkeletonCoordinates(position); + } + + @override + Widget build(BuildContext context) { + reportLeaks(); + + return Scaffold( + appBar: AppBar(title: const Text('IK Following')), + body: GestureDetector( + onPanDown: (drag) => _updateBonePosition(drag.localPosition), + onPanUpdate: (drag) => _updateBonePosition(drag.localPosition), + child: SpineWidget.asset("assets/spineboy-pro.skel", "assets/spineboy.atlas", controller), + )); + } +} diff --git a/spine-flutter/example/lib/main.dart b/spine-flutter/example/lib/main.dart index 7d9a817bf..2cfc3c23d 100644 --- a/spine-flutter/example/lib/main.dart +++ b/spine-flutter/example/lib/main.dart @@ -5,6 +5,7 @@ import 'animation_state_events.dart'; import 'pause_play_animation.dart'; import 'skins.dart'; import 'dress_up.dart'; +import 'ik_following.dart'; class ExampleSelector extends StatelessWidget { const ExampleSelector({super.key}); @@ -77,6 +78,18 @@ class ExampleSelector extends StatelessWidget { ); }, ), + spacer, + ElevatedButton( + child: const Text('IK Following'), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const IkFollowing(), + ), + ); + }, + ), spacer ] ) diff --git a/spine-flutter/example/lib/pause_play_animation.dart b/spine-flutter/example/lib/pause_play_animation.dart index 651e948b2..bf735f2af 100644 --- a/spine-flutter/example/lib/pause_play_animation.dart +++ b/spine-flutter/example/lib/pause_play_animation.dart @@ -15,7 +15,7 @@ class PlayPauseAnimationState extends State { @override void initState() { super.initState(); - controller = SpineWidgetController((controller) { + controller = SpineWidgetController(onInitialized: (controller) { controller.animationState.setAnimationByName(0, "walk", true); }); isPlaying = true; diff --git a/spine-flutter/example/lib/simple_animation.dart b/spine-flutter/example/lib/simple_animation.dart index a3c00af88..ff813806e 100644 --- a/spine-flutter/example/lib/simple_animation.dart +++ b/spine-flutter/example/lib/simple_animation.dart @@ -7,7 +7,7 @@ class SimpleAnimation extends StatelessWidget { @override Widget build(BuildContext context) { reportLeaks(); - final controller = SpineWidgetController((controller) { + final controller = SpineWidgetController(onInitialized: (controller) { // Set the walk animation on track 0, let it loop controller.animationState.setAnimationByName(0, "walk", true); }); diff --git a/spine-flutter/example/lib/skins.dart b/spine-flutter/example/lib/skins.dart index 9edbdb99d..9792c699a 100644 --- a/spine-flutter/example/lib/skins.dart +++ b/spine-flutter/example/lib/skins.dart @@ -21,7 +21,7 @@ class SkinsState extends State { for (var skin in drawable.skeletonData.getSkins()) { _selectedSkins[skin.getName()] = false; } - _controller = SpineWidgetController((controller) { + _controller = SpineWidgetController(onInitialized: (controller) { controller.animationState.setAnimationByName(0, "walk", true); }); drawable.skeleton.setSkinByName("full-skins/girl"); diff --git a/spine-flutter/lib/spine_widget.dart b/spine-flutter/lib/spine_widget.dart index 3e0236fad..c4b79e8ac 100644 --- a/spine-flutter/lib/spine_widget.dart +++ b/spine-flutter/lib/spine_widget.dart @@ -8,15 +8,18 @@ import 'spine_flutter.dart'; class SpineWidgetController { SkeletonDrawable? _drawable; + double _offsetX = 0, _offsetY = 0, _scaleX = 1, _scaleY = 1; final void Function(SpineWidgetController controller)? onInitialized; - bool initialized = false; + final void Function(SpineWidgetController controller)? onBeforeUpdateWorldTransforms; + final void Function(SpineWidgetController controller)? onAfterUpdateWorldTransforms; + final void Function(SpineWidgetController controller, Canvas canvas)? onBeforePaint; + final void Function(SpineWidgetController controller, Canvas canvas)? onAfterPaint; - SpineWidgetController([this.onInitialized]); + SpineWidgetController({this.onInitialized, this.onBeforeUpdateWorldTransforms, this.onAfterUpdateWorldTransforms, this.onBeforePaint, this.onAfterPaint}); void _initialize(SkeletonDrawable drawable) { if (_drawable != null) throw Exception("SpineWidgetController already initialized. A controller can only be used with one widget."); _drawable = drawable; - initialized = true; onInitialized?.call(this); } @@ -49,6 +52,19 @@ class SpineWidgetController { if (_drawable == null) throw Exception("Controller is not initialized yet."); return _drawable!; } + + void _setCoordinateTransform(double offsetX, double offsetY, double scaleX, double scaleY) { + _offsetX = offsetX; + _offsetY = offsetY; + _scaleX = scaleX; + _scaleY = scaleY; + } + + Offset toSkeletonCoordinates(Offset position) { + var x = position.dx; + var y = position.dy; + return Offset(x / _scaleX - _offsetX, y / _scaleY - _offsetY); + } } enum AssetType { Asset, File, Http, Drawable } @@ -166,7 +182,6 @@ class SpineWidget extends StatefulWidget { } class _SpineWidgetState extends State { - SkeletonDrawable? skeletonDrawable; @override void initState() { @@ -179,16 +194,12 @@ class _SpineWidgetState extends State { } void loadDrawable(SkeletonDrawable drawable) { - skeletonDrawable = drawable; - widget._controller._initialize(skeletonDrawable!); - skeletonDrawable?.update(0); + widget._controller._initialize(drawable); + drawable.update(0); setState(() {}); } void loadFromAsset(String skeletonFile, String atlasFile, AssetType assetType) async { - late Atlas atlas; - late SkeletonData skeletonData; - switch (assetType) { case AssetType.Asset: loadDrawable(await SkeletonDrawable.fromAsset(skeletonFile, atlasFile)); @@ -206,9 +217,9 @@ class _SpineWidgetState extends State { @override Widget build(BuildContext context) { - if (skeletonDrawable != null) { + if (widget._controller._drawable != null) { print("Skeleton loaded, rebuilding painter"); - return _SpineRenderObjectWidget(skeletonDrawable!, widget._fit, widget._alignment, widget._boundsProvider, widget._sizedByBounds); + return _SpineRenderObjectWidget(widget._controller._drawable!, widget._controller, widget._fit, widget._alignment, widget._boundsProvider, widget._sizedByBounds); } else { print("Skeleton not loaded yet"); return const SizedBox(); @@ -217,23 +228,24 @@ class _SpineWidgetState extends State { @override void dispose() { - skeletonDrawable?.dispose(); super.dispose(); + widget._controller._drawable?.dispose(); } } class _SpineRenderObjectWidget extends LeafRenderObjectWidget { final SkeletonDrawable _skeletonDrawable; + final SpineWidgetController _controller; final BoxFit _fit; final Alignment _alignment; final BoundsProvider _boundsProvider; final bool _sizedByBounds; - _SpineRenderObjectWidget(this._skeletonDrawable, this._fit, this._alignment, this._boundsProvider, this._sizedByBounds); + const _SpineRenderObjectWidget(this._skeletonDrawable, this._controller, this._fit, this._alignment, this._boundsProvider, this._sizedByBounds); @override RenderObject createRenderObject(BuildContext context) { - return _SpineRenderObject(_skeletonDrawable, _fit, _alignment, _boundsProvider, _sizedByBounds); + return _SpineRenderObject(_skeletonDrawable, _controller, _fit, _alignment, _boundsProvider, _sizedByBounds); } @override @@ -248,6 +260,7 @@ class _SpineRenderObjectWidget extends LeafRenderObjectWidget { class _SpineRenderObject extends RenderBox { SkeletonDrawable _skeletonDrawable; + SpineWidgetController _controller; double _deltaTime = 0; final Stopwatch _stopwatch = Stopwatch(); BoxFit _fit; @@ -255,7 +268,7 @@ class _SpineRenderObject extends RenderBox { BoundsProvider _boundsProvider; bool _sizedByBounds; Bounds _bounds; - _SpineRenderObject(this._skeletonDrawable, this._fit, this._alignment, this._boundsProvider, this._sizedByBounds): _bounds = _boundsProvider.computeBounds(_skeletonDrawable); + _SpineRenderObject(this._skeletonDrawable, this._controller, this._fit, this._alignment, this._boundsProvider, this._sizedByBounds): _bounds = _boundsProvider.computeBounds(_skeletonDrawable); set skeletonDrawable(SkeletonDrawable skeletonDrawable) { if (_skeletonDrawable == skeletonDrawable) return; @@ -368,7 +381,9 @@ class _SpineRenderObject extends RenderBox { _deltaTime = _stopwatch.elapsedTicks / _stopwatch.frequency; _stopwatch.reset(); _stopwatch.start(); + _controller.onBeforeUpdateWorldTransforms?.call(_controller); _skeletonDrawable.update(_deltaTime); + _controller.onAfterUpdateWorldTransforms?.call(_controller); markNeedsPaint(); } @@ -403,12 +418,13 @@ class _SpineRenderObject extends RenderBox { break; } + var offsetX = offset.dx + size.width / 2.0 + (_alignment.x * size.width / 2.0); + var offsetY = offset.dy + size.height / 2.0 + (_alignment.y * size.height / 2.0); canvas - ..translate( - offset.dx + size.width / 2.0 + (_alignment.x * size.width / 2.0), - offset.dy + size.height / 2.0 + (_alignment.y * size.height / 2.0)) + ..translate(offsetX, offsetY) ..scale(scaleX, scaleY) ..translate(x, y); + _controller._setCoordinateTransform(x + offsetX / scaleY, y + offsetY / scaleY, scaleX, scaleY); } @override @@ -420,7 +436,9 @@ class _SpineRenderObject extends RenderBox { canvas.save(); _setCanvasTransform(canvas, offset); + _controller.onBeforePaint?.call(_controller, canvas); _skeletonDrawable.renderToCanvas(canvas); + _controller.onAfterPaint?.call(_controller, canvas); canvas.restore(); SchedulerBinding.instance.scheduleFrameCallback(_beginFrame);