Ignacio Zsabo

Calculate position of expanded Canvas thumbnail

Calculate position of expanded Canvas

5 minutes of read
Canvas
CSS
PixelArt

Hello again, recently I was exploring my creative phase and I started to play with the idea of bringing a UI/UX experience to a Pixel Art Tool, I have been working on this for a long time and although it is not ready for launch I will publish how it has been my journey carrying out this idea.

Motivation

The idea was to have a canvas with the small measurements that pixel arts tend to have but scaling our canvas to fit the UI, the first thing that came to my mind to solve this was to use a virtual resolution and perform conversion of measurements with each change or render made, I managed to make a POC of this but I could notice that it would be quite trivial and unscalable to handle all the circumstances that could arise, it was also possible to suffer performance problems in the future even if it was only rendering frames, these should be painted at 60fps to give a good user experience, so I discarded this idea.

The next thing I thought of was what worked for me, handle it from a more CSS point of view, create a wrapper for our canvas element which could for example be 24x24 and extend this wrapper to fit our interface and voila, it worked!

Example

We declare our canvas in our html (in this case I’m using vanilla Javascript and HTML), this canvas will have the actual size based on the coordinates we plan to handle in this example 24x24

<canvas id="resized-canvas" width="24" height="24"></canvas>

In order to expand to a larger screen size we must wrap our canvas with a container to which we will give the styles to expand it

<div class="canvas-wrapper">
  <canvas id="resized-canvas" width="24" height="24"></canvas>
</div>

We add our styles to the wrapper of our canvas to expand it to the resolution we want for example i expanding to 200px and we also add styles to the canvas to expand it relative to its parent by 100%

.canvas-wrapper {
  width: 200px;
  height: 200px;
}

#resized-canvas {
  width: 100%;
  height: 100%;
}

I have added a border to differentiate our white canvas and know where it is on the screen, it should look like this:

There we can see that we managed to expand our canvas to the size of the parent

In this way if we try to get the coordinates of our cursor in our canvas we find that they do not match and by far!

This is because we need to balance that our lower resolution canvas is now being displayed at a higher resolution set by its parent.

To do this in vanilla I have set up a move listener on the canvas by attaching a getCoords function that we will use to calculate the coordinates of our canvas:

const canvasEl = document.getElementById("resized-canvas");

function getCoords(event) {
...
}

canvasEl.addEventListener("mousemove", getCoords);

The first thing we need to do is get our desired resolution set in the html of our canvas. In this case, let’s remember it’s 24x24 in pixels:

const computedHeight = canvasEl.height;
const computedWidth = canvasEl.width;

we get the cursor position on our canvas from the event:

const { clientX, clientY } = event;

In a similar way to how we obtained the height and width values of our canvas we are going to retrieve the clientHeight and clientWidth values which are dynamic based on the size of our wrapper which we have already set in css with a resolution of 200px x 200px

// Get dynamic size based on
// wrapper size

const dynamicHeight = canvasEl.clientHeight;
const dinamycWidth = canvasEl.clientWidth;

We will also use getBoundingClientRect to calculate the relative position of our element in the viewport, this is useful if our element has padding, margin or are using flex rules etc, this distance from the element we subtract the position of our cursor on the screen with the values extracted from the event:

const rect = canvasEl.getBoundingClientRect();

// Calculate cursor position on screen
// in real distance from canvas element

const distanceY = clientY - rect.top;
const distanceX = clientX - rect.left;

It remains for us to use our previous values to calculate the position of our cursor using Math.round to avoid floating point values.

Our next step is to multiply our computed values with the cursor position on our canvas, we must divide that result by our dynamic resolution, which as we know is set by the wrapper.

// current position of the cursor on
// the canvas based on pixels

const y = Math.round((computedHeight * cursorX) / dynamicHeight);
const x = Math.round((computedWidth * cursorY) / dinamycWidth);

The result of x and y are the coordinates of the cursor on our 24x24 canvas, these coordinates will be useful if we want to position, paint or delete elements on our canvas precisely.

Our full function should look like this:

import "./styles.css";

const canvasEl = document.getElementById("resized-canvas");

function getCoords(event) {
  // We get the computed size
  // in pixels of the canvas

  const computedHeight = canvasEl.height;
  const computedWidth = canvasEl.width;

  const { clientX, clientY } = event;
  // Get dynamic size based on
  // wrapper size

  const dynamicHeight = canvasEl.clientHeight;
  const dinamycWidth = canvasEl.clientWidth;

  const rect = canvasEl.getBoundingClientRect();
  // Calculate cursor position on screen
  // in real distance from canvas element
  const cursorY = clientY - rect.top;
  const cursorX = clientX - rect.left;

  // current position of the cursor on
  // the canvas based on pixels
  const y = Math.round((computedHeight * cursorX) / dynamicHeight);
  const x = Math.round((computedWidth * cursorY) / dinamycWidth);

  return { x, y };
}

canvasEl.addEventListener("mousemove", getCoords);

I made this post while I was rewriting it in CodeSandbox in vanilla Javascript because I have used it with React, I leave an example working in CodeSandbox if you want to take a look at it or need to rely on it for something:

Conclusions

You may know that there are different ways to address different problems, in this case CSS seemed quite optimal considering the needs I have for this project, it is important to note that it is possible to use aspect-ratio property in css to establish a dynamic resolution, this CSS property recently had changes in Chrome to conform to the standards, but there are ways to use it and avoid height and width.