Skip to content

Transform Widget takes a 4x4 matrix but does not store the full 4x4 matrix #82961

@flar

Description

@flar

(This is similar to another issue that happens with the Canvas.transform() method filed under #82955)

The following example demonstrates a problem with the implementation of the Transform widget where it accepts 4x4 matrices in its transform property, but it does not compose the matrix appropriately if there are multiple nested Transform widgets that all use 4x4 matrices.

(Note that when there are no children that require compositing, this issue would be another example of #82955 but a RepaintBoundary is used here to force the Transform to use layers to implement the nested transforms so that this example is testing the Transform layers, not the Canvas transform method.)

This limitation is due to relying on the SkMatrix to maintain the matrix storage when SkMatrix is limited to a 3x3 matrix. As a result, the Transform Layer implementation will reduce all 4x4 matrices down to 3x3 matrices in the constructor. That may work OK if there is only ever a single layer with a 4x4 matrix, but it does not work if there are multiple such layers.

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 applied using several nested Transform widgets each incrementally applying a single element of the transform and the result fails to render correctly. In the bottom example, all transform operations are composed in a Matrix4 object and then only added to the Widget hierarchy in a single Transform widget/layer and it works.

Tested on:

  • iOS, Android, MacOS - fails
  • web with canvaskit and HTML renderer - fails
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;
  double _rotateAroundX = 0.0;
  double _rotateAroundY = 0.0;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this, duration: const Duration(seconds: 3));
    _controller.addListener(() { setState(() {
      final double angle = _controller.value * 2 * pi;
      _rotateAroundX = sin(angle) * pi / 4.0;
      _rotateAroundY = cos(angle) * pi / 4.0;
    }); });
    _controller.repeat();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  Widget transformIncrementally(Widget child) {
    child = RepaintBoundary(child: child);
    child = Transform(
      transform: Matrix4.rotationY(_rotateAroundY),
      child: child,
    );
    child = Transform(
      transform: Matrix4.rotationX(_rotateAroundX),
      child: child,
    );
    child = Transform(
      alignment: Alignment.center,
      transform: Matrix4.identity()..setEntry(3, 2, 0.001),
      child: child,
    );
    return child;
  }

  Widget transformAtOnce(Widget child) {
    return Transform(
      alignment: Alignment.center,
      transform: Matrix4.identity()
          ..setEntry(3, 2, 0.001)
          ..rotateX(_rotateAroundX)
          ..rotateY(_rotateAroundY),
      child: RepaintBoundary(child: child),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
            transformIncrementally(CustomPaint(
              painter: MyPainter(Colors.red),
              child: const SizedBox(width: 150, height: 150),
            )),
            transformAtOnce(CustomPaint(
              painter: MyPainter(Colors.green),
              child: const SizedBox(width: 150, height: 150),
            )),
          ],
        ),
      ),
    );
  }
}

class MyPainter extends CustomPainter {
  MyPainter(this.color);

  final Color color;

  @override
  void paint(Canvas canvas, Size size) {
    canvas.translate(size.width * 0.5, size.height * 0.5);
    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;
}

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 onteam-engineOwned by Engine teamtriaged-engineTriaged by Engine team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions