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

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>

Related

Why does window.requestAnimationFrame not keeping the last transition?

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?

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.

How can I use requestAnimationFrame along with setInterval?

See the following snippet of code.
It creates a loader on click of a button. But the animation is not smooth.
I have recently read about requestAnimationFrame function which can do this job. But how can I use it to replace setInterval altogether since there is no way to specify time in requestAnimationFrame function.
Can it be used in conjunction with setInterval ?
let idx = 1;
const timetoEnd = 5000;
function ProgressBar(width){
this.width = width || 0;
this.id = `pBar-${idx++}`;
this.create = () => {
let pBar = document.createElement('div');
pBar.id = this.id;
pBar.className = `p-bar`;
pBar.innerHTML = `<div class="loader"></div>`;
return pBar;
};
this.animator = () => {
let element = document.querySelector(`#${(this.id)} div`);
if(this.width < 100){
this.width++;
element.style.width = `${this.width}%`;
} else {
clearInterval(this.interval);
}
};
this.animate = () => {
this.interval = setInterval(this.animator, timetoEnd/100);
}
}
function addLoader (){
let bar1 = new ProgressBar(40);
let container = document.querySelector("#container");
container.appendChild(bar1.create());
bar1.animate();
}
.p-bar{
width: 400px;
height: 20px;
background: 1px solid #ccc;
margin: 10px;
overflow:hidden;
border-radius: 4px;
}
.p-bar .loader{
width: 0;
background: #1565C0;
height: 100%;
}
<input type="button" value="Add loader" onclick="addLoader()" />
<div id="container"></div>
You are right, requestAnimationFrame is the recommended way to avoid UI jam when doing animation.
You can remember the absolute starting time at start instead of trying to do this at each frame. Then it's just a matter of computing a width based on the delta time between start and current time.
Also, document.querySelector is considered a relatively "heavy" operation so I added this.element to avoid doing it at each frame.
Here is how to new width is computed: ((100 - this.startWidth) / timetoEnd) * deltaT + this.startWidth
100 - this.startWidth is the total amount of width we have to animate
(100 - this.startWidth) / timetoEnd is how much width each second must add to (1)
((100 - this.startWidth) / timetoEnd) * deltaT is how much width we have to add to (1)
We just have to shift the whole thing this.startWidth px to have the frame's width
Also notice that some of this computation is constant and do not have to be computed on each frame, which I left as an exercise :)
Here is your slightly adapted code:
let idx = 1;
const timetoEnd = 5000;
function ProgressBar(startWidth){
this.startWidth = startWidth || 0;
this.id = `pBar-${idx++}`;
this.create = () => {
let pBar = document.createElement('div');
pBar.id = this.id;
pBar.className = `p-bar`;
pBar.innerHTML = `<div class="loader"></div>`;
return pBar;
};
this.animator = () => {
const deltaT = Math.min(new Date().getTime() - this.start, timetoEnd);
if(deltaT < timetoEnd){
const width = ((100 - this.startWidth) / timetoEnd) * deltaT + this.startWidth;
this.element.style.width = `${width}%`;
requestAnimationFrame(this.animator.bind(this))
}
};
this.animate = () => {
this.element = document.querySelector(`#${(this.id)} div`);
this.start = new Date().getTime();
this.animator();
}
}
function addLoader (){
let bar1 = new ProgressBar(40);
let container = document.querySelector("#container");
container.appendChild(bar1.create());
bar1.animate();
}
.p-bar{
width: 400px;
height: 20px;
background: 1px solid #ccc;
margin: 10px;
overflow:hidden;
border-radius: 4px;
}
.p-bar .loader{
width: 0;
background: #1565C0;
height: 100%;
}
<input type="button" value="Add loader" onclick="addLoader()" />
<div id="container"></div>

How should I create a package for a... thing that takes a DOM Element?

I have written my first piece of code that I feel could actually be reusable and possibly useful for others. The code is a bit of css, a few javascript-classes and some js-functions.
To use the package, you set up some HTML, and then pass a html-element and an optional parameter, for example like this:
dom = new DOM(document.getElementById('fullScreenWrapper'), 175),
I would like to pack it in some way so that you basically include a js-file, a css-file and then you can use it in any project. jQuery has a design-pattern for extending itself, but I don't use jQuery. Is there any best practice for stuff like this?
Here is the mockup, not packaged:
<html>
<head>
<style>
/* specific styles */
&:root {--scrollPosition:0}
.flicWrapper{
height: calc(var(--wrapperHeight));
}
.flicWrapper .flicChild{
top: calc(var(--previousHeight));
position: fixed;
}
.flicWrapper.transition{
height: initial;
}
.flicWrapper.transition .flicChild{
transition: transform .3s cubic-bezier(0, 0, 0.25, 1);
}
.flicWrapper .before{
transform: translate3d(0,calc((var(--previousHeight)*-1)),0);
}
.flicWrapper .active{
transform: translate3d(0, calc((var(--previousHeight)*-1) + var(--stickyScrollPosition)), 0);
}
.flicWrapper .next{
transform: translate3d(0, calc(var(--scrollPosition)), 0);
}
/*custom style*/
body{
margin: 0;
}
section{
width: 100%;
height: 100%;
}
.s1{
height: 100%;
background: repeating-linear-gradient(
45deg,
red,
red 10px,
blue 10px,
blue 20px
);
}
.s2{
height: 150%;
background: repeating-linear-gradient(
45deg,
#606dbc,
#606dbc 10px,
#465298 10px,
#465298 20px
);
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<body>
<div class="fullScreenWrapper" id="fullScreenWrapper">
<section class="s1" ></section>
<section class="s2"></section>
<section class="s3" style="background: red";></section>
<section class="s4" style="background: green";></section>
<section class="s5" style="background: black";></section>
</div>
<script type="text/javascript">
var State = class {
constructor(heights, offset) {
this.breakingPoints = heights;
this.offset = offset;
this.state = this.currentState;
}
get currentState(){
return this.breakingPoints.findIndex((elm, i, arr)=> {
if(window.scrollY >= arr[i] && window.scrollY < arr[i+1] ){
return[i]
}
})
}
get update(){
var isUpdated = Math.sign( this.currentState - this.state)
this.state += isUpdated;
return isUpdated !== 0
}
get snapPos() {
var offset = this.state ? this.offset : 0; // first state should not have offset.
return this.breakingPoints[this.state] + offset;
}
},
DOM = class {
constructor(wrapper, offset) {
this.wrapper = wrapper;
this.offset = offset
this.children = Array.from(wrapper.children);
this.childHeights = [];
this.scrollHeights = this.getScrollHeights;
this.wrapper.classList.add('flicWrapper')
setGlobalProperty('wrapperHeight', this.documentHeight)
//apply custom css-values
this.children.forEach((child, i) => {
child.classList.add('flicChild')
var height = 0
if(i > 0 ){
var height = this.children[i-1].offsetHeight;
}
child.style.setProperty('--previousHeight', height+'px');
this.childHeights.push(child.offsetHeight - window.innerHeight);
})
}
get getScrollHeights() {
var heights = [0];
//calculate al the heights
this.children.forEach((el, i) => {
var mod = 2;
if(i === 0 || i === this.children.length -1){mod = 1}; //first and last should only have one offset
var elheight = el.offsetHeight - window.innerHeight + this.offset * mod;
heights.push( elheight + heights[i] );
})
return heights;
}
get documentHeight() {return this.scrollHeights.slice(-1)[0]+window.innerHeight-1}
},
Scroll = class{
constructor(){
this.on = true;
}
throttle(fn, wait){
var time = Date.now();
return () => {
if ((time + wait - Date.now()) < 0 && this.on) {
fn();
time = Date.now();
}
}
}
},
scrollEvent = () => {
if(state.update){ //check state
scroll.on = false;
dom.wrapper.classList.add('transition')
setClasses();
setScrollPos(0, dom.childHeights[state.state]);
setTimeout(changeDone, 1200);
}
},
setClasses = () => {
var s = state.state;
dom.children.forEach((child, i) => {
let classes = child.classList;
classes.toggle('before', i < s);
classes.toggle('after', i > s);
classes.toggle('active', i === s);
classes.toggle('previous', i === s-1);
classes.toggle('next', i === s+1);
})
},
changeDone = () => {
dom.wrapper.classList.remove('transition');
window.scrollTo(0, state.snapPos);
scroll.on = true;
},
//helper
setGlobalProperty = (name, value) => {
document.documentElement.style.setProperty('--'+name, (value+'px'))
},
setScrollPos = (pos, max) => {
setGlobalProperty('scrollPosition', -pos)
setGlobalProperty('stickyScrollPosition', (Math.min(pos, max)*-1))
},
dom = new DOM(document.getElementById('fullScreenWrapper'), 175),
state = new State(dom.scrollHeights, dom.offset),
scroll = new Scroll();
setClasses();
window.addEventListener('scroll', scroll.throttle(scrollEvent, 50));
window.addEventListener('scroll', scroll.throttle(() => {setScrollPos(window.scrollY - state.snapPos, dom.childHeights[state.state])}, 0))
</script>
</body>

Categories