Check if a piece of the element is in viewport - javascript

I am working on a Javascript code for checking if element is in viewport.
But now I have a code that returns only true if the element is 100% in the viewport.
Is there a way for example if there are 10 pixels returns true or if a percentage... of the element is in the viewport return true?
My code for so far
<script type="text/javascript">
var elem = document.getElementById("result");
var bounding = elem.getBoundingClientRect();
if (bounding.top >= 0 && bounding.left >= 0 && bounding.right <= window.innerWidth && bounding.bottom <= window.innerHeight) {
alert('in viewport');
}
</script>

Based on #Jorg's code, here's the same with the Intersection Observer API, which is a newer way of checking for intersections. This will work on all modern browsers ~ 93.5% according to Can I Use
This is set up to make it consider anything that's 50% within the viewport as within the threshold. I made it such a large value so it's easy to see how it works.
As you'll notice with this, the callback is only called at the threshold (after the initial check). So, if you want an accurate intersection percentage, you'll probably want to increase the number of thresholds checked.
let callback = (entries, observer) => {
entries.forEach(entry => {
entry.target.style.backgroundColor = entry.isIntersecting ? 'green' : 'red';
entry.target.innerHTML = entry.intersectionRatio;
})
}
let observer = new IntersectionObserver(callback, {
threshold: [0.5] // If 50% of the element is in the screen, we count it!
// Can change the thresholds based on your needs. The default is 0 - it'll run only when the element first comes into view
});
['div1', 'div2', 'div3', 'div4'].forEach(d => {
const div = document.getElementById(d);
if (div) observer.observe(div);
})
html,
body {
padding: 0;
margin: 0;
}
body {
height: 200vh;
width: 200vw;
}
#div1 {
position: absolute;
left: calc(100vw - 60px - 10px);
top: 10px;
height: 100px;
width: 60px;
background-color: red;
color: white;
}
#div2 {
position: absolute;
left: 20px;
top: 10px;
height: 50px;
width: 60px;
background-color: blue;
color: white;
}
#div3 {
position: absolute;
left: calc(100vw - 260px + 50px);
top: max(calc(100vh - 350px + 120px), 120px);
height: 350px;
width: 260px;
background-color: green;
color: white;
text-align: left;
}
#div4 {
position: absolute;
height: 9000px;
width: 9000px;
color: black;
background-color: yellow;
}
<div id="div1"></div>
<div id="div2"></div>
<div id="div3"></div>
<!-- enable this div to see an example of a div LARGER than your viewport. -->
<!-- <div id="div4"></div> -->

I guess what you're looking for is the intersection between the element and the viewport? Meaning, find out how much of the div overlaps with the viewport.
Using the function below should tell you, between 0 and 1, how much of the DIV fits inside the viewport. Be aware though, that the div could also just be larger than the viewport this way, in which case the overlapping area is also less than 1.
Here is a working example
const intersection = (r1, r2) => {
const xOverlap = Math.max(0, Math.min(r1.x + r1.w, r2.x + r2.w) - Math.max(r1.x, r2.x));
const yOverlap = Math.max(0, Math.min(r1.y + r1.h, r2.y + r2.h) - Math.max(r1.y, r2.y));
const overlapArea = xOverlap * yOverlap;
return overlapArea;
}
const percentInView = (div) => {
const rect = div.getBoundingClientRect();
const dimension = { x: rect.x, y: rect.y, w: rect.width, h: rect.height };
const viewport = { x: 0, y: 0, w: window.innerWidth, h: window.innerHeight };
const divsize = dimension.w * dimension.h;
const overlap = intersection(dimension, viewport);
return overlap / divsize;
}

Related

html5/css/javascript : How to superpose two canvas in a div

function drawAll() {
// Upper zone, 8 grey transparent buttons
let canvas0 = document.getElementById("layer0");
canvas0.width = 1000;
canvas0.height = 100;
let bandeau = canvas0.getContext("2d");
bandeau.fillStyle = "rgba(128,128,80,0.3)";
for (var i = 0; i < 8; i++) {
bandeau.beginPath;
bandeau.arc(50 + 110 * i, 50, 45, 0, 2 * Math.PI);
bandeau.fill();
}
// Lower zone, a red rectangle partially under the buttons
let canvas1 = document.getElementById("layer1");
canvas1.width = 1000;
canvas1.height = 1000;
let dessin = canvas1.getContext("2d");
dessin.fillStyle = "red";
dessin.fillRect(30, 50, 800, 200);
canvas0.style.visibility = "visible";
canvas1.style.visibility = "visible";
}
drawAll()
body {
background-color: rgb(249, 249, 250);
}
.container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
z-index: -10;
}
.scrollable {
position: absolute;
top: 0px;
left: 0px;
z-index: 1;
}
.fixed {
position: absolute;
top: 0px;
left: 0px;
z-index: 2;
}
<div class="container">
<canvas id="layer0" class="scrollable"></canvas>
<canvas id="layer1" class="fixed"></canvas>
</div>
Hello
I'm stuck on a superposition problem of two canvas. Here is a simplified example. Note that in the real application, buttons and drawings are far more complicated and that I want to keep the structure with html5 / css / javascript.
I suppose that I miss something in the css to succeed to have these two canvas superposed, buttons partially covering the red rectangle, but what ?
Thanks if somebody can help.
The problem is that <body> doesn't have any height, which makes the .container height of 100% equally zero.
Absolutely positioned elements do no contribute to their parent's height. As soon as you start giving .container an actual height, you can see its content. In the example below, I went for 100vw and 100vh for width and height, but since your canvases are 1000px wide, you could also use that or any other value.
An absolutely positioned element no longer exists in the normal document layout flow. Instead, it sits on its own layer separate from everything else.
Source: MDN Web Docs
The other option is to remove overflow: hidden; from .container and show everything outside of it.
function drawAll() {
// Upper zone, 8 grey transparent buttons
let canvas0 = document.getElementById("layer0");
canvas0.width = 1000;
canvas0.height = 100;
let bandeau = canvas0.getContext("2d");
bandeau.fillStyle = "rgba(128,128,80,0.3)";
for (var i = 0; i < 8; i++) {
bandeau.beginPath;
bandeau.arc(50 + 110 * i, 50, 45, 0, 2 * Math.PI);
bandeau.fill();
}
// Lower zone, a red rectangle partially under the buttons
let canvas1 = document.getElementById("layer1");
canvas1.width = 1000;
canvas1.height = 1000;
let dessin = canvas1.getContext("2d");
dessin.fillStyle = "red";
dessin.fillRect(30, 50, 800, 200);
canvas0.style.visibility = "visible";
canvas1.style.visibility = "visible";
}
drawAll()
body {
background-color: rgb(249, 249, 250);
}
.container {
position: relative;
overflow: hidden;
z-index: -10;
height: 100vh;
width: 100vw;
}
.scrollable {
position: absolute;
top: 0px;
left: 0px;
z-index: 1;
}
.fixed {
position: absolute;
top: 0px;
left: 0px;
z-index: 2;
}
<div class="container">
<canvas id="layer0" class="scrollable"></canvas>
<canvas id="layer1" class="fixed"></canvas>
</div>

How do I set the height of an element to match the offset speed of a scaling div?

I am trying to set the maximum scroll of an element, in this case .contain, to match the height needed for .square to fill the entire viewport (both width and height) on scroll. I need to figure out how I can retrieve the remaining height needed to cover the offset value of the scroll.
Here is a codepen showing what currently happens. The scroll reaches the bottom and the square fails to fill the screen. Without the offset I can get this to work perfectly (see line 17), but I'd really like to learn how I can incorporate the parallax offset/speed effect.
https://codepen.io/anon/pen/zbeyQd
The non-offset version to show how the above pen should work. Square fills the screen as the scrollbar hits the bottom: https://codepen.io/anon/pen/Rdvvom
This should do the trick
const sq = document.querySelector('.square')
const contain = document.querySelector('.contain')
//set desired scroll boundaries. can be any size
//the higher the value the longer you'll have to scroll
const scrollOffset = 250
const sqW = sq.offsetWidth
const sqH = sq.offsetHeight
const wHeight = window.innerHeight
const wWidth = window.innerWidth
contain.style.height = `${wHeight + scrollOffset}px`
window.addEventListener('scroll', (e) => {
const percentScroll = window.scrollY * 100 / scrollOffset
const width = (wWidth - sqW) * percentScroll / 100
const height = (wHeight - sqH) * percentScroll / 100
sq.style.width = `${sqW + width}px`
sq.style.height = `${sqH + height}px`
})
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.contain {
height: 100%;
width: 100%;
background-color: papayawhip;
}
.square {
width: 100px;
height: 100px;
background-color: tomato;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0.25;
}
<div class="contain">
<div class="square"></div>
</div>

Slider handle needs to restricted inside the container, should not go beyond the container

In the custom slider i have created, the handle is moving beyond the container. But i want it to stay within the container limits. We could just do it simple by setting margin-left as offset in CSS. But My requirement is when the handle right end detect the container's end the handle should not be allowed to move anymore. Any help is appreciated. Thanks.
Demo Link: https://jsfiddle.net/mohanravi/1pbzdyyd/30/
document.getElementsByClassName('contain')[0].addEventListener("mousedown", downHandle);
function downHandle() {
document.addEventListener("mousemove", moveHandle);
document.addEventListener("mouseup", upHandle);
}
function moveHandle(e) {
var left = e.clientX - document.getElementsByClassName('contain')[0].getBoundingClientRect().left;
var num = document.getElementsByClassName('contain')[0].offsetWidth / 100;
var val = (left / num);
if (val < 0) {
val = 0;
} else if (val > 100) {
val = 100;
}
var pos = document.getElementsByClassName('contain')[0].getBoundingClientRect().width * (val / 100);
document.getElementsByClassName('bar')[0].style.left = pos + 'px';
}
function upHandle() {
document.removeEventListener("mousemove", moveHandle);
document.removeEventListener("mouseup", upHandle);
}
.contain {
height: 4px;
width: 450px;
background: grey;
position: relative;
top: 50px;
left: 40px;
}
.bar {
width: 90px;
height: 12px;
background: transparent;
border: 1px solid red;
position: absolute;
top: calc(50% - 7px);
left: 0px;
cursor: ew-resize;
}
<div class='contain'>
<div class='bar'></div>
</div>
You need to change
this
document.getElementsByClassName('bar')[0].style.left = pos + 'px';
to this
if(pos > 90){
document.getElementsByClassName('bar')[0].style.left = pos - 90 + 'px';
}
else{
document.getElementsByClassName('bar')[0].style.left = 0 + 'px';
}
since width of your bar is 90px I am subtracting 90.
See this updated fiddle

Check if element is partially in viewport

I'm trying to determine if an element is partially or fully in the viewport.
I've found this which will determine if an element is fully in view but kept getting confused when trying to determine partial visibility. I don't want to use jQuery.
Basically, the idea is that there will be an element on the page that could be out of view. Once the user scrolls that element into view, even partially, it should trigger an event. I'll handle the event trigger by binding an onscroll event. I just need the detection to work properly.
function isInViewport(element) {
var rect = element.getBoundingClientRect();
var html = document.documentElement;
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || html.clientHeight) &&
rect.right <= (window.innerWidth || html.clientWidth)
);
}
Any help would be greatly appreciated!
Late answer, but about a month ago I wrote a function that does exactly that, it determines how much an element is visible measured in percent in the viewport. Ive tested it in chrome, firefox, ie11, ios on iphone/ipad. The function returns true when X percent (as a number from 0 to 100) of the element is visible. Only determines if the measurements of the element are visible and not if the element is hidden with opacity, visibility etc..
const isElementXPercentInViewport = function(el, percentVisible) {
let
rect = el.getBoundingClientRect(),
windowHeight = (window.innerHeight || document.documentElement.clientHeight);
return !(
Math.floor(100 - (((rect.top >= 0 ? 0 : rect.top) / +-rect.height) * 100)) < percentVisible ||
Math.floor(100 - ((rect.bottom - windowHeight) / rect.height) * 100) < percentVisible
)
};
You need a solution based on element.offsetTop, element.offsetLeft, element.offsetHeight, element.offsetWidth, window.innerWidth and window.innerHeight
(depending on the situation, you might also want to take the scrolling position into consideration)
function isInViewport(element){
if(element.offsetTop<window.innerHeight &&
element.offsetTop>-element.offsetHeight
&& element.offsetLeft>-element.offsetWidth
&& element.offsetLeft<window.innerWidth){
return true;
} else {
return false;
}
}
function test(){
alert(isInViewport(document.getElementById("elem"))?"Yes":"No");
}
#elem{width: 20px; height: 20px; background: red; }
#elem{position: absolute;top: -9px;left: 600px;}
<div id="elem"></div>
<button onclick="test()">Check</button>
function partInViewport(elem) {
let x = elem.getBoundingClientRect().left;
let y = elem.getBoundingClientRect().top;
let ww = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
let hw = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
let w = elem.clientWidth;
let h = elem.clientHeight;
return (
(y < hw &&
y + h > 0) &&
(x < ww &&
x + w > 0)
);
}
document.addEventListener("scroll", ()=>{
let el = document.getElementById("test");
if (partInViewport(el)) {
document.getElementById("container").style.backgroundColor = "green";
} else {
document.getElementById("container").style.backgroundColor = "red";
}
});
#test {
height: 200px;
width: 145px;
background-color: grey;
}
#container {
height: 400px;
width: 345px;
transform: translate(400px, 360px);
background-color: red;
display: grid;
align-items: center;
justify-items: center;
}
body {
height: 1500px;
width: 1500px;
}
<div id="container">
<div id="test"></div>
</div>
My example for this code:
https://jsfiddle.net/xqpebwtv/27/
The modern way on how to handle this would be Intersection Observer (IO). With IO you can observe (as the name suggest) elements and trigger actions whenver an alement comes into view. You can set the percentages at which the observer is triggered (e.g. 10% in view, 90% in view, ... )
I really like this example from the linked page, there you have 4 different elements. Each with a different trigger percentage.
let observers = [];
startup = () => {
let wrapper = document.querySelector(".wrapper");
// Options for the observers
let observerOptions = {
root: null,
rootMargin: "0px",
threshold: []
};
// An array of threshold sets for each of the boxes. The
// first box's thresholds are set programmatically
// since there will be so many of them (for each percentage
// point).
let thresholdSets = [
[],
[0.5],
[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
[0, 0.25, 0.5, 0.75, 1.0]
];
for (let i = 0; i <= 1.0; i += 0.01) {
thresholdSets[0].push(i);
}
// Add each box, creating a new observer for each
for (let i = 0; i < 4; i++) {
let template = document.querySelector("#boxTemplate").content.cloneNode(true);
let boxID = "box" + (i + 1);
template.querySelector(".sampleBox").id = boxID;
wrapper.appendChild(document.importNode(template, true));
// Set up the observer for this box
observerOptions.threshold = thresholdSets[i];
observers[i] = new IntersectionObserver(intersectionCallback, observerOptions);
observers[i].observe(document.querySelector("#" + boxID));
}
// Scroll to the starting position
document.scrollingElement.scrollTop = wrapper.firstElementChild.getBoundingClientRect().top + window.scrollY;
document.scrollingElement.scrollLeft = 750;
}
intersectionCallback = (entries) => {
entries.forEach((entry) => {
let box = entry.target;
let visiblePct = (Math.floor(entry.intersectionRatio * 100)) + "%";
box.querySelector(".topLeft").innerHTML = visiblePct;
box.querySelector(".topRight").innerHTML = visiblePct;
box.querySelector(".bottomLeft").innerHTML = visiblePct;
box.querySelector(".bottomRight").innerHTML = visiblePct;
});
}
startup();
body {
padding: 0;
margin: 0;
}
svg:not(:root) {
display: block;
}
.playable-code {
background-color: #f4f7f8;
border: none;
border-left: 6px solid #558abb;
border-width: medium medium medium 6px;
color: #4d4e53;
height: 100px;
width: 90%;
padding: 10px 10px 0;
}
.playable-canvas {
border: 1px solid #4d4e53;
border-radius: 2px;
}
.playable-buttons {
text-align: right;
width: 90%;
padding: 5px 10px 5px 26px;
}
.contents {
position: absolute;
width: 700px;
height: 1725px;
}
.wrapper {
position: relative;
top: 600px;
}
.sampleBox {
position: relative;
left: 175px;
width: 150px;
background-color: rgb(245, 170, 140);
border: 2px solid rgb(201, 126, 17);
padding: 4px;
margin-bottom: 6px;
}
#box1 {
height: 300px;
}
#box2 {
height: 175px;
}
#box3 {
height: 350px;
}
#box4 {
height: 100px;
}
.label {
font: 14px "Open Sans", "Arial", sans-serif;
position: absolute;
margin: 0;
background-color: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(0, 0, 0, 0.7);
width: 3em;
height: 18px;
padding: 2px;
text-align: center;
}
.topLeft {
left: 2px;
top: 2px;
}
.topRight {
right: 2px;
top: 2px;
}
.bottomLeft {
bottom: 2px;
left: 2px;
}
.bottomRight {
bottom: 2px;
right: 2px;
}
<template id="boxTemplate">
<div class="sampleBox">
<div class="label topLeft"></div>
<div class="label topRight"></div>
<div class="label bottomLeft"></div>
<div class="label bottomRight"></div>
</div>
</template>
<main>
<div class="contents">
<div class="wrapper">
</div>
</div>
</main>
What your code is saying is that:
The top side of the element must be below the top side of the window,
The left of the element must be to the right of the left side of the window,
The bottom side of the element must be to the top of the bottom side of the window, AND
The right side of the element must be to the left of the right side of the window
What you want:
The top side of the element must be below the top side of the window OR the bottom side of the element must be above the bottom side of the window, AND
The left side of the element must be to the right of the left side of the window OR the right side of the element must be to the left of the right side of the window
Take what you will from that, the code should be simple enough from here.
This should do it, offsets are not needed, since we are comparing client rectangles.
function isPartiallyVisibleInViewport(element, viewport) {
var bound = element.getBoundingClientRect();
var bound2 = viewport.getBoundingClientRect();
return bound.bottom > bound2.top && bound.top < bound2.bottom;
}
This function only checks vertically and must be extended if you also want to check horizontally:
return bound.bottom > bound2.top && bound.top < bound2.bottom && bound.right > bound2.left && bound.left < bound2.right;

jitter when using jquery to alter background-position

Here's the jsfiddle.
It's the interface to cropping an image. As you can see the selection div takes the same background image and positions it to the negative of the top and left attributes of the selection div. In theory this should give a perfect overlap, but there's a jitter as you move the selection div around, and I can't seem to figure out what is causing it.
html
<div id="main">
<div id="selection"></div>
</div>
css
#main {
width: 600px;
height: 450px;
position: relative;
background: url("http://cdn-2.historyguy.com/celebrity_history/Scarlett_Johansson.jpg");
background-size: contain;
}
#selection {
width: 100px;
height: 100px;
position: absolute;
background: url("http://cdn-2.historyguy.com/celebrity_history/Scarlett_Johansson.jpg");
border: 1px dotted white;
background-size: 600px 450px;
}
jquery
$(document).ready(function () {
var move = false;
var offset = [];
var selection = null;
$("#selection").mousedown(function (e) {
move = true;
selection = $(this);
offset = [e.pageX - selection.offset().left, e.pageY - selection.offset().top];
});
$("#selection").mousemove(function (e) {
if (move == true) {
selection.css("left", e.pageX - offset[0]);
selection.css("top", e.pageY - offset[1]);
selection.css("background-position", (((-selection.position().left) - 1) + "px " + ((-selection.position().top ) - 1) + "px"));
}
});
$("#selection").mouseup(function (e) {
move = false;
});
})
It would appear that there is a value of 5 offset that needs to be added to ensure seamlessness
DEMO http://jsfiddle.net/nzx0fcp5/2/
offset = [e.pageX - selection.offset().left + 5, e.pageY - selection.offset().top + 5];
So, while experimenting I discovered that this was only a problem at certain sizes of the image. At the original size it is no problem, neither at half nor a quarter of this size. It wasn't simply a matter of keeping the image in proportion not having the image square or using even pixel sizes. I'm assuming this had something to do with partial pixel sizes, but I'm not sure, and I couldn't see any way to work around this, at least none that seemed worth the effort.
So while checking out the code of other croppers I took a look at POF's image cropper, they seem to have got round the problem by not using the background-position property at all (I'm not sure if it's plugin or they coded it themselves). They just set the image down and then used a transparent selection div with 4 divs stuck to each edge for the shading. So there's no pixel crunching on the fly at all. I like the simplicity and lightweight nature of this design and knocked up a version myself in jsfiddle to see if I could get it to work well.
new jitter free jsfiddle with no pixel crunching
I liked the solution for the preview box as well.
html
<body>
<div id="main">
<img src="http://flavorwire.files.wordpress.com/2012/01/scarlett_johansson.jpg" />
<div id="upperShade" class="shade" > </div>
<div id="leftShade" class="shade" > </div>
<div id="selection"></div>
<div id="rightShade" class="shade"></div>
<div id="lowerShade" class="shade" ></div>
</div>
</body>
css
#main {
position:relative;
width: 450px;
height: 600px;
}
#selection {
width: 148px;
height: 148px;
position: absolute;
border: 1px dotted white;
top: 0px;
left: 0px;
z-index: 1;
}
.shade {
background-color: black;
opacity: 0.5;
position: absolute;
}
#upperShade {
top: 0px;
left: 0px;
width: 600px;
}
#leftShade {
left: 0px;
top: 0px;
height: 150px;
width: auto;
}
#rightShade {
left: 150px;
top: 0px;
height: 150px;
width: 450px;
}
#lowerShade {
left:0px;
top: 150px;
width: 600px;
height: 300px;
}
jquery
$(document).ready(function () {
var move = false;
var offset = [];
var selection = null;
$("#selection").mousedown(function (e) {
move = true;
selection = $(this);
offset = [e.pageX - selection.offset().left, e.pageY - selection.offset().top];
});
$("#selection").mousemove(function (e) {
if (move == true) {
selection.css("left", e.pageX - offset[0]);
selection.css("top", e.pageY - offset[1]);
setShade();
}
});
function setShade() {
$("#upperShade").css("height", selection.position().top);
$("#lowerShade").css("height", 600 - (selection.position().top + 150));
$("#lowerShade").css("top", selection.position().top + 150);
$("#leftShade").css("top", selection.position().top);
$("#leftShade").css("width", selection.position().left);
$("#rightShade").css("top", selection.position().top);
$("#rightShade").css("left", selection.position().left + 150);
$("#rightShade").css("width", 450 - selection.position().left);
}
$("#selection").mouseup(function (e) {
move = false;
});
});

Categories

Resources