Move DIV using CSS Grid - javascript

I created a CSS Grid layout with global variables:
#app{
--width-l: 0.5fr;
--width-c: 0.5fr;
}
So I have a bar in the middle of my screen. On the other hand, in JavaScript I have two events that observe if the mouse is pressed (mousedown) and in motion (mousemove) that move the div. It has a problem, the movement is above the mouse position and at the height of the #app. So it works in parts, when I'm near the top, the div#bar doesn't go up anymore, and near the bottom, the same thing happens but at a greater distance.
I'm looking for a solution to make the transition smoothly, using the grid positions.
This is the code I created to try:
var split = document.querySelector(".split");
var app = document.querySelector("#app");
var position = app.getBoundingClientRect();
var isMouseMove = false;
split.addEventListener("mousedown", (e) => {
isMouseMove = true;
this.addEventListener("mouseup", (e) => {
isMouseMove = false;
});
});
split.addEventListener("mousemove", (e) => {
if (isMouseMove) {
let fullSize = app.offsetHeight;
let average = (100 * (e.y - position.top)) / fullSize;
let up = (average / 100).toFixed(4);
let down = (1 - average / 100).toFixed(4);
app.style.setProperty("--width-l", `${up}fr`);
app.style.setProperty("--width-c", `${down}fr`);
}
});
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
header {
background: aquamarine;
height: 50px;
}
footer {
background: aqua;
height: 50px;
}
#app {
--width-l: 0.5fr;
--width-c: 0.5fr;
height: calc(100vh - 100px);
display: grid;
grid-template: "aside up" var(--width-l) "aside split" 50px "aside down" var(
--width-c
) / 100px auto;
}
#app .aside {
grid-area: aside;
background: blue;
}
#app .up {
grid-area: up;
background: yellow;
resize: horizontal;
}
#app .split {
grid-area: split;
background: floralwhite;
display: flex;
justify-content: center;
align-items: center;
font-weight: 900;
color: tomato;
user-select: none;
}
#app .down {
grid-area: down;
background: green;
}
#app .split:active {
cursor: move;
}
<header></header>
<div id="app">
<div class="aside"></div>
<div class="up"></div>
<div class="split"> CLICK AND MOVE </div>
<div class="down"></div>
</div>
<footer></footer>

Related

Can I use requestAnimationFrame to smooth out scroll behaviour?

I have a small scroll effect which simulate that a logo will disappear if a lower div will scroll over it.
Currently I'm checking if two divs are intersecting. If this is true, then the height of the div of the logo will decrease with the scroll position of the div beneath.
Unfortunately, my demo is not foolproof and some fragments of the logo are still visible.
Is there a way to do this jank-free? Maybe with requestAnimationFrame?
function elementsOverlap(el1, el2) {
const domRect1 = el1.getBoundingClientRect();
const domRect2 = el2.getBoundingClientRect();
return !(
domRect1.top > domRect2.bottom ||
domRect1.right < domRect2.left ||
domRect1.bottom < domRect2.top ||
domRect1.left > domRect2.right
);
}
const el1 = document.querySelector(".logo");
const el2 = document.querySelector(".clickblocks");
let scrollPositionEl2;
let heightDifference;
const logoHeight = el1.offsetHeight;
document.addEventListener("DOMContentLoaded", () => {
var scrollDirectionDown;
scrollDirectionDown = true;
window.addEventListener("scroll", () => {
if (this.oldScroll > this.scrollY) {
scrollDirectionDown = false;
} else {
scrollDirectionDown = true;
}
this.oldScroll = this.scrollY;
// test
if (scrollDirectionDown) {
if (elementsOverlap(el1, el2) === true) {
scrollPositionEl2 = el2.getBoundingClientRect().top;
heightDifference = logoHeight - scrollPositionEl2 + 100;
//console.log(logoHeight - heightDifference);
el1.style.height = `${logoHeight - heightDifference}px`;
}
} else {
//scrolling up
scrollPositionEl2 = el2.getBoundingClientRect().top - 100;
el1.style.height = `${scrollPositionEl2}px`;
//console.log(logoHeight);
}
});
});
#import url("https://fonts.googleapis.com/css2?family=Inter:wght#900&display=swap");
.wrapper {
max-width: 100vw;
margin: 0 auto;
background-image: url("https://picsum.photos/1920/1080");
background-size: cover;
background-attachment: fixed;
height: 1200px;
position: relative;
&::after {
content: "";
position: absolute;
background: rgba(0, 0, 0, 0.3);
width: 100%;
height: 100%;
inset: 0;
}
}
body {
margin: 0;
}
main {
width: 100%;
height: 100vh;
position: relative;
z-index: 1;
}
.clickblocks {
width: 100%;
height: 200px;
display: grid;
grid-template-columns: repeat(12, (minmax(0, 1fr)));
}
.clickblock {
transition: all ease-in-out 0.2s;
backdrop-filter: blur(0px);
border: 1px solid #fff;
height: 100%;
grid-column: span 6 / span 6;
font-size: 54px;
font-weight: 700;
padding: 24px;
font-family: "Inter", sans-serif;
color: white;
text-transform: uppercase;
&:hover {
backdrop-filter: blur(10px);
}
}
.logo {
background: url("https://svgshare.com/i/ivR.svg");
width: 100%;
background-repeat: no-repeat;
background-position: top;
position: fixed;
top: 100px;
}
.logo-wrapper {
position: relative;
}
<div class="wrapper">
<main>
<div class="logo-wrapper" style="height: 390px">
<div class="logo" style="height: 300px">
</div>
</div>
<div class="clickblocks">
<div class="clickblock">
Some Content
</div>
</div>
</main>
</div>
Few things here to optimize your performance.
getBoundingClientRect() is a rather expensive calculation. If there are NO other options it's fine.
The Intersection Observer API is a lot more performant, and you can set the root element on the API. Then observe the element that is moving. This should be able to telly you if their are colliding.
Whenever you do scroll based logic, you should really try and throttle the logic so that the scroll any fires ever 16.6ms. That will reduce the number of times the calculations are made, and speed things up on the FE.
Learn how to use Google Chrome's performance tab. It can be overwhelming at first, but it gives you the ability to drill into the exact piece of code that's slowing your site down.
Learn about JS's event loop, and what's really going on under the hood. This video by Jake Archibald really help me understand it.
Hope this helped, sorry that I didn't give you an actual solution.

How to rotate and scale cards while swiping

As I'm trying to make a carousel that rotates I wonder how to move the cards rotated and it scales from a minimum of 0.8(left and right cards) and a maximum scale of 1(center card) when the user keeps swiping
Scale:
left = 0.8
center = 1
right = 0.8
I'm trying to solve on how to rotate them using transform and z-index properties. The cards will also rotate however I'm still trying to make a formula on how to make a function that makes the cards rotate
Any alternative solutions are accepted The animation is
similar to this carousel from codepen however it doesn't swipe Carousel Rotate
const CONTAINER_FLEX = document.querySelector('.container-flex');
const items = document.querySelectorAll('.item');
let touchStartX = 0;
let touchMoveX = 0;
let count = 0;
let current_translate = 0;
let previous_translate = 0;
CONTAINER_FLEX.addEventListener('touchstart', (event) => {
touchStartX = event.touches[0].pageX;
});
CONTAINER_FLEX.addEventListener('touchmove', (event) => {
touchMoveX = event.touches[0].pageX;
current_translate = previous_translate + (touchMoveX - touchStartX);
console.log(current_translate);
items[1].style.transform = `translateX(${current_translate}px)`;
});
CONTAINER_FLEX.addEventListener('touchend', () => {
current_translate = touchMoveX - touchStartX;
previous_translate = current_translate;
});
*,
::before,
::after {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
font-family: Arial, Helvetica, sans-serif;
line-height: 1.5;
background-color: #131b24;
}
.main-container {
padding: 30px 0;
height: 300px;
width: 900px;
border-top: 1px solid #444;
border-bottom: 1px solid #444;
overflow: hidden;
background-color: white;
}
.container-flex {
height: 100%;
display: flex;
transition: transform 400ms cubic-bezier(0.165, 0.84, 0.44, 1);
}
.item {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
min-width: 300px;
max-width: 300px;
}
.item h1 {
font-size: 40px;
color: white;
}
/* ITEMS */
.item-1 {
background-color: #2c3e50;
transform: translateX(100px);
z-index: 1;
}
.item-2 {
background-color: #3498db;
z-index: 2;
}
.item-3 {
background-color: #1abc9c;
transform: translateX(-100px);
z-index: 1;
}
<div class="main-container">
<div class="container-flex" id="container-flex">
<div class="item item-1">
<h1>1</h1>
</div>
<div class="item item-2">
<h1>2</h1>
</div>
<div class="item item-3">
<h1>3</h1>
</div>
</div>
</div>
Here is a working example https://jsfiddle.net/4ue5sgm9/3/
I wonder how to move the cards when it goes from 0 to 200 however
Have all your cards in an array.
var cards = [ thing1, thing2, thing3];
Use % - modulus operator, it's the secret for cycling back to the beginning
cardIndex = (cardIndex + 1) % cards.length
Copied from the chat. I made it verbose for clarity
scrollLeft() {
nextIndex = items.indexOf(displayed[2])
nextIndex = ++nextIndex % items.length-1
displayed = items[nextIndex]
nextIndex = ++nextIndex % items.length-1;
displayed.push( items[nextIndex] );
nextIndex = ++nextIndex % items.length-1;
displayed.push( items[nextIndex] )
}
P.S. Write and iterator for items array. An iterator stops after the last item. An iterator is why "for (x in thisArray)" works. BUT write the next() function to return "% this.length-1" instead => now you have a never-ending looper. All that code just above goes away.

How can I trigger on mouse movement Variable font in different section?

I was looking to trigger different sections with a variable font based on my mouse movement.
For the first section, everything looks great, but when I tried to trigger the second section, it does not work as I expected since is connected to the first one I guess.
I would need to make the section working independently and in the correct way (to have an idea see section one how react in debug mode)
I was wondering what I have to modify in my Javascript code to make my snippet work with all the sections I want, working independently with their respective variable font interaction. Any ideas?
$('.square').on('mousemove', function(e) {
var x = e.pageX - $(this).offset().left;
var y = e.pageY;
var $tlSquare = $('.division--top.division--left');
var $trSquare = $('.division--top.division--right');
var $blSquare = $('.division--bottom.division--left');
var $brSquare = $('.division--bottom.division--right');
var squareWidth = $(this).width(),
squareHeight = $(this).height();
$tlSquare.width(x).height(y);
$trSquare.width(squareWidth - x).height(y);
$blSquare.width(x).height(squareHeight - y);
$brSquare.width(squareWidth - x).height(squareHeight - y);
stretchLetter(false);
});
stretchLetter(false);
$('.square').on('mouseleave', function() {
$('.division').width('50%').height('50%');
$('.letter').css('transform', '');
stretchLetter(false);
});
function stretchLetter(animation) {
$('.letter').each(function() {
var parentWidth = $(this).parent().width();
var parentHeight = $(this).parent().height();
var thisWidth = $(this).width();
var thisHeight = $(this).height();
var widthPercent = parentWidth / thisWidth;
var heightPercent = parentHeight / thisHeight;
var timing = animation == true ? .5 : 0;
TweenMax.to($(this), timing, {
scaleX: widthPercent,
scaleY: heightPercent
})
//$(this).css('transform', 'scalex('+ widthPercent +') scaley('+ heightPercent +')');
});
}
body,
html {
margin: 0;
padding: 0;
font-weight: bold;
font-family: helvetica;
}
section {
height: 200px;
background: blue;
color: white;
font-size: 28px;
}
.wrapper {
display: flex;
justify-content: center;
/*justify-content: flex-end;*/
width: 100%;
height: 100vh;
//background-color: blue;
overflow: hidden;
}
.square {
display: flex;
flex-wrap: wrap;
width: 100vh;
height: 100vh;
background-color: black;
}
.square-2 {
display: flex;
flex-wrap: wrap;
width: 100vh;
height: 100vh;
background-color: yellow;
}
.division {
//display: flex;
//align-items: center;
//justify-content: center;
width: 50%;
height: 50%;
//background-color: red;
//border: 1px solid white;
box-sizing: border-box;
}
.letter {
cursor: -webkit-grab;
cursor: grab;
}
.letter {
display: inline-block;
font-size: 50vh;
margin: 0;
padding: 0;
line-height: .8;
transform-origin: top left;
color: white;
}
/* .division:nth-child(1){
background-color: blue;
}
.division:nth-child(2){
background-color: red;
}
.division:nth-child(3){
background-color: green;
}
.division:nth-child(4){
background-color: orange;
} */
.circle {
width: 100%;
height: 100%;
border-radius: 50%;
border: 1px solid white;
background-color: blue;
box-sizing: border-box;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<section>SECTION-01</section>
<main>
<div class="wrapper">
<div class="square">
<div class="division division--top division--left">
<div class="letter">L</div>
</div>
<div class="division division--top division--right">
<div class="letter">A</div>
</div>
<div class="division division--bottom division--left">
<div class="letter">S</div>
</div>
<div class="division division--bottom division--right">
<div class="letter">T</div>
</div>
</div>
</main>
<section>SECTION-02</section>
<div class="wrapper">
<div class="square">
<div class="division division--top division--left">
<div class="letter">F</div>
</div>
<div class="division division--top division--right">
<div class="letter">A</div>
</div>
<div class="division division--bottom division--left">
<div class="letter">S</div>
</div>
<div class="division division--bottom division--right">
<div class="letter">T</div>
</div>
</div>
</main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script>
https://jsfiddle.net/CAT999/ohaf61qp/5/
See working FIDDLE
You had to change the y variable because you were calculating with the offset top of the mouse position inn the document. This is always bigger than the element, so you have to extract the offset top of the element you were scrolling on, to get the right value.
var y = e.pageY - $(this).offset().top;

scrollTop or scrollLeft on horizontal website?

I have a website I am working on (here is a basic example), yesterday I got some help to implement active states on the radio-style button navigation, and I am now trying to link this up so that it also changes on page scroll/when in view as currently it's only onClick.
I roughly know how to achieve this as I've done something similar before, but then it occurred to me that because the page and scrollbar are rotated to accommodate the horizontal effect, I don't know if it would now be scrollTop or scrollLeft. I've never used scrollLeft before so I am unsure how to use it correctly. I am wondering if anyone has implemented something similar before and what the correct way would be? I've tried both and nothing seems to be working. This is what I'm roughly trying to achieve (but only one class active at a time).
I thought maybe using Waypoints could be another option, but again it's hard to find anything online which explains how this works when a site is rotated multiple times.
My JS knowledge is shaky (still learning!), I'm only trying to implement what I think would work so this is probably not even correct, any help understanding what I'm doing wrong would be appreciated!
Heres the latest thing I've tried.
// --- change span classes on click
const setIconState = (icon, state) => icon.className = state
? icon.className.replace('button-off', 'button-on')
: icon.className.replace('button-on', 'button-off')
const toggleIcon = element => {
const className = element.className;
element.className = className.indexOf('button-on') > -1
? setIconState(element, false)
: setIconState(element, true);
}
const setIconActiveState = (icon, state) => icon.className = state
? icon.className = `${icon.className} active`
: icon.className = icon.className.replace('active', '')
document.querySelectorAll('.bottomnav span.icon')
.forEach(icon => {
icon.onclick = (e) => {
const {
target: clickedSpan
} = e;
const siblings = [...clickedSpan.parentElement.parentElement.querySelectorAll('span.icon')]
.filter(sibling => sibling != clickedSpan);
siblings.forEach(icon => {
setIconState(icon, false);
setIconActiveState(icon, false);
});
setIconState(clickedSpan, true);
setIconActiveState(clickedSpan, true);
};
});
// --- change span classes on scroll test
function onScroll(event){
var scrollPos = $(document).scrollTop();
$('.bottomnav a').each(function () {
var currLink = $(this);
var refElement = $(currLink.attr("href"));
if (refElement.position().top <= scrollPos && refElement.position().top + refElement.height() > scrollPos) {
$('.bottomnav a span').removeClass("active");
currLink.addClass("active");
}
else{
currLink.removeClass("active");
}
});
}
* {
margin: 0;
padding: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html,
body {
color: #000;
font-family: 'IBM Plex Sans', sans-serif;
font-weight: 100;
font-size: 7px;
text-rendering: optimizeLegibility;
overflow-x: hidden;
scroll-behavior: smooth;
}
.bottomnav {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
overflow: hidden;
position: fixed;
bottom: 0px;
width: 100%;
z-index: 2;
}
.bottomnav span {
float: left;
display: block;
color: #888;
text-align: center;
padding: 14px 16px;
text-decoration: none;
font-size: 26px;
}
.bottomnav span:hover {
color: #fac123;
}
.bottomnav span.active {
color: #fac123;
}
#container {
overflow-y: scroll;
overflow-x: hidden;
transform: rotate(270deg) translateX(-100vh);
transform-origin: top left;
position: absolute;
width: 100vh;
height: 100vw;
white-space: nowrap;
scroll-snap-type: y mandatory;
}
#container .card {
width: 100vw;
height: 100vh;
display: inline-flex;
position: relative;
scroll-snap-align: start;
}
#player {
transform: rotate(90deg) translateY(-100vh);
transform-origin: top left;
font-size: 0;
width: 100vh;
height: 100vh;
display: flex;
/* position: absolute;*/
}
#player section > object {
width: 100vw;
overflow-x: hidden;
}
section object > div {
white-space: normal;
}
.container::-webkit-scrollbar {
display: none;
}
section {
padding: 5%;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
position: relative;
transition: .5s ease;
}
.cardwhite {
color: white;
background-color: black;
}
.cardblack {
color: black;
background-color: white;
}
h2 {
font-size: 40px;
font-weight: 700;
font-family: 'IBM Plex Serif', sans-serif;
}
p {
font-size: 10px;
margin-bottom: 15px;
font-weight: 100;
font-family: 'IBM Plex Sans', sans-serif;
}
<link href="https://unpkg.com/ionicons#4.5.5/dist/css/ionicons.min.css" rel="stylesheet">
<div class="bottomnav" id="bottomnav">
<span class="icon ion-ios-radio-button-on active"></span>
<span class="icon ion-ios-radio-button-off"></span>
<span class="icon ion-ios-radio-button-off"></span>
</div>
<div class="container" id="container">
<div id="player">
<section class="card cardwhite" id="1">
<object>
<h2>Section 1</h2>
<p>Description</p>
</object>
</section>
<section class="card cardblack" id="2">
<object>
<h2>Section 2</h2>
<p>Description</p>
</object>
</section>
<section class="card cardwhite" id="3">
<object>
<h2>Section 3</h2>
<p>Description</p>
</object>
</section>
</div>
</div>
For horizontal scrolling I would go the following route, simplifying your HTML and whaty you listen for. Since touch devices can easily just swipe to scroll, all you need to do is make it accessible for people with scroll wheels. You could also add an animation, but it makes this snippet too long.
const main = document.querySelector( 'main' );
const nav = document.querySelector( 'nav' );
let scrollend;
function onwheel(){
/* When using the scrollwheel, translate Y direction scrolls to X direction. This way scrollwheel users get the benefit of scrolling down to go right, while touch and other users get default behaviour. */
event.preventDefault();
event.stopImmediatePropagation();
main.scrollLeft += event.wheelDeltaY;
}
function onscroll(){
/* When scrolling, find the nearest element to the center of the screen. Then find the link in the nav that links to it and activate it while deactivating all others. */
const current = Array.from( main.children ).find(child => {
return child.offsetLeft >= main.scrollLeft - innerWidth / 2;
});
const link = Array.from( nav.children ).reduce((find, child) => {
child.classList.remove( 'selected' );
return find || (child.href.indexOf( current.id ) >= 0 ? child : find);
}, false);
if( link ) link.classList.add( 'selected' );
clearTimeout( scrollend );
scrollend = setTimeout( onscrollend, 100 );
}
function onscrollend(){
/* After scrolling ends, snap the appropriate element. This could be done with an animation. */
clearTimeout( scrollend );
const current = Array.from( main.children ).find(child => {
return child.offsetLeft >= main.scrollLeft - innerWidth / 2;
});
main.scrollLeft = current.offsetLeft;
}
/* Bind and initial call */
main.addEventListener( 'wheel', onwheel );
main.addEventListener( 'scroll', onscroll );
onscroll();
html,
body,
main {
height: 100%;
}
body {
padding: 0;
margin: 0;
}
main {
display: flex;
overflow: auto;
width: 100%;
height: 100%;
scroll-snap-type: x proximity;
}
main section {
width: 100%;
height: 100%;
flex: 0 0 100%;
}
nav {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
nav a {
width: 1em;
height: 1em;
margin: 1em;
display: block;
overflow: hidden;
color: transparent;
border: 1px solid black;
border-radius: 50%;
}
nav a.selected {
background: black;
}
.bland { background: gray; }
.dark { background: darkgray; color: white; }
.bright { background: yellow; }
<nav>
Section 1
Section 2
Section 3
</nav>
<main>
<section class="bright" id="section-1">
<h2>Section 1</h2>
</section>
<section class="dark" id="section-2">
<h2>Section 2</h2>
</section>
<section class="bland" id="section-3">
<h2>Section 3</h2>
</section>
</main>
As mentioned, I would also prefer a design that does not flip the X and Y axis.
Doing so might bite us in the future, when we try to include non-trivial content on our pages.
Also if we don't do that axis flip, we have no need at all to do positional calculations.
So both the HTML structure and the CSS will be simpler.
AFAIK, it's not possible to do the scrolling purely in non-hacky CSS.
/**
* Change icon state on click.
*/
const icons = Array.from( document.querySelectorAll( '.icon' ));
const toggleIcon = icon => {
icon.classList.toggle( 'ion-ios-radio-button-on' );
icon.classList.toggle( 'ion-ios-radio-button-off' );
};
const clickIcon = event => {
// toggle previous active state
toggleIcon( document.querySelector( 'i.ion-ios-radio-button-on' ));
// toggle own state
toggleIcon( event.target );
};
icons.forEach( icon => icon.addEventListener( 'click', clickIcon ));
/**
* Scroll horizontally on scroll wheel.
* The combination of "scroll-behavior: smooth;" and the "<a href=#>" anchor links,
* can be reused to do and endless snapping cycle on wheel event.
*/
let scroll_state = 0;
window.addEventListener( 'wheel', event => {
window.requestAnimationFrame(() => {
// cast to -1 or +1
const offset = event.deltaY / Math.abs( event.deltaY );
scroll_state += offset;
// Arrays are zero-based.
// So if the length matches our state, restart over from the first page.
if ( scroll_state === icons.length ) scroll_state = 0;
else if ( scroll_state < 0 ) scroll_state = icons.length - 1;
// scrolll_state will now always contain the next icon to click.
icons[ scroll_state ].click();
});
});
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
overflow-x: scroll;
width: 100%;
}
main {
display: block;
height: 90vh;
width: 300vw;
}
nav {
background-color: orange;
display: block;
height: 10vh;
position: fixed;
width: 100%;
}
a {
text-decoration: none;
}
.page {
display: inline-block;
float: left;
height: 100%;
padding: 50px;
width: 100vw;
}
<link href="https://unpkg.com/ionicons#4.5.5/dist/css/ionicons.min.css" rel="stylesheet">
<body>
<main>
<section class="page" id="myapp_first">
<h1>First</h1>
<p>Lorem Ipsum</p>
</section>
<section class="page" id="myapp_second">
<h1>Second</h1>
<p>Lorem Ipsum</p>
</section>
<section class="page" id="myapp_third">
<h1>Third</h1>
<p>Lorem Ipsum</p>
</section>
</main>
<nav id="myapp_navigation">
<a href="#myapp_first">
<i class="icon ion-ios-radio-button-on active"></i>
</a>
<a href="#myapp_second">
<i class="icon ion-ios-radio-button-off"></i>
</a>
<a href="#myapp_third">
<i class="icon ion-ios-radio-button-off"></i>
</a>
</nav>
</body>
By leveraging the click event of the icons, we get the icons changing class and the transition for free. Adding more pages now just becomes adding the correct HTML and updating the width of the <main> element.
A last thing I would personally add, is a debounce function around the wheel event, so we don't try to scroll faster than we can render.
Without debouncing, we might want to merge the functions so we can include the class changing inside the animationFrame for hopefully less yanky visuals, but that would complicate the click events again, so i'd prefer debouncing the wheel handler.
/**
* Change icon state on click.
*/
const icons = Array.from( document.querySelectorAll( '.icon' ));
const toggleIcon = icon => {
icon.classList.toggle( 'ion-ios-radio-button-on' );
icon.classList.toggle( 'ion-ios-radio-button-off' );
icon.classList.toggle( 'active' );
};
const clickIcon = event => {
// toggle previous active state
toggleIcon( document.querySelector( '.ion-ios-radio-button-on' ));// toggle own state
toggleIcon( event.target );
};
icons.forEach( icon => icon.addEventListener( 'click', clickIcon ));
/**
* Scroll horizontally on scroll wheel.
* The combination of "scroll-behavior: smooth;" and the "<a href=#>" anchor links,
* can be reused to do and endless snapping cycle on wheel event.
*/
let scroll_state = 0;
window.addEventListener( 'wheel', event => {
// ANimation frame to smooth out the transition.
window.requestAnimationFrame(() => {
// cast to -1 or +1
const offset = event.deltaY / Math.abs( event.deltaY );
scroll_state += offset;
// Arrays are zero-based.
// So if the length matches our state, restart over from the first page.
if ( scroll_state === icons.length ) scroll_state = 0;
else if ( scroll_state < 0 ) scroll_state = icons.length - 1;
// scrolll_state will now always contain the next icon to click.
icons[ scroll_state ].click();
});
});
* {
margin: 0;
padding: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html,
body {
color: #000;
font-family: 'IBM Plex Sans', sans-serif;
font-weight: 100; /* EDIT: font-weight: 100 basically equals no font weight at all */
font-size: 7px; /* EDIT: Why so small ? */
text-rendering: optimizeLegibility;
overflow-x: scroll;
overflow-y: hidden;
scroll-behavior: smooth;
height: 100vh; /* EDIT: add height, so we can scale off this */
width: 100vw; /* EDIT: add width, so we can scale off this */
}
.bottomnav {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
/*
overflow: hidden;
*/
position: fixed;
/*bottom: 0px; EDIT: not needed after we place the nav at the bottom */
height: 15vh; /* EDIT: .bottomnav height + #container height = 100vh */
width: 100%;
z-index: 2;
background-color: black;
}
.bottomnav span {
/*float: left; /* why float when flex lets us position exactly? */
display: block;
color: #888;
text-align: center;
padding: 14px 16px;
text-decoration: none;
font-size: 26px;
}
.bottomnav span:hover {
color: #fac123;
}
.bottomnav span.active {
color: #fac123;
}
#container {
/*
overflow-y: scroll;
overflow-x: hidden;
transform: rotate(270deg) translateX(-100vh);
transform-origin: top left;
position: absolute;
*/
width: 300vw; /* EDIT: 300vw, 100 per page of 100vw */
height: 85vh; /* EDIT: .bottomnav height + #container height = 100vh */
/*scroll-snap-type: y mandatory; EDIT: only needed if we use snappoints */
}
/* EDIT: .card and section are the same elements, merged rule "container" here */
.card {
width: 100vw; /* EDIT: 100vw for each page of 100vw width */
height: 100%; /* EDIT: 100% so it scales with the container, not the screen */
display: inline-block; /* EDIT: block level, since we do not need to flex these */
float: left; /* EDIT: float left so our pages leave no space between them so 300vw = 100+100+100 . THis can be done with flexbox or grid as well, but is more complicated than needed */
/*position: relative; EDIT: not needed */
/* scroll-snap-align: start; EDIT: only needed if we use snappoints */
padding: 50px;
/* EDIT:
justify-content: center;
align-items: center;
flex-wrap: wrap;
position: relative;
*/
/* transition: .5s ease; EDIT: I would think that "scroll-behavior: smooth;" already does this */
}
/* EDIT: Since there's no use for the extra wrapper element, positioning it absolute + flex only harms us instead of helping
#player {
transform: rotate(90deg) translateY(-100vh);
transform-origin: top left;
font-size: 0;
width: 100vh;
height: 100vh;
display: flex;
position: absolute;
}
#player section > object {
width: 100vw;
overflow-x: hidden;
}
*/
/* EDIT: I don't see any <div>s inside the objects
section object > div {
white-space: normal;
}
*/
/* EDIT: ? Attempt to remove vertical scroll? Not needed
.container::-webkit-scrollbar {
display: none;
}
*/
.cardwhite {
color: white;
background-color: black;
}
.cardblack {
color: black;
background-color: white;
}
h2 {
font-size: 40px;
font-weight: 700;
font-family: 'IBM Plex Serif', sans-serif;
}
p {
font-size: 10px;
margin-bottom: 15px;
font-weight: 100;
font-family: 'IBM Plex Sans', sans-serif;
}
<link href="https://unpkg.com/ionicons#4.5.5/dist/css/ionicons.min.css" rel="stylesheet">
<div id="container">
<!-- the extra player <div> is useless since the cards fully overlap it.
so it can be removed -->
<section class="card cardwhite" id="1">
<object>
<h2>Section 1</h2>
<p>Description</p>
</object>
</section>
<section class="card cardblack" id="2">
<object>
<h2>Section 2</h2>
<p>Description</p>
</object>
</section>
<section class="card cardwhite" id="3">
<object>
<h2>Section 3</h2>
<p>Description</p>
</object>
</section>
</div>
<!-- EDIT: Put the nav at the bottom so we do not have position issues -->
<div class="bottomnav" id="bottomnav">
<a href="#1">
<span class="icon ion-ios-radio-button-on active"></span>
</a>
<a href="#2">
<span class="icon ion-ios-radio-button-off"></span>
</a>
<a href="#3">
<span class="icon ion-ios-radio-button-off"></span>
</a>
</div>

Scroll Points / overflow-y breaking javascript function with window.pageYOffset

I have a page where I was using JS - specifically window.pageYOffset - and HTML data to change the inner HTML of the h1 footer, use l1 links to scroll the page to each section, and to add classes to each li when I reached the top of each section.work-page.
However, after I implemented CSS scroll points and added the div.container over the scrollable sections my javascript stopped working. Specifically when I set the overflow-y: scroll.
Basically when I made the div.container overflow-y: scroll; the doWork function stopped working and I can't figure out why.
^^^^ div.container in CSS
const doWork = function () {
const p01Tag = document.getElementById("p01")
const p02Tag = document.getElementById("p02")
const p03Tag = document.getElementById("p03")
const p04Tag = document.getElementById("p04")
const container = document.querySelector("div.container")
const sections = document.querySelectorAll("section.work-page")
const clientTag = document.querySelector("h2.about")
document.addEventListener("scroll", function () {
const pixels = window.pageYOffset
console.log(pixels)
sections.forEach(section => {
if(section.offsetTop - 400 <= pixels) {
clientTag.innerHTML = section.getAttribute("data-client")
if (section.hasAttribute("data-seen-1")) {
p01Tag.classList.add("move")
} else {
p01Tag.classList.remove("move")
}
if (section.hasAttribute("data-seen-2")) {
p02Tag.classList.add("move")
} else {
p02Tag.classList.remove("move")
}
if (section.hasAttribute("data-seen-3")) {
p03Tag.classList.add("move")
} else {
p03Tag.classList.remove("move")
}
if (section.hasAttribute("data-seen-4")) {
p04Tag.classList.add("move")
} else {
p04Tag.classList.remove("move")
}
}
})
})
// scrolling between projects ============================
function smoothScroll(target, duration) {
const targetTag = document.querySelector(target);
let targetPosition = targetTag.getBoundingClientRect().top;
const startPosition = window.pageYOffset;
let startTime = null;
function animation(currentTime) {
if(startTime === null ) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const run = ease(timeElapsed, startPosition, targetPosition, duration);
window.scrollTo(0,run);
if (timeElapsed < duration) requestAnimationFrame(animation)
}
function ease(t, b, c, d) {
t /= d / 2;
if (t < 1) return c / 2 * t * t + b;
t--;
return -c / 2 * (t * (t - 2) - 1) + b;
}
requestAnimationFrame(animation)
}
p01Tag.addEventListener("click", function() {
smoothScroll('section.fn-up', 800)
})
p02Tag.addEventListener("click", function() {
smoothScroll('section.cameron', 800)
})
p03Tag.addEventListener("click", function() {
smoothScroll('section.truax', 800)
})
p04Tag.addEventListener("click", function() {
smoothScroll('section.romero', 800)
})
}
doWork()
const doInfo = function () {
const toggleTag = document.querySelector("a.contact")
const sectionTag = document.querySelector("section.info-page")
toggleTag.addEventListener("click", function () {
sectionTag.classList.toggle("open")
if (sectionTag.classList.contains("open")) {
toggleTag.innerHTML = "Close"
} else {
toggleTag.innerHTML = "Info"
}
})
}
doInfo()
html {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
*, *:before, *:after {
-webkit-box-sizing: inherit;
-moz-box-sizing: inherit;
box-sizing: inherit;
}
body {
font-family: 'IBM Plex Mono', monospace;
font-size: 14px;
background-color: #050505;
color: #ffffff;
line-height: 1.1;
}
header {
width: 100%;
z-index: 3;
position: fixed;
top: 0;
left: 0;
padding-top: 40px;
padding-left: 40px;
padding-right: 40px;
}
.contact {
float: right;
}
ul {
font-family: 'IBM Plex Mono', Arial;
font-size: 14px;
}
p {
margin-bottom: 50px;
}
/* Info page -------------------- */
section.info-page {
z-index: 2;
position: fixed;
top: -100vh;
left: 0;
display: flex;
margin-top: 100px;
margin-left: 40px;
margin-right: 40px;
width: 100vw;
min-height: 100vh;
max-width: 100vw;
transition: top 0.5s;
}
section.info-page.open {
top: 0;
}
/* Work page ------------------------*/
div.container {
top: 0;
left: 0;
max-width: 100vw;
max-height: 100vh;
/* WHEN WE ADD THIS OVERFLOW SETTING IN ORDER TO GET THE CSS SCROLL SNAP POINTS TO WORK IT BREAKS THE JAVASCRIPT */
/* overflow-y: scroll; */
scroll-snap-type: y mandatory;
position: relative;
z-index: 1;
}
div.work-info {
width: 13vw;
top: 0;
left: 0;
height: 100vh;
position: fixed;
z-index: 2;
padding-right: 80px;
display: flex;
align-items: center;
margin-left: 40px;
}
div.work-info li {
padding-bottom: 30px;
transition: transform 0.3s;
}
div.work-info li.move {
transform: translateX(15px);
}
footer {
width: 100%;
z-index: 1;
position: fixed;
bottom: 0;
left: 0;
padding-left: 40px;
padding-right: 40px;
padding-bottom: 40px;
color: #979797;
}
section.work-page {
scroll-snap-align: start;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
position: relative;
}
section.work-page img {
max-width: 60vw;
}
<body>
<!-- hidden modal that runs off of the info.js script -->
<section class="info-page">
<h1>
Hello
</h1>
</section>
<header>
<a class="contact" href="#">Info</a>
</header>
<!-- objects that get new classes with javascript on pageYOffset -->
<div class="work-info">
<ul>
<li id="p01" data-number="FN-UP Magazine">01</li>
<li id="p02" data-number="Cameron Tidball-Sciullo">02</li>
<li id="p03" data-number="Jacob Truax">03</li>
<li id="p04" data-number="Alexander Romero">04</li>
</ul>
</div>
<!-- scollable sections using the scroll points and triggering the pageYOffset -->
<div class="container">
<section class="work-page fn-up" data-client="FN-UP Magazine" data-seen-1="yes">
<div class="content">
<img src="lib/fn-up.png">
</div>
</section>
<section class="work-page cameron" data-client="Cameron Tidball-Sciullo" data-seen-2="yes">
<div class="content">
<img src="lib/alex.png">
</div>
</section>
<section class="work-page truax" data-client="Jacob Truax" data-seen-3="yes">
<div class="content">
<img src="lib/old.png">
</div>
</section>
<section class="work-page romero" data-client="Alexander Romero" data-seen-4="yes">
<div class="content">
<img src="lib/alex.png">
</div>
</section>
</div>
<footer class="footer">
<h2 class="about">FN-UP Magazine</h2>
</footer>
</body>
You have added a event listener to the page's Document object.
document.addEventListener("scroll", function () {
Then you calculate the number of pixels the document is currently scrolled along the vertical axis using window.pageYOffset.
const pixels = window.pageYOffset
When you set the CSS attribute overflow-y to scroll in the div.container element, new scrollbars appears on the window. According to MDN:
scroll
Content is clipped if necessary to fit the padding box. Browsers display scrollbars whether or not any content is actually clipped. (This prevents scrollbars from appearing or disappearing when the content changes.) Printers may still print overflowing content.
From that moment on, you are not scrolling the document, you are scrolling div.container. That won't trigger you scroll event.
You need to bound the event to the div element:
const container = document.querySelector("div.container")
container.addEventListener("scroll", function () {
And, instead of calculating how much document has scrolled, get the scrollTop property of the div.container:
const pixels = container.scrollTop
You need to make the same changes in whatever part of the code that involves the above calculations. In smoothScroll():
// const startPosition = window.pageYOffset;
const startPosition = container.scrollTop;
// window.scrollTo(0,run);
container.scrollTo(0,run);

Categories

Resources