Skip to content

Instantly share code, notes, and snippets.

@shshaw
Created August 5, 2021 20:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shshaw/a40b053686e3057ed22210792ac8541a to your computer and use it in GitHub Desktop.
Save shshaw/a40b053686e3057ed22210792ac8541a to your computer and use it in GitHub Desktop.
React Panning Image Grid, Part 2 | @keyframers 4.7.2
<div id="app"></div>
<a href="https://youtu.be/zOGgcd6nkYs" target="_blank" data-keyframers-credit style="color: #FFF"></a>
<script src="https://codepen.io/shshaw/pen/QmZYMG.js"></script>

React Panning Image Grid, Part 2 | @keyframers 4.7.2

Want to see how we started? Watch PART 1: https://youtu.be/_arMUygzYH0

David Khourshid & Stephen Shaw build a panning image grid that expands images on selection with React.... kinda.

🎥 Part 1 Video: https://youtu.be/_arMUygzYH0 🎥 Part 2 Video: https://youtu.be/zOGgcd6nkYs 💡 Inspiration: https://dribbble.com/shots/16075830-Archived-chronicles 💻 Part 1 Code: https://codepen.io/team/keyframers/pen/KKmZLEE 💻 Part 2 Code: https://codepen.io/team/keyframers/pen/oNWPEEd

Like what we're doing? Support @keyframers so we can keep live coding awesome animations!

A Pen by @keyframers on CodePen.

License.

import React, {
useState,
useEffect,
useRef
} from "https://cdn.skypack.dev/react";
import ReactDOM from "https://cdn.skypack.dev/react-dom";
console.clear();
const image_width = 600;
const image_height = 338;
const images = [
"https://source.unsplash.com/8wmbUbLUsz4/",
"https://source.unsplash.com/RxE6AYn-Y5k/",
"https://source.unsplash.com/wcE2fS3Amoc/",
"https://source.unsplash.com/ujBYjagUDiY/",
"https://source.unsplash.com/igLQW_yY9oo/",
"https://source.unsplash.com/Wj-GUhugkxY/",
"https://source.unsplash.com/5tKEB1a_5Cw/",
"https://source.unsplash.com/Q_KBDeMCo9w/",
"https://source.unsplash.com/Ep_T4Aepor8/",
"https://source.unsplash.com/Oq13yYb3eHI/",
"https://source.unsplash.com/LTmdkzm2y1g/",
"https://source.unsplash.com/ibIqYtrxXds/",
"https://source.unsplash.com/CaYxfP8IlHM/",
"https://source.unsplash.com/l0iOHra9kNc/",
"https://source.unsplash.com/vHgeNO82JMc/",
"https://source.unsplash.com/1XvjS1fCrms/",
"https://source.unsplash.com/eKKaiHgJitE/",
"https://source.unsplash.com/7i2by8a0tK8/",
"https://source.unsplash.com/v0a-jLLPYL8/",
"https://source.unsplash.com/2_Q61ZrCzMQ/",
"https://source.unsplash.com/g71SbI2oGbs/",
"https://source.unsplash.com/0rjY456aTiE/",
"https://source.unsplash.com/ykK6Kmh9LL8/",
"https://source.unsplash.com/fGXqq6HU2-4/",
"https://source.unsplash.com/IjPWFZncmxs/"
];
const rows = 5;
const cols = 5;
function App() {
const [selected, setSelected] = useState(12);
const [delta, setDelta] = useState({ x: "0%", y: "0%", xp: 0, yp: 0 });
const imagesRef = useRef({});
const galleryRef = useRef(null);
const measurementsRef = useRef({});
// "gallery", -> "focus", -> "item"
const [mode, setMode] = useState("gallery");
useEffect(() => {
if (selected) {
setMode("focus");
const elImage = imagesRef.current[selected];
const galleryRect = galleryRef.current.getBoundingClientRect();
const itemRect = elImage.getBoundingClientRect();
const xOffset = itemRect.left - galleryRect.left;
const yOffset = itemRect.top - galleryRect.top;
Object.assign(measurementsRef.current, {
top: galleryRect.top,
left: galleryRect.left,
xOffset,
xCenterOffset: xOffset + itemRect.width / 2,
yOffset,
yCenterOffset: yOffset + itemRect.height / 2,
halfWidth: galleryRect.width / 2,
halfHeight: galleryRect.height / 2,
widthRatio: itemRect.width / galleryRect.width,
heightRatio: itemRect.height / galleryRect.height
});
} else {
setMode("gallery");
}
}, [selected]);
useEffect(() => {
if (mode === "gallery") {
setDelta({
"--dx": 0,
"--dy": 0,
"--rw": 1,
"--rh": 1,
"--origin-x": measurementsRef.current.xOffset,
"--origin-y": measurementsRef.current.yOffset
});
} else if (mode === "focus") {
setDelta({
"--dx":
measurementsRef.current.halfWidth -
measurementsRef.current.xCenterOffset,
"--dy":
measurementsRef.current.halfHeight -
measurementsRef.current.yCenterOffset,
"--origin-x": measurementsRef.current.xOffset,
"--origin-y": measurementsRef.current.yOffset
});
} else if (mode === "item") {
setDelta({
"--dx": measurementsRef.current.left - measurementsRef.current.xOffset,
"--dy": measurementsRef.current.top - measurementsRef.current.yOffset,
"--rw": 1 / measurementsRef.current.widthRatio,
"--rh": 1 / measurementsRef.current.heightRatio,
"--origin-x": measurementsRef.current.xOffset,
"--origin-y": measurementsRef.current.yOffset
});
}
}, [selected, mode]);
useEffect(() => {
const handler = (event) => {
const getIndex = (n) => Math.min(images.length - 1, Math.max(0, n));
switch (event.key) {
case "ArrowLeft":
setSelected((selected) => getIndex(selected - 1));
// Left pressed
break;
case "ArrowRight":
setSelected((selected) => getIndex(selected + 1));
break;
case "ArrowUp":
setSelected((selected) => getIndex(selected - rows));
// Up pressed
break;
case "ArrowDown":
setSelected((selected) => getIndex(selected + rows));
// Down pressed
break;
case "Enter":
setMode((mode) => {
if (mode === "focus") {
return "item";
} else {
return "gallery";
}
});
break;
}
};
document.body.addEventListener("keydown", handler);
return () => document.body.removeEventListener("keydown", handler);
}, []);
return (
<div className="app" data-mode={mode} style={delta}>
<div
className="gallery"
ref={galleryRef}
style={{
"--rows": rows,
"--cols": cols
}}
>
{images.map((src, index) => {
let isSelected = selected === index;
return (
<img
ref={(node) => {
imagesRef.current[index] = node;
}}
src={`${src}/${image_width}x${image_height}/`}
onClick={(e) => {
e.preventDefault();
if (mode === "item") {
setSelected(null);
} else if (mode === "focus" && isSelected) {
setMode("item");
} else {
setSelected(index);
}
}}
data-selected={isSelected || null}
/>
);
})}
</div>
</div>
);
}
ReactDOM.render(<App />, document.querySelector("#app"));
* {
box-sizing: border-box;
position: relative;
}
html {
background: #000;
color: #fff;
height: 100%;
width: 100%;
overflow: hidden;
}
// body {
// height: 100%;
// display: grid;
// place-items: center;
// }
.app {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(5, 1fr);
gap: 2em;
--radius: 0.5vmin;
// padding: 2em;
> .gallery {
grid-column: 1 / -1;
grid-row: 1 / -1;
}
&:before {
--scale-x: calc(var(--rw, 1) * 1.1);
--scale-y: calc(var(--rh, 1) * 1.1);
grid-column: 3 / 4;
grid-row: 3 / 4;
content: "";
display: block;
border: 0.5vmin solid white;
border-radius: var(--radius);
z-index: 1;
pointer-events: none;
transform: scale(var(--rw, 1), var(--rh 1));
transition: all 500ms cubic-bezier(0.6, 0, 0.4, 1);
margin: -6px;
}
&[data-mode="item"] {
&:before {
opacity: 0;
}
img:not([data-selected]) {
opacity: 0;
}
}
}
.gallery {
--image-width: 20vw;
display: grid;
grid: inherit;
gap: inherit;
width: fit-content;
transform: translate(calc(var(--dx, 0) * 1px), calc(var(--dy, 0) * 1px))
scale(var(--rw, 1), var(--rh, 1));
transform-origin: calc(var(--origin-x, 0) * 1px)
calc(var(--origin-y, 0) * 1px);
transition: all 500ms cubic-bezier(0.6, 0, 0.4, 1);
img {
width: 100%;
border-radius: var(--radius);
opacity: 0.5;
transition: opacity 0.3s ease;
&:hover,
&:focus {
opacity: 0.8;
}
&[data-selected] {
opacity: 1;
//border: .5vmin solid white;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment