Search code examples
javascripthtmlimagecanvas

Javascript & canvas, image rotation coordinates problem


I have a problem with rotated image coordinates. When I press right mouse button on image, a yellow circle should appear in the place where the mouse was clicked. It works just fine in starting image, as well as on resized image (mouse wheel) and dragged image (left mouse click and drag). The problem is, I can't correct calculate right coordinates of mouse after rotating image with slider - for example, when I rotate image by 180 degrees, the circle appears diagonally, but it should appear in the place where I clicked. The interesting thing is, that when I put circles before rotation, and then rotate the image, the circles rotates with correct coordinates. I am missing some transformation in my code but I just cannot see which one and where to put it in. I would appreciate help. Thanks in advance;)

Here is my code:

window.onload = () => {
  const canvas = document.getElementById("canvas");
  const context = canvas.getContext("2d");
  context.imageSmoothingEnabled = false;
  const mousePos = document.getElementById("mouse-pos");
  const transformedMousePos = document.getElementById("transformed-mouse-pos");

  const image = new Image();
  image.src =
    "https://e7.pngegg.com/pngimages/584/916/png-clipart-bright-blue-sky-aqua-blue-button-rounded-blank-download-free-download-png-download-vector-download-svg-download-transparent-thumbnail.png";
  image.onload = drawImageToCanvas;

  let isDragging = false;
  let dragStartPosition = { x: 0, y: 0 };
  let currentTransformedCursor;

  class Obraz {
    constructor(x, y) {
      this.x = x;
      this.y = y;
    }
  }

  let points = [];

  function markPoint(x, y) {
    let obraz = new Obraz(x, y);
    points.push(obraz);
  }

  function drawPoints() {
    const centerX = canvas.width / 2;
    const centerY = canvas.height / 2;

    context.save();
    context.translate(centerX, centerY);
    context.rotate((rotationAngle * Math.PI) / 180);
    const radius = 10;
    context.lineWidth = 3;
    context.strokeStyle = "#FFFF00";
    context.filter = "none";
    points.forEach((item) => {
      context.beginPath();
      context.arc(
        item.x - centerX,
        item.y - centerY,
        radius,
        0,
        2 * Math.PI,
        false
      );
      context.stroke();
    });
    context.restore();
  }

  function drawImageToCanvas() {
    context.save();
    context.setTransform(1, 0, 0, 1, 0, 0);
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.restore();

    context.save();
    context.translate(canvas.width / 2, canvas.height / 2);
    context.rotate((rotationAngle * Math.PI) / 180);

    context.drawImage(image, -100, -100, 200, 200);
    context.restore();

    drawPoints();
  }

  let rotateSlider = document.getElementById("rotateSlider");
  let rotationAngle = 0;

  rotateSlider.addEventListener("input", function () {
    rotationAngle = this.value;
    drawImageToCanvas(rotationAngle);
  });

  function getTransformedPoint(x, y) {
    const originalPoint = new DOMPoint(x, y);
    return context.getTransform().invertSelf().transformPoint(originalPoint);
  }

  function onMouseDown(event) {
    isDragging = true;
    dragStartPosition = getTransformedPoint(event.offsetX, event.offsetY);

    if (event.button === 2) {
      currentTransformedCursor = getTransformedPoint(
        event.offsetX,
        event.offsetY
      );
      markPoint(currentTransformedCursor.x, currentTransformedCursor.y);

      drawImageToCanvas();
    }
  }

  function onMouseMove(event) {
    currentTransformedCursor = getTransformedPoint(
      event.offsetX,
      event.offsetY
    );
    mousePos.innerText = `Original X: ${event.offsetX}, Y: ${event.offsetY}`;
    transformedMousePos.innerText = `Transformed X: ${currentTransformedCursor.x}, Y: ${currentTransformedCursor.y}`;

    if (isDragging) {
      context.translate(
        currentTransformedCursor.x - dragStartPosition.x,
        currentTransformedCursor.y - dragStartPosition.y
      );
      drawImageToCanvas();
    }
  }

  function onMouseUp() {
    isDragging = false;
  }

  function onWheel(event) {
    const zoom = event.deltaY < 0 ? 1.1 : 0.9;

    context.translate(currentTransformedCursor.x, currentTransformedCursor.y);
    context.scale(zoom, zoom);
    context.translate(-currentTransformedCursor.x, -currentTransformedCursor.y);

    drawImageToCanvas();
    event.preventDefault();
  }

  let buttonReset = document.getElementById("reset");

  buttonReset.addEventListener("click", function (event) {
    rotateSlider.value = 0;
    rotationAngle = 0;
    drawImageToCanvas();
  });

  let buttonClear = document.getElementById("clear");

  buttonClear.addEventListener("click", function (event) {
    points = [];
    drawImageToCanvas();
  });

  canvas.addEventListener("mousedown", onMouseDown);
  canvas.addEventListener("mousemove", onMouseMove);
  canvas.addEventListener("mouseup", onMouseUp);
  canvas.addEventListener("wheel", onWheel);

  canvas.addEventListener("contextmenu", function (ev) {
    ev.preventDefault();
  });
};
.nav-header {
    font-size: 1.5rem;
}
 
.row {
    margin-bottom: 0;
}
 
#sourceImage,
.image-controls,
.image-save,
.preset-filters {
    display: none;
}
 
.image-preview {
    display: flex;
    justify-content: center;
    margin-top: 20px;
}
 
#canvas {
    max-height: 420px;
    object-fit: contain;
}

.container-image {
    overflow: auto;
    justify-content: center;
    align-items: center;

    width: 90%;
    height: 90%;

    position: absolute;
}

.image {
    object-fit: cover;
    transform: translate(20%, 20%);
}
<canvas id="canvas" width="350" height="350"></canvas>

<div class="col s6">
  <span class="range-field">
    <input id="rotateSlider" type="range" value="0" min="-180" max="180">
  </span>
  <button class="btn btn-flat red white-text" id="reset">
    Reset
  </button>
  <button class="btn btn-flat red white-text" id="clear">
    Clear
  </button>
</div>

<div id="mouse-pos"></div>
<div id="transformed-mouse-pos"></div>


Solution

  • I found solution for the problem, needed to "slightly" modfiy transforms under click method. I will leave this topic in case someone will look after similar solution.

    function onMouseDown(event) {
        isDragging = true;
        dragStartPosition = getTransformedPoint(event.offsetX, event.offsetY);
        
        if (event.button === 2) {
            currentTransformedCursor = getTransformedPoint(event.offsetX, event.offsetY);
            const transformedX = currentTransformedCursor.x - canvas.width / 2;
            const transformedY = currentTransformedCursor.y - canvas.height / 2;
            const unrotatedX = Math.cos(-rotationAngle * Math.PI / 180) * transformedX - Math.sin(-rotationAngle * Math.PI / 180) * transformedY + canvas.width / 2;
            const unrotatedY = Math.sin(-rotationAngle * Math.PI / 180) * transformedX + Math.cos(-rotationAngle * Math.PI / 180) * transformedY + canvas.height / 2;
            markPoint(unrotatedX, unrotatedY);
            drawImageToCanvas();
        }
    }