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.
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 bycreateRectangle()
) to be used later on as we draw the shape.
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:
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,
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
andclientY
local properties serve to cache the values returned bye.clientX
ande.clientY
, respectively. so that we don't have to refer to the properties again and again. - The four global properties
left
,right
,top
andbottom
are meant to hold the corresponding values for theleft
,right
,top
andbottom
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 ofleft
,right
,top
andbottom
.
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.
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.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.
And it works absolutely flawlessly!