javascripthtmlcssdrag-and-dropdrag

How to calculate correct position of a element when dragged using translate with scale applied in javascript


I am working on the following Drag and Drop library as a side project. I am using transform translate to make a element draggable. I store the initial client.x and client.y and calculate the translate value by subtracting the current client.x and client.y when the mouse move. In simple terms I record the initial click coords(x,y) and when I subtract the current coords(x,y) on mouse move it will give me the distance by which mouse has moved from the initial position which I then apply to the element using translate.

It works as expected. I have even implemented cursor offset which also works but the only thing I am not able to figure out is how to adjust the position if scale property is applied to the element. When the element scale is changed from 1 to any other value the size and position of the element will change which is messing up my current calculation.

I have created a simple diagram which you can see here. I'm also sharing the screenshot bellow.

diagram for drag and drop element

You might need to understand the flow of the program so below is a simple explanation

The position of the element is handle by these two following methods

  #translateDragPreview(x?: number, y?: number) {
    if (this.#draggedPreview.element === null) return;
    const cords = {
      x: x ?? this.#internalState.activeCords.x,
      y: y ?? this.#internalState.activeCords.y,
    };

    const initialX = this.#initialPosition.x / this.#draggedPreview.scale;
    const initialY = this.#initialPosition.y / this.#draggedPreview.scale;

    this.#draggedPreview.element.style.transform = `translate(
    ${cords.x - initialX}px, 
    ${cords.y - initialY}px) 
    scale(${this.#draggedPreview.scale})`;
  }
  #setCursorOffset(
    dragElement: HTMLElement,
    event: PointerEvent,
    offset: BaseDraggableType["modifiers"]["cursor"]["offset"],
  ) {
    const cursorOffset = offset;
    const rect = dragElement.getBoundingClientRect();
    const { scale } = this.#draggedPreview;

    const clickX = this.#internalState.initialCords.x - rect.left;
    const clickY = this.#internalState.initialCords.y - rect.top;

    const adjustRect: (value: number) => number = (value) => value + (value - value / scale);
    const adjustedRect = {
      left: rect.left / scale,
      right: adjustRect(rect.right),
      top: adjustRect(rect.top),
      bottom: adjustRect(rect.bottom),
      width: adjustRect(rect.width),
      height: adjustRect(rect.height),
    };

    const presetOffset = {
      "top-left": {
        x: adjustedRect.left - cursorOffset.x,
        y: adjustedRect.top - cursorOffset.y,
      },
      "top-right": {
        x: adjustedRect.right + cursorOffset.x,
        y: adjustedRect.top - cursorOffset.y,
      },
      "bottom-left": {
        x: adjustedRect.left - cursorOffset.x,
        y: adjustedRect.bottom + cursorOffset.y,
      },
      "bottom-right": {
        x: adjustedRect.right + cursorOffset.x,
        y: adjustedRect.bottom + cursorOffset.y,
      },
      center: {
        x: adjustedRect.left + adjustedRect.width / 2 + cursorOffset.x - 4,
        y: adjustedRect.top + adjustedRect.height / 2 + cursorOffset.y - 2,
      },
      auto: {
        x: adjustedRect.left + clickX + cursorOffset.x,
        y: adjustedRect.top + clickY + cursorOffset.y,
      },
    };

    this.#initialPosition = presetOffset[cursorOffset.preset];
  }

Some more information

Here is a code sandbox link with setup. please check the index.html and main.ts file . In the main.ts file you fill see dragOptions with scale option set to 1 if you will change it to any other value the element position will not be set properly. You can also change the preset to any of the following

if you don't want to go through all the code, you can just help me formulate how to calculate a elements correct position when it is dragged using translate and adjusted for scale value. When scale is applied the size and position is change and how to adjust for that I need a formula for that.

I have also created a simple demo in codepen. Please have a look. it is the same approach I have used in my library.

Please let me know if any further information is required I'm happy to help.


Solution

  • As I did not get any response, I tried to solve it myself. I also found a working solution which I have added in the codepen link.

    Here is the link to the working solution - https://codepen.io/gokunik/pen/YzmqQJV

    Below is a small snippet

    function onPointerDown(e, element) {
      const originalRect = element.getBoundingClientRect();
      const rect = scaleRect(element.getBoundingClientRect(), scale);
    
      initialCoords = {
        x:
          e.clientX +
          (rect.right - e.clientX) +
          (e.clientX - originalRect.right) * scale,
        y:
          e.clientY +
          (rect.bottom - e.clientY) +
          (e.clientY - originalRect.bottom) * scale,
      };
      draggedPreview = element;
      translateDragPreview(e.clientX, e.clientY);
    }
    

    Approach used to solve the question.

    I used a simple approach. I initially thought about this while solving this but did not pursue it then but it worked when I tried it now.

    So the idea is the element is scaled down when it is dragged so it's either shrinks or increases in size, changing it's rect values like x, y, top, left etc which displaces the dragged element which needs to be adjusted. So first I added a function to give me a updated rect for a element when scaling is applied. Now I have the rect of both original and dragged scaled element.

    If you notice in the screenshot I have shared, the scaled element shrinks and increases in size by maintaining it's centre with the original element so first I tried to align the scaled element edges with the initial point of click using (rect.right - e.clientX) and (rect.bottom - e.clientY) which just aligned the element to the bottom right corner.

    Now All I had to do is to find where the click has happened in the original rect and scale it down for the updated scaled rect. So I repeated the same same thing and multiply it with the scale value (e.clientX - originalRect.right) * scale and (e.clientY - originalRect.bottom) * scale so it adjust it according to the scaled element.

    I hope you find this helpful. I did not find the answer to this problem no where on the internet that's why I'm sharing it here.