[flutter] Add SkeletonDrawable.renderToRawImageData(), clean-up dress up example.

This commit is contained in:
Mario Zechner 2022-11-24 11:18:26 +01:00
parent dbc2e164f0
commit b0c63b8fa8
9 changed files with 53 additions and 151 deletions

View File

@ -16,8 +16,7 @@ class DressUp extends StatefulWidget {
class DressUpState extends State<DressUp> {
static const double thumbnailSize = 200;
late SpineWidgetController _controller;
SkeletonDrawable? _drawable;
late SkeletonDrawable _drawable;
Skin? _customSkin;
final Map<String, RawImageData> _skinImages = {};
final Map<String, bool> _selectedSkins = {};
@ -27,42 +26,18 @@ class DressUpState extends State<DressUp> {
reportLeaks();
super.initState();
SkeletonDrawable.fromAsset("assets/mix-and-match.atlas", "assets/mix-and-match-pro.skel").then((drawable) async {
_drawable = drawable;
for (var skin in drawable.skeletonData.getSkins()) {
if (skin.getName() == "default") continue;
var skeleton = drawable.skeleton;
skeleton.setSkin(skin);
skeleton.setToSetupPose();
skeleton.updateWorldTransform();
var bounds = skeleton.getBounds();
var scale = 1 / (bounds.width > bounds.height ? bounds.width / thumbnailSize : bounds.height / thumbnailSize);
var recorder = ui.PictureRecorder();
var canvas = Canvas(recorder);
var bgColor = Random().nextInt(0xffffffff) | 0xff0000000;
var paint = Paint()
..color = ui.Color(bgColor)
..style = PaintingStyle.fill;
canvas.drawRect(const Rect.fromLTWH(0, 0, thumbnailSize, thumbnailSize), paint);
canvas.translate(thumbnailSize / 2, thumbnailSize / 2);
canvas.scale(scale, scale);
canvas.translate(-(bounds.x + bounds.width / 2), -(bounds.y + bounds.height / 2));
canvas.drawRect(Rect.fromLTRB(-5, -5, 5, -5), paint..color = Colors.red);
drawable.renderToCanvas(canvas);
var rawImageData = (await (await recorder.endRecording().toImage(thumbnailSize.toInt(), thumbnailSize.toInt())).toByteData(format: ui.ImageByteFormat.rawRgba))!.buffer.asUint8List();
_skinImages[skin.getName()] = (RawImageData(rawImageData, thumbnailSize.toInt(), thumbnailSize.toInt()));
_skinImages[skin.getName()] = await drawable.renderToRawImageData(thumbnailSize, thumbnailSize);
_selectedSkins[skin.getName()] = false;
}
_drawable = drawable;
_controller = SpineWidgetController(onInitialized: (controller) {
controller.animationState.setAnimationByName(0, "dance", true);
});
setState(() {
_selectedSkins["full-skins/girl"] = true;
drawable.skeleton.setSkinByName("full-skins/girl");
drawable.skeleton.setToSetupPose();
});
_toggleSkin("full-skins/girl");
setState(() {});
});
}
@ -72,16 +47,20 @@ class DressUpState extends State<DressUp> {
_customSkin = Skin("custom-skin");
for (var skinName in _selectedSkins.keys) {
if (_selectedSkins[skinName] == true) {
var skin = _controller.skeletonData.findSkin(skinName);
var skin = _drawable.skeletonData.findSkin(skinName);
if (skin != null) _customSkin?.addSkin(skin);
}
}
_controller.skeleton.setSkin(_customSkin!);
_controller.skeleton.setToSetupPose();
_drawable.skeleton.setSkin(_customSkin!);
_drawable.skeleton.setToSetupPose();
}
@override
Widget build(BuildContext context) {
final controller = SpineWidgetController(onInitialized: (controller) {
controller.animationState.setAnimationByName(0, "dance", true);
});
return Scaffold(
appBar: AppBar(title: const Text('Dress Up')),
body: _skinImages.isEmpty
@ -107,7 +86,7 @@ class DressUpState extends State<DressUp> {
),
),
Expanded(
child: SpineWidget.drawable(_drawable, _controller, boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"]),)
child: SpineWidget.drawable(_drawable, controller, boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"]),)
)
]
)

View File

@ -37,9 +37,8 @@ class SpineComponent extends PositionComponent {
Iterable<Component>? children,
int? priority,
}) async {
final drawable = await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle);
return SpineComponent(
drawable,
await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle),
ownsDrawable: true,
boundsProvider: boundsProvider,
position: position,
@ -54,6 +53,11 @@ class SpineComponent extends PositionComponent {
if (_ownsDrawable) {
_drawable.dispose();
}
@override
void onDetach() {
}
}
@override
@ -133,9 +137,11 @@ class PreloadAndShareSpineDataExample extends FlameGame {
@override
void onDetach() {
// Dispose the pre-loaded atlas and skeleton data when the game/scene is removed
// Dispose the pre-loaded atlas and skeleton data when the game/scene is removed.
cachedAtlas.dispose();
cachedSkeletonData.dispose();
// Dispose each spineboy and its internal SkeletonDrawable.
for (final spineboy in spineboys) {
spineboy.dispose();
}

View File

@ -6,7 +6,6 @@ import 'flame_example.dart';
import 'simple_animation.dart';
import 'animation_state_events.dart';
import 'pause_play_animation.dart';
import 'skins.dart';
import 'dress_up.dart';
import 'ik_following.dart';
@ -70,18 +69,6 @@ class ExampleSelector extends StatelessWidget {
},
),
spacer,
ElevatedButton(
child: const Text('Skins'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => const Skins(),
),
);
},
),
spacer,
ElevatedButton(
child: const Text('Dress Up'),
onPressed: () {

View File

@ -1,85 +0,0 @@
import 'package:flutter/material.dart';
import 'package:esotericsoftware_spine_flutter/spine_flutter.dart';
class Skins extends StatefulWidget {
const Skins({Key? key}) : super(key: key);
@override
SkinsState createState() => SkinsState();
}
class SkinsState extends State<Skins> {
SkeletonDrawable? _drawable;
late SpineWidgetController _controller;
final Map<String, bool> _selectedSkins = {};
Skin? _customSkin;
@override
void initState() {
super.initState();
SkeletonDrawable.fromAsset("assets/mix-and-match.atlas", "assets/mix-and-match-pro.skel").then((drawable) {
for (var skin in drawable.skeletonData.getSkins()) {
_selectedSkins[skin.getName()] = false;
}
_controller = SpineWidgetController(onInitialized: (controller) {
controller.animationState.setAnimationByName(0, "walk", true);
});
drawable.skeleton.setSkinByName("full-skins/girl");
_selectedSkins["full-skins/girl"] = true;
_drawable = drawable;
setState(() {});
});
}
void _toggleSkin(String skinName) {
_selectedSkins[skinName] = !_selectedSkins[skinName]!;
if (_customSkin != null) _customSkin?.dispose();
_customSkin = Skin("custom-skin");
for (var skinName in _selectedSkins.keys) {
if (_selectedSkins[skinName] == true) {
var skin = _controller.skeletonData.findSkin(skinName);
if (skin != null) _customSkin?.addSkin(skin);
}
}
_controller.skeleton.setSkin(_customSkin!);
_controller.skeleton.setToSetupPose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Skins')),
body: _drawable == null
? const SizedBox()
: Row(
children: [
Expanded(
child:ListView(
children: _selectedSkins.keys.map((skinName) {
return CheckboxListTile(
title: Text(skinName),
value: _selectedSkins[skinName],
onChanged: (bool? value) {
_toggleSkin(skinName);
setState(() => {});
},
);
}).toList()
)
),
Expanded(
child: SpineWidget.drawable(_drawable, _controller, boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"]))
)
]
)
);
}
@override
void dispose() {
super.dispose();
_drawable?.dispose();
}
}

View File

@ -132,15 +132,8 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
raw_image_provider:
dependency: "direct main"
dependency: transitive
description:
name: raw_image_provider
url: "https://pub.dartlang.org"

View File

@ -13,7 +13,6 @@ dependencies:
esotericsoftware_spine_flutter:
path: ../
cupertino_icons: ^1.0.2
raw_image_provider: ^0.2.0
flame: ^1.4.0
dev_dependencies:

View File

@ -1,5 +1,6 @@
import 'dart:convert' as convert;
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';
@ -8,6 +9,7 @@ import 'package:flutter/rendering.dart' as rendering;
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:raw_image_provider/raw_image_provider.dart';
import 'spine_flutter_bindings_generated.dart';
import 'ffi_proxy.dart';
@ -2595,7 +2597,6 @@ class Skeleton {
}
}
// FIXME expose timelines and apply?
class Animation {
final spine_animation _animation;
@ -3307,6 +3308,26 @@ class SkeletonDrawable {
}
}
Future<RawImageData> renderToRawImageData(double width, double height) async {
var bounds = skeleton.getBounds();
var scale = 1 / (bounds.width > bounds.height ? bounds.width / width : bounds.height / height);
var recorder = PictureRecorder();
var canvas = Canvas(recorder);
var bgColor = Random().nextInt(0xffffffff) | 0xff0000000;
var paint = Paint()
..color = material.Color(bgColor)
..style = PaintingStyle.fill;
canvas.drawRect(Rect.fromLTWH(0, 0, width, height), paint);
canvas.translate(width / 2, height / 2);
canvas.scale(scale, scale);
canvas.translate(-(bounds.x + bounds.width / 2), -(bounds.y + bounds.height / 2));
canvas.drawRect(const Rect.fromLTRB(-5, -5, 5, -5), paint..color = material.Colors.red);
renderToCanvas(canvas);
var rawImageData = (await (await recorder.endRecording().toImage(width.toInt(), height.toInt())).toByteData(format: ImageByteFormat.rawRgba))!.buffer.asUint8List();
return RawImageData(rawImageData, width.toInt(), height.toInt());
}
void dispose() {
if (_disposed) return;
_disposed = true;

View File

@ -106,17 +106,18 @@ class SkinAndAnimationBounds extends BoundsProvider {
@override
Bounds computeBounds(SkeletonDrawable drawable) {
var data = drawable.skeletonData;
var customSkin = Skin("custom-skin");
for (var skinName in skins) {
var skin = data.findSkin(skinName);
final data = drawable.skeletonData;
final oldSkin = drawable.skeleton.getSkin();
final customSkin = Skin("custom-skin");
for (final skinName in skins) {
final skin = data.findSkin(skinName);
if (skin == null) continue;
customSkin.addSkin(skin);
}
drawable.skeleton.setSkin(customSkin);
drawable.skeleton.setToSetupPose();
final animation = data.findAnimation(this.animation!);
final animation = this.animation != null ? data.findAnimation(this.animation!) : null;
double minX = double.infinity;
double minY = double.infinity;
double maxX = double.negativeInfinity;
@ -142,6 +143,7 @@ class SkinAndAnimationBounds extends BoundsProvider {
customSkin.dispose();
drawable.skeleton.setSkinByName("default");
drawable.animationState.clearTracks();
if (oldSkin != null) drawable.skeleton.setSkin(oldSkin);
drawable.skeleton.setToSetupPose();
drawable.update(0);
return Bounds(minX, minY, maxX - minX, maxY - minY);

View File

@ -13,7 +13,6 @@ environment:
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
ffi: ^2.0.1
web_ffi_fork: ^0.7.4
inject_js: ^2.0.0
@ -21,6 +20,7 @@ dependencies:
meta: ^1.3.0
http: ^0.13.5
path: ^1.8.2
raw_image_provider: ^0.2.0
dev_dependencies:
ffigen: ^6.1.2