Skip to content

Commit 93cd26b

Browse files
committed
Implement custom zooming
1 parent 96ba06b commit 93cd26b

File tree

3 files changed

+107
-15
lines changed

3 files changed

+107
-15
lines changed
Lines changed: 97 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
"use client"
22

3-
import { useState } from "react"
3+
import { useRef, useState } from "react"
4+
import { motion, useMotionTemplate, useSpring } from "motion/react"
45
import { clsx } from "clsx"
56
import Image from "next-image-export-optimizer"
7+
import type { StaticImageData } from "next/image"
8+
69
import { Marquee } from "@/app/conf/_design-system/marquee"
10+
import ZoomInIcon from "../../pixelarticons/zoom-in.svg?svgr"
11+
import ZoomOutIcon from "../../pixelarticons/zoom-out.svg?svgr"
712

813
import { imagesByYear } from "./images"
914

@@ -15,6 +20,8 @@ export interface GalleryStripProps extends React.HTMLAttributes<HTMLElement> {}
1520
export function GalleryStrip({ className, ...rest }: GalleryStripProps) {
1621
const [selectedYear, setSelectedYear] = useState<Year>("2024")
1722

23+
const previousZoomedImage = useRef<HTMLElement | null>(null)
24+
1825
return (
1926
<section
2027
role="presentation"
@@ -27,7 +34,7 @@ export function GalleryStrip({ className, ...rest }: GalleryStripProps) {
2734
key={year}
2835
onClick={() => setSelectedYear(year)}
2936
className={clsx(
30-
"p-1 typography-menu",
37+
"gql-focus-visible p-1 typography-menu",
3138
selectedYear === year
3239
? "bg-sec-light text-neu-900 dark:text-neu-0"
3340
: "text-neu-800",
@@ -39,25 +46,100 @@ export function GalleryStrip({ className, ...rest }: GalleryStripProps) {
3946
</div>
4047

4148
<div className="mt-6 w-full md:mt-10">
42-
<Marquee gap={8} speed={35} speedOnHover={15} drag reverse>
49+
<Marquee
50+
gap={8}
51+
speed={35}
52+
speedOnHover={15}
53+
drag
54+
reverse
55+
className="!overflow-visible"
56+
>
4357
{imagesByYear[selectedYear].map((image, i) => {
58+
const key = `${selectedYear}-${i}`
59+
4460
return (
45-
<div
46-
key={`${selectedYear}-${i}`}
47-
className="md:px-2"
48-
role="presentation"
49-
>
50-
<Image
51-
src={image}
52-
alt=""
53-
height={320}
54-
className="pointer-events-none"
55-
/>
56-
</div>
61+
<GalleryStripImage
62+
key={key}
63+
image={image}
64+
previousZoomedImage={previousZoomedImage}
65+
/>
5766
)
5867
})}
5968
</Marquee>
6069
</div>
6170
</section>
6271
)
6372
}
73+
74+
function GalleryStripImage({
75+
image,
76+
previousZoomedImage,
77+
}: {
78+
image: StaticImageData
79+
previousZoomedImage: React.MutableRefObject<HTMLElement | null>
80+
}) {
81+
const [isZoomed, setIsZoomed] = useState(false)
82+
const scale = useSpring(1)
83+
const transform = useMotionTemplate`translate3d(0,0,var(--translate-z,-16px)) scale(${scale})`
84+
85+
// if we set scale in useEffect the UI glitches
86+
const zoomIn = (current: HTMLElement | null) => {
87+
if (previousZoomedImage.current) {
88+
previousZoomedImage.current.style.zIndex = "0"
89+
previousZoomedImage.current.style.setProperty("--translate-z", "0px")
90+
}
91+
92+
if (current) {
93+
current.style.zIndex = "2"
94+
current.style.setProperty("--translate-z", "16px")
95+
}
96+
97+
previousZoomedImage.current = current
98+
99+
scale.set(1.665625)
100+
setIsZoomed(true)
101+
}
102+
103+
const zoomOut = () => {
104+
scale.set(1)
105+
setIsZoomed(false)
106+
}
107+
108+
return (
109+
<motion.div
110+
role="presentation"
111+
className="relative md:px-2"
112+
style={{ transform }}
113+
onPointerOut={event => {
114+
const target = event.currentTarget
115+
const relatedTarget = event.relatedTarget as Node | null
116+
117+
if (!relatedTarget || !target.contains(relatedTarget)) {
118+
zoomOut()
119+
}
120+
}}
121+
>
122+
<Image
123+
src={image}
124+
alt=""
125+
role="presentation"
126+
width={799}
127+
height={533}
128+
className="pointer-events-none aspect-[799/533] h-[320px] w-auto object-cover"
129+
/>
130+
<button
131+
type="button"
132+
className="absolute right-2 top-0 z-[1] bg-neu-50/10 p-4"
133+
onClick={event => {
134+
isZoomed ? zoomOut() : zoomIn(event.currentTarget.parentElement)
135+
}}
136+
>
137+
{isZoomed ? (
138+
<ZoomOutIcon className="size-12" />
139+
) : (
140+
<ZoomInIcon className="size-12" />
141+
)}
142+
</button>
143+
</motion.div>
144+
)
145+
}
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)