|
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")); |