I'm currently using the matrix_gesture_detector package to scale, transform and rotate a Transform widget.
Everything works fine but to improve UX I would like to snap the widget at 90, 180, 270 or 360 degrees once the user rotates the widget close enough to said angles.
Edit: To clarify I would like the user to be able to freely rotate the widget, but snap into the nearest 90 degree rotation within whichever quadrant it is, once it gets close enough.
Hence, the solution should detect that "closeness" and then act accordingly. Please visit this link to see a GIF which shows the desired effect
How can I achieve this?
Below is the code snippet
Widget transformContainer() {
Matrix4 matrix;
GlobalKey matrixDetectorKey = GlobalKey();
return MatrixGestureDetector(
key: matrixDetectorKey,
onMatrixUpdate: (m, tm, sm, rm) {
setState(() {
matrix = MatrixGestureDetector.compose(matrix, tm, sm, rm);
});
},
child: Transform(
transform: matrix,
child: Container(
padding: EdgeInsets.all(24.0),
width: 100.0,
height: 200.0,
color: Colors.teal,
),
),
);}
For this to work, you have to check if the rotation of the matrix is closing in on a snapping point and rotate the Z-Axis to that point. I was able to write this code which works for this scenario. You may have to tune the threshold to adjust the "snappiness" to your taste.
import vector_math and use the following code:
import 'package:vector_math/vector_math_64.dart' as vec;
onMatrixUpdate: (Matrix4 m, Matrix4 tm, Matrix4 sm, Matrix4 rm) {
Matrix4 ogRm = rm.clone();
double radian = MatrixGestureDetector.decomposeToValues(m).rotation;
double degrees = vec.degrees(radian);
double delta_0 = vec.absoluteError(degrees, 0);
double delta_90 = vec.absoluteError(degrees, 90);
double delta_180 = vec.absoluteError(degrees, 180);
double delta_270 = vec.absoluteError(degrees, -90);
double threshold = 4;
if (delta_0 <= threshold) {
rm.rotateZ(vec.radians(0) - radian);
} else if (delta_90 <= threshold) {
rm.rotateZ(vec.radians(90) - radian);
} else if (delta_180 <= threshold) {
rm.rotateZ(vec.radians(180) - radian);
} else if (delta_270 <= threshold) {
rm.rotateZ(vec.radians(270) - radian);
}
// update gesture matrix
if (ogRm != rm) m = m * rm;
setState(() {
//transform your widget using this matrix
matrix = m;
});
}
Note that 360 is the same as 0, so there's no need to check for it.
MatrixGestureDetector(
onMatrixUpdate: (m, tm, sm, rm) {
setState(() {
//270 is the angle
m.rotate(m.getTranslation(), 270);
matrix4 = m;
});
},
child: Transform(
transform: matrix4,
child: Container(
padding: EdgeInsets.all(24.0),
width: 100,
height: 200,
color: Colors.teal,
),
),
),
You will have the rotate function inside the library. this code will work for snapping to 0 degrees. Basically what I am doing is if the difference in rotation and x-axis is less than 0.2 radians, I snap to 0 degrees while keeping the rotation in a separate variable. When the user moves beyond this, I add this value again and keep rotating normally.
Matrix4 _rotate(double angle, Offset focalPoint) {
double toBeRotated = 0;
var array = matrix.applyToVector3Array([0, 0, 0, 1, 0, 0]);
Offset delta = Offset(array[3] - array[0], array[4] - array[1]);
double rotation = delta.direction;
deltaAngle = deltaAngle + angle;
if ((rotation + deltaAngle).abs() > 0.2) {
toBeRotated = deltaAngle;
deltaAngle = 0;
} else if (rotation != 0 && (rotation + deltaAngle).abs() <= 0.2) {
toBeRotated = -rotation;
deltaAngle = deltaAngle + rotation;
} else {
toBeRotated = 0;
}
var c = cos(toBeRotated);
var s = sin(toBeRotated);
var dx = (1 - c) * focalPoint.dx + s * focalPoint.dy;
var dy = (1 - c) * focalPoint.dy - s * focalPoint.dx;
// ..[0] = c # x scale
// ..[1] = s # y skew
// ..[4] = -s # x skew
// ..[5] = c # y scale
// ..[10] = 1 # diagonal "one"
// ..[12] = dx # x translation
// ..[13] = dy # y translation
// ..[15] = 1 # diagonal "one"
return Matrix4(c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, dx, dy, 0, 1);
}
Related
I am trying to use bottomNavigationBar with notched effect in flutter, and of course it is fine. But when I try to add padding to BottomAppBar on the left and right to make the navigator floating-like, the notch position shifts to the right!!
This means the floating action button is in its correct spot but the notch is not exactly under it but shifted to the right. It shifts to the right exactly as the amount of padding I give to the BottomAppBar! Why!
The shape feature I used for BottomAppBar is this:
AutomaticNotchedShape(RoundedRectangleBorder(borderRadius:
BorderRadius.all(Radius.circular(35))), StadiumBorder()),
Just to be clear, I want something like this but with the notched effect on the middle (FloatingActionButtonLocation.centerDocked).
Like this but with notch in the center
Edit
Sample code to demonstrate this issue:
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(
20.0,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(30.0),
child: BottomAppBar(
notchMargin: 5.0,
shape: const CircularNotchedRectangle(),
child: Container(height: 50.0),
color: Colors.blue,
clipBehavior: Clip.hardEdge,
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
);
}
Render for sample code:
I solved this problem by creating own CustomCircularNotchedRectangle. I'm added to original custom Offset - notchOffset...
Using:
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0).copyWith(bottom: 13),
child: BottomAppBar(
shape: CustomCircularNotchedRectangle(
notchOffset: Offset(-10, 0),
),
notchMargin: 6.0,
color: widget.backgroundColor,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: items,
),
),
);
CustomCircularNotchedRectangle:
class CustomCircularNotchedRectangle extends NotchedShape {
CustomCircularNotchedRectangle({
this.notchOffset = const Offset(0, 0),
});
final Offset notchOffset;
#override
Path getOuterPath(Rect host, Rect? guest) {
if (guest == null || !host.overlaps(guest)) return Path()..addRect(host);
// The guest's shape is a circle bounded by the guest rectangle.
// So the guest's radius is half the guest width.
final double notchRadius = guest.width / 2.0;
// We build a path for the notch from 3 segments:
// Segment A - a Bezier curve from the host's top edge to segment B.
// Segment B - an arc with radius notchRadius.
// Segment C - a Bezier curve from segment B back to the host's top edge.
//
// A detailed explanation and the derivation of the formulas below is
// available at: goo.gl/Ufzrqn
const double s1 = 30.0;
const double s2 = 1.0;
final double r = notchRadius;
final double a = -1.0 * r - s2;
final double b = host.top - guest.center.dy;
final double n2 = math.sqrt(b * b * r * r * (a * a + b * b - r * r));
final double p2xA = ((a * r * r) - n2) / (a * a + b * b);
final double p2xB = ((a * r * r) + n2) / (a * a + b * b);
final double p2yA = math.sqrt(r * r - p2xA * p2xA);
final double p2yB = math.sqrt(r * r - p2xB * p2xB);
final List<Offset?> p = List<Offset?>.filled(6, null);
// p0, p1, and p2 are the control points for segment A.
p[0] = Offset(a - s1, b);
p[1] = Offset(a, b);
final double cmp = b < 0 ? -1.0 : 1.0;
p[2] = cmp * p2yA > cmp * p2yB ? Offset(p2xA, p2yA) : Offset(p2xB, p2yB);
// p3, p4, and p5 are the control points for segment B, which is a mirror
// of segment A around the y axis.
p[3] = Offset(-1.0 * p[2]!.dx, p[2]!.dy);
p[4] = Offset(-1.0 * p[1]!.dx, p[1]!.dy);
p[5] = Offset(-1.0 * p[0]!.dx, p[0]!.dy);
// translate all points back to the absolute coordinate system.
for (int i = 0; i < p.length; i += 1) {
p[i] = p[i]! + guest.center + notchOffset;
}
return Path()
..moveTo(host.left, host.top)
..lineTo(p[0]!.dx, p[0]!.dy)
..quadraticBezierTo(p[1]!.dx, p[1]!.dy, p[2]!.dx, p[2]!.dy)
..arcToPoint(
p[3]!,
radius: Radius.circular(notchRadius),
clockwise: false,
)
..quadraticBezierTo(p[4]!.dx, p[4]!.dy, p[5]!.dx, p[5]!.dy)
..lineTo(host.right, host.top)
..lineTo(host.right, host.bottom)
..lineTo(host.left, host.bottom)
..close();
}
}
Before:
After:
I faced the problem in my Flutter app that I can't draw this kind of a shape for my Slider
What I have now is:
final paint = Paint()
..color = Colors.black
..style = PaintingStyle.fill;
final rect = Rect.fromCircle(center: center, radius: thumbRadius);
final rrect = RRect.fromRectAndRadius(
Rect.fromPoints(
Offset(rect.left - 5, rect.top),
Offset(rect.right + 5, rect.bottom),
),
Radius.circular(thumbRadius + 2),
);
canvas.drawRRect(rrect, paint);
Also, it would be great to change height of all bar, because next code changes only the size after player
SliderTheme(
data: SliderThemeData(
trackHeight: 2,
thumbShape: CustomSliderPlayer(),
),
child: Slider(...)
From the comments it looks like you are not familiar with quadratic bezier curves, they are very simple, I would recommend you to start on a Javascript canvas, they are easier to test that way and logic is the same, we move to the starting point then we draw the curve, see sample snippet below
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function dobleQuad(x, y, w, h) {
ctx.beginPath();
ctx.moveTo(x - w, y);
ctx.quadraticCurveTo(x, y - h, x + w, y);
ctx.moveTo(x - w, y);
ctx.quadraticCurveTo(x, y + h, x + w, y);
ctx.fill();
}
function drawSlider(x, y) {
ctx.moveTo(0, y - 2);
ctx.fillRect(0, y - 2, canvas.width, 4);
dobleQuad(x, y, 20, 22)
}
drawSlider(50, 50)
canvas.addEventListener('mousemove', function(evt) {
ctx.clearRect(0, 0, canvas.width, canvas.height)
var rect = canvas.getBoundingClientRect();
drawSlider( evt.clientX - rect.left, 50 )
})
<canvas id="canvas"></canvas>
Just keep in mind that in JS it's quadraticCurveTo but in flutter quadraticBezierTo
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/quadraticCurveTo
void ctx.quadraticCurveTo(cpx, cpy, x, y);
cpx, cpy
The coordinates of the control point.
x, y
The coordinates of the end point.
https://api.flutter.dev/flutter/dart-ui/Path/quadraticBezierTo.html
void quadraticBezierTo(
double x1, double y1,
double x2, double y2
)
Adds a quadratic bezier segment that curves from the current point
to the given point (x2,y2), using the control point (x1,y1).
my first post here but hopefully I can explain my dilemma with building a perspective projection matrix similar to the one in OpenGL. Being new to the 3D graphics space, I'm having trouble understanding what to do after multiplying my matrix after using a perspective projection multiplication. I'm attempting to create this in Flutter but it should be a moot point as I believe my conversion is off.
Here is what I have:
var center = {
'x': size.width / 2,
'y': size.height / 2
};
List points = [];
points.add(createVector(-50, -50, -50, center));
points.add(createVector(50, -50, -50, center));
points.add(createVector(50, 50, -50, center));
points.add(createVector(-50, 50, -50, center));
points.add(createVector(-50, -50, 50, center));
points.add(createVector(50, -50, 50, center));
points.add(createVector(50, 50, 50, center));
points.add(createVector(-50, 50, 50, center));
for (int i = 0; i < points.length; i++) {
var matrix = matmul(projection, points[i]);
var w = matrix[3][0];
projected.add(
Offset(
(matrix[0][0] / w),
(matrix[1][0] / w)
)
);
}
And these are the 2 custom functions I've created:
List createVector(x, y, z, center) {
return [
[center['x'] + x],
[center['y'] + y],
[z],
[0]
];
}
List matmul(a, b) {
int colsA = a[0].length;
int rowsA = a.length;
int colsB = b[0].length;
int rowsB = b.length;
if (colsA != rowsB) {
return null;
}
List result = [];
for (int j = 0; j < rowsA; j++) {
result.add([]);
for (int i = 0; i < colsB; i++) {
double sum = 0.0;
for (int n = 0; n < colsA; n++) {
sum += a[j][n] * b[n][i];
}
result[j].add(sum);
}
}
return result;
}
My projection matrix that I'm multiplying each point with is:
var aspect = size.width / size.height;
var fov = 100;
var near = 200;
var far = 300;
List projection = [
[1 / (aspect * tan(fov / 2)), 0, 0, 0],
[0, 1 / (tan(fov / 2)), 0, 0],
[0, 0, (near + far) / (near - far), (2 * near * far) / (near - far)],
[0, 0, -1, 0]
];
I believe I am using the correct projection matrix to multiply each vector point that I have. The only thing is, after I get the result from this multiplication, I'm not entirely sure what to do with the resultant vector. I've read about the perspective divide so I am dividing the x, y and z values by the 4th values but I could be incorrect.
Any insight or help is much appreciated. Have been stumped for a long time as I have been learning this online on my own.
In OpenGL the projection matrix turns from a right handed system to a left handed system. See Right-hand rule). This is accomplished by mirroring the z axis.
The terms in the 3rd column have to be inverted (- (near+far) / (near-far) respectively - (2*near*far) / (near-far)):
List projection = [
[1 / (aspect * tan(fov/2)), 0, 0, 0],
[0, 1 / (tan(fov/2)), 0, 0],
[0, 0, - (near+far) / (near-far), - (2*near*far) / (near-far)],
[0, 0, -1, 0]
];
The perspective projection matrix defines a Viewing frustum. It defines a 3 dimensional space (clip space) which is projected on the 2 dimensional viewport.
In OponGL all the geometry which is not in clip space is clipped. You have to ensure that the geometry is in between the near and far plane.
I have 4 points of football pitch (corner points):
P1(lat, lon, alt), P2(lat, lon, alt), P3(lat, lon, alt), P4(lat, lon, alt).
and a location on the pitch:
L(lat, lon, alt)
I want to convert L(lat, lon, alt) to L(x, y) on a rectangle with size of (W, H).
How to implement this conversion function? (I preferred C# language but implementation language is not important)
The following image describes my problem (I don't know how to implement the Function box):
First off, because output coordinates are 2D, I'm going to assume that we can get rid of altitude information from input coordinates. So input data consist of four points defining the input rectangle:
P1(lat, lon), P2(lat, lon), P3(lat, lon), P4(lat, lon)
and dimensions of the output rectangle: w, h.
I'm also going to ignore the curvature of the Earth (football pitch is small enough). With those assumptions we can implement the conversion function, by performing affine transformation. It would be wasteful to create transformation matrix each time we want to perform a transformation. For that reason we need two functions: first one to create the transformation matrix (called only once), and the second one that will use that matrix to perform transformation itself (called possibly many times, one time for each point we want to transform), something like:
tm = createTransformationMatrix(P1, P2, P4, w, h)
inPoint = (200, 50)
outPoint = transform(inPoint, tm)
Note that we only need three of four input points to unambiguously define a rotated rectangle in 2D euclidean space.
Here is the implementation of createTransformationMatrix and transform functions:
const run = function() {
// Creates transformation matrix to transform
// from rectangle somewhere in 2D space with coordinates p0, px, pi, py
// to rectangle with coordinates (x=0, y=0), (x=w, y=0), (x=w, y=h), (x=0, y=h).
// Note that: p0 is mapped to (x=0, y=0)
// px is mapped to (x=w, y=0)
// py is mapped to (x=0, y=h)
const createTransformationMatrix = function(p0, px, py, w, h) {
// Translate px and py by p0 - pxt and pyt are px and py vectors in coordinate system in which p0 is at the origin
const pxt = {
x: px.x - p0.x,
y: px.y - p0.y,
};
const pyt = {
x: py.x - p0.x,
y: py.y - p0.y,
};
// Create transformation matrix, which is inverse of transformation matrix that:
// 1. Transforms (x=0, y=0) to (x=p0.x, y=p0.y)
// 2. Transforms (x=1, y=0) to (x=p0.x + pxt.x / w, y=p0.y + pxt.y / w)
// 3. Transforms (x=0, y=1) to (x=p0.x + pyt.x / h, y=p0.y + pyt.y / h)
return Matrix.invert3([
[pxt.x / w, pyt.x / h, p0.x],
[pxt.y / w, pyt.y / h, p0.y],
[0 , 0 , 1 ],
]);
};
const transform = function(point, transformationMatrix) {
// Convert point to homogeneous coordinates
const inputVector = [
[point.x],
[point.y],
[1],
];
// Transform inputVector
const outputVector = Matrix.multiply(transformationMatrix, inputVector);
// Convert outputVector back to cartesian coordinates and return
return {
x: outputVector[0][0] / outputVector[2][0],
y: outputVector[1][0] / outputVector[2][0],
};
};
const w = 220;
const h = 115;
const p1 = {x:-79, y:80 };
const p2 = {x:9, y:-96};
const p3 = {x:55, y:-72};
const p4 = {x:-34, y:105};
const tm = createTransformationMatrix(p1, p2, p4, w, h);
const inPoint = {x: 200, y: 50};
const outPoint = transform(inPoint, tm);
console.log(`(${inPoint.x}, ${inPoint.y}) --[transform]--> (${outPoint.x}, ${outPoint.y})`);
}
//// Matrix ////
const Matrix = {};
Matrix.scale = (s, m) => m.map(x => Array.isArray(x) ? Matrix.scale(s, x) : s * x);
Matrix.multiply = function(a, b) {
const aNumRows = a.length, aNumCols = a[0].length;
const bNumRows = b.length, bNumCols = b[0].length;
const m = new Array(aNumRows);
for (let r = 0; r < aNumRows; ++r) {
m[r] = new Array(bNumCols);
for (let c = 0; c < bNumCols; ++c) {
m[r][c] = 0;
for (let i = 0; i < aNumCols; ++i)
m[r][c] += a[r][i] * b[i][c];
}
}
return m;
};
Matrix.invert3 = function(m) {
const [[a, b, c],
[d, e, f],
[g, h, i]] = m;
const det = a*(e*i - f*h) - b*(d*i - f*g) + c*(d*h - e*g);
return Matrix.scale(1/det, [
[e*i - f*h, c*h - b*i, b*f - c*e],
[f*g - d*i, a*i - c*g, c*d - a*f],
[d*h - e*g, b*g - a*h, a*e - b*d],
]);
};
//////////////
run();
I've included all the matrix processing logic, so that this code snippet is self contained, but I would suggest you to instead use some linear algebra library for matrix processing.
I've also made a more visual demo.
I need this type of gauge chart
How do I create the above guage chart
I have the speedometer gauge working, but it doesn't meet the needs.
Is there a way within the highchart api to use a triangle for the gauge and not the speedometer?
Unfortunately, it is not supported by default. However, you can achieve it wrapping Highcharts.seriesTypes.gauge.prototype.translate method and changing gauge dial element path. Check demo and code posted below.
Wrapper code:
(function(H) {
H.seriesTypes.gauge.prototype.translate = function() {
var series = this,
yAxis = series.yAxis,
options = series.options,
center = yAxis.center,
pInt = H.pInt,
merge = H.merge,
pick = H.pick,
isNumber = H.isNumber;
series.generatePoints();
series.points.forEach(function(point) {
var dialOptions = merge(options.dial, point.dial),
radius = (pInt(pick(dialOptions.radius, 80)) * center[2]) /
200,
baseLength = (pInt(pick(dialOptions.baseLength, 70)) * radius) /
100,
rearLength = (pInt(pick(dialOptions.rearLength, 10)) * radius) /
100,
baseWidth = dialOptions.baseWidth || 3,
arrowHeight = dialOptions.arrowHeight || 10,
arrowWidth = dialOptions.arrowWidth || 5,
topWidth = dialOptions.topWidth || 1,
overshoot = options.overshoot,
rotation = yAxis.startAngleRad +
yAxis.translate(point.y, null, null, null, true);
// Handle the wrap and overshoot options
if (isNumber(overshoot)) {
overshoot = overshoot / 180 * Math.PI;
rotation = Math.max(
yAxis.startAngleRad - overshoot,
Math.min(yAxis.endAngleRad + overshoot, rotation)
);
} else if (options.wrap === false) {
rotation = Math.max(
yAxis.startAngleRad,
Math.min(yAxis.endAngleRad, rotation)
);
}
rotation = rotation * 180 / Math.PI;
point.shapeType = 'path';
point.shapeArgs = {
d: dialOptions.path || [
'M', -rearLength, -baseWidth / 2,
'L',
baseLength, -baseWidth / 2,
baseLength, -arrowWidth,
baseLength + arrowHeight, topWidth / 2,
baseLength, arrowWidth,
baseLength, baseWidth / 2, -rearLength, baseWidth / 2,
'z'
],
translateX: center[0],
translateY: center[1],
rotation: rotation
};
// Positions for data label
point.plotX = center[0];
point.plotY = center[1];
});
}
})(Highcharts);
Demo:
https://jsfiddle.net/BlackLabel/07c3dkn4/1/