So I want to have a specific scrolling section on my website, where while you scroll the wheel down (or drag the scrollbar down), the content scrolls sideways while the scrollbar on the right of the screen also moves down.
I googles like a ton yesterday for examples or packages, but didn´t find what I need - actually kind of. The best thing I found was this codepen:
https://codepen.io/alvarotrigo/pen/VwWMjVp
<div class="vertical-section">
Content above
</div>
<div class="sticky-container">
<main>
<section>
<h1>Beep</h1>
</section>
<section>
<h1>Boop</h1>
</section>
<section>
<h1>Boooom</h1>
</section>
<section>
<h1>The End</h1>
</section>
</main>
</div>
<div class="vertical-section">
Content Below
</div>
<div class="sticky-container">
<main>
<section>
<h1>Beep</h1>
</section>
<section>
<h1>Boop</h1>
</section>
<section>
<h1>Boooom</h1>
</section>
<section>
<h1>The End</h1>
</section>
</main>
</div>
-----------------------
<a href="https://alvarotrigo.com/blog/scroll-horizontally-with-mouse-wheel-vanilla-java/" target="_blank" class="read-article">
Read the article đ
</a>
html,
body {
margin: 0;
font-family: sans-serif;
}
.vertical-section{
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
main {
overflow-x: hidden;
display: flex;
position: sticky;
top:0;
}
h1 {
margin: 0;
padding: 0;
}
section {
min-width: 50vw;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-size: 4ch;
}
section:nth-child(even) {
background-color: teal;
color: white;
}
.read-article{
position: absolute;
top: 10px;
left: 10px;
z-index: 999;
color: #000;
background: white;
padding: 10px 20px;
border-radius: 10px;
font-family: arial;
text-decoration: none;
box-shadow: rgb(50 50 93 / 25%) 0 0 100px -20px, rgb(0 0 0 / 30%) 0 0 60px -15px;
}
.read-article:hover{
background: #d5d5d5;
box-shadow: rgb(50 50 93 / 25%) 0 0 100px -20px, rgb(0 0 0 / 30%) 0 0 60px 0px;
}
iframe[sandbox] .read-article{
display: none;
}
/**
* By Alvaro Trigo
* Follow me on Twitter: https://twitter.com/imac2
*/
(function(){
init();
var g_containerInViewport;
function init(){
setStickyContainersSize();
bindEvents();
}
function bindEvents(){
window.addEventListener("wheel", wheelHandler);
}
function setStickyContainersSize(){
document.querySelectorAll('.sticky-container').forEach(function(container){
const stikyContainerHeight = container.querySelector('main').scrollWidth;
container.setAttribute('style', 'height: ' + stikyContainerHeight + 'px');
});
}
function isElementInViewport (el) {
const rect = el.getBoundingClientRect();
return rect.top <= 0 && rect.bottom > document.documentElement.clientHeight;
}
function wheelHandler(evt){
const containerInViewPort = Array.from(document.querySelectorAll('.sticky-container')).filter(function(container){
return isElementInViewport(container);
})[0];
if(!containerInViewPort){
return;
}
var isPlaceHolderBelowTop = containerInViewPort.offsetTop < document.documentElement.scrollTop;
var isPlaceHolderBelowBottom = containerInViewPort.offsetTop + containerInViewPort.offsetHeight > document.documentElement.scrollTop;
let g_canScrollHorizontally = isPlaceHolderBelowTop && isPlaceHolderBelowBottom;
if(g_canScrollHorizontally){
containerInViewPort.querySelector('main').scrollLeft += evt.deltaY;
}
}
})();
This is pretty much exactly what I was looking for but I wonder:
If there isn´t a more intuitive package that does exactly this?
If this is really that "clean"
but mostly the problem here is, because there is an eventlistener on the window only for the mousewheel, which means when not scrolling down via the mouse wheel, but with the scrollbar at the right, it won´t work.
Do you have any more ideas, what you could use or is this actually as good as it can get?
Thanks!
Related
I am trying to show how much a user has scrolled through the page a with progress bar and I have done it. But I have a little confusion here.
Here is the code that I found to calculate scrollPercent which works well
windowHeight = Math.max(
html.clientHeight,
html.scrollHeight,
html.offsetHeight,
body.scrollHeight,
body.offsetHeight
);
const scrolledPercent =
((html.scrollTop || body.scrollTop) / (windowHeight - html.clientHeight)) *
100;
Initially, I thought, to get the scrollPercent , I need to get the current scrollPosition and divide that number with the total height of the page and multiply by 100% . which is like normally how we get % of something.
const scrolledPercent =
((html.scrollTop || body.scrollTop) / windowHeight) * 100;
but this line doesnot worked as I expected . If I do this the progress bar wont reach 100% even if I scroll to end of the page. I don't understand why am I wrong here !
So, my question is why do we need to decrease the html.clientHeight from windowHeight ?
Thank you.
Demo here:
// --------------------------------------------
// variables
// --------------------------------------------
const html = document.documentElement,
body = document.body,
countryList = document.querySelector(".country__list");
scrollNavigated = document.querySelector(".scroll__navigated");
let windowHeight;
// --------------------------------------------
// function
// --------------------------------------------
async function prepareListOfCountries() {
let list = await fetch("https://restcountries.eu/rest/v2/all");
list = Array.from(await list.json());
let markup = list
.map((country, index) => {
return `<li class="country__item card">
<span class="country__name">${country.name}</span
><span class="country__capital">${country.capital}</span>
<a href="javascript:;" class="country__flag">
<img src= '${country.flag}'> </a>
</li>`;
})
.slice(0, 30)
.join(" ");
countryList.innerHTML = markup;
}
function updateScrolledStatus(e) {
windowHeight = Math.max(
html.clientHeight,
html.scrollHeight,
html.offsetHeight,
body.scrollHeight,
body.offsetHeight
);
const scrolledPercent =
((html.scrollTop || body.scrollTop) / (windowHeight - html.clientHeight)) *
100;
// const scrolledPercent =
// ((html.scrollTop || body.scrollTop) / windowHeight) * 100; // this line doesnot work
scrollNavigated.style.width = scrolledPercent + "%";
}
prepareListOfCountries();
// --------------------------------------------
// event-handler
// --------------------------------------------
window.addEventListener("scroll", updateScrolledStatus);
*::after,
*::before,
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #fff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
.container {
max-width: 980px;
margin: 0 auto;
}
.justify-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.items-center {
display: flex;
align-items: center;
}
.card {
background-color: #fff;
box-shadow: 0 0 12px 12px rgba(0, 0, 0, 0.054);
border-radius: 4px;
padding: 16px;
}
.country__flag img {
height: 100%;
width: 100%;
}
.header {
padding: 24px 0;
background-color: #333;
color: #f1f1f1;
position: -webkit-sticky;
position: sticky;
}
.content {
padding: 50px 0;
}
.content__form {
margin: 0 auto;
margin-bottom: 32px;
}
.content__search {
width: 50%;
padding: 12px 16px;
border-radius: 20px;
border: 1px solid #ddd;
transition: 0.2s;
}
.content__search:hover {
box-shadow: 0 1px 6px 0 rgba(32, 33, 36, 0.28);
}
.content__search:focus {
outline: none;
}
.country__list {
margin-top: 50px;
margin: 10px auto;
}
.country__item {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
.country__name, .country__capital, .country__flag {
width: 33.33%;
}
.country__flag {
width: 32px;
height: 24px;
}
.scroll__navigator {
height: 2px;
margin: 0 auto 32px;
background-color: #333;
position: -webkit-sticky;
position: sticky;
top: 0;
}
.scroll__navigated {
display: block;
height: 100%;
width: 0;
background: orangered;
transition: 0.3s linear;
}
<body>
<header class="header">
<div class="container">
All countries list
</div>
</header>
<main class="content">
<div class="container">
<form class="content__form">
<input class="content__search" />
</form>
<div class="scroll__navigator">
<span class="scroll__navigated"></span>
</div>
<section class="country">
<ul class="country__list">
<li class="country__item card">
<span class="country__name">Nepal</span
><span class="country__capital">Kathmandu</span>
</li>
</ul>
</section>
</div>
</main>
</body>
As an example, say that the height of your client is 100px and the height of your whole page is 500px.
When the scroll position is 0px, you're able to see the first 100px of your site, so from 0px to 100px.
At scroll position 100px, you can see the range 100px to 200px, because you've moved the page, and therefore the visible range, on by 100px.
At scroll position 400px, you can therefore see the range 400px to 500px â in other words, you've scrolled to the bottom.
This demonstrates that the scrollable height of the page (400px) is less than the actual height of the page (500px), namely by the height of the client.
To get the percentage scrolled, you need to use the scrollable height, so it is necessary to subtract the height of the client from the height of the page to get a correct value, or you'll never be able to scroll to the bottom. It's not possible to scroll by 500px on a site that is only 500px long!
I'm experiencing a JavaScript error on a pen on codepen.io.
TypeError: window.CP is undefined
I tried to look it up and understood that it's connected to an infinite loop protection, but I can't find a way to solve the problem.
Here is the link to the pen on CodePen (where it doesn't work) and to a JSFiddle (where it works).
Here is the code on the snippet (where it also works).
(The green block is supposed to change color when you scroll)
<script>
function scrollFunction() {
var content = document.getElementById('content').querySelectorAll('p')
var contentY = []
for (i = 0; i < content.length; i++) {
contentY[i] = content[i].offsetTop
}
var html = document.documentElement
var y = html.scrollTop
var windowY = window.innerHeight
var phone = document.getElementById('phone')
for (i = 0; i < content.length; i++) {
if (y > contentY[i] - windowY * 0.4) {
phone.classList.add('color' + (i + 1))
phone.classList.remove('color' + i)
} else {
phone.classList.remove('color' + (i + 1))
}
}
}
window.onscroll = function () {
scrollFunction()
}
</script>
body {
background: white;
color: #323232;
font-weight: 300;
height: 100vh;
margin: 0;
font-family: Helvetica neue, roboto;
}
nav {
position: fixed;
top: 0;
left: 0;
width: 100%;
background: white;
-webkit-box-shadow: 0px -6px 25px 20px rgba(0, 0, 0, 0.44);
-moz-box-shadow: 0px -6px 25px 20px rgba(0, 0, 0, 0.44);
box-shadow: 0px -6px 25px 20px rgba(0, 0, 0, 0.44);
}
nav ul {
list-style: none;
display: flex;
flex-wrap: wrap;
}
nav ul li {
padding: 0 1rem;
}
main {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
}
#content {
width: 50%;
}
/* The first paragraph has a margin-top = the size of the screen*/
#content p:first-child {
margin-top: 100vh;
}
#content p {
margin: 0;
margin-bottom: 100vh;
}
/* Same margin-top as the 1st paragraph + sticky at 40% from the top*/
#phone {
margin-top: 100vh;
width: 8rem;
height: 13rem;
max-height: 70vh;
position: sticky;
top: 40%;
background: lightgreen;
transition: background 0.2s;
}
#phone.color1 {
background: palevioletred;
}
#phone.color2 {
background: purple;
}
#phone.color3 {
background: royalblue;
}
#phone.color4 {
background: rgb(30, 150, 104);
}
<nav class="menu">
<ul>
<li>Menu</li>
<li>Bar</li>
<li>Scrolling</li>
<li>Effect</li>
</ul>
</nav>
<main>
<div id="content" class="content">
<p>
One advanced diverted domestic sex repeated bringing you old. Possible procured her trifling laughter thoughts property she met way.
</p>
<p>
Finished her are its honoured drawings nor. Pretty see mutual thrown all not edward ten. Particular an boisterous up he reasonably frequently.
</p>
<p>
May musical arrival beloved luckily adapted him. Shyness mention married son she his started now. Rose if as past near were. To graceful he elegance oh moderate attended entrance pleasur
</p>
<p>
Out believe has request not how comfort evident. Up delight cousins we feeling minutes.
</p>
</div>
<div id="phone">
</div>
</main
CodePen has a problem with the normal loops but working well with Array methods like: forEach, map, or reduce.
Chrome console output:
Uncaught TypeError: Cannot read property âshouldStopExecutionâ of undefined
In order to solve it, I had to change the normal loop with forEach instead. Example
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;
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>
I have this amateur function that changes the box-shadow on a group of elements based on the window.scrollTop position and element offset. When the user scrolls up or down from the photos, the box-shadow will move down or up respectively. So it gives the illusion of perspective. When these photo elements are in the center of the screen, the box-shadow is centered.
Right now it's not ideal and by that I mean it isn't very symmetrical and doesn't feel right. I know it because my "golden-ratio" (Not the actual Phy golden ratio, i just used this as a variable name) is just a random number. I know there is a way to use my variables to set this up properly I just can't come up with it. Looking for golden ratio (box-shadow vertical offset) to be from about -20 to 20, 0 when centered, seems to look okay.
So my question is can anyone optimize my algorithm so that the box-shadow changes as described above but more realistic?
This should only happen inside a certain vertical scroll window so
that this function isn't running while the photos are off the screen (already pretty much implemented)
The box-shadow change is subtle so there is not a HUGE shadow above or below it.
In my real code I have another function that changes the photos to be the same height as the width on window resize. Here I just have a fixed height of 160px.
Here is my code (modified to give a barebones example). Feel free to redo my function from scratch if that is cleaner.
var halfHeight;
var eleHalfHeight;
var scrollTop;
var photosOffset;
var profOffset
var distPhotos;
var goldenRatio;
$(window).scroll(function() {
halfHeight = $(window).height() / 2;
eleHalfHeight = $('.photo').height() / 2;
scrollTop = $(window).scrollTop();
photosOffset = $('.photos').offset().top - halfHeight + eleHalfHeight;
distPhotos = (photosOffset - scrollTop);
if (distPhotos < photosOffset && distPhotos > -photosOffset) {
goldenRatio = distPhotos / 25;
$('.photo-frame').css('box-shadow', '0px ' + (goldenRatio + 6) + 'px 4px 2px #c9c9c9');
};
});
* {
box-sizing: border-box;
}
body {
margin: 0;
}
.wrapper {
display: flex;
align-items: center;
min-height: 500vh;
background-color: #fff;
}
.photos {
display: flex;
width: 100%;
justify-content: space-around;
margin-bottom: 60px;
background-color: #fff;
}
.photos .photo-frame {
width: 25%;
height: 160px;
border: 20px solid whitesmoke;
box-shadow: 0px 0px 4px 2px gray;
background-color: #000;
}
.photos .photo-frame .photo {
width: 100%;
background-color: #000;
z-index: 1;
position: relative;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrapper">
<section class="photos">
<div class="photo-frame">
<div class="photo"></div>
</div>
<div class="photo-frame">
<div class="photo"></div>
</div>
<div class="photo-frame">
<div class="photo"></div>
</div>
</section>
</div>
Is it perfect�
Nope.
Do I like it better than yours?
A little bit.
Will it help you?
I hope so, since I think it's easier to tweak â at least this was my intention. You tell me.
let wH = $(window).height(),
sO = 210, // shadowOffset (px); 50 => from -25 to +25
tO = 4, // topOffset (px) 0 => vertical symmetry,
lO = 4, // leftOffset (px) 0 => horizontal symmetry
moveshadow = function() {
let diff = $(window).scrollTop() - $(this).offset().top,
tOH = $(this)[0].offsetHeight,
should = (-diff < wH) && (diff < tOH),
factor = should ?
-((diff / wH + 1) / (1 + tOH / wH) - .5) * sO + tO :
0;
if (should) {
$(this).css({
"box-shadow": lO +"px " +
factor +
"px 8px -2px rgba(0,0,0,.1), " + lO + "px " +
(factor + sO / 20) +
"px 17px 4px rgba(0,0,0,.07), " + lO + "px " +
(factor - sO / 20) +
"px 22px 8px rgba(0,0,0,.06), " + lO/2 + "px " +
factor / 20 +
"px 21px 1px rgba(0,0,0,.12)"
})
}
};
$(window)
.on('resize', function() {
wH = $(window).height()
})
.on('scroll resize', function() {
$('.photo-frame').each(moveshadow)
})
body {
background-color: #fff;
text-align: center;
}
.wrapper {
padding: 120vh 30px;
}
.photos {
display: flex;
align-items: center;
justify-content: space-between;
}
.photo {
width: 20vw;
height: 32.36vw;
background-color: #eee;
}
.photo-frame {
padding: 20px;
border: 1px solid #eee;
box-sizing: border-box;
}
#media(min-width: 1000px) {
.wrapper {
width: 900px;
display: inline-block;
}
.photo {
width: 200px;
height: 323.6px;
background-color: #eee;
}
}
#media(min-width: 641px) {
.photo-frame:first-child { transform: translateY(-10vw) }
.photo-frame:last-child { transform: translateY(10vw) }
}
#media(max-width: 640px) {
.photos {
flex-direction: column;
}
.photo {
width: 60vw;
height: 37.08vw;
background-color: #eee;
}
.photo-frame {
margin: 7.5vw 0;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrapper">
<section class="photos">
<div class="photo-frame">
<div class="photo"></div>
</div>
<div class="photo-frame">
<div class="photo"></div>
</div>
<div class="photo-frame">
<div class="photo"></div>
</div>
</section>
</div>
playground
:: }<(((*> ::)