The pencil-drawn pixel on the canvas is positioned off-center

Currently, I am using p5.js to draw pixels on canvas with a pencil tool. However, I have encountered an issue where the pixel does not appear centered when the size of the pencil is set to 1 or smaller. It seems to be offset towards the left or right.

Upon analyzing our code, I made an interesting observation. It seems that when the mouse's x and y positions are close to .5, the drawn pixel appears centered; but in other cases, it appears slightly off-center. To address this issue, I plan to enhance our rendering logic by ensuring the pixel density is set to 1 to avoid any scaling problems. Additionally, I will explore adjusting the pixel position by introducing a correction factor, potentially subtracting 0.5 from both x and y coordinates. It's important to review any transformations or scaling operations in our code that may affect the precise positioning of pixels.

I have also noticed that P5.image.set method does not accept floating-point numbers; instead, it rounds to the nearest integer. Are there any workarounds for this limitation, or have I overlooked something in the documentation?

Here you can find the code. And here is the link to the deployed application.

To replicate the issue, follow these steps:

  1. Open the provided link
  2. Zoom in (using mouse press and wheel)
  3. Select the pencil tool
  4. Click on the canvas with your mouse

Thank you in advance for your assistance. Feel free to leave any comments if you need further clarification.

Below is an excerpt of the 'mousepressed' method in which we set the pixel values within a loop:

mousePressed(mouseX, mouseY) {
      if (this.menuSelection === 'drawPencil') {
        this.mapImage.loadPixels()
        let size = 1
        let y = mouseY
        for (let i = mouseX - size / 2; i < mouseX + size / 2; i++) {
          for (let j = y - size / 2; j < y + size / 2; j++) {
            this.mapImage.set(i, j, this.p5.color(255))
            //console.log('i,j', i, j)
          }
        }
        this.mapImage.updatePixels()
        return
      }
    },

https://i.stack.imgur.com/W3EUS.png

Answer №1

You mentioned:

It appears that the P5.image.set method does not accept decimal numbers; it automatically rounds to the nearest whole number. Is there a workaround for this issue, or did I overlook something in the documentation?

As a result, it is necessary to always truncate the values,

For instance, 0.9 should be set to 0 instead of rounding to 1. Therefore, any floating-point number must be truncated before being passed to the P5.image.set method.

https://i.stack.imgur.com/HPmGY.png

Consider this scenario: when an image is scaled up to fit the screen, clicking on the green area may select pixel A instead of B (due to rounding the float values). If you manually truncate the values, it will select B instead.

Answer №2

Whenever the user clicks on draw, a scrolling action occurs to position it based on the mouse's location on the screen rather than the canvas.

p5.mousePressed = () => {
        ;(this.x_value = p5.mouseX), (this.y_value = p5.mouseY)
        // if (this.firstTime) {
        this.mx = p5.mouseX
        this.my = p5.mouseY
        this.firstTime = false
        // }

        this.mousePressedFlag = true
        this.mousePressed(p5.mouseX, p5.mouseY)
      }
mousePressed(mouseX, mouseY) {
      if (this.menuSelection === 'drawPencil') {
        
        this.mapImage.set(mouseX, mouseY, this.p5.color(255))
        this.mapImage.updatePixels()
        return
      }
    },

Revise these functions and ensure that they function as intended.

Revised Version

I've made some modifications using a slightly different approach, and it works well as per your requirements. However, there are some issues that need to be fixed due to the code rewrite.

import p5 from 'p5'

export default {
  data() {
    return {
      imageLoad: null,
      menuSelection: null,
      mapImage: null,
      x_value: 0,
      y_value: 0,
      mousePressedFlag: false,
      sf: 1,
      mx: 0,
      my: 0,
      zoom: 1,
      loadImageWidth: 1500,
      loadImageHeight: 700,
      arrZoomScale: [
        0.1, 0.25, 0.33, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 3, 3.5, 4, 4.5, 5, 6, 7, 8,
        10, 12
      ],
      viewPort: {
        width: 0,
        height: 0
      },
      points: [],
      firstTime: true
    }
  },
  mounted() {
    let p5Handler = (p5) => {
      p5.preload= () => {
        this.imageLoad = p5.loadImage('image.png', (img) => {
          p5.resizeCanvas(img.width, img.height)
        })
      }
      p5.setup = () => {
        const canvas = p5.createCanvas(1500, 700)
        canvas.parent(this.$refs.p5Container)
        p5.imageMode(p5.CENTER)
        p5.noStroke()
      }
      p5.draw = () => {
        this.draw(p5)
      }
      p5.mouseClicked = (event) => {
        ;(this.x_value = event.clientX), (this.y_value = event.clientY)
        // if (this.firstTime) {
        this.mx = (event.clientX  - p5.width / 2) / this.zoom
        this.my = (event.clientY  - p5.height / 2) / this.zoom
        this.firstTime = false
        // }

        this.mousePressedFlag = true
        this.mousePressed(event.clientX, event.clientY)
      }

      p5.mouseWheel = (event) => {
        this.mouseWheel(event)
      }
    }
    let P5 = p5
    this.p5 = new P5(p5Handler)
  },
  methods: {
    draw(p5) {
      p5.background(255);
      p5.translate(p5.width / 2, p5.height / 2);
      p5.scale(this.zoom);
      p5.image(this.imageLoad, 0, 0);

      this.points.forEach(({ x, y, size }) => {
        p5.fill(255, 255, 255)
        p5.rect(x, y, size)
      })

      if (this.menuSelection === 'drawPencil') {
        p5.translate(this.mx, this.my)
        p5.translate(-this.mx, -this.my)
        p5.translate()

        p5.cursor(p5.CROSS)
        p5.strokeWeight(0.3 / this.zoom)
        p5.noFill()
        p5.stroke(69, 69, 69)
        p5.rect((p5.mouseX - p5.width / 2) / this.zoom, (p5.mouseY - p5.height / 2) / this.zoom, 1)
      }
    },
    mousePressed(mouseX, mouseY) {
      const cMouseX = (mouseX - this.p5.width / 2) / this.zoom
      const cMouseY = (mouseY - this.p5.height / 2) / this.zoom
      this.points.push({ x: cMouseX, y: cMouseY, size: 1 })
    },
    mouseWheel(event) {
      
      if (this.mousePressedFlag) {
        if (event.deltaY > 0) {
          this.zoomOut()
        } else {
          this.zoomIn()
        }
      }
    },
    zoomIn() {
      for (let zoom of this.arrZoomScale) {
        if (zoom > this.zoom) {
          this.setZoom(zoom, 'zoomIn')
          break
        }
      }
    },
    zoomOut() {
     
      this.arrZoomScale.sort((a, b) => a - b);

      
      const index = this.arrZoomScale.findIndex(scale => scale >= this.zoom) - 1;

      if (index >= 0) {
        const newZoom = this.arrZoomScale[index];
       
        this.setZoom(newZoom, 'zoomOut');
      }
    },
    setZoom(newZoom, val) {
      if (val === 'zoomIn') {
        this.sf = newZoom
      } else if (val === 'zoomOut') {
        this.sf = newZoom
      }
      this.zoom = newZoom
    },
  }
}

These resources provided valuable assistance:

Answer №3

Don't forget to insert the code snippet below:

p5.mouseMoved = (event) => {
   this.mouseMoved(event)
}

right after

p5.mouseWheel = (event) => {
   this.mouseWheel(event)
}

Then, make the following modifications to the methods block:

methods: {
    draw(p5) {
      let ctx = p5;
      if (this.mousePressedFlag) {
        p5.translate(this.mx, this.my);
        p5.scale(this.sf);
        p5.translate(-this.mx, -this.my);
      }

      this.drawMapImage(ctx); // passing ctx as an argument
      if (this.menuSelection === 'drawPencil') {
        this.p5.cursor(this.p5.CROSS);
        p5.strokeWeight(0.3 / this.zoom);
        p5.noFill();
        p5.stroke(69, 69, 69);
        p5.rect(p5.mouseX - 0.5, p5.mouseY - 0.5, 1, 1); // Adjusting pixel position
      }
    },
    mousePressed(mouseX, mouseY) {
      if (this.menuSelection === 'drawPencil') {
        this.mapImage.loadPixels();
        let size = 1;
        let y = mouseY;
        for (let i = mouseX - size / 2; i < mouseX + size / 2; i++) {
          for (let j = y - size / 2; j < y + size / 2; j++) {
            this.mapImage.set(Math.floor(i), Math.floor(j), this.p5.color(255)); // Using Math.floor for integer coordinates
          }
        }
        this.mapImage.updatePixels();
        return;
      }
    },
    mouseMoved() {
      if (this.mousePressedFlag) {
        this.setZoomCenter(this.p5.mouseX, this.p5.mouseY);
      }
    },
    mouseWheel(event) {
      if (this.mousePressedFlag) {
        let zoomChange = event.deltaY > 0 ? -0.1 : 0.1; // Adjust zoom factor as needed
        this.setZoom(this.zoom + zoomChange);
        this.setZoomCenter(this.p5.mouseX, this.p5.mouseY);
      }
    },
    drawMapImage(p5) {
      let ctx = this.mapImage.drawingContext
      ctx.imageSmoothingEnabled = false
      this.imageContext = ctx

      ctx = p5.canvas.getContext('2d')
      ctx.imageSmoothingEnabled = false
      this.htmlContext = ctx

      p5.push()

      p5.image(
        this.mapImage,
        0,
        0,
        this.viewPort.width / this.zoom,
        this.viewPort.height / this.zoom
      )
      p5.pop()
    },
    zoomIn() {
      for (let zoom of this.arrZoomScale) {
        if (zoom > this.zoom) {
          this.setZoom(zoom, 'zoomIn')
          break
        }
      }
    },
    zoomOut() {
      for (let i = this.arrZoomScale.length - 1; i >= 0; i--) {
        let zoom = this.arrZoomScale[i]
        if (zoom < this.zoom) {
          this.setZoom(zoom, 'zoomOut')
          break
        }
      }
    },
    setZoom(newZoom) {
      this.sf = newZoom;
      this.zoom = newZoom;
      this.recalViewPort();
    },
    setZoomCenter(x, y) {
      this.mx = x;
      this.my = y;
    },
    recalViewPort() {
      this.viewPort.width = this.zoom * this.loadImageWidth
      this.viewPort.height = this.zoom * this.loadImageHeight
    }
  }

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 determine if a value stored in localStorage is present in an

After reading this particular response, I learned that I should utilize $.inArray. Following this advice, my code looks like this: var curPostId = $(".my_post_id").attr("data-id"); if($.inArray(curPostId, lines)) { $('#'+localStorage.getItem( ...

Guide for creating a function that accepts an array containing multiple arrays as input

I am working with a function called drawSnake and it is being invoked in the following manner: drawSnake( [ [0, 0], [0, 1], [0, 2], [0, 3], [0, 4], ] ); How should I format the input for this function? I have attempted using Array<Array<[numb ...

What is the best way to align an element next to another using id or class?

I am looking to align the search element next to either "Media Heading 1" or "Media Heading 2" based on their id/class. For instance: Assume I have an element with the class of "media-item-1" and I aim to position the search div alongside that element us ...

What is the best way to add a border around an image along with a button using VueJS?

I am struggling to link a button and an image in VueJS to display a border around the picture. While I can successfully display the border on the button, I am unsure how to extend it to the image as well. Vue.component('my-button', 'my-img& ...

Output the information retrieved from the Axios call

How can I display data fetched from axios in a table? In my controller: public function index() { return User::latest(); } Inside user.vue: export default { data() { return { users: {}, } }, methods: { loadUsers() { a ...

Tips for updating the appearance of a specific column in React Native

I've been working on creating a matrix-style table design to show available seats in a bus! I'm iterating through 2D and 3D arrays to achieve this layout! Here is an image of the current output: In the image, you can see that the first row cons ...

Optimal method for retrieving data from asynchronous functions in JavaScript

Currently, I am using the twit library for nodejs which has async calls. In my code, I have created functions like the following: function getUserFromSearch(phrase) { T.get('search/tweets', { q: phrase+' lang:pt', count: 1 }, funct ...

[Vue alert]: Issue in created function: "TypeError: Unable to assign value to undefined property"

I'm currently in the process of developing a VueJS application where I have implemented a child component called Child.vue that receives data from its parent. Child.vue export default{ props:['info'], data: function(){ ...

Is there a feature in impress.js that allows for navigating to the following slide easily?

Impress.js is an innovative web presentation tool created with Javascript. I am interested in setting up custom events to navigate through the slides. This could involve adding buttons for "Next" and "Previous", for example. The impress.js file itself fun ...

What could be causing the Angular router outlet to not route properly?

Check out this demo showcasing 2 outlets (Defined in app.module.ts): <router-outlet></router-outlet> <router-outlet name="b"></router-outlet> The specified routes are: const routes: Routes = [ { path: 'a', com ...

The Evolution of Bulma's Navigation Menu

Creating a transparent menu in Bulma has been successful for the desktop viewport: VIEW DESKTOP MENU However, when attempting to implement the same design on mobile, the menu ends up like this: VIEW MOBILE/TABLET MENU The mobile version seems to inheri ...

(Spotify Web API) Issue with Creating New Playlist - Received Error 403 (Forbidden) upon POST request

Looking for guidance on creating a new playlist using the Web API? Check out the notes here: When making a POST request to https://api.spotify.com/v1/users/{user_id}/playlists, make sure you include the access token and data with a content type of 'a ...

The module 'SharedModule' has imported an unexpected value of 'undefined'

When working with an Angular application, I want to be able to use the same component multiple times. The component that needs to be reused is called DynamicFormBuilderComponent, which is part of the DynamicFormModule. Since the application follows a lib ...

Press the button to activate the function

<table class="calc" cellpadding=2> <td><input type="button" class="calc" id="screen" value="0" ></td> <td><input type="button" class="calc" id="screen" value="0" ></td> <tr> </ ...

Checking the status of a checkbox after submitting using AngularJs

For my first application, I am utilizing AngularJs and JavaScript to display information from an API as checkboxes. Currently, the functionality is working well, but I am unsure how to validate if any checkbox options are checked with a submit button. Aft ...

javascript include new attribute adjustment

I am working with this JavaScript code snippet: <script> $('.tile').on('click', function () { $(".tile").addClass("flipOutX"); setTimeout(function(){ $(".tile-group.main").css({ marginLeft:"-40px", widt ...

Unable to append item to document object model

Within my component, I have a snippet of code: isLoaded($event) { console.log($event); this.visible = $event; console.log(this.visible); this.onClick(); } onClick() { this.listImage = this.imageService.getImage(); let span = docu ...

Transferring Information Between Vue Components

In my project, I have implemented 3 Vue components to work together seamlessly. Component A is responsible for listening to an event triggered by a user clicking on HTML text and storing the data into a variable. Component B utilizes this data to make an A ...

Ensure the Firebase real-time database in Javascript purges the active session upon tab or browser closure

I need to implement a feature in my Firebase real-time database project using JavaScript where the current session is logged out automatically after closing the tab or browser. When I log in with my email and password, if I copy the URL and paste it into ...

Guide on utilizing the carousel component in Bootstrap and populating it with data generated from Node.js

My goal is to design a carousel that displays 5 different pieces of information pulled from a separate app.js file. I attempted to implement a forEach loop, but encountered issues when trying to create a second Bootstrap carousel container. Here's th ...