确定有多少视口被元素覆盖(IntersectionObserver)
Determine how much of the viewport is covered by element (IntersectionObserver)
我正在使用 IntersectionObserver
在元素进入视口时添加和删除 classes。
与其说"when X% of the element is visible - add this class",不如说"when X% of the element is visible or when X% of the viewport is covered by the element - add this class"。
我假设这不可能?如果是这样,我认为 IntersectionObserver
有点缺陷,因为如果你有一个比视口高 10 倍的元素,除非你将阈值设置为 10% 或更低,否则它永远不会被视为可见。当你有可变高度的元素时,尤其是在响应式设计中,你必须将阈值设置为 0.1% 之类的东西才能 "sure" 元素将收到 class (你永远不可能真正做到当然可以)。
编辑:回应摩西的回复。
Edit2:更新了几个阈值以强制它更频繁地计算 percentOfViewport。还是不理想。
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>
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);
在回调中,给定视口的大小,给定元素的大小,给定重叠的百分比,您可以计算视口中的重叠百分比:
const percentViewPort = viewPortSquarePixel/100;
const percentOverlapped = (targetSquarePixel * percent ) / percentViewPort;
示例:
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>
用于计算目标与 getBoundingClientRect()
的重叠 area/percent 的替代数学
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>
你需要做的是给每个元素一个不同的阈值。如果元素比默认阈值短(相对于 window),则默认阈值可以正常工作,但如果它更高,则需要为该元素设置一个唯一阈值。
假设您要触发以下元素之一:
- 50% 可见或
- 覆盖 50% 的屏幕
然后你需要检查:
- 如果元素短于 window 的 50%,您可以使用选项 1
- 如果元素高于 window 的 50%,则需要给它一个阈值,即 windows' 高度除以元素高度乘以阈值 (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);
});
这是一个使用 ResizeObserver
的解决方案,当元素 > 比率可见 或完全覆盖视口时触发回调。
/**
* Detect when an element is > ratio visible, or when it fully
* covers the viewport.
* Callback is called only once.
* Only works for height / y scrolling.
*/
export function onIntersection(
elem: HTMLElement,
ratio: number = 0.5,
callback: () => void
) {
// This helper is needed because IntersectionObserver doesn't have
// an easy mode for when the elem is taller than the viewport.
// It uses ResizeObserver to re-observe intersection when needed.
const maxRatio = window.innerHeight / elem.getBoundingClientRect().height;
const threshold = maxRatio < ratio ? 0.99 * maxRatio : ratio;
const intersectionObserver = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry.isIntersecting && entry.intersectionRatio >= threshold) {
disconnect();
callback();
}
},
{ threshold: [threshold] }
);
const resizeObserver = new ResizeObserver(() => {
const diff =
maxRatio - window.innerHeight / elem.getBoundingClientRect().height;
if (Math.abs(diff) > 0.0001) {
disconnect();
onIntersection(elem, ratio, callback);
}
});
const disconnect = () => {
intersectionObserver.disconnect();
resizeObserver.disconnect();
};
resizeObserver.observe(elem);
intersectionObserver.observe(elem);
}
我正在使用 IntersectionObserver
在元素进入视口时添加和删除 classes。
与其说"when X% of the element is visible - add this class",不如说"when X% of the element is visible or when X% of the viewport is covered by the element - add this class"。
我假设这不可能?如果是这样,我认为 IntersectionObserver
有点缺陷,因为如果你有一个比视口高 10 倍的元素,除非你将阈值设置为 10% 或更低,否则它永远不会被视为可见。当你有可变高度的元素时,尤其是在响应式设计中,你必须将阈值设置为 0.1% 之类的东西才能 "sure" 元素将收到 class (你永远不可能真正做到当然可以)。
编辑:回应摩西的回复。
Edit2:更新了几个阈值以强制它更频繁地计算 percentOfViewport。还是不理想。
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>
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);
在回调中,给定视口的大小,给定元素的大小,给定重叠的百分比,您可以计算视口中的重叠百分比:
const percentViewPort = viewPortSquarePixel/100;
const percentOverlapped = (targetSquarePixel * percent ) / percentViewPort;
示例:
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>
用于计算目标与 getBoundingClientRect()
的重叠 area/percent 的替代数学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>
你需要做的是给每个元素一个不同的阈值。如果元素比默认阈值短(相对于 window),则默认阈值可以正常工作,但如果它更高,则需要为该元素设置一个唯一阈值。
假设您要触发以下元素之一:
- 50% 可见或
- 覆盖 50% 的屏幕
然后你需要检查:
- 如果元素短于 window 的 50%,您可以使用选项 1
- 如果元素高于 window 的 50%,则需要给它一个阈值,即 windows' 高度除以元素高度乘以阈值 (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);
});
这是一个使用 ResizeObserver
的解决方案,当元素 > 比率可见 或完全覆盖视口时触发回调。
/**
* Detect when an element is > ratio visible, or when it fully
* covers the viewport.
* Callback is called only once.
* Only works for height / y scrolling.
*/
export function onIntersection(
elem: HTMLElement,
ratio: number = 0.5,
callback: () => void
) {
// This helper is needed because IntersectionObserver doesn't have
// an easy mode for when the elem is taller than the viewport.
// It uses ResizeObserver to re-observe intersection when needed.
const maxRatio = window.innerHeight / elem.getBoundingClientRect().height;
const threshold = maxRatio < ratio ? 0.99 * maxRatio : ratio;
const intersectionObserver = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry.isIntersecting && entry.intersectionRatio >= threshold) {
disconnect();
callback();
}
},
{ threshold: [threshold] }
);
const resizeObserver = new ResizeObserver(() => {
const diff =
maxRatio - window.innerHeight / elem.getBoundingClientRect().height;
if (Math.abs(diff) > 0.0001) {
disconnect();
onIntersection(elem, ratio, callback);
}
});
const disconnect = () => {
intersectionObserver.disconnect();
resizeObserver.disconnect();
};
resizeObserver.observe(elem);
intersectionObserver.observe(elem);
}