web: match the lightbox corner radius to the thumbnail during the fly-in/out (#125)
The FLIP scales the photo, which also scaled its fixed 8px corner radius — so the fly-out ended on a rounding that didn't match the thumbnail it landed on (a message's 18px corner, or a circular avatar), and the mismatch snapped at the very end. Animate border-radius alongside the transform so the corners line up at both ends: the source's radius at the thumbnail, the rest radius at full size.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 file changed,
+21
−3
web/static/chat.js+21 −3
diff --git a/web/static/chat.js b/web/static/chat.js| index a4eb6ca..bb00baa 100644 |
| --- a/web/static/chat.js |
| +++ b/web/static/chat.js |
| @@ -3091,6 +3091,21 @@ function openLightbox(url, source) { |
| document.addEventListener("keydown", onKey); |
| document.body.appendChild(back); |
| + // The photo's rounding is a fixed CSS radius, but the FLIP scales it — so a naive |
| + // fly-out lands on a corner radius that doesn't match the thumbnail underneath it |
| + // (a message's rounded corner, or a circular avatar) and snaps at the end. We |
| + // animate border-radius across the transition so the corners line up at both |
| + // ends: the source's radius at the thumbnail, the CSS radius at rest. Since the |
| + // transform scales the element, the CSS radius that *renders* as `r` at scale `s` |
| + // is `r / s`. |
| + const restRadius = parseFloat(getComputedStyle(img).borderTopLeftRadius) || 8; |
| + const sourceRadius = (rect) => { |
| + if (!source) return restRadius; |
| + const raw = getComputedStyle(source).borderTopLeftRadius; |
| + const r = raw.endsWith("%") ? (parseFloat(raw) / 100) * Math.min(rect.width, rect.height) : parseFloat(raw); |
| + return r || 0; |
| + }; |
| + |
| // --- gesture state ------------------------------------------------------- |
| // One transform drives everything: translate3d + rotate + scale (GPU-composited). |
| // A single finger at 1× flick-dismisses; two fingers pinch-zoom-and-rotate about |
| @@ -3179,9 +3194,11 @@ function openLightbox(url, source) { |
| const fromY = S.top + S.height / 2 - baseCY; |
| img.style.transition = "none"; |
| img.style.transform = `translate3d(${fromX}px, ${fromY}px, 0) scale(${s})`; |
| - void img.offsetWidth; // reflow so the change below animates from here |
| - img.style.transition = "transform 0.3s cubic-bezier(0.2,0.8,0.2,1)"; |
| + img.style.borderRadius = sourceRadius(S) / s + "px"; // match the thumbnail's rounding to start |
| + void img.offsetWidth; // reflow so the changes below animate from here |
| + img.style.transition = "transform 0.3s cubic-bezier(0.2,0.8,0.2,1), border-radius 0.3s ease"; |
| img.style.transform = "translate3d(0,0,0)"; |
| + img.style.borderRadius = restRadius + "px"; |
| backdrop.style.transition = "opacity 0.3s"; |
| backdrop.style.opacity = "1"; |
| }; |
| @@ -3206,8 +3223,9 @@ function openLightbox(url, source) { |
| const s = Math.max(S.width / img.clientWidth, S.height / img.clientHeight); |
| const toX = S.left + S.width / 2 - baseCX; |
| const toY = S.top + S.height / 2 - baseCY; |
| - img.style.transition = "transform 0.28s cubic-bezier(0.4,0,0.2,1)"; |
| + img.style.transition = "transform 0.28s cubic-bezier(0.4,0,0.2,1), border-radius 0.28s ease"; |
| img.style.transform = `translate3d(${toX}px, ${toY}px, 0) scale(${s})`; |
| + img.style.borderRadius = sourceRadius(S) / s + "px"; // shrink to the thumbnail's rounding, no snap |
| backdrop.style.transition = "opacity 0.28s"; |
| backdrop.style.opacity = "0"; |
| setTimeout(close, 300); |