Why are my cursor and my drawing line on opposite sides?

I've been working on a JavaScript drawing app, but I'm facing an issue where the drawn elements are not aligned with my cursor. The positioning seems off, especially when moving to the right or up on the canvas. As I move towards the furthest left or bottom side of the canvas, the alignment improves and matches the cursor position accurately. Can someone help me troubleshoot this?

const canvas = document.getElementById("canvas");
const increaseBtn = document.getElementById("increase");
const decreaseBtn = document.getElementById("decrease");
const sizeEl = document.getElementById("size");
const colorEl = document.getElementById("color");
const clearEl = document.getElementById("clear");

//Core Drawing Functionality (with some research)

const ctx = canvas.getContext("2d");

let size = 5;
let isPressed = false;
let color = "black";
let x;
let y;
let fakeSize = 1;

canvas.addEventListener("mousedown", (e) => {
  isPressed = true;
  x = e.offsetX;
  y = e.offsetY;
});

canvas.addEventListener("mouseup", (e) => {
  isPressed = false;
  x = undefined;
  y = undefined;
});

canvas.addEventListener("mousemove", (e) => {
  if (isPressed) {
    const x2 = e.offsetX;
    const y2 = e.offsetY;

    drawCircle(x2, y2);
    drawLine(x, y, x2, y2);

    x = x2;
    y = y2;
  }
});

function drawCircle(x, y) {
  ctx.beginPath();
  ctx.arc(x, y, size, 0, Math.PI * 2);

  ctx.fillStyle = color;
  ctx.fill();
}

function drawLine(x1, y1, x2, y2) {
  ctx.beginPath();
  ctx.moveTo(x1, y1);
  ctx.lineTo(x2, y2);
  ctx.strokeStyle = color;
  ctx.lineWidth = size * 2;
  ctx.stroke();
}

function updateSizeOnScreen() {
  sizeEl.innerHTML = fakeSize;
}

increaseBtn.addEventListener("click", () => {
  size += 5;
  fakeSize++;
  if (fakeSize > 10) {
    fakeSize = 10;
  }

  if (size > 50) {
    size = 50;
  }

  updateSizeOnScreen();
});

decreaseBtn.addEventListener("click", () => {
  size -= 5;
  fakeSize--;
  if (fakeSize < 1) {
    fakeSize = 1;
  }

  if (size < 5) {
    size = 5;
  }

  updateSizeOnScreen();
});

colorEl.addEventListener("change", (e) => {
  color = e.target.value;
});

clearEl.addEventListener("click", () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
});

//Eraser and Pencil Actions (my own algorithm)

const eraser = document.getElementById("eraser");
const pencil = document.getElementById("pencil");

eraser.addEventListener("click", () => {
  localStorage.setItem("colorEl", JSON.stringify(color));
  color = "#fff";
  colorEl.disabled = true;
  canvas.classList.add("eraseractive");
  eraser.classList.add("eraseractive");
  colorEl.classList.add("eraseractive");
  canvas.classList.remove("pencilactive");
  eraser.classList.remove("pencilactive");
  colorEl.classList.remove("pencilactive");
});

pencil.addEventListener("click", () => {
  JSON.parse(localStorage.getItem("colorEl"));
  color = colorEl.value;
  colorEl.disabled = false;
  canvas.classList.remove("eraseractive");
  eraser.classList.remove("eraseractive");
  colorEl.classList.remove("eraseractive");
  canvas.classList.add("pencilactive");
  eraser.classList.add("pencilactive");
  colorEl.classList.add("pencilactive");
});

// Dark/Light Mode

const darkMode = document.getElementById("darkMode");
const lightMode = document.getElementById("lightMode");
const toolbox = document.getElementById("toolbox");

darkMode.addEventListener("click", () => {
  darkMode.classList.add("mode-active");
  lightMode.classList.remove("mode-active");
  lightMode.classList.add("rotate");
  darkMode.classList.remove("rotate");
  toolbox.style.backgroundColor = "#293462";
  document.body.style.backgroundImage =
    "url('/assets/images/darkModeBackground.svg')";

  document.body.style.backgroundSize = "1920px 1080px";
  canvas.style.borderColor = "#293462";
  toolbox.style.borderColor = "#293462";
});

lightMode.addEventListener("click", () => {
  lightMode.classList.add("mode-active");
  darkMode.classList.remove("mode-active");
  darkMode.classList.add("rotate");
  lightMode.classList.remove("rotate");
  toolbox.style.backgroundColor = "#293462";
  document.body.style.backgroundImage =
    "url('/assets/images/lightModeBackground.svg')";

  document.body.style.backgroundSize = "1920px 1080px";
  canvas.style.borderColor = "#293462";
  toolbox.style.borderColor = "#293462";
});
* {
  box-sizing: border-box;
  font-size: 20px !important;
}

body {
  background: url("https://drawing-app-green.vercel.app/assets/images/lightModeBackground.svg");
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  margin: 0;
  position: relative;
  max-height: 100vh;
  overflow: hidden;
}

::selection {
  background: transparent;
}

::-moz-selection {
  background: transparent;
}

.mode {
  display: flex;
  position: absolute;
  top: 10px;
  right: 25px;
  cursor: pointer;
}

.light-mode {
  color: yellow;
}

.dark-mode {
  color: #16213e;
}

.container {
  display: flex;
  flex-direction: column;
  max-width: 1200px;
  width: 100%;
  max-height: 600px;
  height: 100%;
}

canvas {
  display: flex;
  border: 2px solid #293462;
  cursor: url("https://drawing-app-green.vercel.app/assets/images/pencilCursor.png") 2 48, pointer;
  background-color: #fff;
  margin-top: 3rem;
  width: 100%;
  height: 600px;
}

.toolbox {
  background-color: #293462;
  border: 1px solid #293462;
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: center;
  padding: 0.2rem;
}

.toolbox > * {
  background-color: #fff;
  border: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 2rem;
  height: 30px;
  width: 30px;
  margin: 0.25rem;
  padding: 0.25rem;
  cursor: pointer;
}

.toolbox > *:last-child {
  margin-left: auto;
}

canvas.eraseractive {
  cursor: url("https://drawing-app-green.vercel.app/assets/images/eraserCursor.png") 2 48, pointer;
}

#color.eraseractive {
  cursor: not-allowed;
}

canvas.pencilactive {
  cursor: url("https://drawing-app-green.vercel.app/assets/images/pencilCursor.png") 2 48, pointer;
}

.mode-active {
  visibility: hidden;
}

.rotate {
  transform: rotate(360deg);
  transition: transform 1s linear;
}
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Drawing App</title>
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
      integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
      crossorigin="anonymous"
      referrerpolicy="no-referrer"
    />
  </head>

  <body>
    <i class="fa-solid fa-moon dark-mode fa-2x mode" id="darkMode"></i>
    <i
      class="fa-solid fa-sun light-mode fa-2x mode mode-active"
      id="lightMode"
    ></i>
    <div class="container">
      <canvas id="canvas" width="1024" height="600"></canvas>
      <div class="toolbox" id="toolbox">
        <button id="decrease">-</button>
        <span id="size">1</span>
        <button id="increase">+</button>
        <input type="color" id="color" />
        <button id="pencil">
          <img src="assets/images/pencilCursor.png" alt="" />
        </button>
        <button id="eraser">
          <img src="assets/images/eraserCursor.png" alt="" />
        </button>
        <button id="clear">X</button>
      </div>
    </div>
    <script src="assets/js/script.js"></script>
  </body>
</html>

Answer №1

The issue you are facing is due to the mismatch in dimensions between your canvas and the HTML element that contains it. Your canvas has fixed width and height attributes, while the canvas element in your HTML has a width of 100%. This discrepancy means that the container's dimensions may vary but the canvas inside remains constant, leading to problems with accurately determining the clicked pixel.

You have two potential solutions:

Option 1: Adjust click position accounting for canvas deformation

If you want your canvas to resize dynamically, calculate the actual position using a ratio formula. For instance, if your canvas is 100 pixels wide but its container is only 10 pixels wide, clicking at pixel 5 should result in a dot being drawn at pixel 50. Essentially, you need to multiply the position by the factor of size difference.

In your code, this adjustment would look something like this:

// In lines 33 and 34, multiply the offset by the ratio between canvas size and container size
const x2 = e.offsetX * (canvas.width / ctx.canvas.getBoundingClientRect().width);
const y2 = e.offsetY * (canvas.height / ctx.canvas.getBoundingClientRect().height);


Option #2: Prevent canvas deformation

To address this issue, remove the container class and the width:100% from your canvas CSS. This will cause the canvas to overflow and create a scrollbar, but the pixel positions will be calculated correctly within your code.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best way to utilize Protractor to comb through a specific class, locate a p tag within that class, and validate certain text within it?

My current task involves using Protractor to locate a specific class and then search through all the p tags within that class to identify any containing the text "Glennville, GA". In my spec file, I have attempted the following steps: it('should fil ...

Turn off scrolling for the body content without removing the scrollbar visibility

Whenever I click a thumbnail, a div overlay box appears on top: .view-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; overflow-y:scroll; background-color:rgba(0,0,0,0.7); z-index:999; } Some browsers with non-f ...

Animating with JQuery utilizing a dynamic attribute

We are facing a challenge with animating multiple divs that share the same class name and have different attribute values: <div class="chart-bar" style="width:10%;" bar-width="100%"></div> <div class="chart-bar" style="width:10%;" bar-wid ...

Background image not displaying

My web page has a background image set using this CSS: body, html { background-image: url(Content/Images/bg-lounge-2-l.jpg); background-repeat: repeat; background-attachment: fixed; /*background-position: 0 -390px;*/ } ...

Tips for using Jquery to round up currency values

Is there a way to round up various currencies using jQuery? I have a specific requirement: 10.30 → 10 //Round down if below .5 10.60 → 11 //Round up if after .5 849.95 → 850 1,022.20 → 1022 ...

Troubleshooting: Node.js Express Server GET Handler Failing to Function

Recently, I've been attempting to build a GET request handler in Express.js. Here's the snippet of code I've put together: // include necessary files and packages const express = require('./data.json'); var app = express(); var m ...

Is there a way for me to increment the value of 'sessionStorage.fgattempt' whenever either 'fgMade()' or 'threeMade()' are called?

Currently, I am developing a basketball game stat tracker and need to update the field goal attempts every time I make a field goal or three-pointer. Additionally, I am looking for ways to optimize the javascript code provided. The main requirement is to ...

Discover multiple keys within a new Map object

Our usual approach involves creating a new Map like this: const hash = new Map() hash.set(key,value) To retrieve the information, we simply use: hash.get(specificKey) An advantage of using Map is that we have flexibility in choosing keys and values. Cur ...

How to Use Jquery to Delete a Specific String

I've been attempting to remove certain strings from a string using Jquery, however it seems like the code isn't working and my variable remains unchanged. var classes = $("body").attr('class'); alert('.side-nav' + classes); c ...

The face textures are not being applied correctly to the model imported in THREE.js

I'm having trouble importing a model exported from blender using the THREEJS exporter. The model loads correctly in my scene with the materials applied, such as the car appearing yellow and the glass being transparent as intended. However, I am facin ...

What is the best way to divide a GraphQL schema to avoid circular dependencies?

I have a question that is similar to the issue of circular dependency in GraphQL code discussed on Stack Overflow, but my problem lies within JavaScript (ES6). The size of my schema definition has become too large, and I am struggling to find a way to bre ...

Error encountered during react parsing: An unexpected '#' error was encountered when using the react-router-sitemap npm package

I'm currently working on building a sitemap using the react-router-sitemap npm package. After installing all the necessary dependencies related to react-router-sitemap, I created a file called sitemap-generator.js and added the following code. router ...

What is the best way to adjust the specific scope of every element generated by ng-repeat within a directive?

Attempting to simplify tables in Angular, I am working on a directive. My goal is for the user to only need to provide the list object and the formatting of each list element in the HTML, using the my-table tag as shown below... <h3>My Table</h3& ...

Resetting CSS animations using animation-delay

My aim is to animate a series of images on my landing page using Next.js and SCSS. The issue arises when I try to add dynamic animations that reset after each cycle. The delay in the animation causes the reset to also be delayed, which disrupts the flow. I ...

I constantly encounter a TypeError message that returns as undefined

I'm currently facing an issue while trying to run my Node.js server locally. The error message I keep receiving is: /Users/rogerjorns/Desktop/templateassignment/node_modules/express/lib/router/index.js:458 throw new TypeError('Router.use() ...

What is the process for retrieving the component along with the data?

As I work on compiling a list of div elements that require content from my firebase cloud storage, I find myself unsure about how to effectively run the code that retrieves data from firebase and returns it in the form of MyNotes. Consider the following: ...

variable identifier looping through elements

I'm attempting to assign a dynamic ID to my div using ng-repeat. Here's an example: <div id="$index" ng-repeat="repeat in results.myJsonResults"> <div id="$index" ng-click="saveID($index)" ng-repeat="subRepeat in results.myJsonResul ...

Is it possible to leverage Create-React-App's npm start in conjunction with Node.js?

I've recently started diving into React and node.js. My goal is to have a node.js server that can serve up React.js pages/views. After running 'create-react-app' and then using 'npm start', I'm not sure if I also need to man ...

Utilizing a large logo alongside the NavBar in Bootstrap

Trying to design a sleek navbar that is proportional to the logo size, while also allowing for additional content below it. The jsFiddle link below showcases the current setup. The black line signifies the bottom of the nav bar, which has expanded to matc ...

What is the best way to extract text from a list item in an unordered list (

I am trying to search a ul list that populates a ddSlick dropdown box. ddSlick adds pictures to list items, making it a visually appealing choice. To see more about ddSlick, you can visit their website here: Here is the code I am using to loop through the ...