GenArtCanvas

使用 Flutter & Firebase 构建的协作式生成艺术画布应用程序。它是为了一个名为“使用 Flutter & Firebase 进行实时动画生成艺术”的演讲而构建的,该演讲于2023年Flutter Firebase Festival上进行。

应用内

最初,应用程序加载到一个由灰度颜色的动画立方体形状组成的空画布。

initial-animation.mp4

通过应用程序中一个简单的UI允许用户添加自己的立方体后,画布可能看起来像这样:(这些是Flutter Firebase Festival讲座中观众输入的立方体)

audience-creation-0.mp4

创建立方体的UI看起来是这样的

cuboid-form.mp4

? 通过这个演示链接,您可以在大屏幕设备上查看画布,或者在小屏幕设备上访问来查看立方体创建UI

应用架构和文件夹结构

使用Flutter进行绘制(当然!),Riverpod进行依赖注入和状态管理,以及Firebase作为后端,该应用的架构和文件夹结构深受Andrea Bizzotto“使用Flutter和Firebase的入门架构”存储库的启发(?您可以查看Andrea的文章,以阅读更多关于使用Riverpod的Flutter应用架构的内容)。

image

使用Flutter的CustomPainter绘制画布

对于每个立方体形状,每个面都使用Rect.fromLTWH()绘制成一个规则的矩形,然后使用画布变换将其面变换成所需的形状,以获得等距(3D式)外观,具体取决于其方向。

  • 顶面在Y轴上缩放0.5倍,旋转45度以获得等距外观。
  • 左面在Y轴上倾斜0.5倍,并在X轴上缩小,以匹配顶面并获得等距外观。
  • 右面在Y轴上倾斜-0.5倍,并在X轴上缩小,以匹配顶面并获得等距外观。
  • 执行额外的翻译以确保所有立方体面的完美对齐。
  • canvas.save()canvas.restore()方法用于在执行画布变换之前保存它,以便在变换下一个立方体面的变换之前可以恢复画布。
  • 对于每个面,在恢复画布之前,可以根据来自数据库的立方体数据进行额外的绘制,这就是用户(即艺术家)在表单中选择的配置如何绘制到最终画布上的。

// Inside the `CustomPainter`'s `paint()` method
final topFacePath = Path()..addRect(Rect.fromLTWH(0, 0, side, side));

// Paint top face
canvas.save()
canvas.translate(diagonal / 2, 0)
canvas.scale(1, yScale)
canvas.rotate(45 * pi / 180)
canvas.drawPath(topFacePath, topFacePaint);
// ⚠️ User- (a.k.a Artists-) crafted paths for the top face
canvas.restore();

// Paint left face
final leftFacePath = Path()..addRect(Rect.fromLTWH(0, 0, side, size.height));

canvas.save()
canvas.translate(0, diagonal / 2 * yScale)
canvas.skew(0, yScale)
canvas.scale(skewedScaleX, 1)
canvas.drawPath(leftFacePath, leftFacePaint);
// ⚠️ User- (a.k.a Artists-) crafted paths for the left face
canvas.restore();

// Paint right face
final rightFacePath = Path()..addRect(Rect.fromLTWH(0, 0, side, size.height));

canvas.save()
canvas.translate(diagonal / 2, diagonal * yScale)
canvas.skew(0, -yScale)
canvas.scale(skewedScaleX, 1)
canvas.drawPath(rightFacePath, rightFacePaint);
// ⚠️ User- (a.k.a Artists-) crafted paths for the right face
canvas.restore();

动画画布

  • 为了实现动画,首先生成一组随机偏移量,这些偏移量是立方体在每次迭代中将要动画到的偏移量。并且这些偏移量在动画的每次迭代中都会重新生成,以实现更多随机性。
  • 为了实际运行动画,会创建一个AnimationController,并在其initState方法中调用animationController.repeat(reverse: true)
  • 然后,此动画控制器被传递给CustomPainterrepaint参数,这基本上使其在动画控制器运行时保持重绘。
  • 提供的动画控制器用于创建具有所需Curve的自定义Animation对象,并使用Flutter内置的Offset.lerp方法在立方体的初始偏移量和其随机偏移量之间创建动画。

class CuboidsCanvasPainter extends CustomPainter {
  CuboidsCanvasPainter({
    required this.randomYOffsets,
    required AnimationController animationController,
    //...
  })  : animation = CurvedAnimation(
          parent: animationController,
          curve: Curves.easeInOut,
        ),
        super(repaint: animationController);

  late final Animation<double> animation;
  final List<double> randomYOffsets;
  //...

  @override
  void paint(Canvas canvas, Size size) {
    //...
    for (var index = 0; index < settings.cuboidsTotalCount; index++) {
      final j = index ~/ cuboidsCrossAxisCount;
      final i = index % cuboidsCrossAxisCount;

      final cuboidData = cuboids.length - 1 >= index ? cuboids[index] : null;
      final xOffset = _someMath(index);
      final yOffset = _moreMath(index);
      final beginOffset = Offset(xOffset, yOffset);
      final endOffset = Offset(beginOffset.dx, beginOffset.dy + randomYOffsets[index]);
      // ⚠️ Using the animation
      final animatedYOffset = Offset.lerp(beginOffset, endOffset, animation.value) ?? beginOffset;

      // Painting the cuboid
    }
  }

  //...
}

Firebase匿名认证

使用Firebase认证,您可以通过调用以下代码轻松地匿名登录用户:

FirebaseAuth.instance.signInAnonymously();

在应用中,当用户首次打开链接(在小屏幕设备上)时,会显示一个简单的对话框。

image

在后台,点击“开始”会使用Firebase匿名认证匿名登录用户。这是让用户访问应用程序而无需进行繁琐的注册过程的最快方法,同时允许他们使用受安全规则保护的数据库。之后,您可以为用户提供注册选项,并将他们的永久帐户与匿名登录关联,并保留您为他们存储的任何数据。

代码片段

从上面的架构可以看出,应用程序代码通过存储库访问外部世界。对于认证,AuthRepository实现了匿名登录方法。

class AuthRepository {
  AuthRepository(this._auth);

  final FirebaseAuth _auth;

  Future<User?> signInAnonymously() async {
    final credentials = await _auth.signInAnonymously();
    return credentials.user;
  }

  //...
}

Riverpod提供程序用于注入FirebaseAuth.instance并允许全局访问此存储库。

final authRepositoryProvider = Provider<AuthRepository>(
  (ref) => AuthRepository(FirebaseAuth.instance),
);

Firebase Firestore数据库

该应用程序的功能,通过观众创建的画布进行实时更新,是通过Firestore数据库实现的。为此,数据库中实现了3个集合:canvas_settingscuboidsartists

  • 立方体画布设置文档保存了最初布局的立方体预定义数量,以及您在制作立方体时可选择的颜色和填充类型。

/canvas_settings/cuboids_canvas_settings
                  ├── cuboidsTotalCount
                  ├── colors
                  ├── fillTypes
                  .
                  .
  • 立方体文档保存了应用程序需要知道的每个面绘制信息的必要信息。

/cuboids/[cuboidId]
          ├── artistId
          ├── createdAt
          ├── topFace
          │   ├── fillType
          │   ├── fillColor
          │   ├── strokeColor
          │   ├── strokeWidth
          │   └── intensity
          ├── rightFace
          │   ├── fillType
          .   .
  • Artists集合补充了匿名用户,并保存了与他们创作相关的数据。

/artists/[artistId]
          ├── createdCuboidsCount
          ├── joinedAt
          └── nickname

代码片段

执行以下查询以订阅cuboids集合的变化。它过滤掉在预定义日期之后创建的立方体,按其createdAt时间戳排序,并将立方体数量限制为设置文档中预定义的数量。

FirebaseFirestore.instance.collection('cuboids')
    .where(
      'createdAt',
      isGreaterThan: Timestamp.fromDate(
        DateTime.now().subtract(const Duration(hours: 1)),
      ),
    )
    .orderBy('createdAt', descending: true)
    .limit(settingsCuboidsTotalCount)
    .snapshots()

在应用程序内部,这通过CuboidsRepository完成。

class CuboidsRepository implements FirestoreRepository<Cuboid> {
  CuboidsRepository(this._firestore);

  final FirebaseFirestore _firestore;

  @override
  String get collectionName => 'cuboids';

  //...

  Stream<List<Cuboid>> watchCuboids({int limit = 1}) {
    return collection
        .where(
          'createdAt',
          isGreaterThan: Timestamp.fromDate(
            DateTime.now().subtract(const Duration(hours: 1)),
          ),
        )
        .orderBy('createdAt', descending: true)
        .limit(limit)
        .snapshots()
        .map((snapshot) => snapshot.docs.map((e) => e.data()).toList());
  }
}

然后,使用注入了FirebaseFirestore.instance的Riverpod提供程序进行访问。

final cuboidsRepositoryProvider = Provider<CuboidsRepository>(
  (ref) => CuboidsRepository(FirebaseFirestore.instance),
);

开始创作吧!?

为何不通过访问此链接亲自尝试一下?请记住,您需要在具有大屏幕的设备上才能看到画布。

GitHub

查看 Github