How do I know the IntersectionObserver scroll direction? - javascript

So, how do I know the scroll direction when the event it's triggered?
In the returned object the closest possibility I see is interacting with the boundingClientRect kind of saving the last scroll position but I don't know if handling boundingClientRect will end up on performance issues.
Is it possible to use the intersection event to figure out the scroll direction (up / down)?
I have added this basic snippet, so if someone can help me.
I will be very thankful.
Here is the snippet:
var options = {
rootMargin: '0px',
threshold: 1.0
}
function callback(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('entry', entry);
}
});
};
var elementToObserve = document.querySelector('#element');
var observer = new IntersectionObserver(callback, options);
observer.observe(elementToObserve);
#element {
margin: 1500px auto;
width: 150px;
height: 150px;
background: #ccc;
color: white;
font-family: sans-serif;
font-weight: 100;
font-size: 25px;
text-align: center;
line-height: 150px;
}
<div id="element">Observed</div>
I would like to know this, so I can apply this on fixed headers menu to show/hide it

I don't know if handling boundingClientRect will end up on performance issues.
MDN states that the IntersectionObserver does not run on the main thread:
This way, sites no longer need to do anything on the main thread to watch for this kind of element intersection, and the browser is free to optimize the management of intersections as it sees fit.
MDN, "Intersection Observer API"
We can compute the scrolling direction by saving the value of IntersectionObserverEntry.boundingClientRect.y and compare that to the previous value.
Run the following snippet for an example:
const state = document.querySelector('.observer__state')
const target = document.querySelector('.observer__target')
const thresholdArray = steps => Array(steps + 1)
.fill(0)
.map((_, index) => index / steps || 0)
let previousY = 0
let previousRatio = 0
const handleIntersect = entries => {
entries.forEach(entry => {
const currentY = entry.boundingClientRect.y
const currentRatio = entry.intersectionRatio
const isIntersecting = entry.isIntersecting
// Scrolling down/up
if (currentY < previousY) {
if (currentRatio > previousRatio && isIntersecting) {
state.textContent ="Scrolling down enter"
} else {
state.textContent ="Scrolling down leave"
}
} else if (currentY > previousY && isIntersecting) {
if (currentRatio < previousRatio) {
state.textContent ="Scrolling up leave"
} else {
state.textContent ="Scrolling up enter"
}
}
previousY = currentY
previousRatio = currentRatio
})
}
const observer = new IntersectionObserver(handleIntersect, {
threshold: thresholdArray(20),
})
observer.observe(target)
html,
body {
margin: 0;
}
.observer__target {
position: relative;
width: 100%;
height: 350px;
margin: 1500px 0;
background: rebeccapurple;
}
.observer__state {
position: fixed;
top: 1em;
left: 1em;
color: #111;
font: 400 1.125em/1.5 sans-serif;
background: #fff;
}
<div class="observer__target"></div>
<span class="observer__state"></span>
If the thresholdArray helper function might confuse you, it builds an array ranging from 0.0 to 1.0by the given amount of steps. Passing 5 will return [0.0, 0.2, 0.4, 0.6, 0.8, 1.0].

This solution is without the usage of any external state, hence simpler than solutions which keep track of additional variables:
const observer = new IntersectionObserver(
([entry]) => {
if (entry.boundingClientRect.top < 0) {
if (entry.isIntersecting) {
// entered viewport at the top edge, hence scroll direction is up
} else {
// left viewport at the top edge, hence scroll direction is down
}
}
},
{
root: rootElement,
},
);

Comparing boundingClientRect and rootBounds from entry, you can easily know if the target is above or below the viewport.
During callback(), you check isAbove/isBelow then, at the end, you store it into wasAbove/wasBelow.
Next time, if the target comes in viewport (for example), you can check if it was above or below. So you know if it comes from top or bottom.
You can try something like this:
var wasAbove = false;
function callback(entries, observer) {
entries.forEach(entry => {
const isAbove = entry.boundingClientRect.y < entry.rootBounds.y;
if (entry.isIntersecting) {
if (wasAbove) {
// Comes from top
}
}
wasAbove = isAbove;
});
}
Hope this helps.

:)
I don't think this is possible with a single threshold value. You could try to watch out for the intersectionRatio which in most of the cases is something below 1 when the container leaves the viewport (because the intersection observer fires async). I'm pretty sure that it could be 1 too though if the browser catches up quickly enough. (I didn't test this :D )
But what you maybe could do is observe two thresholds by using several values. :)
threshold: [0.9, 1.0]
If you get an event for the 0.9 first it's clear that the container enters the viewport...
Hope this helps. :)

My requirement was:
do nothing on scroll-up
on scroll-down, decide if an element started to hide from screen top
I needed to see a few information provided from IntersectionObserverEntry:
intersectionRatio (should be decreasing from 1.0)
boundingClientRect.bottom
boundingClientRect.height
So the callback ended up look like:
intersectionObserver = new IntersectionObserver(function(entries) {
const entry = entries[0]; // observe one element
const currentRatio = intersectionRatio;
const newRatio = entry.intersectionRatio;
const boundingClientRect = entry.boundingClientRect;
const scrollingDown = currentRatio !== undefined &&
newRatio < currentRatio &&
boundingClientRect.bottom < boundingClientRect.height;
intersectionRatio = newRatio;
if (scrollingDown) {
// it's scrolling down and observed image started to hide.
// so do something...
}
console.log(entry);
}, { threshold: [0, 0.25, 0.5, 0.75, 1] });
See my post for complete codes.

Related

How to get all sibling div's inside overlapping div area?

I have a large div and smaller siblings divs positioned inside it like this:
.large{
height:20rem;
width:20rem;
background-color:red;
position:absolute;
}
.item1{
height:5rem;
width:5rem;
background-color:blue;
top:1rem;
position:absolute;
}
.item2{
height:5rem;
width:5rem;
background-color:green;
top:3rem;
left:2rem;
position:absolute;
}
.item3{
height:5rem;
width:5rem;
background-color:yellow;
top:1rem;
left:6rem;
position:absolute;
}
<div class="large"></div>
<div class="item1"></div>
<div class="item2"></div>
<div class="item3"></div>
How do I get all the small divs within the large div dimensions?
Is there something similar to elementsFromPoint? Maybe something like elementsFromArea
Edit:
assume .large spans 320 pixels x 320 pixels
and I have multiple smaller divs on my screen, which can either be overlapping .large or outside it
How do I find divs which are overlapping .large?
Maybe we could get the position of .large & we already have the height and width of it and add it to some function like this:
elementsFromArea(large_x,large_y,large_height,large_width);
This should return an array of all the divs within that given range
(.large is merely for reference sake, I simply want to pass any given square area & find all the divs lying within it )
Bounty Edit:
The solution provided by #A Haworth works but I'm looking for a solution which doesn't involve having to loop and check every single element
this fiddle explains what I'm ultimately trying to achieve
Any clever work around will be accepted too!
You can use getBoundingClientRect to find the left, right, top and bottom bounds of each element.
Then test whether there is overlap with the large element by seeing whether the left is to the left of the right side of the large element and so on:
if ( ((l <= Right) && (r >= Left)) && ( (t <= Bottom) && (b >= Top)) )
To give a more thorough test, in this snippet the blue element has been pushed down so it only partially overlaps the large one and the yellow element doesn't overlap at all.
const large = document.querySelector('.large');
const largeRect = large.getBoundingClientRect();
const Left = largeRect.left;
const Right = largeRect.right;
const Top = largeRect.top;
const Bottom = largeRect.bottom;
const items = document.querySelectorAll('.large ~ *');
let overlappers = [];
items.forEach(item => {
const itemRect = item.getBoundingClientRect();
const l = itemRect.left;
const r = itemRect.right;
const t = itemRect.top;
const b = itemRect.bottom;
if (((l <= Right) && (r >= Left)) && ((t <= Bottom) && (b >= Top))) {
overlappers.push(item);
}
});
console.log('The items with these background colors overlap the large element:');
overlappers.forEach(item => {
console.log(window.getComputedStyle(item).backgroundColor);
});
.large {
height: 20rem;
width: 20rem;
background-color: red;
position: absolute;
}
.item1 {
height: 5rem;
width: 5rem;
background-color: blue;
top: 19rem;
position: absolute;
}
.item2 {
height: 5rem;
width: 5rem;
background-color: green;
top: 3rem;
left: 2rem;
position: absolute;
}
.item3 {
height: 5rem;
width: 5rem;
background-color: yellow;
top: 1rem;
left: 26rem;
position: absolute;
}
<div>
<div class="large"></div>
<div class="item1"></div>
<div class="item2"></div>
<div class="item3"></div>
</div>
Note, this snippet tests only those elements which are siblings of large in the CSS sense, that is that follow large. If you want all siblings whether they follow large or come before it then go back up to large's parent and get all its children (which will of course include large).
The IntersectionObserver API describes exactly what you are looking for. It's a relatively new API so I'm not surprised the other answers have not referenced it.
I have personally used it in a lazy loading context for displaying large tables without rendering 9001 rows at once. In my case, I would use the IntersectionObserver to determine when the last table row was in the user's field of view, and then I would load additional rows. It's very performant as it doesn't require any loops that poll the position of DOM elements, and the browser is free to optimize it however it likes.
Stealing from MDN, here's a simple way to create an IntersectionObserver. I've commented out options which I don't think you need.
let options = {
root: document.querySelector('.large'),
// rootMargin: '0px',
// threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);
The callback is a function that fires whenever an element's intersection of .large changes by a certain threshold. If threshold = 0 (the default value and what I think you want in your case), then it will fire even if only 1 pixel overlaps.
Once you've created an IntersectionObserver with .large as the root, you will then want to .observe() the smaller divs so the IntersectionObserver can report on when they intersect .large.
Again, stealing from MDN, the format of the callback is as follows. Please note that the callback fires on intersection changes, meaning that if a smaller div that used to intersect .large no longer does, it will be in the list of entries. To get elements that are intersecting .large you will want to filter entries such that only those where entry.isInterecting === true are present. From the filtered list of entries you can then grab entry.target from every entry.
let callback = (entries, observer) => {
entries.forEach(entry => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
The solution provided by #A Haworth works but I'm looking for a solution which doesn't involve having to loop and check every single element
I don't know how to achieve this without a loop, if we are handle an array of elements, but you can test this solution with the resizeObserver and loops.
// Init elements
const items = [...document.querySelectorAll('.item')];
const frame = document.getElementById('frame');
const resultElement = document.getElementById('for-result');
// Creating an array of properties
// Math.trunc() removing any fractional digits
const itemsProperties = items.map(item => {
return {
width: item.getBoundingClientRect().width,
height: item.getBoundingClientRect().height,
x: Math.trunc(item.getBoundingClientRect().x),
y: Math.trunc(item.getBoundingClientRect().y),
};
});
function within_frame(frameSize) {
const inside = [];
for (const i in itemsProperties) {
// Determine current height and width of the square
// Because X, Y is TOP, LEFT, and we need RIGHT, BOTTOM values.
const positionY = itemsProperties[i].height + itemsProperties[i].y;
const positionX = itemsProperties[i].width + itemsProperties[i].x;
// If the position square less than or equal to the size of the inner frame,
// then we will add values to the array.
if (
positionY <= frameSize.blockSize &&
positionX <= frameSize.inlineSize
) {
inside.push(itemsProperties[i]);
}
}
//returns all the elements within the frame bounds
return inside;
}
// Initialize observer
const resizeObserver = new ResizeObserver(entries => {
// Determine height and width of the 'frame'
const frameSize = entries[0].borderBoxSize[0];
// Return an array of values inside 'frame'
const result = within_frame(frameSize);
//console.log(result);
// for result
resultElement.innerHTML = result.map(
(el, idx) => `<code>square${idx + 1} position: ${el.x}px x ${el.y}px</code>`
);
});
// Call an observer to watch the frame
resizeObserver.observe(frame);
#frame {
height: 10rem;
width: 10rem;
display: inline-block;
resize: both;
border: solid black 0.5rem;
overflow: auto;
position: absolute;
z-index: 1;
}
.item {
height: 2rem;
width: 2rem;
position: absolute;
}
/* for result */
pre {
position: fixed;
right: 0.5rem;
top: 0.5rem;
border: 2px solid black;
padding: 0.5rem 1rem;
display: flex;
flex-flow: column;
}
#for-result {
font-weight: bold;
font-size: 1.5em;
}
<div id="frame"></div>
<div class="item" style="background-color: red"></div>
<div class="item" style="background-color: green; top: 50%"></div>
<div class="item" style="background-color: blue; top: 20%; left: 30%"></div>
<div class="item" style="background-color: pink; top: 60%; left: 20%"></div>
<div class="item" style="background-color: yellow; top: 25%; left: 10%"></div>
<pre id="for-result"></pre>
Heads up: A frivolous and probably useless answer
However the question itself seems quite frivolous too. No real world use case has been provided yet and I can't think of any either. Similarly, in theory my answer could be useful, but you're more likely struck by an asteroid than finding yourself needing it.
The point of posting is more that it provides some perspective on the performance of the other proposed solution. You can see you need at least hundreds of elements before performance starts being a concern.
My "answer" only works if:
items are rectangles
items cannot overlap
The potential "performance problem"
Perhaps the "not a loop" requirement refers to having a solution that doesn't require you to loop through a potentially large amount of other items in JS? This could be a valid concern, if the number of items can ever get really large.
Say that the area you're testing is relatively small compared to the items, and there are thousands of items that may or may not be inside, looping all of them might be relatively costly. Especially if you need to give each an event listener.
As already pointed out, it would be nice if a native API similar to document.getElementFromPoint existed, as that would undoubtedly be more performant than implementing in JS.
However that API does not exist. Probably because nobody ever found themselves needing it in a real world use case.
Sampling points of the frame
Now you could just use the document.ElementFromPoint API on every single point of the frame. However that would scale even worse with the frame's size.
But do we need to check every point to guarantee we're detecting all elements? Not if the elements can't overlap: since the smallest element is likely still many pixels high and wide, we could create a grid of points with those minimum values. As long as the elements don't have changing dimensions (or they can only grow) we only need to loop them once (to determine the smallest), not on updates. Note I do loop them every time, to account for setting changes. If you're sure elements have fixed dimensions you only need it once at the start of your script.
Of course, you do now have to loop over points instead. However...
In the best case scenario, where the minimum element is equally wide and high (or bigger), you only need to check 4 points. In fact I used this in a function to generate random cubes, to avoid overlap with earlier cubes.
It doesn't work on overlapping elements as document.ElementFromPoint only knows about the topmost. You could work around that by temporarily setting a z-index, but I had to stop somewhere.
Does it perform better?
I'm not sure at all whether this would ever make sense to do, but I don't immediately see another way to handle large amounts of items.
In the best case of needing just 4 points (small area to check overlap), it's hard to imagine another approach being faster, if the other approach needs to go through thousands of elements in JS. Even with up to a few tens of points it'll probably still be "fast" regardless of how many elements on the page.
let allItems = [...document.querySelectorAll('.item')];
const frame = document.getElementById('frame')
function measureLoop() {
const start = performance.now();
const large = document.querySelector('#frame');
const largeRect = large.getBoundingClientRect();
const Left = largeRect.left;
const Right = largeRect.right;
const Top = largeRect.top;
const Bottom = largeRect.bottom;
const items = document.querySelectorAll('#frame ~ *');
let overlappers = [];
items.forEach(item => {
const itemRect = item.getBoundingClientRect();
const l = itemRect.left;
const r = itemRect.right;
const t = itemRect.top;
const b = itemRect.bottom;
if (((l <= Right) && (r >= Left)) && ((t <= Bottom) && (b >= Top))) {
overlappers.push(item);
}
});
document.getElementById('result-loop').innerHTML = overlappers.length;
document.getElementById('time-loop').innerHTML = performance.now() - start;
}
function randomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
function within_frame(frame, items) {
const rect = frame.getBoundingClientRect();
const frameX = rect.left;
const frameY = rect.top;
const frameWidth = frame.clientWidth;
const frameHeight = frame.clientHeight;
const smallestWidth = Math.min(...(items.map(i => i.clientWidth)));
const smallestHeight = Math.min(...(items.map(i => i.clientHeight)));
const set = new Set();
let points = 0;
const lastY = frameHeight + smallestHeight;
const lastX = frameWidth + smallestWidth;
for (let y = 0; y < lastY; y += smallestHeight) {
for (let x = 0; x < lastX; x += smallestWidth) {
points++;
const checkX = Math.min(frameX + x, rect.right)
const checkY = Math.min(frameY + y, rect.bottom)
// Note there is always a result, but sometimes it's not the elements we're looking for.
// Set takes care of only storing unique, so we can loop a small amount of elements at the end and filter.
set.add(document.elementFromPoint(checkX, checkY));
}
}
set.forEach(el => (el === frame || el === document.documentElement || !items.includes(el)) && set.delete(el))
document.getElementById('points').innerHTML = points;
return set;
}
function measure() {
// Frame needs to be on top for resizing, put it below while calculating.
frame.style.zIndex = 1;
const start = performance.now();
const result = within_frame(frame, allItems)
const duration = performance.now() - start
document.getElementById('result').innerHTML = [...result.entries()].length;
document.getElementById('time').innerHTML = duration;
// Restore.
frame.style.zIndex = 3;
}
document.getElementById('measure').addEventListener('click', () => {measure(); measureLoop();})
const overlapsExisting = (el) => {
return within_frame(el, allItems);
}
let failedGenerated = 0;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function spawnCubes() {
frame.style.zIndex = 1;
allItems.forEach(item => item.parentNode.removeChild(item));
const nPoints = document.getElementById('nCubes').value;
const cubeSize = document.getElementById('size').value;
let newItems = [];
let failedGenerated = 0;
for (let i = 0; i < nPoints && failedGenerated < 1000; i++) {
// Sleep so that stuff is drawn.
if ((i + failedGenerated) % 100 === 0) {
document.getElementById('nCubes').value = newItems.length;
await sleep(0);
}
const el = document.createElement('div');
el.className = 'item';
//el.innerHTML = i;
el.style.backgroundColor = randomColor();
el.style.top = `${Math.round(Math.random() * 90)}%`;
el.style.left = `${Math.round(Math.random() * 60)}%`;
el.style.width = `${cubeSize}px`;
el.style.height = `${cubeSize}px`;
frame.after(el);
const existingOverlapping = within_frame(el, newItems);
if (existingOverlapping.size > 0) {
i--;
failedGenerated++;
el.parentNode.removeChild(el);
continue;
}
newItems.push(el);
}
console.log('failedAttempts', failedGenerated);
allItems = newItems;
frame.style.zIndex = 3;
document.getElementById('nCubes').value = newItems.length;
}
frame.addEventListener('mouseup', () => {measure(); measureLoop()});
spawnCubes().then(() => {measure(); measureLoop();});
document.getElementById('randomize').addEventListener('click', e => {
spawnCubes().then(measure);
})
#frame {
height: 3rem;
width: 3rem;
display: inline-block;
resize: both;
border: solid black 0.1rem;
overflow: auto;
position: absolute;
z-index: 3;
}
.item {
height: 1rem;
width: 1rem;
position: absolute;
z-index: 2;
}
.controls {
position: fixed;
bottom: 4px;
right: 4px;
text-align: right;
}
<div id="frame"></div>
<div class="controls">
<button id="measure">
measure
</button>
<button id="randomize">
spawn cubes
</button>
<div>
N cubes:
<input id="nCubes" type="number" value="40">
</div>
<div>
Cube size:
<input id="size" type="number" value="16">
</div>
<div>
N inside large:
<span id="result">
</span>
</div>
<div>
Time (ms):
<span id="time">
</span>
</div>
<div>
Points:
<span id="points">
</span>
</div>
<div>
N inside large (loop):
<span id="result-loop">
</span>
</div>
<div>
Time (ms) (loop):
<span id="time-loop">
</span>
</div>
</div>

How to make horizontal scroll smoother?

I added this code to my WordPress based website to make its front page horizontal. But it's not smooth and I cannot add scroll snap or anchor points. Can you help me about these? My website is https://kozb.art
<script type="text/javascript">
function replaceVerticalScrollByHorizontal( event ) {
if ( event.deltaY !== 0 ) {
window.scroll(window.scrollX + event.deltaY * 2, window.scrollY );
event.preventDefault();
}
}
const mediaQuery = window.matchMedia( '(min-width: 770px)' );
if ( mediaQuery.matches ) {
window.addEventListener( 'wheel', replaceVerticalScrollByHorizontal );
}
</script>
Edit: Here's my CSS code to make front page horizontal:
.elementor-section-wrap{
display: inline-flex;
}
.elementor-section{
width:100vw;
}
body{
overflow-y: hidden;
overscroll-behavior-y: none;
}
#media (max-width:768px){
.elementor-section-wrap{
display: block;
}
body{
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior-x: none;
}
}
You need animation knowledge to move the horizontal scroll smoothly. Let's start with a horizontal scrolling environment.
// index.html
...
<head>
<link href="index.css" rel="stylesheet">
</head>
<body>
<div id="screen">
<div id="content1"></div><div id="content2"></div><div id="content3"></div><div id="content4"></div>
</div>
<script src="./main.js"></script>
</body>
...
/* index.css */
body {
margin: 0;
width: 100vw;
height: 100vh;
overflow-y: hidden;
}
#screen {
white-space: nowrap;
height: 100%;
}
#screen > div {
width: 100vw;
height: 100vh;
display: inline-block;
}
#screen > div:nth-child(1) {
background: aqua;
}
#screen > div:nth-child(2) {
background: blueviolet
}
#screen > div:nth-child(3) {
background: chocolate
}
#screen > div:nth-child(4) {
background: darkolivegreen;
}
A web page has now been created that has four sections and occupies one screen size per section. For smooth, snap horizontal scroll applications, let's think step by step about what we need to make code for animation.
To implement Snap, you need to know what value scroll X should move to. In the current layout, the value is the offsetLeft value of the section element. And the section size changes depending on the browser size. So the code to get the offsetLeft of the section can be created as follows:
let sectionAnchorPointer = [];
const resizeHandler = () => {
const content1 = document.getElementById('content1');
const content2 = document.getElementById('content2');
const content3 = document.getElementById('content3');
const content4 = document.getElementById('content4');
sectionAnchorPointer = [content1.offsetLeft, content2.offsetLeft, content3.offsetLeft, content4.offsetLeft];
};
addEventListener('resize', resizeHandler);
addEventListener('DOMContentLoaded', () => {
resizeHandler();
});
In order to store the section offsetLeft value from the beginning, a function was executed to update the section offsetLeft when DOMContentLoaded occurred. If you want to make it more efficient, please apply debounce to the resize event handler.
Next, when the wheel occurs, find the section to move. In order to find the section to be moved, it is necessary to determine where the section is located and then perform the calculation according to the direction of the wheel. The code is as follows:
let nextSectionIndex = 0;
const getCurrentSectionIndex = () => sectionAnchorPointer.findIndex((leftValue, i, array) => {
const scrollX = Math.ceil(window.scrollX); // Fixed a bug where scrollX was decimalized
const rightValue = array[i + 1] ?? Infinity;
return leftValue <= scrollX && scrollX < rightValue;
});
window.addEventListener('wheel', ({ deltaY }) => {
const currentSectionIndex = getCurrentSectionIndex();
const add = Math.abs(deltaY) / deltaY;
nextSectionIndex = currentSectionIndex + add;
nextSectionIndex = Math.min(sectionAnchorPointer.length - 1, Math.max(0, nextSectionIndex)); // To avoid pointing to a section index that does not exist
console.log(sectionAnchorPointer[nextSectionIndex]);
});
To save the scroll position when accessing the page, Call the function when a DOMContentLoaded event occurs.
...
addEventListener('DOMContentLoaded', () => {
resizeHandler();
nextSectionIndex = getCurrentSectionIndex();
});
...
Next, apply the animation so that it slowly changes to the offsetLeft value of the section that needs to move the current scrollX value. For ease of understanding, let's make it a linear animation without acceleration. The code is as follows:
const SCROLL_SPEED = 70; // It can be changed for speed control.
requestAnimationFrame(function scroll() {
const nextScrollX = sectionAnchorPointer[nextSectionIndex];
// linear animtion
if (Math.abs(window.scrollX - nextScrollX) > SCROLL_SPEED) {
const val = -Math.abs(window.scrollX - nextScrollX) / (window.scrollX - nextScrollX);
window.scroll(window.scrollX + val * SCROLL_SPEED, window.scrollY);
} else {
window.scroll(nextScrollX, window.scrollY);
}
requestAnimationFrame(scroll);
});
To apply dynamic animation by adding acceleration, add the following code instead of the code above.
requestAnimationFrame(function scroll() {
const nextScrollX = sectionAnchorPointer[nextSectionIndex];
// curve animation
if (Math.abs(window.scrollX - nextScrollX) > 1) {
let val = (nextScrollX - window.scrollX) / 8; // You can change 8 to another value to adjust the animation speed.
val = val > 0 ? Math.max(val, 1) : Math.min(val, -1);
window.scroll(window.scrollX + val, window.scrollY);
} else {
window.scroll(nextScrollX, window.scrollY);
}
requestAnimationFrame(scroll);
});
This is a simple example of implementation for understanding. Here is a link to the project using the code. If you are interested, please refer to the following link to understand Javascript animation. For your information, it would be more convenient to make an animation using a known library such as anime.js or popmotion.
This is the script code that fits your structure. Remove the existing wheel listener and insert this content.
const wrap = document.querySelectorAll('.elementor-section-wrap')[1]
let sectionAnchorPointer = [];
let resultX = 0;
const resizeHandler = () => {
sectionAnchorPointer = [...new Set([...wrap.children].map(el => el.offsetLeft))];
};
addEventListener('resize', resizeHandler);
addEventListener('DOMContentLoaded', () => {
resizeHandler();
nextSectionIndex = getCurrentSectionIndex();
});
resizeHandler();
let nextSectionIndex = 0;
const getCurrentSectionIndex = () => sectionAnchorPointer.findIndex((leftValue, i, array) => {
const scrollX = Math.ceil(resultX); // Fixed a bug where scrollX was decimalized
const rightValue = array[i + 1] ?? Infinity;
return leftValue <= resultX && resultX < rightValue;
});
window.addEventListener('wheel', (ev) => {
const {deltaY} = ev;
const currentSectionIndex = getCurrentSectionIndex();
const add = Math.abs(deltaY) / deltaY;
nextSectionIndex = currentSectionIndex + add;
nextSectionIndex = Math.min(sectionAnchorPointer.length - 1, Math.max(0, nextSectionIndex)); // To avoid pointing to a section index that does not exist
console.log(sectionAnchorPointer[nextSectionIndex]);
});
const SCROLL_SPEED = 70; // It can be changed for speed control.
requestAnimationFrame(function scroll() {
const nextScrollX = sectionAnchorPointer[nextSectionIndex];
// linear animtion
if (Math.abs(resultX - nextScrollX) > SCROLL_SPEED) {
const val = -Math.abs(resultX - nextScrollX) / (resultX - nextScrollX);
resultX = resultX + val * SCROLL_SPEED;
} else {
resultX = nextScrollX;
}
window.scroll(resultX , 0);
requestAnimationFrame(scroll);
});

How to make items draggable and clickable?

I'm new to Matter JS, so please bear with me. I have the following code I put together from demos and other sources to suit my needs:
function biscuits(width, height, items, gutter) {
const {
Engine,
Render,
Runner,
Composites,
MouseConstraint,
Mouse,
World,
Bodies,
} = Matter
const engine = Engine.create()
const world = engine.world
const render = Render.create({
element: document.getElementById('canvas'),
engine,
options: {
width,
height,
showAngleIndicator: true,
},
})
Render.run(render)
const runner = Runner.create()
Runner.run(runner, engine)
const columns = media({ bp: 'xs' }) ? 3 : 1
const stack = Composites.stack(
getRandom(gutter, gutter * 2),
gutter,
columns,
items.length,
0,
0,
(x, y, a, b, c, i) => {
const item = items[i]
if (!item) {
return null
}
const {
width: itemWidth,
height: itemHeight,
} = item.getBoundingClientRect()
const radiusAmount = media({ bp: 'sm' }) ? 100 : 70
const radius = item.classList.contains('is-biscuit-4')
? radiusAmount
: 0
const shape = item.classList.contains('is-biscuit-2')
? Bodies.circle(x, y, itemWidth / 2)
: Bodies.rectangle(x, y, itemWidth, itemHeight, {
chamfer: { radius },
})
return shape
}
)
World.add(world, stack)
function positionDomElements() {
Engine.update(engine, 20)
stack.bodies.forEach((block, index) => {
const item = items[index]
const xTrans = block.position.x - item.offsetWidth / 2 - gutter / 2
const yTrans = block.position.y - item.offsetHeight / 2 - gutter / 2
item.style.transform = `translate3d(${xTrans}px, ${yTrans}px, 0) rotate(${block.angle}rad)`
})
window.requestAnimationFrame(positionDomElements)
}
positionDomElements()
World.add(world, [
Bodies.rectangle(width / 2, 0, width, gutter, { isStatic: true }),
Bodies.rectangle(width / 2, height, width, gutter, { isStatic: true }),
Bodies.rectangle(width, height / 2, gutter, height, { isStatic: true }),
Bodies.rectangle(0, height / 2, gutter, height, { isStatic: true }),
])
const mouse = Mouse.create(render.canvas)
const mouseConstraint = MouseConstraint.create(engine, {
mouse,
constraint: {
stiffness: 0.2,
render: {
visible: false,
},
},
})
World.add(world, mouseConstraint)
render.mouse = mouse
Render.lookAt(render, {
min: { x: 0, y: 0 },
max: { x: width, y: height },
})
}
I have a HTML list of links that mimics the movements of the items in Matter JS (the positionDomElements function). I'm doing this for SEO purposes and also to make the navigation accessible and clickable.
However, because my canvas sits on top of my HTML (with opacity zero) I need to be able to make the items clickable as well as draggable, so that I can perform some other actions, like navigating to the links (and other events).
I'm not sure how to do this. I've searched around but I'm not having any luck.
Is it possible to have each item draggable (as it already is) AND perform a click event of some kind?
Any help or steer in the right direction would be greatly appreciated.
It seems like your task here is to add physics to a set of DOM navigation list nodes. You may be under the impression that matter.js needs to be provided a canvas to function and that hiding the canvas or setting its opacity to 0 is necessary if you want to ignore it.
Actually, you can just run MJS headlessly using your own update loop without injecting an element into the engine. Effectively, anything related to Matter.Render or Matter.Runner will not be needed and you can use a call to Matter.Engine.update(engine); to step the engine forward one tick in the requestAnimationFrame loop. You can then position the DOM elements using values pulled from the MJS bodies. You're already doing both of these things, so it's mostly a matter of cutting out the canvas and rendering calls.
Here's a runnable example that you can reference and adapt to your use case.
Positioning is the hard part; it takes some fussing to ensure the MJS coordinates match your mouse and element coordinates. MJS treats x/y coordinates as center of the body, so I used body.vertices[0] for the top-left corner which matches the DOM better. I imagine a lot of these rendering decisions are applicaton-specific, so consider this a proof-of-concept.
const listEls = document.querySelectorAll("#mjs-wrapper li");
const engine = Matter.Engine.create();
const stack = Matter.Composites.stack(
// xx, yy, columns, rows, columnGap, rowGap, cb
0, 0, listEls.length, 1, 0, 0,
(xx, yy, i) => {
const {x, y, width, height} = listEls[i].getBoundingClientRect();
return Matter.Bodies.rectangle(x, y, width, height, {
isStatic: i === 0 || i + 1 === listEls.length
});
}
);
Matter.Composites.chain(stack, 0.5, 0, -0.5, 0, {
stiffness: 0.5,
length: 20
});
const mouseConstraint = Matter.MouseConstraint.create(
engine, {element: document.querySelector("#mjs-wrapper")}
);
Matter.Composite.add(engine.world, [stack, mouseConstraint]);
listEls.forEach(e => {
e.style.position = "absolute";
e.addEventListener("click", e =>
console.log(e.target.textContent)
);
});
(function update() {
requestAnimationFrame(update);
stack.bodies.forEach((block, i) => {
const li = listEls[i];
const {x, y} = block.vertices[0];
li.style.top = `${y}px`;
li.style.left = `${x}px`;
li.style.transform = `translate(-50%, -50%)
rotate(${block.angle}rad)
translate(50%, 50%)`;
});
Matter.Engine.update(engine);
})();
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
height: 100%;
}
body {
min-width: 600px;
}
#mjs-wrapper {
/* position this element */
margin: 1em;
height: 100%;
}
#mjs-wrapper ul {
font-size: 14pt;
list-style: none;
user-select: none;
position: relative;
}
#mjs-wrapper li {
background: #fff;
border: 1px solid #555;
display: inline-block;
padding: 1em;
cursor: move;
}
#mjs-wrapper li:hover {
background: #f2f2f2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<div id="mjs-wrapper">
<ul>
<li>Foo</li>
<li>Bar</li>
<li>Baz</li>
<li>Quux</li>
<li>Garply</li>
<li>Corge</li>
</ul>
</div>

Javascript animation trigger only when reaching a certain part of webpage

I want a counter animation which is triggered only when webpage reaches that certain part. For example, the js file would be this. I want the count to start only when the page reaches that certain section.
const counters = document.querySelectorAll('.counter');
const speed = 200; // The lower the slower
counters.forEach(counter => {
const updateCount = () => {
const target = +counter.getAttribute('data-target');
const count = +counter.innerText;
// Lower inc to slow and higher to slow
const inc = target / speed;
// console.log(inc);
// console.log(count);
// Check if target is reached
if (count < target) {
// Add inc to count and output in counter
counter.innerText = count + inc;
// Call function every ms
setTimeout(updateCount, 1);
} else {
counter.innerText = target;
}
};
updateCount();
});
Yo can easily do it using Jquery
$(document).ready(function() {
$(window).scroll(function() {
if ($(document).scrollTop() > 50) {
$("div").css("background-color", "#111111");
} else {
$("div").css("background-color", "transparent");
}
});
});
div {
height: 120vh;
transition-duration: 0.5s;
}
<div>
Scroll to Change Background
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.0/jquery.min.js"></script>
You can use Intersection Observer to do that
const $observeSection = document.querySelector('#second');
const options = {
root: null,
rootMargin: '0px',
threshold: 0.5
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.intersectionRatio > options.threshold) {
$observeSection.classList.add('yellow');
} else {
$observeSection.classList.remove('yellow');
}
});
}, options);
observer.observe($observeSection);
main {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
main section {
width: 100vw;
height: 100vh;
}
#first {
background: red;
}
#second {
background: blue;
}
#third {
background: green;
}
.yellow {
background: yellow!important;
}
<main>
<section id="first"></section>
<section id="second"></section>
<section id="third"></section>
</main>
In this example, I observe the second section, and when the scroll come to the middle of the section (threshold: 0.5), I add a class to change the color of this section.
Be careful if you need to handle legacies browsers as you can see here :
https://caniuse.com/#feat=intersectionobserver
You don't need jquery to achieve this.
Here is a VanillaJS solution:
window.onscroll = (e) => {
e.stopPropagation();
window.pageYOffset > 50 && console.log("do smh");
}

How to use scripts in React JS

In my original work I had a <div class="cursor"></div> with this styling:
.cursor {
pointer-events: none;
position: fixed;
padding: 0.7rem;
background-color: #fff;
border-radius: 50%;
mix-blend-mode: difference;
transition: transform 0.3s ease;
}
In the script section of my HTML file I have this javascript function for my cursor animation:
(function () {
const link = document.querySelectorAll('nav > .hover-this');
const cursor = document.querySelector('.cursor');
const animateit = function (e) {
const span = this.querySelector('span');
const { offsetX: x, offsetY: y } = e,
{ offsetWidth: width, offsetHeight: height } = this,
move = 25,
xMove = x / width * (move * 2) - move,
yMove = y / height * (move * 2) - move;
span.style.transform = `translate(${xMove}px, ${yMove}px)`;
if (e.type === 'mouseleave') span.style.transform = '';
};
const editCursor = e => {
const { clientX: x, clientY: y } = e;
cursor.style.left = x + 'px';
cursor.style.top = y + 'px';
};
link.forEach(b => b.addEventListener('mousemove', animateit));
link.forEach(b => b.addEventListener('mouseleave', animateit));
window.addEventListener('mousemove', editCursor);
})();
How do I change this so it can work within React?
This is a very broad question but here are a few pointers to get you started...
Start with the React tutorial available from the react site itself at reactjs.org/tutorial/tutorial.html. It will cover the basics and also many 'react-y' programming patterns and best practices.
Once you've got that there are a huge number of tutorials available that go much more in depth. I would highly recommend Robin Wieruch's stuff: https://www.robinwieruch.de/ (he has also written a number of books on the subject)
Looking at your problem more closely. I would start by creating a component for the cursor and moving the bulk of the js to that. The problem is that without a good grounding in React (or at least the fundamentals) the question you're actually asking is 'How to program in React' rather than 'How do I do this particular thing in React' which would take a lot longer to write an answer to than most of us here on SO have time to do!

Categories