eleven / messenger Public

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>
Paul Campbell 6331bea2cf577c79922cc75edbbacbc94816826d parent 2e2d493
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);