fix(lightbox): correct zoom origin, scroll-close, blur-up rendering
several bugs found via in-browser debugging, all in lightbox.js + the
blur-up CSS:
1. DOMRect attribute mismatch: getBoundingClientRect returns .width/
.height, but code read .w/.h -> NaN transforms. map rectOf() to a
uniform {x,y,w,h}.
2. zoom scale base was the huge original natural size (2942px), so the
centered state collapsed to a tiny scale and the open animation ran
backwards (shrink). base the scale on originRect instead: first
frame is the in-article image at scale 1, centered state scales to
target.w/originRect.w. open is always grow, close always shrink.
3. scroll-to-close fired on a phantom scroll jump: the oversized
original img (layout size = naturalW) expanded the document's
scrollable area. set img layout size explicitly and add
overflow:hidden on the (position:fixed) overlay so the img cannot
expand the body.
4. first open animation jumped from the wrong position: a single rAF
was not enough to commit the no-transition first frame. use a forced
reflow (offsetHeight) + double-rAF before starting the transition.
5. blur-up full layer stuck at opacity 0 until a forced reflow (opening
the lightbox) repainted it. drop the opacity transition on .blur-img-
full and set opacity:1 from JS on load (deterministic, no repaint
race). mark the container .is-loaded and hide the placeholder via
the container class.
6. a 16px grey gap above in-article images: .md-content img { margin:
1rem 0 } applied to the absolute .blur-img-full too, pushing it
down. reset margin:0 / max-width:none on the blur-up img layers.
This commit is contained in:
parent
940236d84f
commit
8bdc3553bd
27
input.css
27
input.css
@ -531,12 +531,17 @@
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
/* overflow:hidden 裁剪超大的 absolute img,防止原图(可能数千 px)
|
||||
撑大文档可滚动区,否则会触发非预期的 scroll 事件。 */
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.92);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 灯箱图片:绝对定位,尺寸/位置由 JS 的 transform 控制。
|
||||
transform-origin 为 top left,配合 translate+scale 实现原地缩放。 */
|
||||
transform-origin 为 top left,配合 translate+scale 实现原地缩放。
|
||||
显式 width/height 由 JS 设置为原图尺寸,确保布局尺寸确定,
|
||||
transform 只做视觉缩放,不改变占位。 */
|
||||
.lightbox-img {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@ -722,9 +727,13 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
/* 重置 .md-content img 的 margin/max-width/height,避免把 absolute 图层推偏 */
|
||||
margin: 0;
|
||||
max-width: none;
|
||||
/* 20px 占位图放大后模糊,scale 遮边缘 */
|
||||
filter: blur(20px) saturate(1.2);
|
||||
transform: scale(1.1);
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
.blur-img-full {
|
||||
position: absolute;
|
||||
@ -732,13 +741,25 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
/* 重置 .md-content img 的 margin/max-width/height,避免把 absolute 图层推偏 */
|
||||
margin: 0;
|
||||
max-width: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
.blur-img-full.is-loaded {
|
||||
/* full 加载完成后由 JS 直接设 opacity:1(不依赖 CSS transition,
|
||||
避免合成层重绘时机导致 opacity 卡在 0)。 */
|
||||
.blur-img-full.is-loaded,
|
||||
.blur-img.is-loaded .blur-img-full {
|
||||
opacity: 1;
|
||||
}
|
||||
/* 容器标记 loaded 后:显式隐藏占位图 + 去掉灰底,避免重绘时机导致残留/灰边 */
|
||||
.blur-img.is-loaded .blur-img-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
.blur-img.is-loaded {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 暗色模式灰底 */
|
||||
.dark .blur-img {
|
||||
|
||||
@ -11,8 +11,17 @@
|
||||
}
|
||||
|
||||
// 读取元素当前在视口里的 rect(用于飞行起点/终点)。
|
||||
// 统一映射成 {x,y,w,h}:getBoundingClientRect 返回的 DOMRect 用
|
||||
// left/top/width/height,而 fitCentered/transformFor 用 x/y/w/h,
|
||||
// 这里转成一致格式,避免 .w 读到 undefined。
|
||||
function rectOf(el) {
|
||||
return el.getBoundingClientRect();
|
||||
var r = el.getBoundingClientRect();
|
||||
return {
|
||||
x: r.left,
|
||||
y: r.top,
|
||||
w: r.width,
|
||||
h: r.height,
|
||||
};
|
||||
}
|
||||
|
||||
// 计算图片在视口居中、contain 适配后的目标 rect。
|
||||
@ -31,11 +40,14 @@
|
||||
};
|
||||
}
|
||||
|
||||
// 把目标 rect 转成 transform 字符串(基于 naturalW/H 做缩放)。
|
||||
// 把目标 rect 转成 transform 字符串。
|
||||
// baseW/baseH 是 img 元素的布局尺寸(=居中目标尺寸),scale 相对它缩放。
|
||||
// transform-origin 为 top left(见 CSS),translate 到 rect 左上角后 scale。
|
||||
function transformFor(rect, naturalW, naturalH) {
|
||||
var sx = rect.w / naturalW;
|
||||
var sy = rect.h / naturalH;
|
||||
// - 居中态:scale=1(base 就是居中尺寸)
|
||||
// - originRect 态:scale = originRect.w / base.w(缩小)
|
||||
function transformFor(rect, baseW, baseH) {
|
||||
var sx = baseW > 0 ? rect.w / baseW : 1;
|
||||
var sy = baseH > 0 ? rect.h / baseH : 1;
|
||||
return (
|
||||
"translate(" +
|
||||
rect.x +
|
||||
@ -67,9 +79,21 @@
|
||||
var fullSrc = fullImg.getAttribute("data-src");
|
||||
if (!fullSrc) return;
|
||||
|
||||
fullImg.addEventListener("load", function () {
|
||||
this.classList.add("is-loaded");
|
||||
});
|
||||
var onFullLoaded = function () {
|
||||
// 给容器加 is-loaded,CSS 据此显式隐藏 placeholder。
|
||||
// 直接把 full 层 opacity 设为 1(清掉 transition),不依赖 CSS 的 opacity
|
||||
// 过渡:合成层重绘时机不稳定,可能导致 full 层卡在 opacity:0,直到一次
|
||||
// 强制重排才更新。
|
||||
container.classList.add("is-loaded");
|
||||
fullImg.style.transition = "none";
|
||||
fullImg.style.opacity = "1";
|
||||
};
|
||||
fullImg.addEventListener("load", onFullLoaded);
|
||||
// 缓存兜底:若设 src 时图片已在缓存(load 几乎立即触发,可能早于监听注册),
|
||||
// 用 complete 补一次。注意无 src 的 img complete 也为 true,故先判 src。
|
||||
if (fullImg.getAttribute("src") && fullImg.complete) {
|
||||
onFullLoaded();
|
||||
}
|
||||
|
||||
if ("IntersectionObserver" in window) {
|
||||
var io = new IntersectionObserver(
|
||||
@ -141,6 +165,10 @@
|
||||
var img = document.createElement("img");
|
||||
img.className = "lightbox-img";
|
||||
img.setAttribute("alt", altText);
|
||||
// 加载前先占 0 尺寸,避免原图(可能数千 px)在加载期间撑大文档
|
||||
// 可滚动区、触发非预期的 scroll 事件。start() 拿到 natural 尺寸后再设真实值。
|
||||
img.style.width = "0px";
|
||||
img.style.height = "0px";
|
||||
|
||||
var caption = document.createElement("figcaption");
|
||||
caption.className = "lightbox-caption";
|
||||
@ -211,11 +239,25 @@
|
||||
var naturalH = img.naturalHeight || img.clientHeight || 1;
|
||||
var originRect = rectOf(originNode);
|
||||
|
||||
// 基准 = originRect(文章里图片的实际尺寸)。
|
||||
// img 的布局尺寸固定为 originRect,transform 的 scale 相对它缩放:
|
||||
// 首帧(文章位置)scale=1,居中态 scale=target.w/originRect.w。
|
||||
// 这样无论 target 比 originRect 大或小,动画都是「从文章图原样连续缩放」,
|
||||
// 视觉上是原地展开,不会像「从外面飞来」。灯箱图尺寸恒为视口最大(fitCentered)。
|
||||
var target = fitCentered(naturalW, naturalH, vw, vh);
|
||||
var baseW = originRect.w;
|
||||
var baseH = originRect.h;
|
||||
// 存基准与目标,供关闭/滚动关闭复用同一对(保证 scale 连续)。
|
||||
state.target = target;
|
||||
state.baseW = baseW;
|
||||
state.baseH = baseH;
|
||||
img.style.width = baseW + "px";
|
||||
img.style.height = baseH + "px";
|
||||
|
||||
// reduced-motion:直接淡入居中
|
||||
if (state.reduced) {
|
||||
img.style.opacity = "0";
|
||||
var target = fitCentered(naturalW, naturalH, vw, vh);
|
||||
img.style.transform = transformFor(target, naturalW, naturalH);
|
||||
img.style.transform = transformFor(target, baseW, baseH);
|
||||
img.style.left = "0";
|
||||
img.style.top = "0";
|
||||
overlay.style.opacity = "0";
|
||||
@ -230,25 +272,30 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 首帧:放到 originRect 位置与尺寸,透明
|
||||
// 首帧:文章位置 + 原尺寸(scale=1),透明,且关闭 transition
|
||||
img.style.transition = "none";
|
||||
img.style.left = "0";
|
||||
img.style.top = "0";
|
||||
img.style.transform = transformFor(originRect, naturalW, naturalH);
|
||||
img.style.transform = transformFor(originRect, baseW, baseH);
|
||||
img.style.opacity = "0";
|
||||
overlay.style.opacity = "0";
|
||||
// 强制 reflow,确保首帧的 transform 已提交到渲染层。
|
||||
// 否则单层 rAF 里浏览器可能合并首帧与目标帧,动画从错误位置起跳。
|
||||
void img.offsetHeight;
|
||||
|
||||
// 下一帧:飞到居中
|
||||
// double-rAF:第一帧绘制首帧(无动画),第二帧才启动 transition 到居中。
|
||||
requestAnimationFrame(function () {
|
||||
if (!state) return;
|
||||
requestAnimationFrame(function () {
|
||||
if (!state) return;
|
||||
var tgt = fitCentered(naturalW, naturalH, vw, vh);
|
||||
img.style.transition =
|
||||
"transform 250ms ease-out, opacity 250ms ease-out";
|
||||
overlay.style.transition = "opacity 250ms ease-out";
|
||||
img.style.transform = transformFor(tgt, naturalW, naturalH);
|
||||
img.style.transform = transformFor(target, baseW, baseH);
|
||||
img.style.opacity = "1";
|
||||
overlay.style.opacity = "1";
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (img.complete && img.naturalWidth) {
|
||||
@ -266,8 +313,9 @@
|
||||
|
||||
var s = state;
|
||||
|
||||
var naturalW = s.img.naturalWidth || s.img.clientWidth || 1;
|
||||
var naturalH = s.img.naturalHeight || s.img.clientHeight || 1;
|
||||
// 基准 = originRect 尺寸(与打开时一致),scale 相对它缩放。
|
||||
var baseW = s.baseW || (s.target ? s.target.w : 1);
|
||||
var baseH = s.baseH || (s.target ? s.target.h : 1);
|
||||
var originRect = rectOf(s.originNode); // 实时读,处理期间滚动过的情况
|
||||
|
||||
if (s.reduced || immediate) {
|
||||
@ -275,11 +323,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 飞回 originRect
|
||||
// 飞回 originRect:scale 从 1 缩到 originRect.w/baseW
|
||||
s.img.style.transition =
|
||||
"transform 250ms ease-out, opacity 250ms ease-out";
|
||||
s.overlay.style.transition = "opacity 250ms ease-out";
|
||||
s.img.style.transform = transformFor(originRect, naturalW, naturalH);
|
||||
s.img.style.transform = transformFor(originRect, baseW, baseH);
|
||||
s.img.style.opacity = "0";
|
||||
s.overlay.style.opacity = "0";
|
||||
|
||||
@ -287,8 +335,8 @@
|
||||
removeOverlay();
|
||||
};
|
||||
// 250ms 兜底,避免 transitionend 不触发
|
||||
setTimeout(done, 280);
|
||||
s.img.addEventListener("transitionend", done, { once: true });
|
||||
var timer = setTimeout(done, 280);
|
||||
s.img.addEventListener("transitionend", function () { clearTimeout(timer); done(); }, { once: true });
|
||||
}
|
||||
|
||||
function removeOverlay() {
|
||||
@ -380,15 +428,10 @@
|
||||
return;
|
||||
}
|
||||
var progress = Math.min(dy / 120, 1);
|
||||
var naturalW = st.img.naturalWidth || st.img.clientWidth || 1;
|
||||
var naturalH = st.img.naturalHeight || st.img.clientHeight || 1;
|
||||
var target = st.target;
|
||||
var baseW = st.baseW || (target ? target.w : 1);
|
||||
var baseH = st.baseH || (target ? target.h : 1);
|
||||
var originRect = rectOf(st.originNode);
|
||||
var target = fitCentered(
|
||||
naturalW,
|
||||
naturalH,
|
||||
window.innerWidth,
|
||||
window.innerHeight
|
||||
);
|
||||
// 在 originRect 与居中 target 之间按 progress 线性插值
|
||||
var cur = {
|
||||
x: target.x + (originRect.x - target.x) * progress,
|
||||
@ -397,7 +440,7 @@
|
||||
h: target.h + (originRect.h - target.h) * progress,
|
||||
};
|
||||
st.img.style.transition = "none";
|
||||
st.img.style.transform = transformFor(cur, naturalW, naturalH);
|
||||
st.img.style.transform = transformFor(cur, baseW, baseH);
|
||||
st.img.style.opacity = String(1 - progress);
|
||||
st.overlay.style.opacity = String(1 - progress);
|
||||
if (progress >= 1) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user