Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions flying-gallery/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Flying Gallery — @wethegit/react-gallery</title>
<style>
body {
margin: 0;
overflow-x: hidden;
}

img {
inline-size: 100%;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="../src/flying-gallery/index.jsx"></script>
</body>
</html>
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

146 changes: 146 additions & 0 deletions src/flying-gallery/FlyingGallery.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import {
Gallery,
GalleryMain,
GalleryItem,
GalleryNav,
GalleryPagination,
GalleryPaginationItem,
} from "../lib"
import styles from "./flying-gallery.module.css"

const ITEMS = [
{
id: 1,
image:
"https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?auto=format&fit=crop&w=1200&h=700&q=80",
thumb:
"https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?auto=format&fit=crop&w=200&h=120&q=60",
alt: "Sun rays through a forest",
},
{
id: 2,
image:
"https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=1200&h=700&q=80",
thumb:
"https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=200&h=120&q=60",
alt: "Snow-capped mountain peak at night",
},
{
id: 3,
image:
"https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1200&h=700&q=80",
thumb:
"https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=200&h=120&q=60",
alt: "Mountain reflected in a still lake",
},
{
id: 4,
image:
"https://images.unsplash.com/photo-1501854140801-50d01698950b?auto=format&fit=crop&w=1200&h=700&q=80",
thumb:
"https://images.unsplash.com/photo-1501854140801-50d01698950b?auto=format&fit=crop&w=200&h=120&q=60",
alt: "Aerial view of green hills",
},
{
id: 5,
image:
"https://images.unsplash.com/photo-1465146344425-f00d5f5c8f07?auto=format&fit=crop&w=1200&h=700&q=80",
thumb:
"https://images.unsplash.com/photo-1465146344425-f00d5f5c8f07?auto=format&fit=crop&w=200&h=120&q=60",
alt: "Orange wildflower field",
},
{
id: 6,
image:
"https://images.unsplash.com/photo-1547234935-80c7145ec969?auto=format&fit=crop&w=1200&h=700&q=80",
thumb:
"https://images.unsplash.com/photo-1547234935-80c7145ec969?auto=format&fit=crop&w=200&h=120&q=60",
alt: "Tropical beach with clear water",
},
]

export function FlyingGallery() {
return (
<div className={styles.root}>
<Gallery
items={ITEMS}
loop
draggable={false}
visibleRange={2}
style={{
"--item-width": "min(640px, 72vw)",
"--duration": "0.55s",
}}
>
<div className={styles.slideArea}>
<GalleryMain
renderGalleryItem={({ item, index, activeIndex, active }) => {
const isPrev = activeIndex > index

return (
<GalleryItem
key={item.id}
index={index}
active={active}
className={styles.item}
data-side={active ? "active" : isPrev ? "prev" : "next"}
>
<div className={styles.slide}>
<img src={item.image} alt={item.alt} />
</div>
</GalleryItem>
)
}}
/>
<GalleryNav direction={0} className={styles.navPrev}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<polyline points="15 18 9 12 15 6" />
</svg>
<span className={styles.srOnly}>Previous</span>
</GalleryNav>
<GalleryNav direction={1} className={styles.navNext}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<polyline points="9 18 15 12 9 6" />
</svg>
<span className={styles.srOnly}>Next</span>
</GalleryNav>
</div>

<GalleryPagination
className={styles.thumbs}
renderPaginationItem={({ item, index, active }) => (
<GalleryPaginationItem
key={item.id}
index={index}
active={active}
className={styles.thumbItem}
buttonClassName={styles.thumbButton}
>
<img src={item.thumb} alt={item.alt} />
</GalleryPaginationItem>
)}
/>
</Gallery>
</div>
)
}
206 changes: 206 additions & 0 deletions src/flying-gallery/flying-gallery.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
.root {
--perspective: 100px;
--xoffset: 600px;
--yoffset: 200px;
--rotation: 16deg;
--blur: 2px;

background: #0a0a0f;
color: #fff;
display: flex;
flex-direction: column;
gap: 1.5rem;
min-height: 100vh;
padding: 3rem 0 2rem;
overflow: hidden;
}

.slideArea {
position: relative;
}

/*
* The gallery is configured with --item-width: 4px; --gap: 0, so all <li>
* elements collapse to near-zero horizontal positions.
* All visual positioning is handled here on .slide using CSS vars inherited
* from the library-managed <li>:
*
* --center-offset absolute distance from active (0, 1, 2 …)
* --duration transition speed
*
* className={styles.item} is appended to the library's class list on each <li>.
* Since this CSS module is loaded after the library's, same-specificity rules here
* win on source order..
*/
.item {
transform: translateX(0) !important;
}

[data-side="prev"].item {
z-index: calc(20 - var(--center-offset, 0));
}

.slide {
aspect-ratio: 16 / 9;
border-radius: 10px;
overflow: hidden;
position: relative;
transition:
transform var(--duration, 0.5s),
filter var(--duration, 0.5s),
opacity var(--duration, 0.5s);
width: 100%;
}

.slide img {
display: block;
height: 100%;
object-fit: cover;
width: 100%;
}

[data-side="active"] .slide {
filter: none;
opacity: 1;
transform: perspective(var(--perspective, 1000px));
}

[data-side="prev"] .slide {
filter: blur(calc(var(--center-offset, 0) * var(--blur, 2px)));
opacity: calc(1 - var(--center-offset, 0) * 0.68);
transform: perspective(var(--perspective, 1000px))
translateX(calc(var(--center-offset, 0) * -1 * var(--xoffset, 600px)))
translateY(calc(var(--center-offset, 0) * -1 * var(--yoffset, 200px)))
translateZ(calc(var(--center-offset, 0) * 40px))
rotateZ(
calc(
var(--center-offset, 0) * -1 * var(--rotation, 16deg) +
sin(calc(var(--i, 0) * 1rad)) * 6deg
)
);
}

[data-side="next"] .slide {
filter: blur(calc(var(--center-offset, 0) * var(--blur, 2px)));
opacity: calc(1 - var(--center-offset, 0) * 0.3);
transform: perspective(var(--perspective, 1000px))
translateX(calc(var(--center-offset, 0) * var(--xoffset, 600px)))
translateY(calc(var(--center-offset, 0) * var(--yoffset, 200px)))
translateZ(calc(var(--center-offset, 0) * -80px))
rotateZ(
calc(
var(--center-offset, 0) * var(--rotation, 16deg) +
sin(calc(var(--i, 0) * 1.7rad)) * 6deg
)
);
}

@media (prefers-reduced-motion: reduce) {
.slide {
transition: none;
}
}

.navPrev,
.navNext {
align-items: center;
appearance: none;
background: rgb(255 255 255 / 12%);
backdrop-filter: blur(8px);
border: 1px solid rgb(255 255 255 / 20%);
border-radius: 50%;
color: #fff;
cursor: pointer;
display: flex;
height: 3rem;
justify-content: center;
position: absolute;
top: 50%;
transform: translateY(-50%);
transition:
background 0.2s,
border-color 0.2s;
width: 3rem;
z-index: 1000;
}

.navPrev:hover,
.navNext:hover {
background: rgb(255 255 255 / 25%);
border-color: rgb(255 255 255 / 40%);
}

.navPrev:disabled,
.navNext:disabled {
opacity: 0.3;
pointer-events: none;
}

.navPrev {
left: 1rem;
}

.navNext {
right: 1rem;
}

.thumbs {
display: flex !important;
gap: 0.5rem !important;
justify-content: center;
list-style: none;
margin-top: 10px;
overflow-x: auto;
padding: 0 1.5rem;
position: relative;
z-index: 10;
}

.thumbItem {
flex-shrink: 0;
line-height: 0;
}

.thumbButton {
appearance: none;
background: none;
border: 2px solid transparent;
border-radius: 5px;
cursor: pointer;
display: block;
height: 52px;
opacity: 0.45;
overflow: hidden;
padding: 0;
transition:
opacity 0.2s,
border-color 0.2s;
width: 88px;
}

.thumbButton img {
display: block;
height: 100%;
object-fit: cover;
width: 100%;
}

.thumbButton[aria-current="true"] {
border-color: #fff;
opacity: 1;
}

.thumbButton:hover {
opacity: 0.8;
}

.srOnly {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
Loading
Loading