Skip to content

Canvas takes a 4x4 matrix but does not store the full 4x4 matrix #82955

@flar

Description

@flar

(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);
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listengineflutter/engine related. See also e: labels.found in release: 2.2Found to occur in 2.2found in release: 2.3Found to occur in 2.3has reproducible stepsThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer version

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions