// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:math' as math; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:vector_math/vector_math_64.dart'; import 'basic_types.dart'; /// Utility functions for working with matrices. class MatrixUtils { MatrixUtils._(); /// Returns the given [transform] matrix as an [Offset], if the matrix is /// nothing but a 2D translation. /// /// Otherwise, returns null. static Offset getAsTranslation(Matrix4 transform) { assert(transform != null); final Float64List values = transform.storage; // Values are stored in column-major order. if (values[0] == 1.0 && // col 1 values[1] == 0.0 && values[2] == 0.0 && values[3] == 0.0 && values[4] == 0.0 && // col 2 values[5] == 1.0 && values[6] == 0.0 && values[7] == 0.0 && values[8] == 0.0 && // col 3 values[9] == 0.0 && values[10] == 1.0 && values[11] == 0.0 && values[14] == 0.0 && // bottom of col 4 (values 12 and 13 are the x and y offsets) values[15] == 1.0) { return new Offset(values[12], values[13]); } return null; } /// Returns the given [transform] matrix as a [double] describing a uniform /// scale, if the matrix is nothing but a symmetric 2D scale transform. /// /// Otherwise, returns null. static double getAsScale(Matrix4 transform) { assert(transform != null); final Float64List values = transform.storage; // Values are stored in column-major order. if (values[1] == 0.0 && // col 1 (value 0 is the scale) values[2] == 0.0 && values[3] == 0.0 && values[4] == 0.0 && // col 2 (value 5 is the scale) values[6] == 0.0 && values[7] == 0.0 && values[8] == 0.0 && // col 3 values[9] == 0.0 && values[10] == 1.0 && values[11] == 0.0 && values[12] == 0.0 && // col 4 values[13] == 0.0 && values[14] == 0.0 && values[15] == 1.0 && values[0] == values[5]) { // uniform scale return values[0]; } return null; } /// Returns true if the given matrices are exactly equal, and false /// otherwise. Null values are assumed to be the identity matrix. static bool matrixEquals(Matrix4 a, Matrix4 b) { if (identical(a, b)) return true; assert(a != null || b != null); if (a == null) return isIdentity(b); if (b == null) return isIdentity(a); assert(a != null && b != null); return a.storage[0] == b.storage[0] && a.storage[1] == b.storage[1] && a.storage[2] == b.storage[2] && a.storage[3] == b.storage[3] && a.storage[4] == b.storage[4] && a.storage[5] == b.storage[5] && a.storage[6] == b.storage[6] && a.storage[7] == b.storage[7] && a.storage[8] == b.storage[8] && a.storage[9] == b.storage[9] && a.storage[10] == b.storage[10] && a.storage[11] == b.storage[11] && a.storage[12] == b.storage[12] && a.storage[13] == b.storage[13] && a.storage[14] == b.storage[14] && a.storage[15] == b.storage[15]; } /// Whether the given matrix is the identity matrix. static bool isIdentity(Matrix4 a) { assert(a != null); return a.storage[0] == 1.0 // col 1 && a.storage[1] == 0.0 && a.storage[2] == 0.0 && a.storage[3] == 0.0 && a.storage[4] == 0.0 // col 2 && a.storage[5] == 1.0 && a.storage[6] == 0.0 && a.storage[7] == 0.0 && a.storage[8] == 0.0 // col 3 && a.storage[9] == 0.0 && a.storage[10] == 1.0 && a.storage[11] == 0.0 && a.storage[12] == 0.0 // col 4 && a.storage[13] == 0.0 && a.storage[14] == 0.0 && a.storage[15] == 1.0; } /// Applies the given matrix as a perspective transform to the given point. /// /// This function assumes the given point has a z-coordinate of 0.0. The /// z-coordinate of the result is ignored. static Offset transformPoint(Matrix4 transform, Offset point) { final Vector3 position3 = new Vector3(point.dx, point.dy, 0.0); final Vector3 transformed3 = transform.perspectiveTransform(position3); return new Offset(transformed3.x, transformed3.y); } /// Returns a rect that bounds the result of applying the given matrix as a /// perspective transform to the given rect. /// /// This function assumes the given rect is in the plane with z equals 0.0. /// The transformed rect is then projected back into the plane with z equals /// 0.0 before computing its bounding rect. static Rect transformRect(Matrix4 transform, Rect rect) { final Offset point1 = transformPoint(transform, rect.topLeft); final Offset point2 = transformPoint(transform, rect.topRight); final Offset point3 = transformPoint(transform, rect.bottomLeft); final Offset point4 = transformPoint(transform, rect.bottomRight); return new Rect.fromLTRB( _min4(point1.dx, point2.dx, point3.dx, point4.dx), _min4(point1.dy, point2.dy, point3.dy, point4.dy), _max4(point1.dx, point2.dx, point3.dx, point4.dx), _max4(point1.dy, point2.dy, point3.dy, point4.dy) ); } static double _min4(double a, double b, double c, double d) { return math.min(a, math.min(b, math.min(c, d))); } static double _max4(double a, double b, double c, double d) { return math.max(a, math.max(b, math.max(c, d))); } /// Returns a rect that bounds the result of applying the inverse of the given /// matrix as a perspective transform to the given rect. /// /// This function assumes the given rect is in the plane with z equals 0.0. /// The transformed rect is then projected back into the plane with z equals /// 0.0 before computing its bounding rect. static Rect inverseTransformRect(Matrix4 transform, Rect rect) { assert(rect != null); assert(transform.determinant != 0.0); if (isIdentity(transform)) return rect; transform = new Matrix4.copy(transform)..invert(); return transformRect(transform, rect); } /// Create a transformation matrix which mimics the effects of tangentially /// wrapping the plane on which this transform is applied around a cylinder /// and then looking at the cylinder from a point outside the cylinder. /// /// The `radius` simulates the radius of the cylinder the plane is being /// wrapped onto. If the transformation is applied to a 0-dimensional dot /// instead of a plane, the dot would simply translate by +/- `radius` pixels /// along the `orientation` [Axis] when rotating from 0 to +/- 90 degrees. /// /// A positive radius means the object is closest at 0 `angle` and a negative /// radius means the object is closest at π `angle` or 180 degrees. /// /// The `angle` argument is the difference in angle in radians between the /// object and the viewing point. A positive `angle` on a positive `radius` /// moves the object up when `orientation` is vertical and right when /// horizontal. /// /// The transformation is always done such that a 0 `angle` keeps the /// transformed object at exactly the same size as before regardless of /// `radius` and `perspective` when `radius` is positive. /// /// The `perspective` argument is a number between 0 and 1 where 0 means /// looking at the object from infinitely far with an infinitely narrow field /// of view and 1 means looking at the object from infinitely close with an /// infinitely wide field of view. Defaults to a sane but arbitrary 0.001. /// /// The `orientation` is the direction of the rotation axis. /// /// Because the viewing position is a point, it's never possible to see the /// outer side of the cylinder at or past +/- π / 2 or 90 degrees and it's /// almost always possible to end up seeing the inner side of the cylinder /// or the back side of the transformed plane before π / 2 when perspective > 0. static Matrix4 createCylindricalProjectionTransform({ @required double radius, @required double angle, double perspective = 0.001, Axis orientation = Axis.vertical, }) { assert(radius != null); assert(angle != null); assert(perspective >= 0 && perspective <= 1.0); assert(orientation != null); // Pre-multiplied matrix of a projection matrix and a view matrix. // // Projection matrix is a simplified perspective matrix // http://web.iitd.ac.in/~hegde/cad/lecture/L9_persproj.pdf // in the form of // [[1.0, 0.0, 0.0, 0.0], // [0.0, 1.0, 0.0, 0.0], // [0.0, 0.0, 1.0, 0.0], // [0.0, 0.0, -perspective, 1.0]] // // View matrix is a simplified camera view matrix. // Basically re-scales to keep object at original size at angle = 0 at // any radius in the form of // [[1.0, 0.0, 0.0, 0.0], // [0.0, 1.0, 0.0, 0.0], // [0.0, 0.0, 1.0, -radius], // [0.0, 0.0, 0.0, 1.0]] Matrix4 result = new Matrix4.identity() ..setEntry(3, 2, -perspective) ..setEntry(2, 3, -radius) ..setEntry(3, 3, perspective * radius + 1.0); // Model matrix by first translating the object from the origin of the world // by radius in the z axis and then rotating against the world. result *= ( orientation == Axis.horizontal ? new Matrix4.rotationY(angle) : new Matrix4.rotationX(angle) ) * new Matrix4.translationValues(0.0, 0.0, radius); // Essentially perspective * view * model. return result; } } /// Returns a list of strings representing the given transform in a format /// useful for [TransformProperty]. /// /// If the argument is null, returns a list with the single string "null". List<String> debugDescribeTransform(Matrix4 transform) { if (transform == null) return const <String>['null']; final List<String> matrix = transform.toString().split('\n').toList(); matrix.removeLast(); return matrix; } /// Property which handles [Matrix4] that represent transforms. class TransformProperty extends DiagnosticsProperty<Matrix4> { /// Create a diagnostics property for [Matrix4] objects. /// /// The [showName] and [level] arguments must not be null. TransformProperty(String name, Matrix4 value, { bool showName = true, Object defaultValue = kNoDefaultValue, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(showName != null), assert(level != null), super( name, value, showName: showName, defaultValue: defaultValue, level: level, ); @override String valueToString({ TextTreeConfiguration parentConfiguration }) { if (parentConfiguration != null && !parentConfiguration.lineBreakProperties) { // Format the value on a single line to be compatible with the parent's // style. final List<Vector4> rows = <Vector4>[ value.getRow(0), value.getRow(1), value.getRow(2), value.getRow(3), ]; return '[${rows.join("; ")}]'; } return debugDescribeTransform(value).join('\n'); } }