Search code examples
javascripthtmlcsszoomingscale

Applying zoom into point like Aseprite


Hello I need some help with applying an answer on another question to my code. I'm currently trying to recreate aseprite's zoom functionality but, with my current code, when I zoom on different sides of the canvas it shifts a couple of pixels.

Previously I was told that my question was a duplicate of: Zoom in on a point (using scale and translate)

And I have looked at: https://stackoverflow.com/a/3151987/24209432, but I can't seem to get their answer to work with my code. Please help me with this.

Here is a gif displaying what currently happens: Zoom shift problem

And here is how it should work: Aseprite zoom

Here is my relavant code:

<html>
  <body>
    <main>
      <section Id="canvas-panel">
        <section
          id="canvas"
          style="width: 256px; height: 256px; background-size: 12.5%"
        >
          <canvas width="256px" height="256px"></canvas>
        </section>
      </section>
    </main>
  </body>
</html>

<script>
  let canvasPanel = document.getElementById("canvas-panel");
  let canvas = document.getElementById("canvas");

  // Function to calculate the closest point on the border of the canvas to the click position.
  function calculateClosestBorderPoint(event) {
    const canvasRect = canvas.getBoundingClientRect();

    const clickX = event.clientX;
    const clickY = event.clientY;

    let closestX = canvasRect.left;
    let closestY = canvasRect.top;

    // Determine if the click is closer to the left or right side of the canvas.
    if (clickX < canvasRect.left) {
      closestX = canvasRect.left;
    } else if (clickX > canvasRect.right) {
      closestX = canvasRect.right;
    } else {
      closestX = clickX;
    }

    // Determine if the click is closer to the top or bottom of the canvas.
    if (clickY < canvasRect.top) {
      closestY = canvasRect.top;
    } else if (clickY > canvasRect.bottom) {
      closestY = canvasRect.bottom;
    } else {
      closestY = clickY;
    }

    return { x: closestX, y: closestY };
  }

  // Initialize scale.
  let scale = 1;

  // Define zoom limits
  const minScale = 0.5; // Minimum zoom limit.
  const maxScale = 4.0; // Maximum zoom limit.

  // Function to update the canvas transformation.
  function updateCanvasTransform() {
    canvas.style.transform = `scale(${scale})`;
  }

  canvasPanel.addEventListener("wheel", function (e) {
    e.preventDefault();

    const zoomDirection = e.deltaY < 0 ? 1 : -1;

    if (scale == minScale && zoomDirection == -1) return;
    if (scale == maxScale && zoomDirection == 1) return;

    const zoomFactor = 1.1;

    const closestPoint = calculateClosestBorderPoint(event);

    scale *= zoomFactor ** zoomDirection; // Apply zoom factor based on direction

    // Clamp scale to limits
    scale = Math.min(scale, maxScale);
    scale = Math.max(scale, minScale);

    // Update canvas position to keep the closest point centered relative to the viewport
    const canvasRect = canvas.getBoundingClientRect();
    const viewportCenterX = canvasRect.width / 2;
    const viewportCenterY = canvasRect.height / 2;

    // Convert closest point to relative position within the viewport (0-1 scale)
    let relativeX =
      ((closestPoint.x - canvasRect.left) / canvasRect.width) * 100;
    let relativeY =
      ((closestPoint.y - canvasRect.top) / canvasRect.height) * 100;

    canvas.style.transformOrigin = `${relativeX}% ${relativeY}%`;

    // Update canvas transformation using only scale
    updateCanvasTransform();
  });
</script>

<style>
  #canvas {
    position: fixed;
    overflow: hidden;
    padding: 0;
    margin: 0;
    transform: scale(1) translate(0px, 0px);
    background-color: #1d1d1d;
  }

  #canvas-panel {
    position: relative;
    width: 100%;
    height: 100%;
    background-color: #303030;
  }

  html,
  body {
    background-color: #161616;
    margin: 0;
    width: 100%;
    height: 100%;
  }
</style>

Solution

  • This worked, good luck people that find this later! :)

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <div id="canvas-panel" onclick="calculateClosestBorderPoint(event)">
      <div id="canvas"></div>
    </div>
    
    <style>
      body {
        background-color: orange;
      }
      #canvas-panel {
        margin: 0;
        width: 800px;
        height: 800px;
        background-color: white;
        position: relative;
        overflow: hidden;
      }
      #canvas {
        position: absolute;
        top: 0;
        bottom: 0;
        left: 0;
        right: 0;
        margin: auto;
        background-color: red;
        width: 600px;
        height: 600px;
      }
    </style>
    
    <script>
      const MIN_SCALE = 0.2;
      const MAX_SCALE = 10;
      const zoomFactor = 1.2;
    
      var scale = 1;
    
      var offsetX = 0;
      var offsetY = 0;
    
      var $canvas = $("#canvas");
      var $canvasPanel = $("#canvas-panel");
    
      var canvasPanelWidth = $canvasPanel.width();
      var canvasPanelHeight = $canvasPanel.height();
    
      var canvasWidth = $canvas.width();
      var canvasHeight = $canvas.height();
      
      $(document).ready(setMargin);
    
      // Set the margin of the canvas for when the canvas is bigger than the canvasPanel.
      // This is all because margin: auto; doesn't work when the canvas is bigger than the panel.
      function setMargin() {
        var marginX = (canvasPanelWidth - canvasWidth) / 2;
        var marginY = (canvasPanelHeight - canvasHeight) / 2;
        $canvas.css('margin', `${marginY} ${marginX}`);
    }
    
      function calculateClosestBorderPoint(rect, x, y) {
        const clickX = x;
        const clickY = y;
    
        let closestX = rect.left;
        let closestY = rect.top;
    
        // Determine if the click is closer to the left or right side of the canvas.
        if (clickX < rect.left) {
          closestX = rect.left;
        } else if (clickX > rect.right) {
          closestX = rect.right;
        } else {
          closestX = clickX;
        }
    
        // Determine if the click is closer to the top or bottom of the canvas.
        if (clickY < rect.top) {
          closestY = rect.top;
        } else if (clickY > rect.bottom) {
          closestY = rect.bottom;
        } else {
          closestY = clickY;
        }
    
        return { x: closestX, y: closestY };
      }
    
      $canvasPanel.on("wheel", function (event) {
        event.preventDefault();
    
        var closestPoint = calculateClosestBorderPoint(
          canvas.getBoundingClientRect(),
          event.originalEvent.pageX,
          event.originalEvent.pageY
        );
    
        var borderX = closestPoint.x - $canvasPanel.offset().left;
        var borderY = closestPoint.y - $canvasPanel.offset().top;
    
        var zoomDirection = event.originalEvent.deltaY < 0 ? 1 : -1;
    
        if (scale == MIN_SCALE && zoomDirection == -1) return;
        if (scale == MAX_SCALE && zoomDirection == 1) return;
        
        var nextScale = scale * zoomFactor ** zoomDirection;
    
        nextScale = Math.min(nextScale, MAX_SCALE);
        nextScale = Math.max(nextScale, MIN_SCALE);
    
        var percentXInCurrentBox = borderX / canvasPanelWidth;
        var percentYInCurrentBox = borderY / canvasPanelHeight;
    
        var currentBoxWidth = canvasPanelWidth / scale;
        var currentBoxHeight = canvasPanelHeight / scale;
    
        var nextBoxWidth = canvasPanelWidth / nextScale;
        var nextBoxHeight = canvasPanelHeight / nextScale;
    
        var deltaX =
          (nextBoxWidth - currentBoxWidth) * (percentXInCurrentBox - 0.5);
        var deltaY =
          (nextBoxHeight - currentBoxHeight) * (percentYInCurrentBox - 0.5);
    
        var nextOffsetX = offsetX - deltaX;
        var nextOffsetY = offsetY - deltaY;
    
        $canvas.css({
          transform: "scale(" + nextScale + ")",
          left: -1 * nextOffsetX * nextScale,
          right: nextOffsetX * nextScale,
          top: -1 * nextOffsetY * nextScale,
          bottom: nextOffsetY * nextScale,
        });
    
        offsetX = nextOffsetX;
        offsetY = nextOffsetY;
        scale = nextScale;
      });
    </script>