When I rotate the cube, 2 Sides of cubes are opaque while others are transparent.
Output:
Code:
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: 'Cube',
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const Cube(),
);
}
}
class Cube extends StatefulWidget {
const Cube({Key? key}) : super(key: key);
@override
_CubeState createState() => _CubeState();
}
class _CubeState extends State<Cube> {
Offset offset = Offset.zero;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Cube"),
backgroundColor: Colors.green,
),
body: GestureDetector(
onPanUpdate: (details) {
print(details);
setState(() {
offset += details.delta;
});
},
child: Center(
child: Transform(
transform: Matrix4.identity()
..rotateX(-offset.dy * pi / 180)
..rotateY(-offset.dx * pi / 180),
alignment: Alignment.center,
child: Stack(
children: [
Transform(
transform: Matrix4.identity()..translate(0.0, 0.0, -100),
alignment: Alignment.center,
child: Container(
color: Colors.amber,
child: const FlutterLogo(
size: 200,
),
)),
Transform(
transform: Matrix4.identity()
..translate(-100.0, 0.0, 0.0)
..rotateY(-pi / 2),
alignment: Alignment.center,
child: Container(
color: Colors.red,
child: const FlutterLogo(
size: 200,
),
)),
Transform(
transform: Matrix4.identity()..translate(0.0, 0.0, 100.0),
child: Container(
color: Colors.blue,
child: const FlutterLogo(
size: 200,
),
)),
Transform(
transform: Matrix4.identity()
..translate(100.0, 0.0, 0.0)
..rotateY(-pi / 2),
alignment: Alignment.center,
child: Container(
color: Colors.purple,
child: const FlutterLogo(
size: 200,
),
)),
],
),
),
),
));
}
}
I used the code from a tutorial from Youtube: https://www.youtube.com/watch?v=hDmWOsOU_Ko
The Flutter will always render the children of the Stack in order, what means that the last widget of the Stack will be in front of every other widget, and that is why the purple side (the last child of the Stack) is always visible regardless of the movement you do.
To simulate a cube you will have to change the order of the cube's sides accordingly to the rotation of the cube. To change the order, you will have to store all the sides in an array and pass this array as the argument of the Stack:
class _CubeState extends State<Cube> {
List<Widget> sides = [];
final side1 = Transform(
key: const ValueKey("side1"), // you must use a key to allow flutter to know that a widget changed its order
transform: Matrix4.identity()..translate(0.0, 0.0, -100),
alignment: Alignment.center,
child: Container(
color: Colors.amber,
child: const FlutterLogo(size: 200),
),
);
//... the other sides follow the same logic of the side1
@override
void initState() {
super.initState();
sides = [side1, side2, side3, side4];
}
void update(Offset value) {
setState(() {
// I recommend to normalize the offset to be always between [0, 360)
offset = Offset((offset.dx + value.dx) % 360, (offset.dy + value.dy) % 360);
sides = [side1, side3, ...] // change the order of the sides here accordingly with the new offset value. This is the hardest part. For a cube where you want to render all the 6 sides, you will have to perform a lot of calculations to know which side is in front of which.
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// The rendered cube is inside the `Center` widget below
Center(
child: Transform(
key: const ValueKey('cube'),
transform: Matrix4.identity()
..rotateX(-offset.dy * pi / 180)
..rotateY(-offset.dx * pi / 180),
alignment: Alignment.center,
child: Stack(
children: sides,
),
),
),
// The gesture recognizer that will receive the movement of the mouse is inside the `Positioned.fill`
Positioned.fill(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: GestureDetector(
onPanUpdate: (details) {
update(details.delta);
},
child: Container(color: Colors.transparent),
),
),
Text('x: ${offset.dx} y: ${offset.dy}')
],
),
),
],
),
);
}
Note that you need to pass an unique key to every widget in the Stack to make it possible to Flutter identify uniquely every widget (you can see more about Keys in https://www.youtube.com/watch?v=kn0EOS-ZiIc).
I made a simple example below with a cube of 4 sides. To do the same in a cube of 6 sides will require more math, but I think I gave you enough knowledge to you figure it out that by yourself.
https://dartpad.dev/?id=3698b3013fe7189d3c3c063d088cf69a
If the GestureRecognizer
has the transformed cube as its child, the movement of the mouse will capture the original cube's side that was being show at the beggining (the blue side in my example), thus depending on how the cube is rotated, if the blue side is not visible anymore, the mouse will not be captured anymore. To solve that we have to split the mouse capture logic of the cube's rendering.
There are many possible ways, one of those is to render a Stack
(to make one child be in front of another child, creating multiple layers). The underlayer will be used to render the cube and the overlayer will be used to capture the mouse movement (this layer needs to be transparent to don't affect the visualization of the cube).
I did that with the cobe below:
Stack(
children: [
Center( // This is the underlayer that renders the cube. It will be always in the center of the screen.
child: Transform(
key: const ValueKey('cube'),
transform: Matrix4.identity()
..rotateX(-offset.dy * pi / 180)
..rotateY(-offset.dx * pi / 180),
alignment: Alignment.center,
child: Stack(
children: sides,
),
),
),
Positioned.fill( // This is the overlayer that receives the mouse/finger movement. It has to be a `Positioned.fill` if you want that all the available space can be used to rotate the cube.
child: GestureDetector(
onPanUpdate: (details) {
update(details.delta);
},
child: Container(color: Colors.transparent), // We need any widget that actually renders pixels on the screen and that can have a size. If not, the `GestureRecognizer` would have size zero and would not work. The colors is transparent to allow the cube (that it is in the underlayer) to be visible.
),
),
],
)