Search code examples
javascriptecmascript-6html5-canvaslinecursor-position

Unable to get proper position in Canvas using ES6 (why isn't this code working properly?)


I am trying make an paint app using ES6. But i am not getting proper position and line in canvas.

This line is not drawn in correct position, like top-left is formed when i click and from 0,0 corner of canvas.

As you can see Line is not starting from the point Cursor is pointing and this difference increases as we move from TOP-LEFT cornor to BOTTOM-RIGHT cornor.

const TOOL_LINE = 'line';

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

class Paint {
  constructor(canvasId) {

    this.canvas = document.getElementById(canvasId);
    this.context = canvas.getContext("2d");
  }
  set activeTool(tool) {
    this.tool = tool;
  }
  init() {
    this.canvas.onmousedown = e => this.onMouseDown(e);
  }
  onMouseDown(e) {
    this.saveData = this.context.getImageData(0, 0, this.canvas.clientWidth, this.canvas.clientHeight);
    this.canvas.onmousemove = e => this.onMouseMove(e);
    document.onmouseup = e => this.onMouseUp(e);
    this.startPos = this.getMouseCoordinatesCanvas(e, this.canvas);
  }
  onMouseMove(e) {
    this.currentPos = this.getMouseCoordinatesCanvas(e, this.canvas);
    switch (this.tool) {
      case TOOL_LINE:
        this.drawShape();
        break;
      default:
        break;
    }
  }
  onMouseUp(e) {
    this.canvas.onmousemove = null;
    document.onmouseup = null;
  }
  drawShape() {
    this.context.putImageData(this.saveData, 0, 0);
    this.context.beginPath();
    this.context.moveTo(this.startPos.x, this.startPos.y);
    this.context.lineTo(this.currentPos.x, this.currentPos.y);
    this.context.stroke();
  }
  getMouseCoordinatesCanvas(e, canvas) {
    let rect = canvas.getBoundingClientRect();
    let x = e.clientX - rect.left;
    let y = e.clientY - rect.top;
    return new Point(x, y);
  }
}

var paint = new Paint("canvas");
paint.activeTool = TOOL_LINE;
paint.init();

document.querySelectorAll("[data-tools]").forEach(
  item => {
    item.addEventListener("click", e => {
      let selectedTool = item.getAttribute("data-tools");
      paint.activeTool = selectedTool;

    });
  }
);
#Container {
  background-color: lime;
  height: 310px;
}

.toolbox,
#canvas {
  display: inline-block;
}

.toolbox {
  background-color: gray;
  padding: 0px 15px 15px 15px;
  left: 10px;
  top: 11px;
}

.group {
  margin: 5px 2px;
}

#line {
  transform: rotate(-90deg);
}

.ico {
  margin: 3px;
  font-size: 23px;
}

.item:hover,
.item.active {
  background-color: rgba(160, 160, 160, 0.5);
  color: white;
}

#canvas {
  background-color: white;
  margin: 5px;
  float: right;
  width: 400px;
  height: 300px;
}
<script src="https://kit.fontawesome.com/c1d28c00bc.js" crossorigin="anonymous"></script>
<div class="container">
  <div id="Container">
    <div class="toolbox">
      <center>
        <div class="group tools">
          <div class="item active" data-tools="line">
            <i class="ico far fa-window-minimize" id="line" title="Line"></i>
          </div>
        </div>
      </center>
    </div>
    <canvas id="canvas"></canvas>
  </div>
</div>

Here is link of code.

Thanks in advance.


Solution

  • The issue is 1 or both of 2 things

    Your canvas is being displayed at 400x300 but it only has 300x150 pixels. Canvases have 2 sizes. One size is the size they are displayed set with CSS. The other is how many pixels which is usually set in code by setting canvas.width and canvas.height. The default number of pixels is 300x150

    If you actually want them to be different sizes then you need to take that into account in your mouse code. The correct code is

      getMouseCoordinatesCanvas(e, canvas) {
        let rect = canvas.getBoundingClientRect();
        let x = (e.clientX - rect.left) * canvas.width  / rect.width;
        let y = (e.clientY - rect.top)  * canvas.height / rect.height;
        return new Point(x, y);
      }
    

    const TOOL_LINE = 'line';
    
    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    }
    
    class Paint {
      constructor(canvasId) {
    
        this.canvas = document.getElementById(canvasId);
        this.context = canvas.getContext("2d");
      }
      set activeTool(tool) {
        this.tool = tool;
      }
      init() {
        this.canvas.onmousedown = e => this.onMouseDown(e);
      }
      onMouseDown(e) {
        this.saveData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
        this.canvas.onmousemove = e => this.onMouseMove(e);
        document.onmouseup = e => this.onMouseUp(e);
        this.startPos = this.getMouseCoordinatesCanvas(e, this.canvas);
      }
      onMouseMove(e) {
        this.currentPos = this.getMouseCoordinatesCanvas(e, this.canvas);
        switch (this.tool) {
          case TOOL_LINE:
            this.drawShape();
            break;
          default:
            break;
        }
      }
      onMouseUp(e) {
        this.canvas.onmousemove = null;
        document.onmouseup = null;
      }
      drawShape() {
        this.context.putImageData(this.saveData, 0, 0);
        this.context.beginPath();
        this.context.moveTo(this.startPos.x, this.startPos.y);
        this.context.lineTo(this.currentPos.x, this.currentPos.y);
        this.context.stroke();
      }
      getMouseCoordinatesCanvas(e, canvas) {
        let rect = canvas.getBoundingClientRect();
        let x = (e.clientX - rect.left) * canvas.width  / rect.width;
        let y = (e.clientY - rect.top)  * canvas.height / rect.height;
        return new Point(x, y);
      }
    }
    
    var paint = new Paint("canvas");
    paint.activeTool = TOOL_LINE;
    paint.init();
    
    document.querySelectorAll("[data-tools]").forEach(
      item => {
        item.addEventListener("click", e => {
          let selectedTool = item.getAttribute("data-tools");
          paint.activeTool = selectedTool;
    
        });
      }
    );
    #Container {
      background-color: lime;
      height: 310px;
    }
    
    .toolbox,
    #canvas {
      display: inline-block;
    }
    
    .toolbox {
      background-color: gray;
      padding: 0px 15px 15px 15px;
      left: 10px;
      top: 11px;
    }
    
    .group {
      margin: 5px 2px;
    }
    
    #line {
      transform: rotate(-90deg);
    }
    
    .ico {
      margin: 3px;
      font-size: 23px;
    }
    
    .item:hover,
    .item.active {
      background-color: rgba(160, 160, 160, 0.5);
      color: white;
    }
    
    #canvas {
      background-color: white;
      margin: 5px;
      float: right;
      width: 400px;
      height: 300px;
    }
    <script src="https://kit.fontawesome.com/c1d28c00bc.js" crossorigin="anonymous"></script>
    <div class="container">
      <div id="Container">
        <div class="toolbox">
          <center>
            <div class="group tools">
              <div class="item active" data-tools="line">
                <i class="ico far fa-window-minimize" id="line" title="Line"></i>
              </div>
            </div>
          </center>
        </div>
        <canvas id="canvas"></canvas>
      </div>
    </div>

    If you don't want them to be different sizes then you need to make the sizes match. I always set the size using CSS and then use code to make the canvas match that size.

    Something like this

    function resizeCanvasToDisplaySize(canvas) {
      const width = canvas.clientWidth;
      const height = canvas.clientHeight;
      const needResize = canvas.width !== width || canvas.height !== height;
      if (needResize) {
        canvas.width = width;
        canvas.height = height;
      }
      return needResize;
    }
    

    Note that anytime you change the canvas size it will get cleared but in any case.

    const TOOL_LINE = 'line';
    
    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    }
    
    function resizeCanvasToDisplaySize(canvas) {
      const width = canvas.clientWidth;
      const height = canvas.clientHeight;
      const needResize = canvas.width !== width || canvas.height !== height;
      if (needResize) {
        canvas.width = width;
        canvas.height = height;
      }
      return needResize;
    }
    
    class Paint {
      constructor(canvasId) {
    
        this.canvas = document.getElementById(canvasId);
        this.context = canvas.getContext("2d");
        resizeCanvasToDisplaySize(canvas);    
      }
      set activeTool(tool) {
        this.tool = tool;
      }
      init() {
        this.canvas.onmousedown = e => this.onMouseDown(e);
      }
      onMouseDown(e) {
        this.saveData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
        this.canvas.onmousemove = e => this.onMouseMove(e);
        document.onmouseup = e => this.onMouseUp(e);
        this.startPos = this.getMouseCoordinatesCanvas(e, this.canvas);
      }
      onMouseMove(e) {
        this.currentPos = this.getMouseCoordinatesCanvas(e, this.canvas);
        switch (this.tool) {
          case TOOL_LINE:
            this.drawShape();
            break;
          default:
            break;
        }
      }
      onMouseUp(e) {
        this.canvas.onmousemove = null;
        document.onmouseup = null;
      }
      drawShape() {
        this.context.putImageData(this.saveData, 0, 0);
        this.context.beginPath();
        this.context.moveTo(this.startPos.x, this.startPos.y);
        this.context.lineTo(this.currentPos.x, this.currentPos.y);
        this.context.stroke();
      }
      getMouseCoordinatesCanvas(e, canvas) {
        let rect = canvas.getBoundingClientRect();
        let x = (e.clientX - rect.left) * canvas.width  / rect.width;
        let y = (e.clientY - rect.top)  * canvas.height / rect.height;
        return new Point(x, y);
      }
    }
    
    var paint = new Paint("canvas");
    paint.activeTool = TOOL_LINE;
    paint.init();
    
    document.querySelectorAll("[data-tools]").forEach(
      item => {
        item.addEventListener("click", e => {
          let selectedTool = item.getAttribute("data-tools");
          paint.activeTool = selectedTool;
    
        });
      }
    );
    #Container {
      background-color: lime;
      height: 310px;
    }
    
    .toolbox,
    #canvas {
      display: inline-block;
    }
    
    .toolbox {
      background-color: gray;
      padding: 0px 15px 15px 15px;
      left: 10px;
      top: 11px;
    }
    
    .group {
      margin: 5px 2px;
    }
    
    #line {
      transform: rotate(-90deg);
    }
    
    .ico {
      margin: 3px;
      font-size: 23px;
    }
    
    .item:hover,
    .item.active {
      background-color: rgba(160, 160, 160, 0.5);
      color: white;
    }
    
    #canvas {
      background-color: white;
      margin: 5px;
      float: right;
      width: 400px;
      height: 300px;
    }
    <script src="https://kit.fontawesome.com/c1d28c00bc.js" crossorigin="anonymous"></script>
    <div class="container">
      <div id="Container">
        <div class="toolbox">
          <center>
            <div class="group tools">
              <div class="item active" data-tools="line">
                <i class="ico far fa-window-minimize" id="line" title="Line"></i>
              </div>
            </div>
          </center>
        </div>
        <canvas id="canvas"></canvas>
      </div>
    </div>

    A common reason to make them different sizes is to support HI-DPI displays. In that case though the mouse code can go back to the way it was if you use the canvas transform.

    function resizeCanvasToDisplaySize(canvas) {
      const width = canvas.clientWidth * devicePixelRatio | 0;
      const height = canvas.clientHeight * devicePixelRatio | 0;
      const needResize = canvas.width !== width || canvas.height !== height;
      if (needResize) {
        canvas.width = width;
        canvas.height = height;
      }
      return needResize;
    }
    

    and then set the transform before drawing

    ctx.scale(devicePixelRatio, devicePixelRatio);
    

    const TOOL_LINE = 'line';
    
    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    }
    
    function resizeCanvasToDisplaySize(canvas) {
      const width = canvas.clientWidth * devicePixelRatio | 0;
      const height = canvas.clientHeight * devicePixelRatio | 0;
      const needResize = canvas.width !== width || canvas.height !== height;
      if (needResize) {
        canvas.width = width;
        canvas.height = height;
      }
      return needResize;
    }
    
    class Paint {
      constructor(canvasId) {
    
        this.canvas = document.getElementById(canvasId);
        this.context = canvas.getContext("2d");
        resizeCanvasToDisplaySize(canvas);    
      }
      set activeTool(tool) {
        this.tool = tool;
      }
      init() {
        this.canvas.onmousedown = e => this.onMouseDown(e);
      }
      onMouseDown(e) {
        this.saveData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
        this.canvas.onmousemove = e => this.onMouseMove(e);
        document.onmouseup = e => this.onMouseUp(e);
        this.startPos = this.getMouseCoordinatesCanvas(e, this.canvas);
      }
      onMouseMove(e) {
        this.currentPos = this.getMouseCoordinatesCanvas(e, this.canvas);
        switch (this.tool) {
          case TOOL_LINE:
            this.drawShape();
            break;
          default:
            break;
        }
      }
      onMouseUp(e) {
        this.canvas.onmousemove = null;
        document.onmouseup = null;
      }
      drawShape() {
        this.context.setTransform(1, 0, 0, 1, 0, 0); // the default
        this.context.putImageData(this.saveData, 0, 0);
        this.context.scale(devicePixelRatio, devicePixelRatio);
        this.context.beginPath();
        this.context.moveTo(this.startPos.x, this.startPos.y);
        this.context.lineTo(this.currentPos.x, this.currentPos.y);
        this.context.stroke();
      }
      getMouseCoordinatesCanvas(e, canvas) {
        let rect = canvas.getBoundingClientRect();
        let x = (e.clientX - rect.left);
        let y = (e.clientY - rect.top);
        return new Point(x, y);
      }
    }
    
    var paint = new Paint("canvas");
    paint.activeTool = TOOL_LINE;
    paint.init();
    
    document.querySelectorAll("[data-tools]").forEach(
      item => {
        item.addEventListener("click", e => {
          let selectedTool = item.getAttribute("data-tools");
          paint.activeTool = selectedTool;
    
        });
      }
    );
    #Container {
      background-color: lime;
      height: 310px;
    }
    
    .toolbox,
    #canvas {
      display: inline-block;
    }
    
    .toolbox {
      background-color: gray;
      padding: 0px 15px 15px 15px;
      left: 10px;
      top: 11px;
    }
    
    .group {
      margin: 5px 2px;
    }
    
    #line {
      transform: rotate(-90deg);
    }
    
    .ico {
      margin: 3px;
      font-size: 23px;
    }
    
    .item:hover,
    .item.active {
      background-color: rgba(160, 160, 160, 0.5);
      color: white;
    }
    
    #canvas {
      background-color: white;
      margin: 5px;
      float: right;
      width: 400px;
      height: 300px;
    }
    <script src="https://kit.fontawesome.com/c1d28c00bc.js" crossorigin="anonymous"></script>
    <div class="container">
      <div id="Container">
        <div class="toolbox">
          <center>
            <div class="group tools">
              <div class="item active" data-tools="line">
                <i class="ico far fa-window-minimize" id="line" title="Line"></i>
              </div>
            </div>
          </center>
        </div>
        <canvas id="canvas"></canvas>
      </div>
    </div>

    note this line

    this.saveData = this.context.getImageData(0, 0, this.canvas.clientWidth, this.canvas.clientHeight);
    

    was wrong too. It should be

    this.saveData = this.context.getImageData(0, 0, this.canvas.width, this.canvas.height);
    

    clientWidth and clientHeight are the display size. width and height are the resolution (number of pixels in the canvas)