-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
(This is similar to another issue that happens with the Transform widget filed under #82961)
The following example demonstrates a problem with the implementation of Canvas where it accepts 4x4 matrices in its transform() method, but it does not compose the matrix appropriately if there are multiple calls that use 4x4 matrices.
This limitation is due to relying on the SkCanvas to maintain the matrix storage when SkCanvas is limited to a 3x3 matrix. As a result, the Canvas implementation will reduce all 4x4 matrices down to 3x3 matrices in the transform() method. That may work OK if there is only ever a single call to transform with a 4x4 matrix, but it does not work if there are multiple such calls.
In this example, a 3D "wobble" effect is rendered by rotating a matrix around the X and Y axes. In the upper example, these operations are composed into the Canvas transform one operation at a time incrementally and the result fails to render correctly. In the bottom example, the two 3D rotations are composed in a Matrix4 object and then only added to the Canvas in a single transform() call and it works.
Tested on:
- iOS, Android, MacOS - fails
- web with canvaskit - fails
- web with HTML renderer - succeeds
Flutter 2.3.0-13.0.pre.46 • channel unknown • unknown source
Framework • revision f54dc5bf95 (2 weeks ago) • 2021-05-05 00:20:19 -0700
Engine • revision 0a372173ff
Tools • Dart 2.14.0 (build 2.14.0-119.0.dev)
Details
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: '3D Transform Compositing Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin<MyHomePage> {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: const Duration(seconds: 3));
_controller.addListener(() { setState(() {}); });
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CustomPaint(
painter: My3DIncrementalPainter(_controller.value),
child: const SizedBox(width: 200, height: 200),
),
CustomPaint(
painter: My3DCombinedPainter(_controller.value),
child: const SizedBox(width: 200, height: 200),
),
],
),
),
);
}
}
abstract class My3DPainter extends CustomPainter {
My3DPainter(this.rotation, this.color);
final double rotation;
final Color color;
void rotate(Canvas canvas, double rotateX, double rotateY);
@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width * 0.5, size.height * 0.5);
rotate(canvas, sin(rotation * 2 * pi) * pi / 4.0, cos(rotation * 2 * pi) * pi / 4.0);
canvas.drawRect(const Rect.fromLTRB(-50, -50, 50, 50), Paint()..color = color);
canvas.drawRect(const Rect.fromLTRB(-30, -30, -20, 30), Paint()..color = Colors.black);
canvas.drawRect(const Rect.fromLTRB(-30, -30, 30, -20), Paint()..color = Colors.black);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
class My3DIncrementalPainter extends My3DPainter {
My3DIncrementalPainter(double rotation) : super(rotation, Colors.red) ;
@override
void rotate(Canvas canvas, double rotateX, double rotateY) {
Matrix4 matrix = Matrix4.identity();
matrix.setEntry(3, 2, 0.001);
canvas.transform(matrix.storage);
matrix.setRotationX(sin(rotation * 2 * pi) * pi / 4.0);
canvas.transform(matrix.storage);
matrix.setRotationY(cos(rotation * 2 * pi) * pi / 4.0);
canvas.transform(matrix.storage);
}
}
class My3DCombinedPainter extends My3DPainter {
My3DCombinedPainter(double rotation) : super(rotation, Colors.green);
@override
void rotate(Canvas canvas, double rotateX, double rotateY) {
Matrix4 matrix = Matrix4.identity();
matrix.setEntry(3, 2, 0.001);
matrix.rotateX(sin(rotation * 2 * pi) * pi / 4.0);
matrix.rotateY(cos(rotation * 2 * pi) * pi / 4.0);
canvas.transform(matrix.storage);
}
}