Performing well in Bootstrap 5 responsive pages

Interactive selection box on a responsive image

Rinaldo Nani
Variance Digital
Published in
6 min readOct 12, 2022

--

Pure Javascript solution for a draggable and resizable highlighted rectangle; the selection rectangle works in a responsive HTML canvas.

In one of our demo projects, the user can outline the thumbnail for an image by moving and resizing an interactive rectangle over the image.

We wanted to implement this functionality using Javascript without third-party libraries or packages.

The animation shows the result of our implementation: you can play with the demo page here. [In our case, the selection polygon is actually a square but our javascript code can be easily adapted to manage a rectangular shape].

One way to achieve this functionality is to layer an HTML transparent <canvas> over the <img> element that needs the selection operation; then draw the rectangle on the <canvas>. Both the <img> and <canvas> elements are wrapped inside a responsive <div>, as shown in Figure 1.

Fig.1 — Canvas and image layered in responsive <div>

Sounds easy enough, but as you will see, the devil is in the details of the HTML + Javascript implementation. An additional difficulty arises when working with responsive pages, where the underlying image is free to move and resize on the webpage at the programmed breakpoints.

Any solution to the responsive selection rectangle problem has to address two issues.

  1. In a responsive site, the image changes size and position whenever the user resizes the browser’s window (or when the site is browsed on a mobile device with unpredictable screen size). The canvas should then “follow” the image size and position, and the selection rectangle should be redrawn on each “resize” operation, recalculating its new left, top, right, and bottom coordinates.
  2. When a shape is drawn on a <canvas>, the shape cannot be really “moved around”: there is no object model that can give the handle of the drawn shape (not at the time of writing this article, at least). This is because once the <canvas> drawing commands are completed, the newly drawn shape is blended with the pre-existing pixels of the <canvas> drawing.
    To give the impression of a hovering rectangle, one must continually perform a fast clear-canvas-and-redraw-rectangle task, responding to drag-and-drop events triggered by dragging events or gestures.

Putting together different ideas found on the Internet (we will always be thankful to Stack Overflow’s angels), we finally packed a viable HTML+Javascript solution.

1. Setting up the HTML elements

If you peek at the source code of our demo HTML page, you may spot the crucial HTML tags: the <img> and <canvas> in the responsive <div>, as in the code snippet below.

...
<div class="row mb-4">
<div class="col-md-7"> <!-- responsive div -->
<img id="full-image" src="[image source]"
class="card-img-top" alt="..."
style="position:relative">
<canvas id="canvas"
style="position:absolute; left: 0px; top: 0px">
</canvas>
</div>
<div class="col-md-5 mt-3 mt-md-0">
...
</div>
</div>
...

The <canvas> has an absolute position in the enclosing <div>; being placed at left: 0px; top: 0, it will end up layered precisely on top of the image. Both <canvas> and <img> have their own id, so Javascript can operate on them.

The rest of the code in the above snippet is just Bootstrap 5 stuff aimed at laying out responsive — and good-looking — rows and columns.

2. The javascript code briefly explained

The javascript code is part of the source of the demo webpage:
https://catloader-api.variancedigital.com/bo/selectthumbnail
You can see the code with your browser's “show page source” function. You’ll find the exact complete code in section 3 below, so it is easier to follow along with the explanatory notes of the next subsections.

2.1 Event handling and canvas positioning/following

On page load, the init() function registers all the necessary event listeners to track the mouse and the touch gestures. Then, the initCanvas() places the canvas above the image; the selection rectangle is initialised with initRect() and drawn with drawRectInCanvas() (in section 3, look at the code of the initCanvas() function, which is not banal).

...
function init(){
canvas.addEventListener('mousedown', mouseDown, false);
canvas.addEventListener('mouseup', mouseUp, false);
canvas.addEventListener('mousemove', mouseMove, false);
canvas.addEventListener('touchstart', mouseDown);
canvas.addEventListener('touchmove', mouseMove);
canvas.addEventListener('touchend', mouseUp);
initCanvas();
initRect();
drawRectInCanvas();
}
window.addEventListener('load',init)
window.addEventListener('resize', repositionCanvas)
...

Note that the resize event is met with the repositionCanvas() function: if you look at the code of this function, you’ll see that it lets the <canvas> piggyback the <img> element position and size.

The repositionCanvas() also calls the upadateCurrentCanvasRect() where the global current_canvas_rect is updated. This global is crucial: when the canvas resizes for any reason, we need its “previous” position and size to calculate the correct proportions of the selection rectangle and draw it with the new correct size (see how current_canvas_rect is used in the upadteCurrentCanvasRect() function).

2.2 React to mouse/touch events to simulate the drag&drop motion

Whenever the canvas is “clicked” by the mouse’s cursor (or touched by the user’s fingers), the triggered mouseDown() function checks whether the cursor is inside the selection rectangle or if the cursor is close to one of the rectangle’s circular handles.

If this is the case, the “drag” variables are set to true so that any subsequent mouse movement is considered a drag operation —this goes on until the mouse-up event triggers the mouseUp() function, which sets the drag variables to false.

...
//drag global variables
var dragTL = dragBL = dragTR = dragBR = false;
var dragWholeRect = false;
...function mouseDown(e) {
var pos = getMousePos(this,e);
mouseX = pos.x;
mouseY = pos.y;
// 0. inside movable rectangle
if (checkInRect(mouseX, mouseY, rect)){
dragWholeRect=true;
startX = mouseX;
startY = mouseY;
}
// 1. top left
else if (checkCloseEnough(mouseX, rect.left) &&
checkCloseEnough(mouseY, rect.top)) {
dragTL = true;
}
// 2. top right
else if (checkCloseEnough(mouseX, rect.left + rect.width) &&
checkCloseEnough(mouseY, rect.top))
{
dragTR = true;
}
// 3. bottom left
... etc.
}
function mouseUp(e) {
dragTL = dragTR = dragBL = dragBR = false;
dragWholeRect = false;
}
...

While dragging, the triggered mouseMove() function calculates — case by case — the new size and position of the selection rectangle and eventually draws it.

...

function mouseMove(e) {
var pos = getMousePos(this, e);
mouseX = pos.x; mouseY = pos.y;
if (dragWholeRect) {
e.preventDefault();
e.stopPropagation();
dx = mouseX - startX;
dy = mouseY - startY;
if ((rect.left+dx)>0 &&
(rect.left+dx+rect.width)<canvas.width)
{
rect.left += dx;
}
if ((rect.top+dy)>0 &&
(rect.top+dy+rect.height)<canvas.height)
{
rect.top += dy;
}
startX = mouseX;
startY = mouseY;
} else if (dragTL) {
e.preventDefault();
e.stopPropagation();
var newSide = (Math.abs(rect.left+rect.width - mouseX) +
Math.abs(rect.height + rect.top - mouseY))/2;
if (newSide>150){
rect.left = rect.left + rect.width - newSide;
rect.top = rect.height + rect.top - newSide;
rect.width = rect.height = newSide;
}
} else if (dragTR) {
... etc.
}
drawRectInCanvas();
}
...

Note that while moving and resizing, the mouseMove() function also assures that the shape stays within the canvas borders, nor becomes too small.

If you need to customize this code so that the draggable and resizable shape is a rectangle (instead of a square), you should modify this mouseMove() function.

2.3 Continuous drawing on canvas

The selection rectangle and its handles must be redrawn on the canvas whenever something happens. This is done via the drawRectInCanvas() function, the most ubiquitous function in the code.

Inside this function, the canvas context is fetched; then, the context is used to clear the canvas and delete all of its contents. On the cleared canvas, the rectangle is redrawn, along with its handles.

...function drawRectInCanvas(){
var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath(); ctx.lineWidth = "6";
ctx.fillStyle = "rgba(199, 87, 231, 0.2)";
ctx.strokeStyle = "#c757e7";
ctx.rect(rect.left, rect.top, rect.width, rect.height);
ctx.fill();
ctx.stroke();
drawHandles();
updateHiddenInputs()
}
...

The last instruction of the drawRectInCanvas() is updateHiddenInputs(), which saves the selection box coordinates so that they can be displayed on the webpage (or submitted to the server).

3. The complete javascript code

Here is the complete javascript code that governs the hovering selection rectangle. See the previous section for some brief explanations.

3. Conclusions (stay tuned)

You have seen the code and played with its online up-and-running implementation. The webpage showcasing the selection box is part of our “CatLoader” demo project that will be disclosed entirely within a few days. Stay tuned!

We are aware that our code can be improved and optimized. If anything in our Javascript is misleading or missing, don’t hesitate to contact us: we are eager to improve this living document.

--

--

Rinaldo Nani
Variance Digital

Algorithmist ▪ Software Engineer ▪ Project manager. I love maths and music + solving hard problems.