skip to Main Content

I am developing a Canvas library in FLutter/Dart, like FabricJS but for much simpler tasks. When I get the project to a certain stage, I will share it on pub.dev and Github.

I’ve been struggling with this problem for a few days, please help.

Basically every drawing object is a class. Each object simply has values Left, Top, Width, Height, ScaleX, ScaleY, Angle (degree of ration), OriginX and OriginY.

OriginX and OriginY is an Enum and contains the following.

enum OriginX { left, right, center }
enum OriginY { top, bottom, center }

If you set the OriginX of an object to "left", the object will be drawn from the left side from the 100th pixel. If you give the value "right", the object will draw itself from the right side at the 100th pixel.

I draw a small box at the 4 corners of the objects and in the middle of the left and right side. I use these to change the size of the object. But I don’t change the size of the objects by changing their Width and Height, but by changing their ScaleX and ScaleY values. So I am scaling.

I wrote an ActionHandler method for the scaling objects in the corner. My main problem is that if the object is rotated, the object moves to different positions in a meaningless way when changing the scale of the object. The reason for this is the problem caused by the rotation applied to the object. For example, if the object is 0 degrees, when I hold the object by the right center handle and enlarge it, it grows smoothly and the width grows following the current position of the mouse. If I grab the object by the left center handle and pull it, no problem.

If the Angle value of the object is 0 and regardless of the OriginX, OriginY value, I can grab it at any point and scale it.

But for example, if OriginX = left and you have given the object a 45 degree tilt and you hold it at the right center scaling point, it will grow smoothly. Because you scaled by holding it from the right side and the object always keeps itself fixed to OriginX.left. But on the contrary, if it is from the left side and the scaling is OriginX.left, the position of the object is distorted. it should be fixed from the right side and scaled 45 degrees upwards.

Changing the Left and Top values of the object directly means that the object takes into account the OriginX and OriginY values of the object and makes the drawing points of the object in the background according to these Origin values.

In addition, I am doing the ScaleX and ScaleY calculations correctly, but when the degree of rotation is involved, the scale of the object does not exactly coincide with the points I hold with the mouse.

I need to assign the results of the whole calculation to the object’s Left, Top, ScaleX and ScaleY properties. Because everything is rendered according to the object’s values.

I made a short video, please watch the video from the link below.

https://streamable.com/sco5de

I also read the article here and it solves the same problem I have. But I could not apply it to my system.

https://shihn.ca/posts/2020/resizing-rotated-elements/

Screenshot

Scale Handler Codes

  void onActionHandler(dynamic details, GestureType gestureType,
      BasicObject object, ObjectControl control) {
    if (gestureType == GestureType.onPanDown) {
      final point = (details as DragDownDetails).localPosition;
      properties.set('firstPoint', point);
    } else if (gestureType == GestureType.onPanUpdate) {
      final lastPoint = (details as DragUpdateDetails).localPosition;

      //This assigns the value of LastPoint to
      // "firstPoint", overwrites it and returns the value of "firstPoint" before overwriting.
      //The properties variable is used for general purpose value storage.
      Offset delta =
          lastPoint - properties.swap<Offset>('firstPoint', lastPoint);


      //These variables indicate if the object is scaled in the right or
      // down direction. If this is not done and you grab the object's left
      // center scaling control and pull it to the left, the object will shrink
      // instead of grow. So the opposite will happen and we prevent this.

      bool positiveHorizontalScale = true;
      bool positiveVerticalScale = true;

      //The "option" variable returns the enum value
      // of whichever checkpoint it is held from.
      final option = getControlOption(control)!;

      if (option == ControlsOptions.tl) {
        positiveHorizontalScale = false;
        positiveVerticalScale = false;
      } else if (option == ControlsOptions.mt || option == ControlsOptions.tr) {
        positiveVerticalScale = false;
      } else if (option == ControlsOptions.bl || option == ControlsOptions.ml) {
        positiveHorizontalScale = false;
      }

      if (!positiveHorizontalScale) {
        delta = Offset(-delta.dx, delta.dy);
      }
      if (!positiveVerticalScale) {
        delta = Offset(delta.dx, -delta.dy);
      }

      //These variables specify whether the object is scaled in both directions
      // or in a particular direction.
      bool adjustHorizontal = false;
      bool adjustVertical = false;

      final scaledWidth = width * scaleX;
      final scaledHeight = height * scaleY;

      //Calculate ScaleX and ScaleY values required for scaling.
      final sX = (scaledWidth + delta.dx) / scaledWidth * scaleX;
      final sY = (scaledHeight + delta.dy) / scaledHeight * scaleY;

      double tempScaleX = scaleX;
      double tempScaleY = scaleY;
      double tempLeft = left;
      double tempTop = top;

      switch (option) {
        case ControlsOptions.tl:
        case ControlsOptions.tr:
        case ControlsOptions.bl:
        case ControlsOptions.br:
          {
            tempScaleX = sX;
            tempScaleY = sY;
            adjustHorizontal = true;
            adjustVertical = true;
          }
          break;
        case ControlsOptions.ml:
        case ControlsOptions.mr:
          {
            tempScaleX = sX;
            adjustHorizontal = true;
          }
          break;
        case ControlsOptions.mt:
        case ControlsOptions.mb:
          {
            tempScaleY = sY;
            adjustVertical = true;
          }
          break;
        case ControlsOptions.mtr:
          {}
          break;
      }

      //If scaling in horizontal direction
      if (adjustHorizontal) {
        if (originX == OriginX.right && positiveHorizontalScale) {
          tempLeft = tempLeft + delta.dx;
        }
        if (originX == OriginX.center) {
          if (positiveHorizontalScale) {
            tempLeft = tempLeft + delta.dx / 2;
          } else {
            tempLeft = tempLeft - delta.dx / 2;
          }
        }
        if (originX == OriginX.left && !positiveHorizontalScale) {
          tempLeft = tempLeft - delta.dx;
        }
      }

      //If scaling in vertical direction
      if (adjustVertical) {
        if (originY == OriginY.bottom && positiveVerticalScale) {
          tempTop = tempTop + delta.dy;
        }
        if (originY == OriginY.center) {
          if (positiveVerticalScale) {
            tempTop = tempTop + delta.dy / 2;
          } else {
            tempTop = tempTop - delta.dy / 2;
          }
        }
        if (originY == OriginY.top && !positiveVerticalScale) {
          tempTop = tempTop - delta.dy;
        }
      }

      left = tempLeft;
      top = tempTop;
      scaleX = tempScaleX;
      scaleY = tempScaleY;
      _controller!.render();
    }
  }

I developed methods for different calculations many times. I tried to solve my problem in ChatGPT.But I could not get the correct calculation in any way, there were always meaningless values.

2

Answers


  1. Chosen as BEST ANSWER

    @pskink

    I reviewed the _resize() function and applied it to my project. It works fine when the angle value is 0. I also disabled working with ScaleX and ScaleY values, I directly update the "width" and "height" parameters. I also fixed the OriginX and OriginY values of my own object class as left and top (but I tested with all Origin values, still negative). But where am I making a mistake?

    final r0 = getRect();
    final i = getControlOptionIndex(option)!;
    final insets = EdgeInsets.only(
        left: pads[i].left * -delta.dx,
        top: pads[i].top * -delta.dy,
        right: pads[i].right * delta.dx,
        bottom: pads[i].bottom * delta.dy);
    
    final r1 = insets.inflateRect(r0);
    final align = -aligns[i];
    final point = align.inscribe(r1.size, Offset.zero & r0.size).center;
    final matrix = getData().matrix;
    final center = MatrixUtils.transformPoint(matrix, point);
    
    final r2 = Rect.fromCenter(
      center: center,
      width: r1.width,
      height: r1.height,
    );
    
    //One interesting thing here is that when I don't add the starting
    // position of r0 to the left value, the object always goes to dx=0, dy=0.
    // I need to change the position of the object by changing the
    // "left" and "top" values. Because later I may want to load these values
    // into json and draw back.
    
    left = r0.left + r2.left;
    top = r0.top + r2.top;
    width = r2.width;
    height = r2.height;
    render();
    

    It's basically my render function.

    void render(Canvas canvas) {
        final data = getData();
        //creates a rectangle path according to the left, top, width and height values.
        final path = createPath();
        
        var matrix = Matrix4.identity();
        
        final translateOffset = getTranslatePoint();
        //As it stands now translatePoint will always come as left, top. So OriginX.left and OriginY.top.
        
        final radians = degree2Radian(angle);
        
        matrix.translate(translateOffset.dx, translateOffset.dy);
        matrix.rotateZ(radians);
        //-disabled matrix.scale(scaleX, scaleY);
        matrix.translate(-translateOffset.dx, -translateOffset.dy);
        
        data.matrix = matrix;
        canvas.transform(matrix.storage);
        
        final paint = Paint();
        paint.style = PaintingStyle.fill;
        paint.color = fill;
        canvas.drawPath(path, paint);
    }
    

  2. @pskink

    I partially solved the problem. it seems to work well except for some angles. For example, when I move the object to 90 degrees, the sizing is almost very slow and the object grows very slowly. Also the handle object moves away from the mouse position as the object grows. I must be missing something, there is no way I can use your library. I need to handle this directly in Canvas.

    final r0 = getRect();
    final i = getControlOptionIndex(option)!;
    final insets = EdgeInsets.only(
        left: pads[i].left * -delta.dx,
        top: pads[i].top * -delta.dy,
        right: pads[i].right * delta.dx,
        bottom: pads[i].bottom * delta.dy);
    
    final r1 = insets.inflateRect(r0);
    final matrix = getMatrix();
    final center = MatrixUtils.transformPoint(matrix, r1.topLeft);
    
    final r2 = Rect.fromLTWH(
      center.dx,
      center.dy,
      r1.width,
      r1.height,
    );
    
    left = r2.left;
    top =  r2.top;
    width = r2.width;
    height = r2.height;
    render();
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search