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

Mastering the Rejection of Promises in Javascript with Graceful Elegance

One effective pattern using ES2017 async/await involves: async function () { try { var result = await some_promised_value() } catch (err) { console.log(`This block will be processed in a reject() callback with promise patterns, which is far mo ...

If the <option> "anyTableName" </option> is chosen, then display the column names of the selected table (PHP, MySQL)

Hey there, I'm a newbie on stackoverflow so feel free to correct me if I'm off base ;) Here's my current dilemma: I have a text.php file that contains 2 <select> elements. The first one allows me to choose a table (like "accounts", "c ...

How to declare a variable using new String() and s = '' in Typescript/Javascript

What is the correct way to declare an array of characters or a string in JavaScript? Is there a distinction between an array of characters and a string? let operators = new String(); or let operators = ''; ...

Insert fresh user information into the div

Learning JavaScript is a challenge I'm tackling. I have a question that may seem trivial, but any assistance would be greatly appreciated. I currently have this code: Javascript <script type="text/javascript"> function fn(){ var Name = ...

Is it possible to alter the state of a controlled value in React without relying on the setValue function?

Check out the Codesandbox link here for more information: https://codesandbox.io/s/magical-black-vp1r0?file=/src/dropdown-selector.js I am currently working on a unique component and I am unsure if it is achievable or not. Your insights would be greatly a ...

Having trouble accessing JSON file again: "Encountered unexpected end of input error"

I have set up a cron-based scheduler to periodically retrieve JSON data from an external API every 2 minutes. The process involves writing the data to a file, reading it, cleaning it, and then storing it in a MongoDB collection. Everything works smoothly o ...

Attempting to implement Vue js extensions without relying on NPM or webpack

The issue Currently, I am trying to follow the jqWidgets guidelines provided in the link below to create a dropdown box. However, the challenge I'm facing is that their setup involves using the IMPORT functionality which is restricted by my tech lead ...

How can I align the isometric camera in THREE.js to the center of a cube?

Hey there, I'm working on a tricky exercise involving setting up a camera and cube with THREE.js. The goal is to maintain the cube's initial ratio while ensuring the camera is focused on the center of the cube. Although I believe I've succes ...

Obtain the value of v-model in a child component within VueJS

In my Vuetify App, I have implemented a Vue2Editor using a custom component called text-editor. Here is how it looks: <vue-editor :value="text" @input="updateText" ></vue-editor> The props for this component are defined as follows: props ...

Troubleshooting issues with AngularJS $watch not triggering properly

Even though Deep watch has been activated in the factory, it is not triggering. What steps can be taken to resolve this issue and ensure that an event is triggered when the 'name' value changes? Javascript Code: var app = angular.module('a ...

Is it possible to retrieve values from my model when working with Django Templates and incorporating them in the JavaScript header?

With Django, I have managed to successfully retrieve and display data from my model in the content section of the template. However, I am facing issues retrieving data from the model in the header section of the template. Below is the code --> view.py ...

mongojs implementation that allows for asynchronous query execution without blocking

Forgive me for asking what may seem like a silly question, but I am struggling to make this work. Currently, as part of my learning journey with node.js and mongojs, I have encountered the following issue: Below is my server.js file server.get("/", funct ...

Retrieve the string data from a .txt document

I am facing an issue with my script that retrieves a value from a .txt file. It works perfectly fine when the value is a number, but when trying to fetch text from another .txt file, I encounter the "NaN" error indicating it's not a number. How can I ...

Listening to LayersControl.Overlay checkbox clicks in React-Leaflet: A guide

My goal is to dynamically render overlay controls and bind a click event listener to the checkbox of each control in React. However, I am unsure how to provide a React ref to LayersControl or an onClick handler to LayersControl.Overlay. Is there a more eff ...

Tips on retrieving an input value from a dynamic list

I am struggling to retrieve the correct value with JavaScript as it always shows me the first input value. Any help would be greatly appreciated! Thank you in advance! <html> <head> </head> <body> <?php while($i < $forid){ ...

Issue with Material-UI DataGrid Component: Unable to access property 'length' as it is undefined

I need to display my JavaScript Object Data in a table format with pagination and sorting capabilities. I have chosen the DataGrid component from Material UI, but I am encountering some errors. Below is the code snippet: import React from 'react&apos ...

Looking to prevent editing on a paragraph tag within CKEditor? Simply add contentEditable=false to the

Within my CKEditor, I am in need of some predefined text that cannot be edited, followed by the rest of my content. This involves combining the predefined verbiage (wrapped in a p tag) with a string variable displayed within a specific div in CKEditor. The ...

Ensure that the icon at the conclusion of the <p> tag remains intact and does not break off on its

Issue at hand: The clock icons on my phone screen sometimes break off, see example here: https://i.sstatic.net/NQz9i.png Preferred outcome: The icons should be intact along with the time value, like this: https://i.sstatic.net/nZmC9.png Here is the H ...

Tips on eliminating the gap between the dropdown menu in Firefox browser

After testing in Chrome and seeing no issues, I noticed that the input field's position changes and there is spacing between the dropdowns when opening in Firefox. View in Firefox browser: https://i.stack.imgur.com/WYpuy.png View in Chrome browser: ...

Identifying with a jQuery IF statement all input fields that contain null values in order to eliminate the respective elements

My goal is to create a jQuery script that checks all input fields to see if they are empty. If an input field is empty, I want to remove the child image of the next occurring span element. Here is what I have attempted so far: if ($("input").val() == "") ...