I usually work with matrices and vectors, what I want to do is when I click on an element, zoom to it and it works, however I also want to change the scale, I want it a little closer, but when I change the value from 1.0 to 2.0 for example, it zooms but it moves to the other side of the controller and I don't know why.
I'm probably skipping a step to adjust the zoom but I can't find a way, any ideas?
CODE ZOOM
Future<void> _zoomToTheElement({
required double x,
required double y,
required MiniatureValues miniatureValues,
}) async {
final double itemHalfWidth = (80 / 2);
final double itemHalfHeight = (50 / 2);
/// Calculation of the position depending from the item and also
/// the half of the screen (Device.w(50))
///
/// x : [itemHalfWidth] means that is half the [width] of an element
/// y : [itemHalfHeight] means that is half the [height] of an element
/// y : [56] means that is the [AppBar]'s size
final double positionX = -(x) + (Device.w(50) - itemHalfWidth);
final double positionY = -(y) + (Device.h(50) - (56) - itemHalfHeight);
final Matrix4 _matrixEndPlan = Matrix4.compose(
vector.Vector3(positionX, positionY, 1),
vector.Quaternion.euler(0.0, 0.0, 0.0),
vector.Vector3(1.0, 1.0, 1.0),
)..scale(1.0); // here i want to write 2.0, but now does not respect the positionX and positionY
/// Animated
_matrixPlanAnimation = Matrix4Tween(
begin: planController.value,
end: _matrixEndPlan,
)
.chain(
CurveTween(
curve: Curves.easeInOutCirc,
),
)
.animate(_animationController);
}
IMAGE ZOOM WITH 1.0, but when i changed to 2.0 or to other scale, moves to another location
----- NEW EDIT WITH #pskink CODE -----
/// Calculation of the position depending from the item and also
/// the half of the screen (Device.w(50))
///
/// [56] means that is the [AppBar]'s size
final double positionX = -(x) + (Device.w(50));
final double positionY = -(y) + (Device.h(50) - (56));
final Matrix4 matrix1 = TransformEntry.fromOffsets(
translate: Offset(positionX, positionY),
scale: 1.0, // change this and goes to left side in the InteractiveViewer
rotation: 0,
anchor: Offset(itemHalfWidth, itemHalfHeight),
).matrix;
----- MY SOLUTION CODE -----
/// Calculation of the position depending from the item and also
/// the half of the screen (Device.w(50))
///
/// [itemHalfWidth] means that is half the [width] of an element
/// [itemHalfHeight] means that is half the [height] of an element
/// [56] means that is the [AppBar]'s size
const double scale = 1.0;
final double dx = (-x * scale) + (Device.w(50) - itemHalfWidth);
final double dy = (-y * scale) + (Device.h(50) - (56) - itemHalfHeight);
Related
I'm learning CustomPaint by making my own line chart. I have already made the chart itself so now i'm making the things around it. One them is a trackball/line whenever you pan. So far i've made the dashed line from top to bottom, and it works perfectly, now i want to make the the ball that's gonna follow along with the line. My problem is getting the ball and dashed line to always be in sync. right now it's the dashed line that follows my finger and the ball that does'nt match up. I found this answer on stack overflow but it's not working quite right for me. This video is how it currently functions with the following code,
where dragX is the current x-axis position of your finger in pixels.
void drawTrackballLine(Canvas canvas, Size size) {
double dashHeight = 5;
double dashSpace = trackBallOptions.dashSpacing;
double y = 0;
final paint = Paint()
..color = trackBallOptions.color
..strokeWidth = trackBallOptions.strokeWidth;
while (y < size.height) {
canvas.drawLine(
Offset(dragX, y),
Offset(dragX, y + dashHeight),
paint,
);
y += dashSpace + dashHeight;
}
}
void drawTrackball(Canvas canvas, Size size, Path path) {
final pathMetric = path.computeMetrics().elementAt(0);
final value = pathMetric.length * (dragX / size.width);
final pos = pathMetric.getTangentForOffset(value);
canvas.drawCircle(pos!.position, 5, Paint());
}
I have a grey image. I want to draw it using a color but with opacity to a canvas. By using Color Filter, I was able to draw it on the canvas by replacing the color. If I adjust the alpha value, it changes image opacity (partial grey/red). The following code snippet shows how to draw a grey image as red onto a canvas. But how can I draw the red image as transparent to the canvas?
static void drawBrushDetail(Canvas canvas, double cx, double cy, double brushSize, double imageAngleRad) {
canvas.translate(cx + brushSize / 2, cy + brushSize / 2);
ui.Image bt = imgBrushTip; //this is a gray image
Color selectedColor = getDrawColor(cx,cy);
final _paint = Paint();
var alphaVal=255;
//if I change alphaVal to 100, the final draw looks more gray instead of red with 100 Alpha
_paint.colorFilter = ColorFilter.mode(selectedColor.withAlpha(alphaVal), BlendMode.srcATop);
if (imageAngleRad != 0.0) canvas.rotate(imageAngleRad);
canvas.translate(-brushSize / 2, -brushSize / 2);
// draw image with specified brushSize
canvas.drawImageRect(bx, Rect.fromLTRB(0, 0, bt.width, bt.height),
Rect.fromLTRB(0, 0, brushSize, brushSize), _paint);
//traslate back, don't want to use save and restore layer since it's expensive
canvas.translate(brushSize / 2, brushSize / 2);
if (imageAngleRad != 0.0) canvas.rotate(-imageAngleRad);
canvas.translate(-cx - brushSize / 2, -cy - brushSize / 2);
}
See the image below. The above code will paint the image with solid red color. (image pointed by the red arrow). But I want the result to be an image with red color and transparent. Since in Flutter, there is no API to set alpha for canvas, don't know if it is possible.
I figured it out but the credit goes to pskink
I only need to draw one image(gray color) but I need to do the followings:
Resize and rotate the image.
Replace the gray image with red color.
Draw the image (now in red) with opacity.
Here is the code snippet.
static void drawBrushDetail1(Canvas canvas, double cx, double cy, double size) {
final _paint = Paint();
const opacity = 0.5;
_paint.color = Color.fromRGBO(0, 0, 0, opacity);
ui.Image bx = uiImg[0];
double scale = size / 60; //size is brush tip size and 60x60 is gray image size
canvas.drawAtlas(
bx,
[
RSTransform.fromComponents(
rotation: imageAngleRad,
scale: scale,
anchorX: 30,
anchorY: 30,
translateX: cx,
translateY: cy)
],
[
/* The size of gray image is 60 x 60 */
const Rect.fromLTWH(0, 0, 60, 60)
],
[Colors.red],
BlendMode.dstIn, /*This is replace the gray image wiht red color
null /* No need for cullRect */,
_paint); //_paint will paint the red image in 0.5 opacity
}
I am using InteractiveViewer to display a video. It is working properly.
But I want set initial zoom level.
Is there a way to do this?
return InteractiveViewer(
minScale: 1,
maxScale: 4,
constrained: false,
child: Container(
color: Colors.red,
width: 320,
height: 180,
child: widget.remoteVideoRenderer(),
),
);
I was able to do this, with varying levels of success, like so (credit to #pskink for pointing me in the right direction):
final viewTransformationController = TransformationController();
#override
void initState() {
final zoomFactor = 2.0;
final xTranslate = 300.0;
final yTranslate = 300.0;
viewTransformationController.value.setEntry(0, 0, zoomFactor);
viewTransformationController.value.setEntry(1, 1, zoomFactor);
viewTransformationController.value.setEntry(2, 2, zoomFactor);
viewTransformationController.value.setEntry(0, 3, -xTranslate);
viewTransformationController.value.setEntry(1, 3, -yTranslate);
super.initState();
}
#override
Widget build(BuildContext context) {
return InteractiveViewer(
transformationController: viewTransformationController,
minScale: 0.1,
maxScale: 6,
child: // ....
);
}
viewTransformationController.value is a 4x4 matrix that is applied to the view, it encodes the translations/rotations/scaling. The representation of these transformations in the matrix is probably documented somewhere (and/or just standard from projective/affine geometry), but I couldn't find them, so I just printed them out while zooming/panning until the roles of each entry became clear.
If you just set the zoom factor to 2 and don't translate, you'll be zoomed-in on the top-left corner of the widget.
Note that you cannot access the windows dimensions in the initState() method using MediaQuery.of(context), which is somewhat inconvenient if you want to e.g. zoom in on the middle of the window. I haven't found a good way to do that yet.
Base on Amos Joshua answer, if you want to always zoom center screen. I have this formula:
zoomFactor = 1, xTrans = 0, yTrans = 0
zoomFactor = 2, xTrans = w / 2, yTrans = h / 2
zoomFactor = 2.8, xTrans = (w / 2)(1 + 0.8), yTrans = (h / 2)(1 + 0.8)
Therefore:
zoomFactor = z, xTrans = (w / 2)(z - 1), yTrans = (h / 2)(z - 1)
I use canvas to draw.
A, B, C is on the same line. There's two points and a x/y axis value of pointC:
Offset pointA = Offset(100, 200);
Offset pointB = Offset(200, 400);
double pointCx = 300;
Offset pointC = calculateOffsetByX(pointA, pointB, pointCx);
https://i.bmp.ovh/imgs/2021/12/0a7ee73b12749799.png
I need calculate the Offset of c if I have x/y value.
And encapsulated as a function like this:
/// #param [pointStart] start point of line
/// #param [point2] second point of line
/// #param [x] the position x of point need to calculate
/// #return Offset of the point on line
Offset calculateOffsetByX(Offset pointStart, Offset point2, double x){
// TODO
}
/// #param [pointStart] start point of line
/// #param [point2] second point of line
/// #param [y] the position y of point need to calculate
/// #return Offset of the point on line
Offset calculateOffsetByY(Offset pointStart, Offset point2, double y){
// TODO
}
And sometime the Offset is not positive number, need to consider those conditions:
https://i.bmp.ovh/imgs/2021/12/c6352f50f5dfb00d.png
You can try this funcion. (I am not test this code, pls try this code and show me result if it not working):
/// #param [pointStart] start point of line
/// #param [point2] second point of line
/// #param [x] the position x of point need to calculate
/// #return Offset of the point on line
Offset calculateOffsetByX(Offset pointStart, Offset point2, double x){
//y = ax + b
final a = (pointStart.dy - point2.dy)/(pointStart.dx - point2.dx);
final b = pointStart.dy - a*pointStart.dx;
return Offset(x, a*x +b);
}
/// #param [pointStart] start point of line
/// #param [point2] second point of line
/// #param [y] the position y of point need to calculate
/// #return Offset of the point on line
Offset calculateOffsetByY(Offset pointStart, Offset point2, double y){
//y = ax + b
final a = (pointStart.dy - point2.dy)/(pointStart.dx - point2.dx);
final b = pointStart.dy - a*pointStart.dx;
return Offset((y - b)/a, y);
}
The Problem
I'm trying to figure out a way to get at which point in the content node the scroll pane's viewport is centered on.
To elaborate on the picture above, the big rectangle is the content (let's say a large image), and the small rectangle is the portion that is shown by the scroll pane. I'm trying to find x and y which would be coordinates from the top left of the content.
What I've Tried
My first thought was to use the getViewportBounds() method of the scroll pane and use its minX and maxX properties to determine the center x point:
Bounds b = scrollPane.getViewportBounds();
double centerX = (b.getMinX() + b.getMaxX()) / 2;
double centerY = (b.getMinY() + b.getMaxY()) / 2;
However, this doesn't work because these numbers are negative and don't seem to accurately describe the x and y I'm looking for anyways.
My next thought was to use the scroll pane's hValue and vValue to get the top left corner of the viewport relative to the content:
Bounds b = scrollPane.getViewportBounds();
double centerX = scrollPane.getHvalue() + b.getWidth() / 2;
double centerY = scrollPane.getVvalue() + b.getHeight() / 2;
This didn't work either though as the hValue and vValue seem to be way too large (when scrolled in only a few pixels, I'm getting numbers like 1600).
My Questions
I seem to have a fundamental misunderstanding of how the viewport works with a scroll pane.
What am I doing wrong here? Can someone explain where these numbers come from? How do I find x and y like in the picture above?
Let (x, y) be the be coordinates of the top, left point shown in the viewport. You can write this as
((contentWidth - viewportWidth) * hValueRel, (contentHeight - viewportHeight) * vValueRel)
vValueRel = vValue / vMax
hValueRel = hValue / hMax
This means assuming hmin and vmin remain 0 you can keep a circle in the center of like this:
// update circle position to be centered in the viewport
private void update() {
Bounds viewportBounds = scrollPane.getViewportBounds();
Bounds contentBounds = content.getBoundsInLocal();
double hRel = scrollPane.getHvalue() / scrollPane.getHmax();
double vRel = scrollPane.getVvalue() / scrollPane.getVmax();
double x = Math.max(0, (contentBounds.getWidth() - viewportBounds.getWidth()) * hRel) + viewportBounds.getWidth() / 2;
double y = Math.max(0, (contentBounds.getHeight() - viewportBounds.getHeight()) * vRel) + viewportBounds.getHeight() / 2;
Point2D localCoordinates = content.parentToLocal(x, y);
circle.setCenterX(localCoordinates.getX());
circle.setCenterY(localCoordinates.getY());
}
private Circle circle;
private Pane content;
private ScrollPane scrollPane;
#Override
public void start(Stage primaryStage) {
// create ui
circle = new Circle(10);
content = new Pane(circle);
content.setPrefSize(4000, 4000);
scrollPane = new ScrollPane(content);
Scene scene = new Scene(scrollPane, 400, 400);
// add listener to properties that may change
InvalidationListener l = o -> update();
content.layoutBoundsProperty().addListener(l);
scrollPane.viewportBoundsProperty().addListener(l);
scrollPane.hvalueProperty().addListener(l);
scrollPane.vvalueProperty().addListener(l);
scrollPane.hmaxProperty().addListener(l);
scrollPane.vmaxProperty().addListener(l);
scrollPane.hminProperty().addListener(l);
scrollPane.vminProperty().addListener(l);
primaryStage.setScene(scene);
primaryStage.show();
}