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应用架构的内容)。
使用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)。 - 然后,此动画控制器被传递给
CustomPainter的repaint参数,这基本上使其在动画控制器运行时保持重绘。 - 提供的动画控制器用于创建具有所需
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();
在应用中,当用户首次打开链接(在小屏幕设备上)时,会显示一个简单的对话框。
在后台,点击“开始”会使用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_settings、cuboids和artists。
- 立方体画布设置文档保存了最初布局的立方体预定义数量,以及您在制作立方体时可选择的颜色和填充类型。
/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),
);
开始创作吧!?
为何不通过访问此链接亲自尝试一下?请记住,您需要在具有大屏幕的设备上才能看到画布。

