Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript math "draw" rectangles around multiple other rectangles to achieve backdrop effect

Note: I will use the word overlay interchangeably with backdrop.

I'm currently working on a guide mode and I highlight elements to make them stand out from the darker semi-transparent background.

So basically, I have a selector for the element that I want to not be overlaid, and then I surround it with semi-transparent dark divs, simulating an overlay with an excluded element.

My solution works great for one highlighted element, but when that number becomes 2+, it becomes hard to calculate how to place the black divs around those elements. And the thing is: The highlight needs will vary significantly from page to page - it's not like those multiple elements will be static.

So to illustrate my issue:

enter image description here

Here you can see the different boxes that need to be put into the page in order to surround Box2 and Box2 with the semi-transparent dark divs, basically simulating the overlay effect with highlighted elements. For one case like this one, I can hardcode the calculations and everything would be fine, but what if Box1 was higher up than Box2? Then what if they were not overlapping each other horizontally anymore? What if there is a Box3 that needs highlighting?

In either case, I wouldn't know how many dark boxes to place and where to place them.

Is there any mathematical formula that would help me in this case? The use of jQuery is also possible as it's included in my project.

How would I approach this problem and make it extendable (multiple boxes, different positions)?

like image 413
aborted Avatar asked Mar 08 '17 09:03

aborted


2 Answers

You're overthinking! What you are looking for can be achieved with a certain HTML and CSS layout.

You have to use a layout similar to this:

.container
    .overlay
    .box
    .box
    .box
    .box

The trick is to use a full-width, semi-transparent overlay and bring elements that need to be highlighted to front. Very important thing is to assign pointer-events: none to the overlay so you can click through it!

Make .box have a z-index value of, say, 1 and .overlay have a z-index value of 100. In order to highlight a certain .box, set its z-index to 101. That way it will appear highlighted.

I'm on mobile right now, but I put a basic proof-of-concept on Codepen. Click on boxes to highlight them, click again to undo. Works with multiple boxes!

like image 200
user7401478 Avatar answered Sep 27 '22 20:09

user7401478


Consider Using Regions

While there are a number of good answers here, and you might want to consider moving the elements like @WearyAdventurer proposed, none of the other answers actually address the original question:

How do I create elements in such a way that they appear to surround other elements?

The answer to that question is best solved using a data type called a Region. Regions allow you to easily perform set operations using chunks of 2-D screen space: Combine this area with that area and then subtract another area, and then turn the result into a set of rectangles I can render. Regions exist in most window systems (MS Windows, X Windows, classic MacOS, among others), and are used heavily in most browsers' internal mechanics for rendering elements, but they're surprisingly absent in JavaScript.

Or they were absent until I wrote a library to do it.

The underlying logic of regions is somewhat complicated. There are many corner cases, and handling all the scenarios that can arise can be challenging. My implementation, Region2D, uses disjoint rows (bands) of disjoint rectangles (1-dimensional regions), which is the same way that many X Windows servers do it. Other implementations use spacial partitioning algorithms, and still others (like modern MacOS) use pathing or polygon-rendering techniques instead of regions altogether.


The Basic Idea

Regardless of which implementation you use, the basic idea remains the same. In your case, you start with a big rectangle that covers the screen, and then simply subtract away the two (or three, or four, or whatever) rectangles you want, and then ask the region which rectangles result from those operations. In JavaScript, using my Region2D library, it looks like this:

var screenRegion = new Region([0, 0, viewportWidth, viewportHeight]);
var elemRegion1 = new Region(element1);
var elemRegion2 = new Region(element2);
var coverRegion = screenRegion.subtract(elemRegion1).subtract(elemRegion2);
var coverRectangles = coverRegion.getRects();

The resulting array of rectangles are simple objects that have x / y / width / height / top / left / right / bottom coordinates, so you just create <div> elements from each one, and you're done.


Working Implementation

So here's a working implementation of a solution to the actual problem as presented, using my Region2D library to do the heavy algorithmic lifting:

// Generate regions for each of the objects.
var containerRegion = new Region2D($(".container")[0]);
var target1Region = new Region2D($(".target1")[0]);
var target2Region = new Region2D($(".target2")[0]);

// Subtract the targets from the container, and make an array of rectangles from it.
var coverRegion = containerRegion.subtract(target1Region).subtract(target2Region);
var coverRects = coverRegion.getRects();

// Create gray <div> elements for each rectangle.
for (var i = 0, l = coverRects.length; i < l; i++) {
    var coverRect = coverRects[i];
    var coverElement = $("<div class='cover'>");
    coverElement.css({
        left: (coverRect.x - 1) + "px", top: (coverRect.y - 1) + "px",
        width: coverRect.width + "px", height: coverRect.height + "px"
    });
    coverElement.appendTo($(".container"));
}
.container, .target1, .target2, .cover { position: absolute; top: 0; left: 0; box-sizing: border-box; }
.target1, .target2 { border: 1px solid red; }
.container { width: 330px; height: 230px; border: 1px solid blue; }
.target1 { top: 40px; left: 40px; width: 100px; height: 80px; }
.target2 { top: 100px; left: 180px; width: 100px; height: 80px; }
.cover { background: rgba(0, 0, 0, 0.5); border: 1px solid #000; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://unpkg.com/[email protected]/plain/region2d.min.js"></script>

<div class="container">
    <div class="target1"></div>
    <div class="target2"></div>
</div>
like image 38
Sean Werkema Avatar answered Sep 27 '22 22:09

Sean Werkema