Flutter: Recalculate Matrix4 X/Y rotation when resize parent

I'm using a Matrix4 to simulate a wall in 3D. I'm having trouble recalculating the X and Y rotation of the matrix after the object is resized.

With the flat object, the resizing works according to the resizing of the parent.

When the object is rotated, the matrix loses perspective.

Does anyone have any tips on how to find the correct calculation?

Below are some examples, as well as a small code snippet that can reproduce the problem in Dartpad.

Example of plain wall:

Initial size 500x375

Resized to 600x450

Example of rotated wall:

Initial size 500x375

Resized to 600x450

Code (runs on dartpad):

import 'package:flutter/material.dart';
import 'dart:math';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: Colors.blue,
      ),
      home: const MyHomePage(title: 'Matrix4 Resizing'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({
    super.key,
    required this.title,
  });

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String exampleImage =
      'https://firebasestorage.googleapis.com/v0/b/carlens-375f2.appspot.com/o/users%2FZvRl4WQMWqa9zsf0UlquGDFfRhE3%2Fuploads%2F1729225149799000.png?alt=media&token=e363445a-6a3c-40ce-a0a0-06f9fbeeebb4';
  double _w = 500; //initial width
  double _h = 375; //initial height .75 from width
  Rect _wallRect = Rect.fromLTRB(0, 48, 1090, 215); //initial wall position

  Offset _wallOffset = Offset(110, 0.5); //resize doesn't work with rotation
  //Offset _wallOffset = Offset.zero; //resize works with zero rotation

  void _resize(double val) {
    double oldW = _w;
    double newW = oldW + val;
    double scale = newW / oldW;
    Rect oldWallRect = _wallRect;

    setState(() {
      _w = newW;
      _h = _w * .75; //keep 4:3 ratio

      _wallRect = Rect.fromLTRB(
        oldWallRect.left,
        oldWallRect.top * scale,
        oldWallRect.right * scale,
        oldWallRect.bottom * scale,
      );
    });

    //rotation calc not working
    double wx = _wallRect.width / oldWallRect.width;
    double wy = _wallRect.height / oldWallRect.height;
    var rotateX = Offset(cos(0) - 1, sin(0)) * wx / 2;
    var rotateY = Offset(-sin(0), cos(0) - 1) * wy / 2;

    setState(() {
      _wallOffset += Offset(rotateX.distance, rotateY.distance);
    });
    //rotation calc not working
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Expanded(
              child: Row(
                mainAxisSize: MainAxisSize.max,
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  //container
                  Container(
                    decoration: BoxDecoration(
                      border: Border.all(
                        color: Colors.red,
                        width: 0,
                      ),
                    ),
                    width: _w,
                    height: _h,
                    child: Stack(
                      alignment: const AlignmentDirectional(0, 0),
                      children: [
                        //bgImage
                        Image.network(
                          width: double.infinity,
                          height: double.infinity,
                          exampleImage,
                          fit: BoxFit.fitWidth,
                        ),
                        // wall
                        Positioned.fromRect(
                          rect: _wallRect,
                          child: Container(
                            transform: Matrix4.identity()
                              ..setEntry(3, 2, 0.0008)
                              ..rotateX(0.01 * _wallOffset.dy)
                              ..rotateY(-0.01 * _wallOffset.dx),
                            transformAlignment: FractionalOffset.centerLeft,
                            decoration: BoxDecoration(
                              border: Border.all(
                                color: Colors.yellow,
                                width: 1,
                              ),
                              color: Colors.yellow.withOpacity(0.5),
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            Padding(
              padding: const EdgeInsetsDirectional.fromSTEB(0, 0, 0, 50),
              child: Row(
                mainAxisSize: MainAxisSize.max,
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  TextButton(
                    style: ButtonStyle(
                      foregroundColor:
                          WidgetStateProperty.all<Color>(Colors.blue),
                      overlayColor: WidgetStateProperty.resolveWith<Color?>(
                        (Set<WidgetState> states) {
                          if (states.contains(WidgetState.hovered)) {
                            return Colors.blue.withOpacity(0.04);
                          }
                          if (states.contains(WidgetState.focused) ||
                              states.contains(WidgetState.pressed)) {
                            return Colors.blue.withOpacity(0.12);
                          }
                          return null;
                        },
                      ),
                    ),
                    onPressed: () => _resize(-100),
                    child: Text('<<< Decrement Size'),
                  ),
                  TextButton(
                    style: ButtonStyle(
                      foregroundColor:
                          WidgetStateProperty.all<Color>(Colors.blue),
                      overlayColor: WidgetStateProperty.resolveWith<Color?>(
                        (Set<WidgetState> states) {
                          if (states.contains(WidgetState.hovered)) {
                            return Colors.blue.withOpacity(0.04);
                          }
                          if (states.contains(WidgetState.focused) ||
                              states.contains(WidgetState.pressed)) {
                            return Colors.blue.withOpacity(0.12);
                          }
                          return null;
                        },
                      ),
                    ),
                    onPressed: () => _resize(100),
                    child: Text('Increment Size >>>'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

I can resize the wall using the parent's resizing scale. I haven't found the correct calculation for rotation yet.

Upvotes: 0

Views: 19

Answers (0)

Related Questions