diff --git a/packages/slides/assets/models/skeleton.glb b/packages/slides/assets/models/skeleton.glb new file mode 100644 index 0000000..769e85c Binary files /dev/null and b/packages/slides/assets/models/skeleton.glb differ diff --git a/packages/slides/lib/main.dart b/packages/slides/lib/main.dart index 9cb736b..604bf60 100644 --- a/packages/slides/lib/main.dart +++ b/packages/slides/lib/main.dart @@ -34,6 +34,7 @@ class FlutterDeckExample extends StatelessWidget { ), ), slides: [ + Slide17Animations(), Slide00Title(), Slide01Intro(), Slide02Agenda(), @@ -53,6 +54,7 @@ class FlutterDeckExample extends StatelessWidget { Slide14BirthTheCamera(), Slide15CameraTypes(), Slide16Models(), + // Slide17Animations(), Slides.title('Sample Slides'), SlideSampleImage(), SlideSampleCode(), diff --git a/packages/slides/lib/slides/slide_17_animations.dart b/packages/slides/lib/slides/slide_17_animations.dart new file mode 100644 index 0000000..6b96afa --- /dev/null +++ b/packages/slides/lib/slides/slide_17_animations.dart @@ -0,0 +1,162 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; +import 'package:flame/text.dart'; +import 'package:flame_3d/camera.dart'; +import 'package:flame_3d/components.dart'; +import 'package:flame_3d/model.dart'; +import 'package:flame_3d/parser.dart'; +import 'package:flame_3d_workshop_slides/slides/components/slide.dart'; +import 'package:flame_3d_workshop_slides/slides/components/slide_game.dart'; +import 'package:flame_3d_workshop_slides/slides/components/slide_text.dart'; +import 'package:flutter/material.dart' hide Matrix4, Material; + +class Slide17Animations extends Slide { + Slide17Animations({super.key}) : super(route: '/slide_17_animations'); + + @override + List children(BuildContext context) { + return [ + SlideText.title('Skeletal Animations'), + const SlideGame(game: _SkeletalAnimations.new), + ]; + } +} + +class _SkeletalAnimations extends FlameGame { + late ModelComponent skeleton; + + _SkeletalAnimations() + : super( + world: World3D(clearColor: const Color(0xFF000000)), + camera: CameraComponent3D( + position: Vector3.all(4), + target: Vector3.zero(), + ), + ); + + @override + FutureOr onLoad() async { + skeleton = ModelComponent( + model: await ModelParser.parse('models/skeleton.glb'), + ); + await world.addAll([ + LightComponent.ambient( + intensity: 10.0, + ), + _RotatingLight(), + skeleton, + ]); + camera.viewport.add(_AnimationsBox(skeleton)); + return super.onLoad(); + } +} + +class _AnimationsBox extends Component { + _AnimationsBox(this.skeleton); + + int startIndex = 0; + static const int displayCount = 20; + final ModelComponent skeleton; + + @override + FutureOr onLoad() async { + await _build(); + } + + Future _build() async { + final animations = skeleton.animationNames + .skip(startIndex) + .take(displayCount) + .indexed; + await add( + ColumnComponent( + position: Vector2.all(16.0), + gap: 4.0, + children: [ + for (final (index, animation) in animations) + _ClickableBox('<${startIndex + index}>: $animation', () { + skeleton.playAnimationByName(animation); + }), + RowComponent( + gap: 8.0, + children: [ + _ClickableBox('Top', () => set(0)), + _ClickableBox('Up', () => move(-1)), + _ClickableBox('Down', () => move(1)), + _ClickableBox( + 'Bottom', + () => set(skeleton.animationNames.length), + ), + ], + ), + ], + ), + ); + return super.onLoad(); + } + + Future _rebuild() async { + removeAll(children); + await _build(); + } + + void set(int index) { + startIndex = index.clamp( + 0, + skeleton.animationNames.length - displayCount, + ); + _rebuild(); + } + + void move(int delta) { + set(startIndex + delta); + } +} + +class _ClickableBox extends TextComponent with TapCallbacks { + static final TextRenderer _style = TextPaint( + style: const TextStyle( + color: Colors.white, + fontSize: 8.0, + ), + ); + + _ClickableBox(String text, this.onTap) + : super( + text: text, + textRenderer: _style, + ); + + final Function() onTap; + + @override + void onTapDown(TapDownEvent event) { + onTap(); + super.onTapDown(event); + } +} + +class _RotatingLight extends LightComponent { + double _angle = 0.0; + _RotatingLight() + : super.point( + intensity: 100.0, + position: Vector3(0, _height, 0), + ); + + @override + void update(double dt) { + _angle = (_angle + dt) % tau; + position.setValues(_radius * sin(_angle), _height, _radius * cos(_angle)); + super.update(dt); + } + + static const _radius = 3.0; + static const _height = 0.75; +} diff --git a/packages/slides/lib/slides/slides.dart b/packages/slides/lib/slides/slides.dart index 6d8e6e9..634d1e4 100644 --- a/packages/slides/lib/slides/slides.dart +++ b/packages/slides/lib/slides/slides.dart @@ -15,6 +15,7 @@ export 'slide_13_the_camera.dart'; export 'slide_14_birth_the_camera.dart'; export 'slide_15_camera_types.dart'; export 'slide_16_models.dart'; +export 'slide_17_animations.dart'; export 'slide_sample_code.dart'; export 'slide_sample_game.dart'; export 'slide_sample_image.dart';