Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Determine how much of the viewport is covered by element (IntersectionObserver)

I'm using the IntersectionObserver to add and remove classes to elements as they enter the viewport.

Instead of saying "when X% of the element is visible - add this class" I would like to say "when X% of the element is visible or when X% of the viewport is covered by the element - add this class".

I'm assuming this isn't possible? If so I think it's a bit of a flaw with the IntersectionObserver because if you have an element that's 10 times taller than the viewport it'll never count as visible unless you set the threshold to 10% or less. And when you have variable height elements, especially in a responsive design, you'll have to set the threshold to something like 0.1% to be "sure" the element will receive the class (you can never be truly sure though).

Edit: In response to Mose's reply.

Edit2: Updated with several thresholds to force it to calculate percentOfViewport more often. Still not ideal.

var observer = new IntersectionObserver(function (entries) {
	entries.forEach(function (entry) {
		var entryBCR = entry.target.getBoundingClientRect();
		var percentOfViewport = ((entryBCR.width * entryBCR.height) * entry.intersectionRatio) / ((window.innerWidth * window.innerHeight) / 100);

		console.log(entry.target.id + ' covers ' + percentOfViewport + '% of the viewport and is ' + (entry.intersectionRatio * 100) + '% visible');

		if (entry.intersectionRatio > 0.25) {
			entry.target.style.background = 'red';
		}
		else if (percentOfViewport > 50) {
			entry.target.style.background = 'green';
		}
		else {
			entry.target.style.background = 'lightgray';
		}
	});
}, {threshold: [0.025, 0.05, 0.075, 0.1, 0.25]});

document.querySelectorAll('#header, #tall-content').forEach(function (el) {
	observer.observe(el);
});
#header {background: lightgray; min-height: 200px}
#tall-content {background: lightgray; min-height: 2000px}
<header id="header"><h1>Site header</h1></header>
<section id="tall-content">I'm a super tall section. Depending on your resolution the IntersectionObserver will never consider this element visible and thus the percentOfViewport isn't re-calculated.</section>
like image 563
powerbuoy Avatar asked Sep 04 '19 10:09

powerbuoy


Video Answer


2 Answers

What you need to do is give each element a different threshold. If the element is shorter than the default threshold (in relation to the window) then the default threshold works fine, but if it's taller you need a unique threshold for that element.

Say you want to trigger elements that are either:

  1. 50% visible or
  2. Covering 50% of the screen

Then you need to check:

  1. If the element is shorter than 50% of the window you can use option 1
  2. If the element is taller than 50% of the window you need to give it a threshold that is the windows' height divided by the height of the element multiplied by the threshold (50%):
function doTheThing (el) {
    el.classList.add('in-view');
}

const threshold = 0.5;

document.querySelectorAll('section').forEach(el => {
    const elHeight = el.getBoundingClientRect().height;
    var th = threshold;

    // The element is too tall to ever hit the threshold - change threshold
    if (elHeight > (window.innerHeight * threshold)) {
        th = ((window.innerHeight * threshold) / elHeight) * threshold;
    }

    new IntersectionObserver(iEls => iEls.forEach(iEl => doTheThing(iEl)), {threshold: th}).observe(el);
});
like image 77
powerbuoy Avatar answered Oct 14 '22 04:10

powerbuoy


let optionsViewPort = {
  root: document.querySelector('#viewport'), // assuming the viewport has an id "viewport"
  rootMargin: '0px',
  threshold: 1.0
}

let observerViewport = new IntersectionObserver(callback, optionsViewPort);
observerViewPort.observe(target);

In callback, given the size of the viewport, given the size of the element, given the % of overlapping, you can calculate the percent overlapped in viewport:

  const percentViewPort = viewPortSquarePixel/100;
  const percentOverlapped = (targetSquarePixel * percent ) / percentViewPort;

Example:

const target = document.querySelector('#target');
const viewport = document.querySelector('#viewport');
const optionsViewPort = {
  root: viewport, // assuming the viewport has an id "viewport"
  rootMargin: '0px',
  threshold: 1.0
}

let callback = (entries, observer) => { 
  entries.forEach(entry => {  
    const percentViewPort = (parseInt(getComputedStyle(viewport).width) * parseInt(getComputedStyle(viewport).height))/100;    
    const percentOverlapped = ((parseInt(getComputedStyle(target).width) * parseInt(getComputedStyle(viewport).height)) * entry.intersectionRatio) / percentViewPort;
    console.log("% viewport overlapped", percentOverlapped);
    console.log("% of element in viewport", entry.intersectionRatio*100);
    // Put here the code to evaluate percentOverlapped and target visibility to apply the desired class
  });
    
};

let observerViewport = new IntersectionObserver(callback, optionsViewPort);
observerViewport.observe(target);
#viewport {
  width: 900px;
  height: 900px;
  background: yellow;
  position: relative;
}

#target {
  position: absolute;
  left: 860px;
  width: 100px;
  height: 100px;
  z-index: 99;
  background-color: red;
}
<div id="viewport">
  <div id="target" />
</div>

Alternate math to calculate overlap area/percent of target with getBoundingClientRect()

const target = document.querySelector('#target');
const viewport = document.querySelector('#viewport');

const rect1 = viewport.getBoundingClientRect();
const rect2 = target.getBoundingClientRect();

const rect1Area = rect1.width * rect1.height;
const rect2Area = rect2.width * rect2.height;

const x_overlap = Math.max(0, Math.min(rect1.right, rect2.right) - Math.max(rect1.left, rect2.left));
const y_overlap = Math.max(0, Math.min(rect1.bottom, rect2.bottom) - Math.max(rect1.top, rect2.top));

const overlapArea = x_overlap * y_overlap;
const overlapPercentOfTarget = overlapArea/(rect2Area/100);

console.log("OVERLAP AREA", overlapArea);
console.log("TARGET VISIBILITY %", overlapPercentOfTarget);
#viewport {
  width: 900px;
  height: 900px;
  background: yellow;
  position: relative;
}

#target {
  position: absolute;
  left: 860px;
  width: 100px;
  height: 100px;
  z-index: 99;
  background-color: red;
}
<div id="viewport">
  <div id="target" />
</div>
like image 36
Mosè Raguzzini Avatar answered Oct 14 '22 03:10

Mosè Raguzzini