mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-21 09:46:02 +08:00
[flutter] Add SkeletonDrawable.renderToRawImageData(), clean-up dress up example.
This commit is contained in:
parent
dbc2e164f0
commit
b0c63b8fa8
@ -16,8 +16,7 @@ class DressUp extends StatefulWidget {
|
|||||||
|
|
||||||
class DressUpState extends State<DressUp> {
|
class DressUpState extends State<DressUp> {
|
||||||
static const double thumbnailSize = 200;
|
static const double thumbnailSize = 200;
|
||||||
late SpineWidgetController _controller;
|
late SkeletonDrawable _drawable;
|
||||||
SkeletonDrawable? _drawable;
|
|
||||||
Skin? _customSkin;
|
Skin? _customSkin;
|
||||||
final Map<String, RawImageData> _skinImages = {};
|
final Map<String, RawImageData> _skinImages = {};
|
||||||
final Map<String, bool> _selectedSkins = {};
|
final Map<String, bool> _selectedSkins = {};
|
||||||
@ -27,42 +26,18 @@ class DressUpState extends State<DressUp> {
|
|||||||
reportLeaks();
|
reportLeaks();
|
||||||
super.initState();
|
super.initState();
|
||||||
SkeletonDrawable.fromAsset("assets/mix-and-match.atlas", "assets/mix-and-match-pro.skel").then((drawable) async {
|
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()) {
|
for (var skin in drawable.skeletonData.getSkins()) {
|
||||||
if (skin.getName() == "default") continue;
|
if (skin.getName() == "default") continue;
|
||||||
|
|
||||||
var skeleton = drawable.skeleton;
|
var skeleton = drawable.skeleton;
|
||||||
skeleton.setSkin(skin);
|
skeleton.setSkin(skin);
|
||||||
skeleton.setToSetupPose();
|
skeleton.setToSetupPose();
|
||||||
skeleton.updateWorldTransform();
|
skeleton.updateWorldTransform();
|
||||||
var bounds = skeleton.getBounds();
|
_skinImages[skin.getName()] = await drawable.renderToRawImageData(thumbnailSize, thumbnailSize);
|
||||||
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()));
|
|
||||||
_selectedSkins[skin.getName()] = false;
|
_selectedSkins[skin.getName()] = false;
|
||||||
}
|
}
|
||||||
_drawable = drawable;
|
_toggleSkin("full-skins/girl");
|
||||||
_controller = SpineWidgetController(onInitialized: (controller) {
|
setState(() {});
|
||||||
controller.animationState.setAnimationByName(0, "dance", true);
|
|
||||||
});
|
|
||||||
setState(() {
|
|
||||||
_selectedSkins["full-skins/girl"] = true;
|
|
||||||
drawable.skeleton.setSkinByName("full-skins/girl");
|
|
||||||
drawable.skeleton.setToSetupPose();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,16 +47,20 @@ class DressUpState extends State<DressUp> {
|
|||||||
_customSkin = Skin("custom-skin");
|
_customSkin = Skin("custom-skin");
|
||||||
for (var skinName in _selectedSkins.keys) {
|
for (var skinName in _selectedSkins.keys) {
|
||||||
if (_selectedSkins[skinName] == true) {
|
if (_selectedSkins[skinName] == true) {
|
||||||
var skin = _controller.skeletonData.findSkin(skinName);
|
var skin = _drawable.skeletonData.findSkin(skinName);
|
||||||
if (skin != null) _customSkin?.addSkin(skin);
|
if (skin != null) _customSkin?.addSkin(skin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_controller.skeleton.setSkin(_customSkin!);
|
_drawable.skeleton.setSkin(_customSkin!);
|
||||||
_controller.skeleton.setToSetupPose();
|
_drawable.skeleton.setToSetupPose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final controller = SpineWidgetController(onInitialized: (controller) {
|
||||||
|
controller.animationState.setAnimationByName(0, "dance", true);
|
||||||
|
});
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Dress Up')),
|
appBar: AppBar(title: const Text('Dress Up')),
|
||||||
body: _skinImages.isEmpty
|
body: _skinImages.isEmpty
|
||||||
@ -107,7 +86,7 @@ class DressUpState extends State<DressUp> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SpineWidget.drawable(_drawable, _controller, boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"]),)
|
child: SpineWidget.drawable(_drawable, controller, boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"]),)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -37,9 +37,8 @@ class SpineComponent extends PositionComponent {
|
|||||||
Iterable<Component>? children,
|
Iterable<Component>? children,
|
||||||
int? priority,
|
int? priority,
|
||||||
}) async {
|
}) async {
|
||||||
final drawable = await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle);
|
|
||||||
return SpineComponent(
|
return SpineComponent(
|
||||||
drawable,
|
await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle),
|
||||||
ownsDrawable: true,
|
ownsDrawable: true,
|
||||||
boundsProvider: boundsProvider,
|
boundsProvider: boundsProvider,
|
||||||
position: position,
|
position: position,
|
||||||
@ -54,6 +53,11 @@ class SpineComponent extends PositionComponent {
|
|||||||
if (_ownsDrawable) {
|
if (_ownsDrawable) {
|
||||||
_drawable.dispose();
|
_drawable.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onDetach() {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -133,9 +137,11 @@ class PreloadAndShareSpineDataExample extends FlameGame {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onDetach() {
|
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();
|
cachedAtlas.dispose();
|
||||||
cachedSkeletonData.dispose();
|
cachedSkeletonData.dispose();
|
||||||
|
|
||||||
|
// Dispose each spineboy and its internal SkeletonDrawable.
|
||||||
for (final spineboy in spineboys) {
|
for (final spineboy in spineboys) {
|
||||||
spineboy.dispose();
|
spineboy.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import 'flame_example.dart';
|
|||||||
import 'simple_animation.dart';
|
import 'simple_animation.dart';
|
||||||
import 'animation_state_events.dart';
|
import 'animation_state_events.dart';
|
||||||
import 'pause_play_animation.dart';
|
import 'pause_play_animation.dart';
|
||||||
import 'skins.dart';
|
|
||||||
import 'dress_up.dart';
|
import 'dress_up.dart';
|
||||||
import 'ik_following.dart';
|
import 'ik_following.dart';
|
||||||
|
|
||||||
@ -70,18 +69,6 @@ class ExampleSelector extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
spacer,
|
spacer,
|
||||||
ElevatedButton(
|
|
||||||
child: const Text('Skins'),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute<void>(
|
|
||||||
builder: (context) => const Skins(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
spacer,
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
child: const Text('Dress Up'),
|
child: const Text('Dress Up'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -132,15 +132,8 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.2"
|
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:
|
raw_image_provider:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: raw_image_provider
|
name: raw_image_provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
|
|||||||
@ -13,7 +13,6 @@ dependencies:
|
|||||||
esotericsoftware_spine_flutter:
|
esotericsoftware_spine_flutter:
|
||||||
path: ../
|
path: ../
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
raw_image_provider: ^0.2.0
|
|
||||||
flame: ^1.4.0
|
flame: ^1.4.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert' as convert;
|
import 'dart:convert' as convert;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ import 'package:flutter/rendering.dart' as rendering;
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:raw_image_provider/raw_image_provider.dart';
|
||||||
|
|
||||||
import 'spine_flutter_bindings_generated.dart';
|
import 'spine_flutter_bindings_generated.dart';
|
||||||
import 'ffi_proxy.dart';
|
import 'ffi_proxy.dart';
|
||||||
@ -2595,7 +2597,6 @@ class Skeleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME expose timelines and apply?
|
|
||||||
class Animation {
|
class Animation {
|
||||||
final spine_animation _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() {
|
void dispose() {
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|||||||
@ -106,17 +106,18 @@ class SkinAndAnimationBounds extends BoundsProvider {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Bounds computeBounds(SkeletonDrawable drawable) {
|
Bounds computeBounds(SkeletonDrawable drawable) {
|
||||||
var data = drawable.skeletonData;
|
final data = drawable.skeletonData;
|
||||||
var customSkin = Skin("custom-skin");
|
final oldSkin = drawable.skeleton.getSkin();
|
||||||
for (var skinName in skins) {
|
final customSkin = Skin("custom-skin");
|
||||||
var skin = data.findSkin(skinName);
|
for (final skinName in skins) {
|
||||||
|
final skin = data.findSkin(skinName);
|
||||||
if (skin == null) continue;
|
if (skin == null) continue;
|
||||||
customSkin.addSkin(skin);
|
customSkin.addSkin(skin);
|
||||||
}
|
}
|
||||||
drawable.skeleton.setSkin(customSkin);
|
drawable.skeleton.setSkin(customSkin);
|
||||||
drawable.skeleton.setToSetupPose();
|
drawable.skeleton.setToSetupPose();
|
||||||
|
|
||||||
final animation = data.findAnimation(this.animation!);
|
final animation = this.animation != null ? data.findAnimation(this.animation!) : null;
|
||||||
double minX = double.infinity;
|
double minX = double.infinity;
|
||||||
double minY = double.infinity;
|
double minY = double.infinity;
|
||||||
double maxX = double.negativeInfinity;
|
double maxX = double.negativeInfinity;
|
||||||
@ -142,6 +143,7 @@ class SkinAndAnimationBounds extends BoundsProvider {
|
|||||||
customSkin.dispose();
|
customSkin.dispose();
|
||||||
drawable.skeleton.setSkinByName("default");
|
drawable.skeleton.setSkinByName("default");
|
||||||
drawable.animationState.clearTracks();
|
drawable.animationState.clearTracks();
|
||||||
|
if (oldSkin != null) drawable.skeleton.setSkin(oldSkin);
|
||||||
drawable.skeleton.setToSetupPose();
|
drawable.skeleton.setToSetupPose();
|
||||||
drawable.update(0);
|
drawable.update(0);
|
||||||
return Bounds(minX, minY, maxX - minX, maxY - minY);
|
return Bounds(minX, minY, maxX - minX, maxY - minY);
|
||||||
|
|||||||
@ -13,7 +13,6 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
plugin_platform_interface: ^2.0.2
|
|
||||||
ffi: ^2.0.1
|
ffi: ^2.0.1
|
||||||
web_ffi_fork: ^0.7.4
|
web_ffi_fork: ^0.7.4
|
||||||
inject_js: ^2.0.0
|
inject_js: ^2.0.0
|
||||||
@ -21,6 +20,7 @@ dependencies:
|
|||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
http: ^0.13.5
|
http: ^0.13.5
|
||||||
path: ^1.8.2
|
path: ^1.8.2
|
||||||
|
raw_image_provider: ^0.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
ffigen: ^6.1.2
|
ffigen: ^6.1.2
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user