Skip to content

Instantly share code, notes, and snippets.

@raiyanu
Created December 4, 2022 10:29
Show Gist options
  • Save raiyanu/2cfcab89d928f274efe89d61e7275f77 to your computer and use it in GitHub Desktop.
Save raiyanu/2cfcab89d928f274efe89d61e7275f77 to your computer and use it in GitHub Desktop.
Scroll Peonies
<!--
Messing around with this:
https://tympanus.net/codrops/2019/07/10/how-to-add-smooth-scrolling-with-inner-image-animations-to-a-web-page/
-->
<div class="loading">
<main>
<div data-scroll>
<div class="content">
<div class="item">
<div class="item__img-wrap">
<div class="item__img item__img--t1"></div>
</div>
<div class="item__caption">
<h2 class="item__caption-title">Central view</h2>
<p class="item__caption-copy">Great turbulent clouds emerged into consciousness citizens.</p>
</div>
</div>
<div class="item">
<div class="item__img-wrap">
<div class="item__img item__img--t2"></div>
</div>
<div class="item__caption">
<h2 class="item__caption-title">Lost in time</h2>
<p class="item__caption-copy">Brain is the seed of intelligence the sky calls to us a very small stage.</p>
</div>
</div>
<div class="item">
<div class="item__img-wrap">
<div class="item__img item__img--t3"></div>
</div>
<div class="item__caption">
<h2 class="item__caption-title">Ready to land</h2>
<p class="item__caption-copy">Cosmos encyclopaedia galactica a billion trillion culture cosmic ocean.</p>
</div>
</div>
<div class="item">
<div class="item__img-wrap">
<div class="item__img item__img--t1"></div>
</div>
<div class="item__caption">
<h2 class="item__caption-title">All equal</h2>
<p class="item__caption-copy">Network of wormholes dream of the mind's eye finite but unbounded concept.</p>
</div>
</div>
<div class="item">
<div class="item__img-wrap">
<div class="item__img item__img--t2"></div>
</div>
<div class="item__caption">
<h2 class="item__caption-title">Connections</h2>
<p class="item__caption-copy">Two ghostly white figures in coveralls and helmets are softly dancing vastness.</p>
</div>
</div>
<div class="item">
<div class="item__img-wrap">
<div class="item__img item__img--t3"></div>
</div>
<div class="item__caption">
<h2 class="item__caption-title">The state of divergence</h2>
<p class="item__caption-copy">Vastness is bearable only through love invent the universe vanquish.</p>
</div>
</div>
<div class="item">
<div class="item__img-wrap">
<div class="item__img item__img--t1"></div>
</div>
<div class="item__caption">
<h2 class="item__caption-title">Open perspective</h2>
<p class="item__caption-copy">The only home we've ever known concept of the number one.</p>
</div>
</div>
<div class="item">
<div class="item__img-wrap">
<div class="item__img item__img--t3"></div>
</div>
<div class="item__caption">
<h2 class="item__caption-title">Discovery of shapes</h2>
<p class="item__caption-copy">Decipherment explorations tesseract as a patch of light.</p>
</div>
</div>
<div class="item">
<div class="item__img-wrap">
<div class="item__img item__img--t2"></div>
</div>
<div class="item__caption">
<h2 class="item__caption-title">The Observer</h2>
<p class="item__caption-copy">Astonishment muse about dispassionate extraterrestrial observer.</p>
</div>
</div>
</div>
</div>
</main>
</div>
{
// helper functions
const MathUtils = {
// map number x from range [a, b] to [c, d]
map: (x, a, b, c, d) => (x - a) * (d - c) / (b - a) + c,
// linear interpolation
lerp: (a, b, n) => (1 - n) * a + n * b
};
// body element
const body = document.body;
// calculate the viewport size
let winsize;
const calcWinsize = () =>
(winsize = { width: window.innerWidth, height: window.innerHeight });
calcWinsize();
// and recalculate on resize
window.addEventListener("resize", calcWinsize);
// scroll position and update function
let docScroll;
const getPageYScroll = () =>
(docScroll = window.pageYOffset || document.documentElement.scrollTop);
window.addEventListener("scroll", getPageYScroll);
// Item
class Item {
constructor(el) {
// the .item element
this.DOM = { el: el };
// the inner image
this.DOM.image = this.DOM.el.querySelector(".item__img");
this.DOM.wrap = this.DOM.el.querySelector(".item__img-wrap");
this.renderedStyles = {
// here we define which property will change as we scroll the page and the items is inside the viewport
// in this case we will be translating the image on the y-axis
// we interpolate between the previous and current value to achieve a smooth effect
innerTranslationY: {
// interpolated value
previous: 0,
// current value
current: 0,
// amount to interpolate
ease: 0.1,
// the maximum value to translate the image is set in a CSS variable (--overflow)
maxValue: parseInt(
getComputedStyle(this.DOM.image).getPropertyValue("--overflow"),
10
),
// current value setter
// the value of the translation will be:
// when the item's top value (relative to the viewport) equals the window's height (items just came into the viewport) the translation = minimum value (- maximum value)
// when the item's top value (relative to the viewport) equals "-item's height" (item just exited the viewport) the translation = maximum value
setValue: () => {
const maxValue = this.renderedStyles.innerTranslationY.maxValue;
const minValue = -1 * maxValue;
return Math.max(
Math.min(
MathUtils.map(
this.props.top - docScroll,
winsize.height,
-1 * this.props.height,
minValue,
maxValue
),
maxValue
),
minValue
);
}
}
};
// set the initial values
this.update();
// use the IntersectionObserver API to check when the element is inside the viewport
// only then the element translation will be updated
this.observer = new IntersectionObserver(entries => {
entries.forEach(
entry => (this.isVisible = entry.intersectionRatio > 0)
);
});
this.observer.observe(this.DOM.el);
// init/bind events
this.initEvents();
}
update() {
// gets the item's height and top (relative to the document)
this.getSize();
// sets the initial value (no interpolation)
for (const key in this.renderedStyles) {
this.renderedStyles[key].current = this.renderedStyles[
key
].previous = this.renderedStyles[key].setValue();
}
// translate the image
this.layout();
}
getSize() {
const rect = this.DOM.el.getBoundingClientRect();
this.props = {
// item's height
height: rect.height,
// offset top relative to the document
top: docScroll + rect.top
};
}
initEvents() {
window.addEventListener("resize", () => this.resize());
}
resize() {
// on resize rest sizes and update the translation value
this.update();
}
render() {
// update the current and interpolated values
for (const key in this.renderedStyles) {
this.renderedStyles[key].current = this.renderedStyles[key].setValue();
this.renderedStyles[key].previous = MathUtils.lerp(
this.renderedStyles[key].previous,
this.renderedStyles[key].current,
this.renderedStyles[key].ease
);
}
// and translates the image
this.layout();
}
layout() {
// translates the image
this.DOM.image.style.transform = `translate3d(0,${
this.renderedStyles.innerTranslationY.previous
}px,0)`;
}
}
// SmoothScroll
class SmoothScroll {
constructor() {
// the <main> element
this.DOM = { main: document.querySelector("main") };
// the scrollable element
// we translate this element when scrolling (y-axis)
this.DOM.scrollable = this.DOM.main.querySelector("div[data-scroll]");
// the items on the page
this.items = [];
[...this.DOM.main.querySelectorAll(".content > .item")].forEach(item =>
this.items.push(new Item(item))
);
// here we define which property will change as we scroll the page
// in this case we will be translating on the y-axis
// we interpolate between the previous and current value to achieve the smooth scrolling effect
this.renderedStyles = {
translationY: {
// interpolated value
previous: 0,
// current value
current: 0,
// amount to interpolate
ease: 0.1,
// current value setter
// in this case the value of the translation will be the same like the document scroll
setValue: () => docScroll
}
};
// set the body's height
this.setSize();
// set the initial values
this.update();
// the <main> element's style needs to be modified
this.style();
// init/bind events
this.initEvents();
// start the render loop
requestAnimationFrame(() => this.render());
}
update() {
// sets the initial value (no interpolation) - translate the scroll value
for (const key in this.renderedStyles) {
this.renderedStyles[key].current = this.renderedStyles[
key
].previous = this.renderedStyles[key].setValue();
}
// translate the scrollable element
this.layout();
}
layout() {
// translates the scrollable element
this.DOM.scrollable.style.transform = `translate3d(0,${-1 *
this.renderedStyles.translationY.previous}px,0)`;
}
setSize() {
// set the heigh of the body in order to keep the scrollbar on the page
body.style.height = `${this.DOM.scrollable.scrollHeight}px`;
}
style() {
// the <main> needs to "stick" to the screen and not scroll
// for that we set it to position fixed and overflow hidden
this.DOM.main.style.position = "fixed";
this.DOM.main.style.width = this.DOM.main.style.height = "100%";
this.DOM.main.style.top = this.DOM.main.style.left = 0;
this.DOM.main.style.overflow = "hidden";
}
initEvents() {
// on resize reset the body's height
window.addEventListener("resize", () => this.setSize());
}
render() {
// update the current and interpolated values
for (const key in this.renderedStyles) {
this.renderedStyles[key].current = this.renderedStyles[key].setValue();
this.renderedStyles[key].previous = MathUtils.lerp(
this.renderedStyles[key].previous,
this.renderedStyles[key].current,
this.renderedStyles[key].ease
);
}
// and translate the scrollable element
this.layout();
// for every item
for (const item of this.items) {
// if the item is inside the viewport call it's render function
// this will update the item's inner image translation, based on the document scroll value and the item's position on the viewport
if (item.isVisible) {
item.render();
}
}
// loop..
requestAnimationFrame(() => this.render());
}
}
/***********************************/
/********** Preload stuff **********/
// Preload images
const preloadImages = () => {
return new Promise((resolve, reject) => {
imagesLoaded(
document.querySelectorAll(".item__img"),
{ background: true },
resolve
);
});
};
// And then..
preloadImages().then(() => {
// Remove the loader
document.body.classList.remove("loading");
// Get the scroll position
getPageYScroll();
// Initialize the Smooth Scrolling
new SmoothScroll();
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.imagesloaded/4.1.4/imagesloaded.pkgd.min.js"></script>
$color-text: #263238;
$color-bg: linear-gradient(to bottom, #804a5d, #ffebee);
$color-link: #f8bbd0;
$color-link-hover: #000;
$color-deco: rgba(#ffcdd2, 0.5);
*,
*::after,
*::before {
box-sizing: border-box;
}
:root {
font-size: 16px;
}
body {
margin: 0;
color: $color-text;
background: $color-bg;
font-family: Inconsolata, monospace;
}
@keyframes loaderAnim {
to {
opacity: 1;
transform: scale3d(0.5, 0.5, 1);
}
}
[data-scroll] {
will-change: transform;
}
.frame {
padding: 2.5rem 3rem;
position: absolute;
z-index: 0;
}
.frame__title {
font-size: 1rem;
margin: 0 0 2.5rem;
}
.frame__demos {
margin: 1rem 0;
}
.frame__demo--current,
.frame__demo--current:hover {
color: $color-text;
}
.content {
display: flex;
flex-direction: column;
position: relative;
align-items: center;
padding: 12rem 0;
counter-reset: figure;
}
.item {
margin: 10vh auto;
max-width: 100%;
position: relative;
will-change: transform;
&:nth-child(even) {
.item__caption {
text-align: right;
}
}
&::before {
counter-increment: figure;
content: counter(figure, decimal-leading-zero);
position: absolute;
font-family: Lora, serif;
mix-blend-mode: color-dodge;
font-weight: 800;
font-size: 15em;
color: $color-deco;
bottom: calc(100% - 0.5em);
z-index: 9;
&:nth-child(even)::before {
right: 0;
}
}
}
.item__img-wrap {
overflow: hidden;
width: 50vw;
margin: 0 auto;
padding-bottom: calc(100% / calc(1/1.5));
max-width: 100%;
will-change: transform;
&::before {
content: "";
width: 100%;
z-index: 12;
background-color: white;
mix-blend-mode: saturation;
height: 100%;
position: absolute;
display: block;
top: 0;
right: 0;
transition: transform 800ms ease-in;
opacity:1;
}
}
.item:first-child .item__img-wrap {
padding-bottom: calc(100% / calc(8/10));
.item__img {
background-image: url(https://images.unsplash.com/photo-1563371385-d1d5df411171?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2475&q=80);
}
}
.item:nth-child(2) .item__img-wrap {
width: 1000px;
padding-bottom: calc(100% / calc(120/76));
.item__img {
background-image: url(https://images.unsplash.com/photo-1535003450606-5cf22e849d87?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2550&q=80);
}
}
.item:nth-child(3) .item__img-wrap {
padding-bottom: calc(100% / calc(60/75));
.item__img {
background-image: url(https://images.unsplash.com/photo-1559763668-94423eb21bab?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2434&q=80);
}
}
.item:nth-child(4) .item__img-wrap {
width: 800px;
padding-bottom: calc(100% / calc(900/505));
.item__img {
background-image: url(https://images.unsplash.com/photo-1547808605-035ba9794319?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2550&q=80);
}
}
.item:nth-child(5) .item__img-wrap {
padding-bottom: calc(100% / calc(6/8));
.item__img {
background-image: url(https://images.unsplash.com/photo-1560583035-6d14e41fac5f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2434&q=80);
}
}
.item:nth-child(6) .item__img-wrap {
padding-bottom: calc(100% / calc(1500/844));
.item__img {
background-image: url(https://images.unsplash.com/photo-1509121003850-4f6c8ff25623?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2553&q=80);
}
}
.item:nth-child(7) .item__img-wrap {
width: 900px;
padding-bottom: calc(100% / calc(1000/749));
.item__img {
background-image: url(https://images.unsplash.com/photo-1560583035-79c3e11ae176?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2434&q=80);
}
}
.item:nth-child(8) .item__img-wrap {
width: 900px;
padding-bottom: calc(100% / calc(1000/562));
.item__img {
background-image: url(https://images.unsplash.com/photo-1494271823928-a80211877d80?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2550&q=80);
}
}
.item:nth-child(9) .item__img-wrap {
padding-bottom: calc(100% / calc(60/75));
.item__img {
background-image: url(https://images.unsplash.com/photo-1507437072862-10c124d3596b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2434&q=80);
}
}
.item__img {
--overflow: 40px;
height: calc(100% + (2 * var(--overflow)));
top: calc(-1 * var(--overflow));
width: 100%;
position: absolute;
background-image: var(--image);
background-size: cover;
background-position: 50% 0%;
will-change: transform;
}
.item__img--t1 {
--overflow: 60px;
}
.item__img--t2 {
--overflow: 80px;
}
.item__img--t3 {
--overflow: 120px;
}
.item__caption {
padding: 2rem 1rem;
}
.item__caption-title {
font-family: Inconsolata, monospace;
letter-spacing: 0.1em;
font-weight: 400;
font-size: 3rem;
text-transform: lowercase;
margin: 0;
}
.item__caption-copy {
margin: 0;
}
.item__caption-copy::before {
content: "__";
line-height: 1;
color: $color-link;
font-weight: 700;
font-size: 1em;
margin: 0 0 1em;
display: block;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment