Why does window.requestAnimationFrame not keeping the last transition? - javascript

i'm trying to build my own simple fullpage.js or something similar, just so I can learn more about how to use requestAnimationFrame function, however I am stuck into this problem of transform3d value resetting, which behaves like this
You can see in the gif that every time I switch slides, the animation starts always at the first slide
Here is the implementation of the slider
html
<div class="scroller">
<div class="scroller-content">
<section class="scroller-section s1">SLIDE 1</section>
<section class="scroller-section s2">SLIDE 2</section>
<section class="scroller-section s3">SLIDE 3</section>
<section class="scroller-section s4">SLIDE 4</section>
</div>
</div>
css
.scroller {
width: 100vw;
height: 100vh;
overflow-y: hidden;
}
.scroller-section {
width: 100vw;
height: 100vh;
display: flex;
text-align: center;
align-items: center;
justify-content: center;
font-size: 25rem;
}
.scroller-section.s1 {
background-color: #535bf2;
}
.scroller-section.s2 {
background-color: #242424;
}
.scroller-section.s3 {
background-color: #213547;
}
.scroller-section.s4 {
background-color: #747bff;
}
const scrollerContent = document.querySelector(".scroller-content")
const offsets = [0, -100, -200, -300]
let index = 0
window.addEventListener("wheel", (e) => {
if (e.deltaY > 0) {
index = shift(index + 1) // go next
const offset = offsets[index]
createAnimationFrames((delta) => {
scrollerContent.style.transform = `translate3d(0, ${delta * offset}vh,0)`
}, 1000)
} else if (e.deltaY < 0) {
index = shift(index - 1) // go prev
const offset = offsets[index]
createAnimationFrames((delta) => {
scrollerContent.style.transform = `translate3d(0, ${delta * offset}vh,0)`
}, 1000)
}
})
Here is the implementation of createAnimationFrames
const createAnimationFrames = (callback: (delta: number) => void, duration: number) => {
let start = new Date();
let raf = requestAnimationFrame;
const animate = () => {
let now = new Date();
const delta = Math.min((now.getTime() - start.getTime()) / duration, 1);
callback(delta);
if (delta < 1) {
raf(animate);
}
};
raf(animate);
};
Basically it runs the callback every frame request and the callback takes delta as a reference for the progress of the animation.
The thing is that it works normally by just having a simple reassign to the transform attribute and not using the requestAnimationFrame. Any solutions for this?

Related

Calculate transform y-scale property as the function of elapsed time

I have a container that is expanded and collapsed on click of chevron icon. The code to collapse/expand the container is in the function transformAnimation. The code of transformAnimation is similar to the code on MDN web docs for requestAnimationFrame. The code to animate (scale) the container has been developed on the guidelines of this article on Building performant expand & collapse animations on Chrome Developers website.
I am not able to figure out how to calculate yScale value (which is nothing but y scale values for collapse/expand animation) as a function of the time elapsed since the start of the animation.
To elaborate what I mean, let's assume that the container is in expanded state. In this state the scaleY value of the container is 6. Now when user clicks on the toggle button, in the transformAnimation function for each animation frame, i.e, execution of the requestAnimationFrame callback step function, the value of scaleY should decrease from 6 (the expanded state) to 1 (the collapsed state) in the exact duration that I want the animation to run for.
In the present state, the code to calculate yScale is not working as expected.
const dragExpandableContainer = document.querySelector('.drag-expandable-container');
const dragExpandableContents = document.querySelector('.drag-expandable__contents');
const resizeableControlEl = document.querySelector('.drag-expandable__resize-control');
const content = document.querySelector(`.content`);
const toggleEl = document.querySelector(`.toggle`);
const collapsedHeight = calculateCollapsedHeight();
/* This height is used as the basis for calculating all the scales for the component.
* It acts as proxy for collapsed state.
*/
dragExpandableContainer.style.height = `${collapsedHeight}px`;
// Apply iniial transform to expand
dragExpandableContainer.style.transformOrigin = 'bottom left';
dragExpandableContainer.style.transform = `scale(1, 10)`;
// Apply iniial reverse transform on the contents
dragExpandableContents.style.transformOrigin = 'bottom left';
dragExpandableContents.style.transform = `scale(1, calc(1/10))`;
let isOpen = true;
const togglePopup = () => {
if (isOpen) {
collapsedAnimation();
toggleEl.classList.remove('toggle-open');
isOpen = false;
} else {
expandAnimation();
toggleEl.classList.add('toggle-open');
isOpen = true
};
};
function calculateCollapsedHeight() {
const collapsedHeight = content.offsetHeight + resizeableControlEl.offsetHeight;
return collapsedHeight;
}
const calculateCollapsedScale = function() {
const collapsedHeight = calculateCollapsedHeight();
const expandedHeight = dragExpandableContainer.getBoundingClientRect().height;
return {
/* Since we are not dealing with scaling on X axis, we keep it 1.
* It can be inverse to if required */
x: 1,
y: expandedHeight / collapsedHeight,
};
};
const calculateExpandScale = function() {
const collapsedHeight = calculateCollapsedHeight();
const expandedHeight = 100;
return {
x: 1,
y: expandedHeight / collapsedHeight,
};
};
function expandAnimation() {
const {
x,
y
} = calculateExpandScale();
transformAnimation('expand', {
x,
y
});
}
function collapsedAnimation() {
const {
x,
y
} = calculateCollapsedScale();
transformAnimation('collapse', {
x,
y
});
}
function transformAnimation(animationType, scale) {
let start, previousTimeStamp;
let done = false;
function step(timestamp) {
if (start === undefined) {
start = timestamp;
}
const elapsed = timestamp - start;
if (previousTimeStamp !== timestamp) {
const count = Math.min(0.1 * elapsed, 200);
//console.log('count', count);
let yScale;
if (animationType === 'expand') {
yScale = (scale.y / 100) * count;
} else yScale = scale.y - (scale.y / 100) * count;
//console.log('yScale', yScale);
if (yScale < 1) yScale = 1;
dragExpandableContainer.style.transformOrigin = 'bottom left';
dragExpandableContainer.style.transform = `scale(${scale.x}, ${yScale})`;
const inverseXScale = 1;
const inverseYScale = 1 / yScale;
dragExpandableContents.style.transformOrigin = 'bottom left';
dragExpandableContents.style.transform = `scale(${inverseXScale}, ${inverseYScale})`;
if (count === 200) done = true;
//console.log('elapsed', elapsed);
if (elapsed < 1000) {
// Stop the animation after 2 seconds
previousTimeStamp = timestamp;
if (!done) requestAnimationFrame(step);
}
}
}
requestAnimationFrame(step);
}
.drag-expandable-container {
position: absolute;
bottom: 0px;
display: block;
overflow: hidden;
width: 100%;
background-color: #f3f7f7;
}
.drag-expandable__contents {
height: 0;
}
.toggle {
position: absolute;
top: 2px;
right: 15px;
height: 10px;
width: 10px;
transition: transform 0.2s linear;
}
.toggle-open {
transform: rotate(180deg);
}
.drag-expandable__resize-control {
background-color: #e7eeef;
}
.burger-icon {
width: 12px;
margin: 0 auto;
padding: 2px 0;
}
.burger-icon__line {
height: 1px;
background-color: #738F93;
margin: 2px 0;
}
.drag-expandable__resize-control:hover {
border-top: 1px solid #4caf50;
cursor: ns-resize;
}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css.css">
</head>
<body>
<div class="drag-expandable-container">
<div class="drag-expandable__contents">
<div class="drag-expandable__resize-control">
<div class="burger-icon">
<div class="burger-icon__line"></div>
<div class="burger-icon__line"></div>
<div class="burger-icon__line"></div>
</div>
</div>
<div class="content" />
<div>
<div class="toggle toggle-open" onclick="togglePopup()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.1.1 by #fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M416 352c-8.188 0-16.38-3.125-22.62-9.375L224 173.3l-169.4 169.4c-12.5 12.5-32.75 12.5-45.25 0s-12.5-32.75 0-45.25l192-192c12.5-12.5 32.75-12.5 45.25 0l192 192c12.5 12.5 12.5 32.75 0 45.25C432.4 348.9 424.2 352 416 352z"/></svg>
</div>
</div>
</div>
</div>
</body>
<script type="text/javascript" src="js.js"></script>
</html>

setTimeout inside setTimeout inside a setInterval

I'm trying to make a string that will write itself letter by letter until completing the sentence, and the speed of appearing each letter is based on an input that varies from 1 to 10. At the end of the string, it will blink for 5 seconds until that an alien will appear. My idea was to create a setInterval to add the letters and when the counter added the array size it would return the final animation of the loop with the new setInterval call, and before it was called again it had already been cleared, and called again in a recursion by setTimout callback to maintain the infinite loop. But it's not reaching setTimout, why?
//script.js
const speedInput = document.getElementsByClassName('speed--input')[0];
const alien = document.getElementsByClassName('alien')[0];
const textDiv = document.getElementsByClassName('text')[0];
const textShow = document.getElementsByClassName('text--show')[0];
const textDB = 'We go to dominate the world.';
const textStr = '';
let count = 0;
let speed = speedInput.value * 100;
const textChangeHandle = (count, textStr, textDB) => setInterval(() => {
if (count < textDB.length) {
textStr += textDB[count];
textShow.innerHTML = textStr;
textDiv.style.width = `${40 * count}px`;
count++;
} else {
textShow.style.animation = 'bip 1s linear 1s infinite'
return () => {
setTimeout(() => {
textShow.style.animation = '';
textStr = '';
count = 0;
textShow.style.opacity = 0;
alien.style.opacity = 1;
setTimeout(() => {
alien.style.opacity = 0;
textShow.style.opacity = 1;
textChangeHandle(count, textStr, textDB)
}, 5000)
}, 5000);
clearInterval(textChangeHandle);
}
}
}, speed)
textChangeHandle(count, textStr, textDB);
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style/style.css">
<title>Document</title>
</head>
<body>
<div class="text">
<p class="text--show"></p>
<img class="alien" src="alien.png" alt="Aki é Jupiter karai">
</div>
<div class="speed">
<span>Speed</span>
<input class="speed--input" type="number" min="1" max="10" value="1">
</div>
<script src="script.js"></script>
</body>
</html>
//style.css
*,
*::after,
*::before {
margin: 0;
padding: 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
body {
font-family: sans-serif;
background-color: #3cd070;
}
.text {
position: fixed;
top: 50%;
left: 50%;
height: auto;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
font-size: 40px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.text .alien {
opacity: 0;
height: 600px;
width: auto;
margin-bottom: 50px;
position: absolute;
}
.text .text--show {
font-size: 40px;
width: 100%;
position: absolute;
display: block;
text-align: center;
-webkit-animation: bip 1s linear 1s infinite;
animation: bip 1s linear 1s infinite;
}
.speed {
position: fixed;
top: 90%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.2);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
padding: 10px 20px;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
}
.speed .speed--input {
border: 0;
outline: 0;
width: 40px;
height: 25px;
margin: 10px;
text-align: center;
}
#-webkit-keyframes bip {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#keyframes bip {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
There is also the animation frame api
It will run your function exactly before the next "natural" repaint of the browser.
Additionally and probably useful in your case, it will pass the current time in your callback, so you can take a time detail and animate according to this. That way you can get a smooth animation of equal length for everyone, even when the frame rate is inconsistent.
Consider the following snippet, which will basically cause an infinite loop.
function draw(now) {
requestAnimationFrame(draw)
}
requestAnimationFrame(draw)
Now you only need to remember the time and take the time delta next time the function is called. If enough time has passed, you can write another character. (Normally you would change a value divided by the time delta, but since you have characters, they will be either there or not.
let speed = 300
let timePassedSinceLastChar = 0
let then = null
function draw(now){
now *= 0.001;
const deltaTime = now - then;
then = now;
timePassedSinceLastChar += deltaTime;
if (timePassedSinceLastChar >= speed) {
drawChar()
timePassedSinceLastChar = 0
}
requestAnimationFrame(draw)
}
requestAnimationFrame(draw)
Furthermore, the requestAnimationFrame function returns the id of the requested frame. That allows to cancel the loop.
const frameId = requestAnimationFrame(draw)
cancelAnimationFrame(frameId)
So the final code could look something like this.
const textDB = 'We go to dominate the world.';
let charIndex = 0;
let frameId = null
let then = 0
let sinceDraw = 0
let speed = () => Math.random() * 0.4 + 0.05 // randomize the speed a bit
let $p = document.querySelector('p')
function draw(now) {
// cancel on end of string
if (charIndex >= textDB.length) {
cancelAnimationFrame(frameId)
console.log('done')
return
}
// get the time delta
now *= 0.001; // convert to seconds
const deltaTime = now - then;
then = now;
sinceDraw += deltaTime
// if its time to draw again, do so
if (sinceDraw >= speed()) {
let char = textDB[charIndex]
$p.textContent += char
charIndex++
sinceDraw = 0
}
// request another frame
frameId = requestAnimationFrame(draw)
}
// request the first frame
frameId = requestAnimationFrame(draw)
<p></p>
The issue is that in the else statement, you are returning a function that is never called.
return () => {
setTimeout(() => {
textShow.style.animation = '';
textStr = '';
count = 0;
textShow.style.opacity = 0;
alien.style.opacity = 1;
setTimeout(() => {
alien.style.opacity = 0;
textShow.style.opacity = 1;
textChangeHandle(count, textStr, textDB)
}, 5000)
}, 5000);
clearInterval(textChangeHandle);
}
Just remove the return statment and call the setTimeout function directly.
const textChangeHandle = (count, textStr, textDB) => setInterval(() => {
if (count < textDB.length) {
textStr += textDB[count];
textShow.innerHTML = textStr;
textDiv.style.width = `${40 * count}px`;
count++;
} else {
textShow.style.animation = 'bip 1s linear 1s infinite'
setTimeout(() => {
textShow.style.animation = '';
textStr = '';
count = 0;
textShow.style.opacity = 0;
alien.style.opacity = 1;
setTimeout(() => {
alien.style.opacity = 0;
textShow.style.opacity = 1;
textChangeHandle(count, textStr, textDB)
}, 5000)
}, 5000);
clearInterval(textChangeHandle);
}
}, speed)
I found the solution. With setInterval, the code was creating multiple instances of callbacks and overloading memory.
///script.js
const textEl = document.getElementById('text');
const inputEl = document.getElementById('input');
const alienEl = document.getElementById('alien');
const text = 'We go to dominate the world!';
let idx = 0;
let speed = 300 / inputEl.value;
writeText();
function writeText () {
if(idx === 0) setTimeout(() => textEl.style.opacity = 1, speed)
textEl.innerText = text.slice(0, idx);
idx++;
if (idx > text.length) {
idx = 0;
textEl.style.animation = 'bip 1s linear 1s infinite';
setTimeout(() => {
textEl.style.animation = ''
textEl.style.opacity = 0;
setTimeout(() => {
alienEl.style.opacity = 1;
setTimeout(() => {
alienEl.style.opacity = 0;
setTimeout(writeText, speed);
}, 5000)
}, 1000);
}, 5000)
} else {
setTimeout(writeText, speed);
}
}
inputEl.addEventListener('input', (e) => speed = 300 / e.target.value);

How to create duration in vanilla JavaScript?

Today i'm asking myself how to create a duration parameter like GreenSock does with its functions.
To be clear, how to create an animating function that takes a duration in pure vanilla JavaScript ?
Are they doing it by adding a transition-duration with JavaScript ? :)
I would like a solution in plain JS.
I have some ideas, maybe width :
new Date()
Or some :
setInterval()
For example how to handle the duration here ?
HTML :
<div class="box"></div>
CSS
.box {
width: 200px;
height: 200px;
background: orange;
}
JavaScript
const box = document.querySelector('.box');
function move(duration){
box.style.transform = "translateX(50px)";
}
move(2000)
Thank's if someone is passing by to give me a hand ! :)
const box = document.querySelector('.box')
function move(duration){
box.style['transition-property'] = 'transform'
box.style['transition-duration'] = `${duration}ms`
setTimeout(() => box.style.transform = "translate3d(200px,0,0)")
}
move(2000)
.box {
width: 200px;
height: 200px;
background: orange;
}
<div class="box"></div>
If you don't want to leverage CSS for the transition control then you will have to control it manually using requestAnimationFrame and your own easing function:
const EASE_IN_OUT = (t) =>
t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
const ease = (f, easing = EASE_IN_OUT) => (...args) => easing(f(...args))
const limit = (f, limit = 1) => (...args) =>
{ const result = f(...args); return result > limit ? limit : result }
const elapsedFraction = ({ start, duration }) =>
(performance.now() - start) / duration
const asInteger = (f) => (...args) => f(...args).toFixed(0)
const calcX = asInteger(({ start, duration, distance }) =>
ease(limit(elapsedFraction))({ start, duration }) * distance)
function move({ el, duration, distance }) {
const start = performance.now()
const startX = el.getBoundingClientRect().x
const tick = () => {
if (el.getBoundingClientRect().x - startX === distance) return
el.style.transform = `translate3d(${calcX({ start, duration, distance })}px,0,0)`
requestAnimationFrame(tick)
}
tick()
}
const el = document.querySelector(".box")
move({ el, duration: 1000, distance: 200 })
.box {
width: 200px;
height: 200px;
background: orange;
}
<div class="box"></div>
I think the best way is to use an animation library. But, I made an example by using "translate3d". Translate3d runs on the GPU and not in the CPU, because of this is more performant. I put a pure CSS example as well.
const box = document.querySelector('.box');
const blue = document.querySelector('.blue');
function move(elem, duration){
let posx = elem.getBoundingClientRect().x
let counter = duration
const timer = setInterval(() => {
if (counter <= 0) {
clearInterval(timer)
}
posx += 2
elem.style.transform = `translate3d(${posx}px,0,0)`
counter -= 10
}, 10)
}
move(box, 2000)
setTimeout(() => blue.classList.add('act'), 1000)
.box {
width: 60px;
height: 60px;
background: orange;
}
.blue {
width: 60px;
height: 60px;
transform: translate3d(0,0,0);
background: lightblue;
transition: all 0.8s ease;
}
.act {
transform: translate3d(100px,0,0)
}
<div class="box"></div>
<br>
<div class="blue"></div>
I think this function do what you want:
Element.prototype.move = function(duration, distance) {
this.style.transition = duration + 's';
this.style.transform = 'translateX(' + distance + 'px)';;
};
.box {
width: 200px;
height: 200px;
background: orange;
}
<div class="box" onclick="this.move(2, 400);">Click me!</div>

Why is the distance between first and last element decreasing?

I'm trying to make an image slider. But as you can see the distance between the first and last element is not consistent. If you keep on dragging to left, the distance decreases and if you keep on dragging to right, the distance increases. Looks like the code is behaving differently on different zoom levels (sometimes?) and hence distance between every elements is changing at times.
//project refers to placeholder rectangular divs
projectContainer = document.querySelector(".project-container")
projects = document.querySelectorAll(".project")
elementAOffset = projects[0].offsetLeft;
elementBOffset = projects[1].offsetLeft;
elementAWidth = parseInt(getComputedStyle(projects[0]).width)
margin = (elementBOffset - (elementAOffset + elementAWidth))
LeftSideBoundary = -(elementAWidth)
RightSideBoundary = (elementAWidth * (projects.length)) + (margin * (projects.length))
RightSidePosition = RightSideBoundary - elementAWidth;
initialPosition = 0; //referring to mouse
mouseIsDown = false
projectContainer.addEventListener("mousedown", e => {
mouseIsDown = true
initialPosition = e.clientX;
})
projectContainer.addEventListener("mouseup", e => {
mouseExit(e)
})
projectContainer.addEventListener("mouseleave", e => {
mouseExit(e);
})
function mouseExit(e) {
mouseIsDown = false
//updates translateX value of transform
projects.forEach(project => {
var style = window.getComputedStyle(project)
project.currentTranslationX = (new WebKitCSSMatrix(style.webkitTransform)).m41
project.style.transform = 'translateX(' + (project.currentTranslationX) + 'px)'
})
}
projectContainer.addEventListener("mousemove", e => {
if (!mouseIsDown) { return };
// adds mousemovement to translateX
projects.forEach(project => {
project.style.transform = 'translateX(' + ((project.currentTranslationX ?? 0) + (e.clientX - initialPosition)) + 'px)'
shiftPosition(e, project)
})
})
//teleports div if it hits left or right boundary to make an infinite loop
function shiftPosition(e, project) {
projectStyle = window.getComputedStyle(project)
projectTranslateX = (new WebKitCSSMatrix(projectStyle.webkitTransform)).m41
//projectVisualPosition is relative to the left border of container div
projectVisualPosition = project.offsetLeft + projectTranslateX
if (projectVisualPosition <= LeftSideBoundary) {
project.style.transform = "translateX(" + ((RightSidePosition - project.offsetLeft)) + "px)"
updateTranslateX(e);
}
if (projectVisualPosition >= RightSidePosition) {
newPosition = -1 * (project.offsetLeft + elementAWidth)
project.style.transform = "translateX(" + newPosition + "px)"
updateTranslateX(e);
}
}
function updateTranslateX(e) {
projects.forEach(project => {
style = window.getComputedStyle(project)
project.currentTranslationX = (new WebKitCSSMatrix(style.webkitTransform)).m41
project.style.transform = 'translateX(' + (project.currentTranslationX) + 'px)'
initialPosition = e.clientX
})
}
*, *::before, *::after{
margin:0px;
padding:0px;
box-sizing: border-box;
font-size:0px;
user-select: none;
}
.project-container{
font-size: 0px;
position: relative;
width:1500px;
height:400px;
background-color: rgb(15, 207, 224);
margin:auto;
margin-top:60px;
white-space: nowrap;
overflow: hidden;
padding-left:40px;
padding-right:40px;
}
.project{
font-size:100px;
margin:40px;
display: inline-block;
height:300px;
width:350px;
background-color:red;
border: black 3px solid;
user-select: none;
}
<div class="project-container">
<div class="project">1</div>
<div class="project">2</div>
<div class="project">3</div>
<div class="project">4</div>
<div class="project">5</div>
<div class="project">6</div>
<div class="project">7</div>
<div class="project">8</div>
</div>
I'm not sure exactly how you would go about fixing your implementation. I played around with it for a while and discovered a few things; dragging more quickly makes the displacement worse, and the displacement seems to happen mainly when the elements are teleported at each end of the container.
I would guess that the main reason for this is that you are looping over all the elements and spacing them individually. Mouse move events generally happen under 20ms apart, and you are relying on all the DOM elements being repainted with their new transform positions before the next move is registered.
I did come up with a different approach using absolutely placed elements and the IntersectionObserver API, which is now supported in all modern browsers. The idea here is basically that when each element intersects with the edge of the container, it triggers an array lookup to see if the next element in the sequence is on the correct end and moves it there if not. Elements are only ever spaced by a static variable, while the job of sliding them is passed up to a new parent wrapper .project-slider.
window.addEventListener('DOMContentLoaded', () => {
// Style variables
const styles = {
width: 350,
margin: 40
};
const space = styles.margin*2 + styles.width;
// Document variables
const projectContainer = document.querySelector(".project-container");
const projectSlider = document.querySelector(".project-slider");
const projects = Array.from(document.querySelectorAll(".project"));
// Mouse interactions
let dragActive = false;
let prevPos = 0;
projectContainer.addEventListener('mousedown', e => {
dragActive = true;
prevPos = e.clientX;
});
projectContainer.addEventListener('mouseup', () => dragActive = false);
projectContainer.addEventListener('mouseleave', () => dragActive = false);
projectContainer.addEventListener('mousemove', e => {
if (!dragActive) return;
const newTrans = projectSlider.currentTransX + e.clientX - prevPos;
projectSlider.style.transform = `translateX(${newTrans}px)`;
projectSlider.currentTransX = newTrans;
prevPos = e.clientX;
});
// Generate initial layout
function init() {
let workingLeft = styles.margin;
projects.forEach((project, i) => {
if (i === projects.length - 1) {
project.style.left = `-${space - styles.margin}px`;
} else {
i !== 0 && (workingLeft += space);
project.style.left = `${workingLeft}px`;
};
});
projectSlider.currentTransX = 0;
};
// Intersection observer
function observe() {
const callback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Find intersecting edge
const { left } = entry.boundingClientRect;
const isLeftEdge = left < projectContainer.clientWidth - left;
// Test and reposition next element
const targetIdx = projects.findIndex(project => project === entry.target);
let nextIdx = null;
const nextEl = () => projects[nextIdx];
const targetLeft = parseInt(entry.target.style.left);
const nextLeft = () => parseInt(nextEl().style.left);
if (isLeftEdge) {
nextIdx = targetIdx === 0 ? projects.length-1 : targetIdx - 1;
nextLeft() > targetLeft && (nextEl().style.left = `${targetLeft - space}px`);
} else {
nextIdx = targetIdx === projects.length-1 ? 0 : targetIdx + 1;
nextLeft() < targetLeft && (nextEl().style.left = `${targetLeft + space}px`);
};
};
});
};
const observer = new IntersectionObserver(callback, {root: projectContainer});
projects.forEach(project => observer.observe(project));
};
init();
observe();
});
*, *::before, *::after{
margin:0px;
padding:0px;
box-sizing: border-box;
font-size:0px;
user-select: none;
}
.project-container {
font-size: 0px;
width: 100%;
height: 400px;
background-color: rgb(15, 207, 224);
margin:auto;
margin-top:60px;
white-space: nowrap;
overflow: hidden;
}
.project-slider {
position: relative;
}
.project {
font-size:100px;
display: block;
position: absolute;
top: 40px;
height:300px;
width:350px;
background-color:red;
border: black 3px solid;
user-select: none;
}
<div class="project-container">
<div class="project-slider">
<div class="project">1</div>
<div class="project">2</div>
<div class="project">3</div>
<div class="project">4</div>
<div class="project">5</div>
<div class="project">6</div>
<div class="project">7</div>
<div class="project">8</div>
</div>
</div>
There is still an issue here which is how to resize the elements for smaller screens, and on browser resizes. You would have to add another event listener for window resizes which resets the positions and styles at certain breakpoints, and also determine the style variables programmatically when the page first loads. I believe this would still have been a partial issue with the original implementation so you'd have to address it at some point either way.

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");
}

Categories