Edit on StackBlitz
"use client";
import { useEffect, useRef } from "react";
import { GestureWatcher } from "lisn.js";
import Image from "next/image";
import styles from "./demo.module.css";
export default function Page() {
const demoRef = useRef(null);
useEffect(() => {
const watcher = GestureWatcher.reuse();
const main = demoRef.current;
if (main) {
watcher.trackGesture(main, null, {
minTotalDeltaY: 0,
maxTotalDeltaY: 960,
minTotalDeltaZ: 1,
maxTotalDeltaZ: 2.4,
});
}
return () => {
// cleanup
if (main) {
watcher.noTrackGesture(main);
}
};
}, []);
return (
<>
<div ref={demoRef} className={[styles.demo, "light-theme"].join(" ")}>
<h1>Scroll or zoom</h1>
<div className={styles.plane} data-plane="5">
<Image
className={styles.background}
src="/images/landscape-5.png"
alt=""
/>
</div>
<div className={styles.plane} data-plane="4">
<Image
className={styles.background}
src="/images/landscape-4.png"
alt=""
/>
</div>
<div className={styles.plane} data-plane="3">
<div className={styles.spacer}></div>
<Image
className={styles.background}
src="/images/landscape-3.png"
alt=""
/>
</div>
<div className={styles.plane} data-plane="2">
<div className={styles.spacer}></div>
<Image
className={styles.background}
src="/images/landscape-2.png"
alt=""
/>
</div>
<div className={styles.plane} data-plane="1">
<div className={styles.spacer}></div>
<Image
className={styles.background}
src="/images/landscape-1.png"
alt=""
/>
</div>
</div>
</>
);
}
.demo {
--max-offset: 240px;
background: var(--bg-color);
position: fixed;
top: 0;
left: 0;
width: 100vw;
width: 100dvw;
height: 100vh;
height: 100dvh;
overflow: hidden;
}
.plane[data-plane] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.spacer,
.background {
transition-duration: 0.25s;
transition-timing-function: linear;
}
.background {
pointer-events: none;
width: 100%;
height: calc(100% + var(--max-offset));
object-fit: cover;
overflow-clip-margin: unset;
transition-property: transform;
transform-origin: 50% 65%;
}
.spacer {
transition-property: height;
min-height: 0px;
}
/* Per-plane transforms */
.plane[data-plane="4"] .background {
margin-top: 0;
transform: scale(calc(0.05 * var(--lisn-js--zoom-delta-z, 1) + 0.95));
}
.plane[data-plane="3"] .background {
margin-top: calc(-1 * var(--max-offset) / 3);
transform: scale(calc(0.2 * var(--lisn-js--zoom-delta-z, 1) + 0.8));
}
.plane[data-plane="3"] .spacer {
height: calc(
var(--max-offset) / 3 - 1px * var(--lisn-js--scroll-delta-y, 0) / 12
);
max-height: calc(var(--max-offset) / 3);
}
.plane[data-plane="2"] .background {
margin-top: calc(-1 * var(--max-offset) / 2);
transform: scale(calc(0.4 * var(--lisn-js--zoom-delta-z, 1) + 0.6));
}
.plane[data-plane="2"] .spacer {
height: calc(
var(--max-offset) / 2 - 1px * var(--lisn-js--scroll-delta-y, 0) / 8
);
max-height: calc(var(--max-offset) / 2);
}
.plane[data-plane="1"] .background {
margin-top: calc(-1 * var(--max-offset));
transform: scale(calc(0.6 * var(--lisn-js--zoom-delta-z, 1) + 0.4));
}
.plane[data-plane="1"] .spacer {
height: calc(var(--max-offset) - 1px * var(--lisn-js--scroll-delta-y, 0) / 4);
max-height: var(--max-offset);
}
/* Misc styles */
.demo h1 {
text-align: center;
color: #03505c;
z-index: 10;
position: relative;
font-weight: 400;
margin-top: 25vh;
margin-left: 4vw;
mix-blend-mode: multiply;
}
Edit on CodePen
document.addEventListener("DOMContentLoaded", () => {
const main = document.getElementById("demo");
LISN.watchers.GestureWatcher.reuse().trackGesture(main, null, {
minTotalDeltaY: 0,
maxTotalDeltaY: 960,
minTotalDeltaZ: 1,
maxTotalDeltaZ: 2.4,
});
});
<div id="demo">
<h1>Scroll or zoom</h1>
<div class="landscape-5 plane">
<!-- static -->
<img class="background" src="images/landscape-5.png" alt="" />
</div>
<div class="landscape-4 plane">
<!-- static offset -->
<img class="background" src="images/landscape-4.png" alt="" />
</div>
<div class="landscape-3 plane">
<div class="spacer"></div>
<img class="background" src="images/landscape-3.png" alt="" />
</div>
<div class="landscape-2 plane">
<div class="spacer"></div>
<img class="background" src="images/landscape-2.png" alt="" />
</div>
<div class="landscape-1 plane">
<div class="spacer"></div>
<img class="background" src="images/landscape-1.png" alt="" />
</div>
</div>
#demo {
--max-offset: 240px;
width: 100%;
height: 100%;
overflow: hidden;
}
#demo .plane {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#demo .plane .spacer,
#demo .plane .background {
transition-duration: 0.25s;
transition-timing-function: linear;
}
#demo .plane .background {
pointer-events: none;
width: 100%;
height: calc(100% + var(--max-offset));
object-fit: cover;
overflow-clip-margin: unset;
transition-property: transform;
transform-origin: 50% 65%;
}
#demo .plane .spacer {
transition-property: height;
min-height: 0px;
}
/* Per-plane transforms */
#demo .landscape-4 .background {
margin-top: 0;
transform: scale(calc(0.05 * var(--lisn-js--zoom-delta-z, 1) + 0.95));
}
#demo .landscape-3 .background {
margin-top: calc(-1 * var(--max-offset) / 3);
transform: scale(calc(0.2 * var(--lisn-js--zoom-delta-z, 1) + 0.8));
}
#demo .landscape-3 .spacer {
height: calc(
var(--max-offset) / 3 - 1px * var(--lisn-js--scroll-delta-y, 0) / 12
);
max-height: calc(var(--max-offset) / 3);
}
#demo .landscape-2 .background {
margin-top: calc(-1 * var(--max-offset) / 2);
transform: scale(calc(0.4 * var(--lisn-js--zoom-delta-z, 1) + 0.6));
}
#demo .landscape-2 .spacer {
height: calc(
var(--max-offset) / 2 - 1px * var(--lisn-js--scroll-delta-y, 0) / 8
);
max-height: calc(var(--max-offset) / 2);
}
#demo .landscape-1 .background {
margin-top: calc(-1 * var(--max-offset));
transform: scale(calc(0.6 * var(--lisn-js--zoom-delta-z, 1) + 0.4));
}
#demo .landscape-1 .spacer {
height: calc(
var(--max-offset) - 1px * var(--lisn-js--scroll-delta-y, 0) / 4
);
max-height: var(--max-offset);
}
/* Misc styles */
#demo h1 {
text-align: center;
color: #03505c;
z-index: 10;
position: relative;
font-weight: 400;
margin-top: 25vh;
margin-left: 4vw;
mix-blend-mode: multiply;
}
#license {
margin: 10px 20px;
color: #6bb6c4;
font-size: 14px;
}
Edit on CodePen
<div
id="demo"
data-lisn-track-gesture="min-delta-y=0
| max-delta-y=960
| min-delta-z=1
| max-delta-z=2.4"
>
<h1>Scroll or zoom</h1>
<div class="landscape-5 plane">
<!-- static -->
<img class="background" src="images/landscape-5.png" alt="" />
</div>
<div class="landscape-4 plane">
<!-- static offset -->
<img class="background" src="images/landscape-4.png" alt="" />
</div>
<div class="landscape-3 plane">
<div class="spacer"></div>
<img class="background" src="images/landscape-3.png" alt="" />
</div>
<div class="landscape-2 plane">
<div class="spacer"></div>
<img class="background" src="images/landscape-2.png" alt="" />
</div>
<div class="landscape-1 plane">
<div class="spacer"></div>
<img class="background" src="images/landscape-1.png" alt="" />
</div>
</div>
#demo {
--max-offset: 240px;
width: 100%;
height: 100vh;
overflow: hidden;
}
#demo .plane {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#demo .plane .spacer,
#demo .plane .background {
transition-duration: 0.25s;
transition-timing-function: linear;
}
#demo .plane .background {
pointer-events: none;
width: 100%;
height: calc(100% + var(--max-offset));
object-fit: cover;
overflow-clip-margin: unset;
transition-property: transform;
transform-origin: 50% 65%;
}
#demo .plane .spacer {
transition-property: height;
min-height: 0px;
}
/* Per-plane transforms */
#demo .landscape-4 .background {
margin-top: 0;
transform: scale(calc(0.05 * var(--lisn-js--zoom-delta-z, 1) + 0.95));
}
#demo .landscape-3 .background {
margin-top: calc(-1 * var(--max-offset) / 3);
transform: scale(calc(0.2 * var(--lisn-js--zoom-delta-z, 1) + 0.8));
}
#demo .landscape-3 .spacer {
height: calc(
var(--max-offset) / 3 - 1px * var(--lisn-js--scroll-delta-y, 0) / 12
);
max-height: calc(var(--max-offset) / 3);
}
#demo .landscape-2 .background {
margin-top: calc(-1 * var(--max-offset) / 2);
transform: scale(calc(0.4 * var(--lisn-js--zoom-delta-z, 1) + 0.6));
}
#demo .landscape-2 .spacer {
height: calc(
var(--max-offset) / 2 - 1px * var(--lisn-js--scroll-delta-y, 0) / 8
);
max-height: calc(var(--max-offset) / 2);
}
#demo .landscape-1 .background {
margin-top: calc(-1 * var(--max-offset));
transform: scale(calc(0.6 * var(--lisn-js--zoom-delta-z, 1) + 0.4));
}
#demo .landscape-1 .spacer {
height: calc(
var(--max-offset) - 1px * var(--lisn-js--scroll-delta-y, 0) / 4
);
max-height: var(--max-offset);
}
/* Misc styles */
#demo h1 {
text-align: center;
color: #03505c;
z-index: 10;
position: relative;
font-weight: 400;
margin-top: 25vh;
margin-left: 4vw;
mix-blend-mode: multiply;
}
#license {
margin: 10px 20px;
color: #6bb6c4;
font-size: 14px;
}