How to check if two elements are intersecting in vanilla JavaScript?

12.8k views Asked by At

I have code like this:

$(function() {
  var $selection = $('.selection');
  $('li').filter(function() {
    var self = $(this);
    return /* ????? */;
  }).addClass('selected');
});
.widget {
  width: 320px;
  height: 200px;
  border: 1px solid gray;
  position: absolute;
  overflow: scroll;
}
.selection {
  position: absolute;
  top: 90px;
  left: 90px;
  border: 1px dotted black;
  height: 120px;
  width: 120px;
}
ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
li {
  float: left;
  background: blue;
  width: 40px;
  height: 40px;
  margin: 10px;
}
li.selected {
  background: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="widget">
  <div class="selection"></div>
  <ul>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
  </ul>
</div>

How can I select only those li elements that are intersecting with .selection rectangle?

2

There are 2 answers

5
Greg On BEST ANSWER

Using standard DOM techniques, you can iterate over each LI element and obtain a bounding rectangle, which gives the coordinates of the LI's rectangle.

Do this for the selection rectangle too and then you can simply check whether the coordinates are within the range of the selection's.

Element.getBoundingClientRect()

The Element.getBoundingClientRect() method returns the size of an element and its position relative to the viewport.

The returned value is a DOMRect object which is the union of the rectangles returned by getClientRects() for the element, i.e., the CSS border-boxes associated with the element.

The returned value is a DOMRect object, which contains read-only left, top, right, bottom, x, y, width, height properties describing the border-box in pixels. Properties other than width and height are relative to the top-left of the viewport.

Please see below, the edited code which selects the LI elements that are fully contained or partially intersecting the selection.

var selection = document.querySelector(".selection");
var rectSelection = selection.getBoundingClientRect();

// Iterate over all LI elements.
[].forEach.call(document.querySelectorAll("li"), function(li) {
    var rect = li.getBoundingClientRect();

    if(rect.bottom > rectSelection.top 
    && rect.right > rectSelection.left 
    && rect.top < rectSelection.bottom 
    && rect.left < rectSelection.right) {
        li.classList.add("selected");
    }
});
.widget {
  width: 320px;
  height: 200px;
  border: 1px solid gray;
  position: absolute;
  overflow: scroll;
}
.selection {
  position: absolute;
  top: 90px;
  left: 90px;
  border: 1px dotted black;
  height: 120px;
  width: 120px;
}
ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
li {
  float: left;
  background: blue;
  width: 40px;
  height: 40px;
  margin: 10px;
}
li.selected {
  background: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="widget">
  <div class="selection"></div>
  <ul>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
  </ul>
</div>

11
Arthur On

The concept to understand box collision Origin answer

If You have to check the 2 rects with rotation you have to make projection of rect corner on the axes of the other rect. If all projections of rect1 hit the rect2, and rect2 projections hit rect1 so the two rect collide.

Some projection don't collide here, the 2 rects are not collide. enter image description here

The 4 projections hit the other rect, the 2 rects are collide. enter image description here

I have made a presentation on this CodePen for more comprehension.

Clone of the code here (it's old and durty, don't be rude it's just to display.. don't look the code to much :p)

function merge_object(obj1, obj2){
    var obj3 = {};
    for (var attrname in obj1) { obj3[attrname] = obj1[attrname]; }
    for (var attrname in obj2) { obj3[attrname] = obj2[attrname]; }
    return obj3;
};
/** 
 * Transforme degrees to radians
 */
Math.radians = function(degrees) {
    return degrees * Math.PI / 180;
};
Math.degrees = function(radians) {
  return radians * 180 / Math.PI;
};

/**
 * Square Javascript File
 *
 * -- Changelog
 * Version 1.0.0 (11/05/2013) AGE
 * - Début du versionnement
 *
 * @package js
 * @author AGE
 * @version 1.0.0
 */

// App Object
var collide = {
    
    // Define
    PROJECT_ID : 'collide',
    PROJECT_SPEED : 50, // FPS : 1000/PROJECT_SPEED
    
    FIXED_SQUARE_SIZE : 100,
    FIXED_SQUARE_X : 250,
    FIXED_SQUARE_Y : 250,
    
    CURSOR_SQUARE_SIZE : 50,
        
    // Attribute
    $el : null,
    $ctx : null,
    project_pos_top : null,
    project_pos_left : null,
    width : null,
    height : null,
    
    cursor_x : 0,
    cursor_y : 0,
    
    fixed_angle : 0,
    fixed_rotaton_speed : null,
    cursor_angle : 0,
    cursor_rotaton_speed : null,
     
    draw_approx : true, 
    fixed_projections : {},
    cursor_projections : {},
        
    /**
     * Init Square Game
     */
    __init : function( options ){
        
        // Get element and informations
        collide.$el = $('#' + collide.PROJECT_ID);
        collide.$ctx = collide.$el[0].getContext('2d');
        
        var offset = collide.$el.offset();
        collide.project_pos_top = offset.top;
        collide.project_pos_left = offset.left;
        collide.width = collide.$el.width();
        collide.height = collide.$el.height();
        
        // Events on Move
        collide.$el.mousemove(function(e){
            // Calculate new position
            collide.cursor_x = e.pageX - collide.project_pos_left;           
            collide.cursor_y = e.pageY - collide.project_pos_top;           
        });
        
        $('#cursor_set_angle').change(function(){
            collide.cursor_angle = parseInt($(this).val());
        });
        $('#fixed_set_angle').change(function(){
            collide.fixed_angle = parseInt($(this).val());
        });
        
        // Run the project
        collide.run();
    },
    
    /**
     * Run the project
     */
    run : function(){
                
        // Reset HTML
        $('.corners, .functions, .projections').remove();
        
        // Get 2 angles rotation speed
        collide.fixed_rotaton_speed = parseInt($('#fixed_rotation_speed').val());
        collide.cursor_rotaton_speed = parseInt($('#cursor_rotation_speed').val());
                                
        // Upadte rotation
        collide.fixed_angle = collide.fixed_angle+collide.fixed_rotaton_speed;  
        if(collide.fixed_angle > 360) {collide.fixed_angle -= 360}; 
        if(collide.fixed_angle < 0) {collide.fixed_angle += 360};             
        $('#fixed_angle').val(collide.fixed_angle);

        collide.cursor_angle = collide.cursor_angle+collide.cursor_rotaton_speed;  
        if(collide.cursor_angle > 360) {collide.cursor_angle -= 360}; 
        if(collide.cursor_angle < 0) {collide.cursor_angle += 360}; 
        $('#cursor_angle').val(collide.cursor_angle);

        // Approx Collide
        if(collide.is_approx_collide()){
            
            collide.draw_approx = false;
            
            // Get corners
            var fixer_corner = collide.get_corners(
                collide.FIXED_SQUARE_X,  collide.FIXED_SQUARE_Y, 
                collide.FIXED_SQUARE_SIZE, collide.fixed_angle);
            var cursor_corner = collide.get_corners(
                collide.cursor_x, collide.cursor_y, 
                collide.CURSOR_SQUARE_SIZE, collide.cursor_angle);
                
                
            // Get projections
                // CURSOR on FIXED.X && FIXED.Y
            collide.cursor_projections = collide.get_projections(
                collide.FIXED_SQUARE_X,  collide.FIXED_SQUARE_Y,
                collide.fixed_angle,cursor_corner);  
                // FIXED on CURSOR.X && CURSOR.Y
            collide.fixed_projections = collide.get_projections(
                collide.cursor_x,  collide.cursor_y,
                collide.cursor_angle, fixer_corner);  
                      
        }else{
            collide.draw_approx = true;
        }
        
        collide.draw();
        
        // Re-lauch Run
        setTimeout(function(){
            collide.run();
        }, collide.PROJECT_SPEED);
    },
    
    
    draw : function(){
        // Clear
        collide.$ctx.clearRect(0, 0, collide.width, collide.height);
        collide.$ctx.setTransform(1, 0, 0, 1, 0, 0);
        collide.$ctx.save();
        
        // Draw squares
        collide.draw_square(collide.FIXED_SQUARE_X, collide.FIXED_SQUARE_Y,
            collide.FIXED_SQUARE_SIZE, collide.fixed_angle, '0,0,255');
        
        collide.draw_square(collide.cursor_x, collide.cursor_y,
            collide.CURSOR_SQUARE_SIZE, collide.cursor_angle, '255,128,0');
            
        if(collide.draw_approx){
            
            // Approx 
            collide.draw_approx_square(collide.FIXED_SQUARE_X, collide.FIXED_SQUARE_Y,
                collide.FIXED_SQUARE_SIZE, '0,0,255');

            collide.draw_approx_square(collide.cursor_x, collide.cursor_y,
                collide.CURSOR_SQUARE_SIZE, '255,128,0');
        }   
        else{
            
            // Axes
            collide.draw_axe(collide.FIXED_SQUARE_X, collide.FIXED_SQUARE_Y,
                collide.fixed_angle, '0,0,255');

            collide.draw_axe(collide.cursor_x, collide.cursor_y,
                collide.cursor_angle, '255,128,0');
                
            // Projections
            collide.draw_projections( collide.fixed_projections, '0,0,255');
            collide.draw_projections( collide.cursor_projections, '255,128,0');
            
        }
    },
    draw_square : function(center_x, center_y, size, angle, rgb){
        
        // Is collide ? 
        if(collide.is_approx_collide() && collide.is_collide()){ rgb = "255,0,0"; } 
        
        collide.$ctx.save();
        
        // Draw Fixed square
        collide.$ctx.translate(center_x, center_y);
        collide.$ctx.rotate( Math.radians( angle ) );
        collide.$ctx.fillStyle = 'rgba('+rgb+',.2)';
        collide.$ctx.fillRect(size / -2, size / -2, size, size);
        
        // Draw Corner
        collide.$ctx.translate(size/2, size/2);
        collide.$ctx.beginPath();
        collide.$ctx.fillStyle = 'rgba('+rgb+',1)';
        collide.$ctx.arc(0, 0, 2, 0, Math.PI*2, true); 
        collide.$ctx.closePath();
        collide.$ctx.fill();
        collide.$ctx.translate(-size, 0);
        collide.$ctx.beginPath();
        collide.$ctx.fillStyle = 'rgba('+rgb+',1)';
        collide.$ctx.arc(0, 0, 2, 0, Math.PI*2, true); 
        collide.$ctx.closePath();
        collide.$ctx.fill();
        collide.$ctx.translate(0, -size);
        collide.$ctx.beginPath();
        collide.$ctx.fillStyle = 'rgba('+rgb+',1)';
        collide.$ctx.arc(0, 0, 2, 0, Math.PI*2, true); 
        collide.$ctx.closePath();
        collide.$ctx.fill();
        collide.$ctx.translate(size,0);
        collide.$ctx.beginPath();
        collide.$ctx.fillStyle = 'rgba('+rgb+',1)';
        collide.$ctx.arc(0, 0, 2, 0, Math.PI*2, true); 
        collide.$ctx.closePath();
        collide.$ctx.fill();
        
        collide.$ctx.restore();
    },
    draw_approx_square : function(center_x, center_y, size, rgb){
        
        collide.$ctx.save();
        
        // Draw Fixed square
        collide.$ctx.translate(center_x, center_y);
        collide.$ctx.strokeStyle = 'rgba('+rgb+',1)';
        collide.$ctx.lineWidth = 1;
        collide.$ctx.strokeRect(-size, -size, 2*size, 2*size);
        
        collide.$ctx.restore();        
    },
    draw_axe : function (center_x, center_y, angle, rgb){
        collide.$ctx.save();
        
        collide.$ctx.translate(center_x, center_y);
        collide.$ctx.rotate( Math.radians( angle ) );
        
        collide.$ctx.beginPath();
        collide.$ctx.strokeStyle = 'rgba('+rgb+',1)';
        collide.$ctx.moveTo(-500, 0);
        collide.$ctx.lineTo(500,0);
        collide.$ctx.stroke();
        
        collide.$ctx.restore();
        collide.$ctx.save();
        
        collide.$ctx.translate(center_x, center_y);
        collide.$ctx.rotate( Math.radians( angle+90 ) );
        
        collide.$ctx.beginPath();
        collide.$ctx.strokeStyle = 'rgba('+rgb+',1)';
        collide.$ctx.moveTo(-500,0);
        collide.$ctx.lineTo(500,0);
        collide.$ctx.stroke();
        
        collide.$ctx.restore();  
    },
    draw_projections : function(projections, rgb){
        for(axe in projections){
            for(key in ['min', 'max']){                
                var projection = projections[axe][key==0?"min":"max"];

                collide.$ctx.save();

                collide.$ctx.translate(projection.x, projection.y);
                collide.$ctx.beginPath();
                collide.$ctx.fillStyle = 'rgba('+rgb+',1)';
                collide.$ctx.arc(0, 0, 2, 0, Math.PI*2, true); 
                collide.$ctx.closePath();
                collide.$ctx.fill();  
                
                collide.$ctx.beginPath();
                collide.$ctx.strokeStyle = 'rgba('+rgb+',.2)';
                collide.$ctx.moveTo(00,0);
                collide.$ctx.lineTo(projection.corner.x - projection.x, 
                                    projection.corner.y - projection.y);
                collide.$ctx.stroke();
                collide.$ctx.restore();  
            }
            
            
            collide.$ctx.save();
            
            collide.$ctx.beginPath();
            collide.$ctx.strokeStyle = (projections[axe].is_collide)? 'rgba(255,0,0,1)' : 'rgba('+rgb+',1)'; 
            collide.$ctx.lineWidth = 2;
            collide.$ctx.moveTo(projections[axe].min.x, projections[axe].min.y);
            collide.$ctx.lineTo(projections[axe].max.x,projections[axe].max.y);
            collide.$ctx.stroke();
            
            collide.$ctx.restore(); 
                
            
        }
    },
    
    /**
     * Calculate corners position and draw it
     */    
    get_corners : function(x, y, size, angle){
        var corners = [];
        var radius = parseInt(Math.sqrt(2*size*size)/2);
        
        for(var i_angle=0; i_angle<=270; i_angle+=90){
            var corner = {
                x : parseInt(x + radius * Math.cos(Math.radians(angle + 45 + i_angle) )),
                y : parseInt(y + radius * Math.sin(Math.radians(angle + 45 + i_angle) ))
            };
            corners.push(corner);
        }
        return corners;
    },
    
    /**
     * Calculate square functions and draw it
     */    
    get_functions : function(x, y, size, angle){
        var functions = {
            x : Math.tan(Math.radians( - angle%90)),
            y : Math.tan(Math.radians(90 - angle%90))
        };
        return functions;
    },
    
    get_projections : function(center_x, center_y,angle, corners){
        
        // Genere start Min-Max projection on center of Square
        var projections = {
            "x": {
                'min' : null,
                'max' : null,
                'distance' : null
            },
            "y": {
                'min' : null,
                'max' : null,
                'distance' : null
            }
        };
        
        for(i in corners){
            var corner = corners[i];
            var projection_x = {}, projection_y = {};
            
            /**
             * Global calcul for projection X and Y
             */ 
            
            // Angle 0:horizontale (center > left) 90:verticatale (center > top)
            var angle90 = -(angle%90);
            
            //Distance :
            var distance_corner_center = Math.floor(Math.sqrt((center_x-corner.x)*(center_x-corner.x) + (center_y-corner.y)*(center_y-corner.y)));
            
            // Angle between segment [center-corner] and real axe X (not square axe), must be negative (radius are negative clockwise) 
            var angle_with_axeX = -Math.floor(Math.degrees(Math.atan((corner.y-center_y) / (corner.x-center_x))));  // Tan(alpha) = opposé (ecart sur Y) / adjacent (ecart sur X)
            // If angle is ]0;90[, he is on the 2em et 4th quart of rotation
            if(angle_with_axeX > 0) {angle_with_axeX -= 180;}
            // If corner as upper (so with less pixel on y) thant center, he is on 3th or 4th quart of rotation
            if(corner.y < center_y || (corner.y == center_y && corner.x < center_x) ){angle_with_axeX -= 180;}

            // Calculate difference between 2 angles to know the angle between [center-corner] and Square axe X
            var delta_angle = angle_with_axeX - angle90;
            // If angle is on ]-180;-360], corner are upper than Square axe X, so set a positive angle on [0;180] 
            if(delta_angle < -180){delta_angle += 360;}
            
            /**
             * Projection on X
             */
            
            // Calculate distance between center and projection on axe X
            var distance_center_projection_x = Math.floor(distance_corner_center * Math.cos(Math.radians( delta_angle )));
            
            // Create projection
            projection_x.x = Math.floor(center_x + distance_center_projection_x * Math.cos(Math.radians( -angle90 ))); 
            projection_x.y =  Math.floor(center_y + distance_center_projection_x * Math.sin(Math.radians( -angle90 ))); 
            
            // If is the min ?   
            if(projections.x.min == null
                || distance_center_projection_x < projections.x.min.distance){
            
                projections.x.min = projection_x;
                projections.x.min.distance = distance_center_projection_x;
                projections.x.min.corner = corner;
            }
            // Is the max ?
            if(projections.x.max == null
                || distance_center_projection_x > projections.x.max.distance){
            
                projections.x.max = projection_x;
                projections.x.max.distance = distance_center_projection_x;
                projections.x.max.corner = corner;
            }
            
            /**
             * Projection on Y
             */
           
            // Calculate distance between center and projection on axe Y
            var distance_center_projection_y = Math.floor(distance_corner_center * Math.cos(Math.radians( delta_angle-90 )));
            
            // Create projection
            projection_y.x = Math.floor(center_x + distance_center_projection_y * Math.cos(Math.radians( -angle90 -90 ))); 
            projection_y.y =  Math.floor(center_y + distance_center_projection_y * Math.sin(Math.radians( -angle90 -90))); 
            
            // If is the min ? 
            if(projections.y.min == null
                || distance_center_projection_y < projections.y.min.distance){
            
                projections.y.min = projection_y;
                projections.y.min.distance = distance_center_projection_y;
                projections.y.min.corner = corner;
            }
            // Is the max ?
            if(projections.y.max == null
                || distance_center_projection_y > projections.y.max.distance){
            
                projections.y.max = projection_y;
                projections.y.max.distance = distance_center_projection_y;
                projections.y.max.corner = corner;
            }            
        }
        
        // Return object
        return projections;
    },
    
    is_approx_collide : function(){
        return (collide.FIXED_SQUARE_X + collide.FIXED_SQUARE_SIZE + collide.CURSOR_SQUARE_SIZE >= collide.cursor_x
            && collide.FIXED_SQUARE_Y + collide.FIXED_SQUARE_SIZE + collide.CURSOR_SQUARE_SIZE  >= collide.cursor_y
            && collide.FIXED_SQUARE_X <= collide.cursor_x + collide.FIXED_SQUARE_SIZE + collide.CURSOR_SQUARE_SIZE
            && collide.FIXED_SQUARE_Y <= collide.cursor_y + collide.FIXED_SQUARE_SIZE + collide.CURSOR_SQUARE_SIZE) ? true : false;
    },
    
    is_collide : function(){
        
        collide.fixed_projections.x.is_collide = 
                ( (collide.fixed_projections.x.min.distance <= -collide.CURSOR_SQUARE_SIZE/2 && collide.fixed_projections.x.max.distance >= -collide.CURSOR_SQUARE_SIZE/2 )
            ||  (collide.fixed_projections.x.min.distance <= collide.CURSOR_SQUARE_SIZE/2 && collide.fixed_projections.x.max.distance >= collide.CURSOR_SQUARE_SIZE/2 )
            ||  (collide.fixed_projections.x.min.distance >= -collide.CURSOR_SQUARE_SIZE/2 && collide.fixed_projections.x.max.distance <= collide.CURSOR_SQUARE_SIZE/2 )) ? true : false;
        collide.fixed_projections.y.is_collide = 
                ( (collide.fixed_projections.y.min.distance <= -collide.CURSOR_SQUARE_SIZE/2 && collide.fixed_projections.y.max.distance >= -collide.CURSOR_SQUARE_SIZE/2 )
            ||  (collide.fixed_projections.y.min.distance <= collide.CURSOR_SQUARE_SIZE/2 && collide.fixed_projections.y.max.distance >= collide.CURSOR_SQUARE_SIZE/2 )
            ||  (collide.fixed_projections.y.min.distance >= -collide.CURSOR_SQUARE_SIZE/2 && collide.fixed_projections.y.max.distance <= collide.CURSOR_SQUARE_SIZE/2 )) ? true : false;
        
        collide.cursor_projections.x.is_collide = 
                ( (collide.cursor_projections.x.min.distance <= -collide.FIXED_SQUARE_SIZE/2 && collide.cursor_projections.x.max.distance >= -collide.FIXED_SQUARE_SIZE/2 )
            ||  (collide.cursor_projections.x.min.distance <= collide.FIXED_SQUARE_SIZE/2 && collide.cursor_projections.x.max.distance >= collide.FIXED_SQUARE_SIZE/2 )
            ||  (collide.cursor_projections.x.min.distance >= -collide.FIXED_SQUARE_SIZE/2 && collide.cursor_projections.x.max.distance <= collide.FIXED_SQUARE_SIZE/2 )) ? true : false;
        collide.cursor_projections.y.is_collide = 
                ( (collide.cursor_projections.y.min.distance <= -collide.FIXED_SQUARE_SIZE/2 && collide.cursor_projections.y.max.distance >= -collide.FIXED_SQUARE_SIZE/2 )
            ||  (collide.cursor_projections.y.min.distance <= collide.FIXED_SQUARE_SIZE/2 && collide.cursor_projections.y.max.distance >= collide.FIXED_SQUARE_SIZE/2 )
            ||  (collide.cursor_projections.y.min.distance >= -collide.FIXED_SQUARE_SIZE/2 && collide.cursor_projections.y.max.distance <= collide.FIXED_SQUARE_SIZE/2 )) ? true : false;
        
        return (collide.fixed_projections.x.is_collide 
            && collide.fixed_projections.y.is_collide
            && collide.cursor_projections.x.is_collide
            && collide.cursor_projections.y.is_collide) ? true : false;
    }
    
};
$(document).ready(function(){
    collide.__init();
});



var debug = function(attr){
    if($('.debugjs').length == 0){
        $('<span class="debugjs"></span>').appendTo('body');
    }
    $('.debugjs').text(attr);
}
#collide {
  margin: 25px;
  border: 1px solid black;
  float: left;
}

#form {
  margin-left: 550px;
  padding-top: 2px;
}

#fixed_form h2 {
  color: #0033FF;
}

#cursor_form h2 {
  color: #FF8800;
}

#fixed_form input, #cursor_form input {
  margin-left: 25px;
  width: 50px;
}

#fixed_rotation_speed, #cursor_rotation_speed {
  margin-right: 150px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="collide" width="500" height="500"></canvas>


<div id="form">
  <h3>Understand how to detect if 2 rotates squares collide.</h3>
  <p>The 2 rotates squares collide only when all the projection of a square hit the second one.</p>
  <div id="fixed_form">
      <h2>Fixed square :</h2>
      Rotation speed : <input type="number" id="fixed_rotation_speed" value="0"/>
      Angle : <input type="number" id="fixed_angle" value="0"/>
      (Set : <input type="number" id="fixed_set_angle" value="0"/>)
  </div>
  <div id="cursor_form">
      <h2>Cursor square :</h2>
      Rotation speed : <input type="number" id="cursor_rotation_speed" value="-1"/>
      Angle : <input type="number" id="cursor_angle" value="0"/>
      (Set : <input type="number" id="cursor_set_angle" value="0"/>)
  </div>
</div>

Do the same job with no-rotate rect (it will be way easier) and it will work on every case