Course: JavaScript

Progress (0%)

  1. Foundation

  2. Numbers

  3. Strings

  4. Conditions

  5. Loops

  6. Arrays

  7. Functions

  8. Objects

  9. Exceptions

  10. HTML DOM

  11. CSSOM

  12. Events

  13. Drag and Drop

  14. opt Touch Events

  15. Misc

  16. Project: Analog Clock

Exercise: Creating Rectangles

Exercise 57 Average

Prerequisites for the exercise

  1. JavaScript Mouse Events
  2. All previous chapters

Objective

Emulate the rectangle tool of graphic-design software by creating a rectangle shape in the same way.

Description

Have you used a graphic-design software to create shapes?

Almost all of them have a pretty standard way of creating rectangle shapes. For example, in Figma, we first select the rectangle tool and then perform a bunch of mouse gestures to create a rectangle shape in the canvas.

Here's a demonstration of using the Rectangle tool in Figma.

To state the interaction precisely, the moment the mouse button goes down, the rectangle-creation logic gets activated. Then, with the button still pressed, as we drag the mouse, a shape gets drawn on the screen following the pointer.

Note that the pivot of the shape is set to the coordinates of the pointer right at the moment when we press the mouse down.

In this exercise, you have to emulate this way of creating shapes using JavaScript.

Here are some points to remember:

  • There is no need to provide a separate button to select the rectangle tool. It should be selected all the time.
  • There is even no need to provide functionality to move the rectangle or maybe resize it. We'll implement such functionalities in later exercises.
  • The background color of the rectangle should always be light grey.
View Solution

New file

Inside the directory you created for this course on JavaScript, create a new folder called Exercise-57-Creating-Rectangles and put the .html solution files for this exercise within it.

Solution

Before we could start coding the solution for this exercise, we first need to understand the given task more carefully. After all, we need to have a rock solid plan on what to do before actually diving into the coding. And this applies not just to our exercise, but to all domains of programming.

So, let's review the example shown above and see what could we do at each step of the mouse interaction in our JavaScript program.

When the mouse button is pressed, i.e. when the mousedown event occurs, the rectangle-creation logic gets activated. Alright. But what could this possibly mean?

Well, for us, it could mean that a rectangle gets initialized on the canvas. Additionally, it could also mean that we save the initial coordinates of the pointer (during this mousedown event) for later use when we draw the shape. We'll refer to this set of coordinates as the pivotal point of the shape from now on.

By reading the line 'a rectangle gets initialized on the canvas', we right away realize that we need an element to represent rectangles in our program.

So going with the most straightforward approach, we'll define a .rectangle class in the CSS, as follows:

.rectangle {
   position: fixed;
   background-color: grey;
}

The style position: fixed is not strictly necessary; we could even use position: absolute. It's just a matter of taste (and sometimes logic too). As for the second background-color property, it's just based on the exercise's description.

Now, we'll setup a mousedown handler on window, and initialize a new rectangle shape in there. This initialization is simply to create a new .rectangle element and save a reference to it for later use.

This is done below:

var rectangleElement;

function createRectangle() {
   var rectangleElement = document.createElement('div');
   rectangleElement.className = 'rectangle';
   document.body.appendChild(rectangleElement);
   return rectangleElement;
}

window.addEventListener('mousedown', function(e) {
   rectangleElement = createRectangle();
});
  • The function createRectangle() nicely encapsulates the logic for creating a new .rectangle element.
  • The rectangleElement global variable, on the other hand, is used to save a reference to the recently-created element (returned by createRectangle()) to be used later on as we draw the shape.
Learn more about how to create element nodes in HTML DOM — Nodes.

Note that all of what we've been able to list here is judged purely by observing the example shown above, by creating rectangle shapes in actual graphic-design programs, and by experience of JavaScript itself. Just by looking at a given program, we could immediately reason as to what would we need, and how to use it, to build the program in JavaScript. This is an indispensable skill that comes with practice, time and experience of building software.

Anyways, moving on, with the button still pressed, as we move the mouse, i.e. the mousemove event occurs, the shape gets drawn out on the canvas (webpage, in our case). This too seems fairly easy to accomplish in our program, but obviously not without some thought.

Perhaps the most important thing to remember is that when the shape is drawn, two edges remain fixed all the time, depending on where the pointer goes relative to the pivotal point.

Let's understand what this means in detail.

Suppose we press the mouse button while the pointer is at the position denoted below as a small green circle, and then move to the position denoted below as a hollow circle with a red border:

Demonstrating mouse interaction for creating a rectangle shape.

As the pointer is moved from the green point to the hollow point, it means that we have to keep the right edge fixed and just move the left edge. And since the green point is vertically above the hollow (pivotal) point, it also means that the top edge of the rectangle must remain fixed while its bottom edge must be moved with the pointer.

As another example, if we have the following configuration,

Demonstrating mouse interaction for creating a rectangle shape.

then the left and bottom edges of the rectangle should remain fixed while the right and top edges must be moved with the pointer.

And you hopefully get the idea.

Keep in mind that with the mousemove event, depending on where the pointer moves relative to the pivotal point, we might have to change which edge is fixed and which one is moving. This characteristic is not a static one; it's purely dynamic and can definitely change as we draw the shape.

So how to implement this?

Well, first let's store the coordinates of the pivotal point, using the handler of mousedown, by means of two intuitively-named global variables initX and initY (where 'init' stands for 'initial'):

var rectangleElement;
var initX, initY;

function createRectangle() {
   var rectangleElement = document.createElement('div');
   rectangleElement.className = 'rectangle';
   document.body.appendChild(rectangleElement);
   return rectangleElement;
}

window.addEventListener('mousedown', function(e) {
   rectangleElement = createRectangle();

   // Save coordinates of pivotal point for later use.
   initX = e.clientX;
   initY = e.clientY;
});

Next, since we don't want to handle mousemove outside mousedown, we'll register an event handler for it only inside the handler of mousedown. Moreover, because we might also need to remove this mousemove handler later on (in mouseup), we'll need to keep a reference to it with us.

Based on these facts, we'll simply create a named function to act as the handler and then register it inside the mousedown handler shown above.

Here's the code we get:

var rectangleElement;
var initX, initY;

function createRectangle() {
   var rectangleElement = document.createElement('div');
   rectangleElement.className = 'rectangle';
   document.body.appendChild(rectangleElement);
   return rectangleElement;
}

function mouseMoveHandler(e) {
   // Handle mousemove handler.
}

window.addEventListener('mousedown', function(e) {
   rectangleElement = createRectangle();

   // Save coordinates of pivotal point for later use.
   initX = e.clientX;
   initY = e.clientY;

   window.addEventListener('mousemove', mouseMoveHandler);
});

So far, so good.

Inside the mousemove handler, we'll first retrieve the current coordinates of the pointer, using clientX and clientY, and then perform a few checks to determine which edge to keep fixed and which one to move.

We'll break down these checks in two axes: x-axis and y-axis.

That is, if the x-coordinate of the moving point is greater than that of the pivotal point, i.e. it's to the right of the pivotal point, the left edge remains fixed while the right edge follows the x-coordinate of the moving point. Otherwise, we do the opposite — fix the right edge and move the left edge with the pointer.

On the exact same lines, if the y-coordinate of the moving point is greater than that of the pivotal point, i.e. it's to the bottom of the pivotal point, then the top edge remains fixed while the bottom edge follows the y-coordinate of the moving point. Otherwise, we again do the opposite — fix the bottom edge and move the top edge with the pointer.

Let's implement this idea using a variety of concepts that we've learnt thus far in this course.

For brevity, we'll only be showing the mouseMoveHandler() function. And that follows:

function mouseMoveHandler(e) {
   var clientX = e.clientX;
   var clientY = e.clientY;

   var left, right, top, bottom;

   // Dealing with left and right edges.
   if (clientX > initX) {
      left = initX;
      right = innerWidth - clientX;
   }
   else {
      right = innerWidth - initX;
      left = clientX;
   }

   // Dealing with top and bottom edges.
   if (clientY > initY) {
      top = initY;
      bottom = innerHeight - clientY;
   }
   else {
      bottom = innerHeight - initY;
      top = clientY;
   }

   rectangleElement.style.left = `${left}px`;
   rectangleElement.style.right = `${right}px`;
   rectangleElement.style.top = `${top}px`;
   rectangleElement.style.bottom = `${bottom}px`;
}
  • The clientX and clientY local properties serve to cache the values returned by e.clientX and e.clientY, respectively. so that we don't have to refer to the properties again and again.
  • The four global properties left, right, top and bottom are meant to hold the corresponding values for the left, right, top and bottom CSS style properties of the creating .rectangle element.
  • The following set of if...else statements merely implement the idea that we discussed above, that is to determine which edges to keep fixed and which ones to move with the pointer.
  • Finally, the styles of rectangleElement are updated using the values of left, right, top and bottom.

The most important thing to note in these conditional statements is the usage of innerWidth and innerHeight. They are used for a very simple reason: the right and bottom CSS properties of .rectangle work relative to the right and bottom edges of the viewport, yet clientX and clientY are determined from its left and top edges.

To account for this difference, we ought to subtract clientX and clientY from innerWidth and innerHeight, respectively, to get the corresponding coordinates from the right and bottom of the viewport.

The innerWidth and innerHeight properties return the width and height of the browser viewport, in pixels. To learn more about them, refer to JavaScript — CSSOM — Viewport.

Live Example

It works. Great.

Now there's just one thing left before we'll be done with this exercise.

And that's to remove the mousemove handler on mouseup. That's it!

This is accomplished below:

/* ... */

function mouseUpHandler() {
   window.removeEventListener('mousemove', mouseMoveHandler);
   window.removeEventListener('mouseup', mouseUpHandler);
}

window.addEventListener('mousedown', function(e) {
   rectangleElement = createRectangle();

   // Save coordinates of pivotal point for later use.
   initX = e.clientX;
   initY = e.clientY;

   window.addEventListener('mousemove', mouseMoveHandler);
window.addEventListener('mouseup', mouseUpHandler); });

Instead of setting up the mouseup handler in the top-most level of the script, we set it inside the mousedown handler. This makes sense because only once we press the mouse down should the corresponding mouse-release action be observed.

Moreover, since upon the occurence of mouseup, we need to remove its own handler as well, we create the handler as a separate function, mouseUpHandler().

And this hopefully completes our exercise.

The real confirmation will be once we test the example and everything works fine.

Live Example

And it works absolutely flawlessly!