【Flutter】SpriteWidgetでFlutterロゴを描いてアニメーションさせてみた

5 min read読了の目安(約4600字

https://twitter.com/7oh_2020/status/1388206387667234817

SpriteWidgetとは

SpriteWidgetはアニメーションや2Dゲームを作成するためのパッケージです。
動くアイコンからゲームまで何でも作れるようです。ドキュメントはこちら

インストール

pubspec.yamlに以下のような依存関係を追記します。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  spritewidget: ^0.9.24

スプライトの作成

スプライトはNodeWithSizeクラスのサブクラスです。
通常は画像からスプライトを作成しますが、paint()メソッドをオーバーライドすることでコードだけで図形を描画することができます。

Flutterロゴをよく観察すると、よく似た3つの図形から構成されていることがわかります。
なので、太さ・色・向きをコンストラクタで受け取るスプライトクラスを作成します。

lib/sprites/flat_node.dart
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:spritewidget/spritewidget.dart';

class FlatNode extends NodeWithSize {
  FlatNode({
    Size size = Size.zero,
     Color color,
     double length,
    bool reverse = false,
  })  : this.color = color,
        this.length = length,
        this.reverse = reverse,
        super(size);
  final Color color;
  final double length;
  final bool reverse;

  
  void paint(Canvas canvas) {
    // パスの作成
    final path = Path();
    path.moveTo(length * 2, 0);
    if (reverse) {
      path.lineTo(length * 3, -length);
      path.lineTo(0, -length);
      path.lineTo(0, 0);
    } else {
      path.lineTo(length * 3, length);
      path.lineTo(0, length);
      path.lineTo(0, 0);
    }

    // 図形の描画
    canvas.drawPath(
      path,
      Paint()..color = color,
    );
  }
}

スプライトをグループ化

スプライトはaddChild()メソッドで入れ子構造にできます。
子となるスプライトはaddChild()した順番、または各zPositionプロパティの降順で描画されます。

lib/sprites/base_node.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:spritewidget/spritewidget.dart';


import 'flat_node.dart';

class BaseNode extends NodeWithSize {
  BaseNode(Size size) : super(size) {
    nodes.forEach((node) {
      // 子スプライトを追加
      this.addChild(node);
    });
  }

  final nodes = <FlatNode>[
    FlatNode(
      color: Colors.blue,
      length: 50,
    )..position = Offset(0, 0),
    FlatNode(
      color: Colors.blue,
      length: 40,
    )..position = Offset(0, 70.0),
    FlatNode(
      color: Colors.blue[700],
      length: 45,
      reverse: true,
    )
      ..position = Offset(0, 70)
      ..rotation = 90.0
      ..zPosition = -1.0,
  ];
}

モーションの追加

スプライトのmotionsプロパティのrun()メソッドにモーションを渡すことでスプライトをアニメーションさせることができます。

以下のコードではMotionTweenクラスでなめらかに変化するモーションをいくつか定義し、MotionSequenceクラスで順序を定義し、最後にMotionRepeatForeverクラスでラップすることで無限ループさせています。

lib/sprites/root_node.dart
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:spritewidget/spritewidget.dart';
import 'package:spritewidget_example/sprites/base_node.dart';

class RootNode extends NodeWithSize {
  RootNode(Size size) : super(size) {
    final move1 = MotionTween<Offset>(
      (animation) => baseNode.position = animation,
      Offset(size.width / 2, size.height),
      Offset(size.width / 2, size.height / 3),
      1.0,
    );
    final move2 = MotionTween<Offset>(
      (animation) => baseNode.position = animation,
      Offset(size.width / 2, size.height / 3),
      Offset(size.width / 2, size.height),
      0.75,
    );
    final rotate1 = MotionTween<double>(
      (animation) => baseNode.rotation = animation,
      0,
      -45.0,
      1.0,
    );
    final rotate2 = MotionTween<double>(
      (animation) => baseNode.rotation = animation,
      -45.0,
      0,
      0.75,
    );
    final delay = MotionDelay(2.0);

    final sequence = MotionSequence([
      move1,
      rotate1,
      delay,
      rotate2,
      move2,
    ]);

    // スプライトにモーションを追加
    baseNode.motions.run(MotionRepeatForever(sequence));

    // 子スプライトを追加
    this.addChild(baseNode);
  }
  final baseNode = BaseNode(Size.zero);
}

Nodeをウィジェットに表示

そして最後にスプライトをウィジェットとして表示します。
方法はとても簡単で、SpriteWidgetクラスにルートとなる単一のスプライトを渡すだけです。

lib/pages/main_page.dart
import 'package:flutter/material.dart';
import 'package:spritewidget/spritewidget.dart';
import 'package:spritewidget_example/sprites/root_node.dart';

class MainPage extends StatefulWidget {
  
  _MainPageState createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;

    return Scaffold(
      body: SafeArea(
        child: SpriteWidget(
          RootNode(size),
        ),
      ),
    );
  }
}

おわりに

SpriteWidgetはとても自由度の高いパッケージで、手軽に何でも描画できます。
ボタンのタップ時にエフェクトを表示したり、背景に雨を降らせたり、テトリスやオセロなどの2Dゲームを作ったりと表現の幅が広がりそうです。

今回のサンプルコードの全体は私のGithubからも見れます。

https://github.com/7oh2020/spritewidget_example

以上です。