A Pen by Katherine Sexton on CodePen.
Created
December 4, 2022 10:29
-
-
Save raiyanu/2cfcab89d928f274efe89d61e7275f77 to your computer and use it in GitHub Desktop.
Scroll Peonies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
// 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(); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.imagesloaded/4.1.4/imagesloaded.pkgd.min.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$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