There is a complex dynamic header. When scrolling, it is necessary that its part (middle floor) follows the user. I need to find out at what point this middle floor disappears from the user's field of view (screen height) and when it returns to the top. any ideas? Without jQuery pls
Here it is with IntersectionObserver:
window.onload = () => {
const options = {
root: null,
rootMargin: '0px',
threshold: 0
}
const observer = new IntersectionObserver((entries, observer) => {
console.log('Scrolled');
}, options);
let header = document.querySelector('header');
observer.observe(header);
}
* {
margin: 0;
padding: 0;
}
body {
height: 150vh;
}
header {
height: 150px;
background-color: orangered;
}
<header>
Here header
</header>
Related
I am looking into creating a website which will serve as a a digital leaflet for a musical theatre. The idea is to have an autoscrolling credits list as landingpage. I've looked at examples on codepen to see how this effect is been achieved. But I would also like the user to interact and scroll themselves if they want to. When they stop scrolling the credits will turn back to autoscroll. I didn't find any example who tackles this issue. Does someone of you know a script (JS, or plain css…) that can help me with this?
The most straightforward way is to set up a requestAnimationFrame() function and increment the value accordingly, then set the scroll position to it.
Then add the wheel event to detect when a user scrolls (don't use the 'scroll' event though, it already gets called when you change the scrollTop value of the body), also don't forget to cancel the requestAnimationFrame() function. The code would look something like this:
let body = document.body,
starter = document.querySelector("h1"),
scroll_counter = 0,
scrolled,
auto_scroll_kicked = false;
starter.addEventListener("click", start_scrolling);
function start_scrolling() {
auto_scroll_kicked = true;
body.offsetHeight > scroll_counter
? (scroll_counter += 1.12)
: (scroll_counter = body.offsetHeight);
document.documentElement.scrollTop = scroll_counter;
scroller = window.requestAnimationFrame(start_scrolling);
if (scroll_counter >= body.offsetHeight) {
window.cancelAnimationFrame(scroller);
}
}
window.addEventListener("wheel", (e) => {
if (auto_scroll_kicked) {
window.cancelAnimationFrame(scroller);
scroll_counter = 0;
}
});
Play with the codepen if you'd like:
https://codepen.io/SaltyMedStudent/pen/QWqVwaR?editors=0010
There are many options to use: easing functions and etc, but hope this will suffice for now.
In your auto scroll routine before changing position check if previous position is the same as current scrolling position, if it's not - the user scrolled it:
let el = document.documentElement,
footer = document.getElementById("status").querySelectorAll("td"),
scroll_position = 0,
scroll_speed = 0,
scroll_delta = 1.12,
scroller,
status = "stopped";
el.addEventListener("click", scroll);
info();
function scroll(e)
{
if (e.type == "click")
{
window.cancelAnimationFrame(scroller);
scroll_position = el.scrollTop; //make sure we start from current position
scroll_speed++; //increase speed with each click
info("auto scroll");
}
//if previous position is different, this means user scrolled
if (scroll_position != el.scrollTop)
{
scroll_speed = 0;
info("stopped by user");
return;
}
el.scrollTop += scroll_delta * scroll_speed; //scroll to new position
scroll_position = el.scrollTop; //get the current position
//loop only if we didn't reach the bottom
if (el.scrollHeight - el.scrollTop - el.clientHeight > 0)
{
scroller = window.requestAnimationFrame(scroll); //loop
}
else
{
el.scrollTop = el.scrollHeight; //make sure it's all the way to the bottom
scroll_speed = 0;
info("auto stopped");
}
}
function info(s)
{
if (typeof s === "string")
status = s;
footer[1].textContent = el.scrollTop;
footer[3].textContent = scroll_speed;
footer[5].textContent = status;
}
//generate html demo sections
for(let i = 2, section = document.createElement("section"); i < 6; i++)
{
section = section.cloneNode(false);
section.textContent = "Section " + i;
document.body.appendChild(section);
}
//register scroll listener for displaying info
window.addEventListener("scroll", info);
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body
{
font-family: "Roboto", Arial;
user-select: none;
}
section
{
min-height: 100vh;
font-size: 3em;
font-weight: 500;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
}
section:nth-child(even)
{
background: #0b0d19;
}
section:nth-child(odd)
{
background: #131524;
}
#status
{
position: fixed;
bottom: 0;
color: #fff;
margin: 0.5em;
}
#status td:first-of-type
{
text-align: end;
padding-right: 0.4em;
}
#status td:last-of-type
{
font-weight: bold;
}
<section>
Click to start Scrolling
</section>
<table id="status">
<tr><td>position:</td><td></td></tr>
<tr><td>speed:</td><td></td></tr>
<tr><td>status:</td><td></td></tr>
</table>
After toying about with timers and intervals I have come to a solution that works to my satisfaction.
See relevant jsFiddle or code below:
HTML:
<div id="foo">irrelevant content</div>
javascript( with jQuery):
var post_array = [ "abc", "123", "xyz" ];
var class_array = [ "red", "blue", "green" ];
var interval = 2000;
var i = 0;
var max = post_array.length;
var id ="#foo";
$(id).html(post_array[0]);
$(id).removeClass().addClass(class_array[0]);
setInterval( function(){
++i;
$(id).fadeOut("slow", function() {
$(id).html(post_array[i%max]).fadeIn("slow");
$(id).removeClass().addClass(class_array[i%max]);
});
}, interval);
Now I wonder what the best way to add two side arrows that allow me to go back and fort would be.
should I have written the relevant code in a named function so I can call it and pass an index parameter when the button is pressed? ( how do i act on the same index variable in that case? )
What's the best practice for button overlays?
Help!
Thanks in advance
Carousels should be modular, reusable and extendable. Don't copy paste JS code when in need to add another Carousel into your DOM.
In order to create PREV / NEXT buttons you'll also need a method to stop your interval: stop
When you hover over your Carousel, you'll need to pause the autoplay to prevent a really bad User Experience (UX)
Don't animate using jQuery. Animate by simply assigning an is-active class to the current index slide, and use CSS to do whatever you want with that class.
Use a variable index (start with 0) to keep track of the current slide index
You Might Not Need jQuery
Aim to create a class instance using the sugary class or the proper prototype syntax - that can be used like:
const myCarousel = new Carousel({
target: "#carousel-one",
slides: [
{
title: "This is slide one",
image: "images/one.jpg"
},
{
title: "This is slide two! Yey.",
image: "images/two.jpg"
}
]
});
So basically, you'll need a constructor that has those methods:
Method
Description
anim()
Fix index if exceeds slides or is negative and animate to new index
prev()
Decrement index and trigger anim()
next()
Increment index and trigger anim()
stop()
Clear loop interval (On mouseenter)
play()
Start loop (Triggers next() every pause milliseconds)
Simple JavaScript carousel example
class Carousel {
constructor(options) {
Object.assign(this, {
slides: [],
index: 0,
pause: 4000, // Pause between slides
EL: document.querySelector(options.target || "#Carousel"),
autoplay: true,
}, options);
this.total = this.slides.length;
this.EL_area = this.EL.querySelector(".Carousel-area");
this.EL_prev = this.EL.querySelector(".Carousel-prev");
this.EL_next = this.EL.querySelector(".Carousel-next");
const NewEL = (tag, prop) => Object.assign(document.createElement(tag), prop);
// Preload images
this.ELs_items = this.slides.reduce((DF, item) => {
const EL_slide = NewEL("div", {
className: "Carousel-slide"
});
const EL_image = NewEL("img", {
className: "Carousel-image",
src: item.image,
alt: item.title
});
const EL_content = NewEL("div", {
className: "Carousel-title",
textContent: item.title
});
EL_slide.append(EL_image, EL_content);
DF.push(EL_slide);
return DF;
}, []);
this.EL_area.append(...this.ELs_items);
// Events
this.EL_prev.addEventListener("click", () => this.prev());
this.EL_next.addEventListener("click", () => this.next());
this.EL.addEventListener("mouseenter", () => this.stop());
this.EL.addEventListener("mouseleave", () => this.play());
// Init
this.anim();
this.play();
}
// Methods:
anim() {
this.index = this.index < 0 ? this.total - 1 : this.index >= this.total ? 0 : this.index;
this.ELs_items.forEach((EL, i) => EL.classList.toggle("is-active", i === this.index));
}
prev() {
this.index -= 1;
this.anim();
}
next() {
this.index += 1;
this.anim();
}
stop() {
clearInterval(this.itv);
}
play() {
if (this.autoplay) this.itv = setInterval(() => this.next(), this.pause);
}
}
// Use like:
new Carousel({
target: "#carousel-one",
slides: [{
title: "We're part of nature",
image: "https://picsum.photos/id/10/400/300"
},
{
title: "Remember to read and learn",
image: "https://picsum.photos/id/24/400/300"
},
{
title: "Up for a coffee?",
image: "https://picsum.photos/id/30/400/300"
},
]
});
/* CAROUSEL */
.Carousel {
position: relative;
height: 300px;
max-height: 100vh;
}
.Carousel-slide {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
transition: opacity 0.5s; /* DESIRED SLIDE TRANSITIONS */
opacity: 0; /* INACTIVE SLIDE*/
}
.Carousel-slide.is-active { /* ACTIVE SLIDE! */
opacity: 1;
z-index: 1;
}
.Carousel-prev,
.Carousel-next {
position: absolute;
z-index: 2;
top: 50%;
transform: translateY(-50%);
user-select: none; /* Prevent highlight */
}
.Carousel-prev {
left: 1em;
}
.Carousel-next{
right: 1em;
}
.Carousel-image {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}
.Carousel-title {
position: absolute;
width: 100%;
height: 100%;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
font-size: 3em;
}
<div class="Carousel" id="carousel-one">
<div class="Carousel-area"></div>
<button class="Carousel-prev" type="button" aria-label="Previous slide">←</button>
<button class="Carousel-next" type="button" aria-label="Next slide">→</button>
<div class="Carousel-desc"></div>
</div>
With the above code you can have an unlimited number of carousels on a single page given every one has a different target ID.
PS: Alternatively, if your code keeps track of the direction for the prev / next, the logic to increment/decrement/loopback the current index can be also written as (pseudocode ahead!):
C = (is_next ? ++C : --C) < 0 ? T-1 : C%T;
where C is the current index, T is the total number of slides, and is_next is a boolean that is true when the direction is Next.
Is it possible to scroll to .project and make the background red without to scroll slow and near the class .project?
Basically I want the user to be able to scroll and get the red color displayed even if he or she scrolls quickly, but when the user is above or under projectPosition.top, the background should be the standard color (black).
var project = document.getElementsByClassName('project')[0];
var projectPosition = project.getBoundingClientRect();
document.addEventListener('scroll', () => {
var scrollY = window.scrollY;
if (scrollY == projectPosition.top) {
project.style.background = "red";
project.style.height = "100vh";
} else {
project.style.background = "black";
project.style.height = "200px";
}
});
.top {
height: 700px;
}
.project {
background: black;
height: 200px;
width: 100%;
}
<div class="top"></div>
<div class="project"></div>
<div class="top"></div>
Thanks in advance.
Instead of listen for the scroll event you could use the Intersection Observer API which can monitor elements that come in and out of view. Every time an observed element either enters or leaves the view, a callback function is fired in which you can check if an element has entered or left the view, and handle accordingly.
It's also highly performant and saves you from some top and height calculations.
Check it out in the example below.
If you have any questions about it, please let me know.
Threshold
To trigger the callback whenever an element is fully into view, not partially, set the threshold option value to [1]. The default is [0], meaning that it is triggered whenever the element is in view by a minimum of 1px. [1] states that 100% of the element has to be in view to trigger. The value can range from 0 to 1 and can contain multiple trigger points. For example
const options = {
threshold: [0, 0.5, 1]
};
Which means, the start, halfway, and fully in to view.
const project = document.querySelector('.project');
const observerCallback = entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('red');
} else {
entry.target.classList.remove('red');
}
});
};
const options = {
threshold: [1]
}
const observer = new IntersectionObserver(observerCallback, options);
observer.observe(project);
.top,
.bottom{
height: 700px;
width: 100%;
}
.project {
background: black;
height: 200px;
width: 100%;
}
.project.red {
background: red;
}
<div class="top"></div>
<div class="project"></div>
<div class="bottom"></div>
To make it 'fast' you better will have to use the >= operator than ==:
var project = document.getElementsByClassName('project')[0];
var projectPosition = project.getBoundingClientRect();
document.addEventListener('scroll', () => {
var scrollY = window.scrollY;
if (scrollY >= projectPosition.top && scrollY <= projectPosition.top + projectPosition.height) {
project.style.background = "red";
project.style.height = "100vh";
} else {
project.style.background = "black";
project.style.height = "200px";
}
});
.top {
height: 700px;
}
.project {
background: black;
height: 200px;
width: 100%;
}
<div class="top"></div>
<div class="project"></div>
<div class="top"></div>
I'm looking for some help, I'm trying to recreate the homepage on this site.
https://madebyarticle.com/
I don't want to use Jquery, either Plain JS or Vue.
I've got it to do most what I want it to but have a few issues.
Codepen Example
HTML
<main id="parent" class="Loop js-loop">
<section>
<h1 class="step1">One</h1>
</section>
<section>
<h1 class="step2">For</h1>
</section>
<section>
<h1 class="step3">All</h1>
</section>
<section>
<h1>And</h1>
</section>
<section>
<h1>All</h1>
</section>
<section>
<h1>For</h1>
</section>
<!--
These blocks are the same as the first blocks to get that looping illusion going.
You need to add clones to fill out a full viewport height.
-->
<section class="green is-clone">
<h1>One</h1>
</section>
<section class="red is-clone">
<h1>For</h1>
</section>
</main>
CSS
html,
body {
height: 100%;
overflow: hidden;
}
.Loop {
position: relative;
height: 100%;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
section {
position: relative;
text-align: center;
/* min-height: 300px;
max-height: 700px; */
height: 100vh;
}
::scrollbar {
display: none;
}
body {
font-family: "Avenir Next", Helvetica, sans-serif;
font-weight: normal;
font-size: 100%;
}
h1 {
margin: 0;
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
font-size: 80px;
letter-spacing: 5px;
/* color: #fff; */
text-transform: uppercase;
}
.mystyle {
background: red;
}
.mystyle1 {
background-image: url(https://images.unsplash.com/photo-1528834379234-2de7f8328fd8?ixlib=rb-1.2.1&auto=format&fit=crop&w=1920&q=10);
}
.mystyle2 {
background-image: url(https://images.unsplash.com/photo-1501854140801-50d01698950b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2600&q=80);
}
JS
var doc = window.document,
context = doc.querySelector(".js-loop"),
clones = context.querySelectorAll(".is-clone"),
disableScroll = false,
scrollHeight = 0,
scrollPos = 0,
clonesHeight = 0,
i = 0;
function getScrollPos() {
return (context.pageYOffset || context.scrollTop) - (context.clientTop || 0);
}
function setScrollPos(pos) {
context.scrollTop = pos;
}
function getClonesHeight() {
clonesHeight = 0;
for (i = 0; i < clones.length; i += 1) {
clonesHeight = clonesHeight + clones[i].offsetHeight;
}
return clonesHeight;
}
function reCalc() {
scrollPos = getScrollPos();
scrollHeight = context.scrollHeight;
clonesHeight = getClonesHeight();
if (scrollPos <= 0) {
setScrollPos(1); // Scroll 1 pixel to allow upwards scrolling
}
}
function scrollUpdate() {
if (!disableScroll) {
scrollPos = getScrollPos();
if (clonesHeight + scrollPos >= scrollHeight) {
// Scroll to the top when you’ve reached the bottom
setScrollPos(1); // Scroll down 1 pixel to allow upwards scrolling
disableScroll = true;
} else if (scrollPos <= 0) {
// Scroll to the bottom when you reach the top
setScrollPos(scrollHeight - clonesHeight);
disableScroll = true;
}
}
if (disableScroll) {
// Disable scroll-jumping for a short time to avoid flickering
window.setTimeout(function() {
disableScroll = false;
}, 40);
}
}
function init() {
reCalc();
context.addEventListener(
"scroll",
function() {
window.requestAnimationFrame(scrollUpdate);
},
false
);
window.addEventListener(
"resize",
function() {
window.requestAnimationFrame(reCalc);
},
false
);
}
if (document.readyState !== "loading") {
init();
} else {
doc.addEventListener("DOMContentLoaded", init, false);
}
// Just for this demo: Center the middle block on page load
window.onload = function() {
setScrollPos(
Math.round(
clones[0].getBoundingClientRect().top +
getScrollPos() -
(context.offsetHeight - clones[0].offsetHeight) / 2
)
);
};
const images = document.querySelectorAll('h1');
observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
entry.target.classList.add('active');
} else {
entry.target.classList.remove('active');
}
});
});
images.forEach(image => {
observer.observe(image);
});
const header = document.getElementById("parent");
const sectionOne = document.querySelector("h1.step1");
const sectionTwo = document.querySelector("h1.step2");
const sectionThree = document.querySelector("h1.step3");
const sectionOneObserver = new IntersectionObserver(function(
entries,
sectionOneObserver
) {
entries.forEach(entry => {
if (!entry.isIntersecting) {
header.classList.add("mystyle");
} else {
header.classList.remove("mystyle");
}
});
});
sectionOneObserver.observe(sectionOne);
const sectionTwoObserver = new IntersectionObserver(function(
entries,
sectionTwoObserver
) {
entries.forEach(entry => {
if (!entry.isIntersecting) {
header.classList.add("mystyle1");
} else {
header.classList.remove("mystyle1");
}
});
});
sectionTwoObserver.observe(sectionTwo);
const sectionThreeObserver = new IntersectionObserver(function(
entries,
sectionThreeObserver
) {
entries.forEach(entry => {
if (!entry.isIntersecting) {
header.classList.add("mystyle2");
} else {
header.classList.remove("mystyle2");
}
});
});
sectionThreeObserver.observe(sectionThree);
// if (document.querySelectorAll('h1.step1.active')){
// document.getElementById("parent").classList.toggle("mystyle");
// } else {
// document.getElementById("parent").classList.remove("mystyle");
// }
// if (document.classList.contains("h1.step1.active")) {
// document.getElementById("parent").classList.add("mystyle");
// } else {
// document.getElementById("parent").classList.remove("mystyle");
// }
I'm having to duplicate the IntersectionObserver for every image I need, is there a cleaner way todo this?
At some points, there are 2 active states so it doesn't display any image only a background colour.
What would be the best way to fade the image on change like the example website?
Are there any examples or scripts that can do what I'm trying todo?
Thanks
Have worked out a solution, see the bottom!
I'm experimenting with a responsive carousel (fluid). I have elements stacked on top of each other so that the width can be fluid depending on the width of the parent. The issue is I need the parent to have overflow hidden which is not possible with children that are absolute positioned.
Tip on cleaning up the JS are appreciated too!
Does anyone have any ideas how to improve this or alternatives? Heres the fiddle: http://jsfiddle.net/j35fy/5/
.carousel-wrap {
position: relative;
}
.carousel-item {
position: absolute;
top: 0;
}
$.fn.mwCarousel = function(options) {
//Default settings.
var settings = $.extend({
changeWait: 3000,
changeSpeed: 800,
reveal: false,
slide: true,
autoRotate: true
}, options );
var CHANGE_WAIT = settings.changeWait;
var CHANGE_SPEED = settings.changeSpeed;
var REVEAL = settings.reveal;
var SLIDE = settings.slide;
var AUTO_ROTATE = settings.autoRotate;
var $carouselWrap = $(this);
var SLIDE_COUNT = $carouselWrap.find('.carousel-item').length;
var rotateTimeout;
if (AUTO_ROTATE) {
rotateTimeout = setTimeout(function(){
rotateCarousel(SLIDE_COUNT-1);
}, CHANGE_WAIT);
}
function rotateCarousel(slide) {
if (slide === 0) {
slide = SLIDE_COUNT-1;
rotateTimeout = setTimeout(function(){
$('.carousel-item').css('margin', 0);
$('.carousel-item').show();
}, CHANGE_WAIT);
if (REVEAL) {
$($carouselWrap.find('.carousel-item')[slide]).slideToggle(CHANGE_SPEED);
} else if (SLIDE) {
var carouselItem = $($carouselWrap.find('.carousel-item')[slide]);
carouselItem.show();
var itemWidth = carouselItem.width();
carouselItem.animate({margin: 0}, CHANGE_SPEED);
} else {
$($carouselWrap.find('.carousel-item')[slide]).fadeIn(CHANGE_SPEED);
}
slide = slide+1;
} else {
if (REVEAL) {
$($carouselWrap.find('.carousel-item')[slide]).slideToggle(CHANGE_SPEED);
} else if (SLIDE) {
var carouselItem = $($carouselWrap.find('.carousel-item')[slide]);
var itemWidth = carouselItem.width();
carouselItem.animate({marginLeft: -itemWidth, marginRight: itemWidth}, CHANGE_SPEED);
} else {
$($carouselWrap.find('.carousel-item')[slide]).fadeOut(CHANGE_SPEED);
}
}
rotateTimeout = setTimeout(function(){
rotateCarousel(slide-1);
}, CHANGE_WAIT);
}
}
$('.carousel-wrap').mwCarousel();
Solution
The first slide actually never moves (last one visible) so that one is set to position: static and all works nicely.
I think by just changing your CSS you're actually there:
.carousel-wrap {
position: relative;
overflow:hidden;
height:80%;
width:90%;
}
Demo: http://jsfiddle.net/robschmuecker/j35fy/2/
Discovered the solution is in fact simple, as the first slide in the DOM (the last you see) never actually moves itself I can set that one slide to be position: static and thus the carousel wrap will set it's height accordingly.
http://jsfiddle.net/j35fy/7/
.container {
background: aliceblue;
padding: 3em;
}
.carousel-wrap {
position: relative;
overflow:hidden;
}
.carousel-item:first-child {
position:static;
}
.carousel-item {
position: absolute;
top: 0;
width: 100%;
}
img {
width: 100%;
}