fix(lightbox): 图集切换不重算几何导致图片压扁
gotoIndex 换图时只替换 src,target/baseW/baseH/transform 全部沿用 第一张图的几何,宽高比不同的图被套用错误的 scale(显示宽高比 = 自身宽高比 × 首图宽高比)。同时 .lightbox-img 的 object-fit: contain 与 transformFor 依赖的「位图填满布局盒」前提冲突,素材网格的方形 缩略图容器下首图即被压扁。 修复: - swap() 在新图加载后按 naturalWidth/Height 重算 fitCentered 目标, 布局盒设为 target 尺寸、transform 归位 scale(1,1),baseW/H 同步 供关闭飞回动画复用 - 先换 src 再判 complete,避免用旧图的 complete 误判缓存命中 - 移除 .lightbox-img 的 object-fit: contain,恢复位图填满布局盒 新增回归测试:切换后几何必须按新图宽高比重算(红绿验证通过)
This commit is contained in:
parent
051ca947e3
commit
99d33b54d2
@ -376,18 +376,39 @@ function gotoIndex(rawIndex: number): void {
|
||||
// 150ms 后换图淡入
|
||||
const swap = (): void => {
|
||||
if (!state) return; // 切换中可能已关闭
|
||||
// 换图后必须按新图真实尺寸重算几何,否则新图沿用第一张的
|
||||
// target/scale,宽高比不同的图会被压扁/拉伸。
|
||||
// 布局盒直接设为 target 尺寸(宽高比 = 新图),transform 归位 scale(1,1);
|
||||
// baseW/H 同步为 target 尺寸,关闭/滚动关闭的飞回动画以它为 scale 基准。
|
||||
const applyGeometry = (): void => {
|
||||
if (!state) return;
|
||||
const naturalW = s.img.naturalWidth || 1;
|
||||
const naturalH = s.img.naturalHeight || 1;
|
||||
const target = fitCentered(naturalW, naturalH, window.innerWidth, window.innerHeight);
|
||||
s.target = target;
|
||||
s.baseW = target.w;
|
||||
s.baseH = target.h;
|
||||
// 几何跳变不播动画(transition 只作用于随后的 opacity 淡入)。
|
||||
s.img.style.transition = 'none';
|
||||
s.img.style.width = `${target.w}px`;
|
||||
s.img.style.height = `${target.h}px`;
|
||||
s.img.style.transform = transformFor(target, target.w, target.h);
|
||||
};
|
||||
const fade = (): void => {
|
||||
if (!state) return;
|
||||
s.img.style.transition = 'opacity 150ms ease-out';
|
||||
s.img.style.opacity = '1';
|
||||
};
|
||||
if (s.img.complete && s.img.naturalWidth) {
|
||||
// 缓存命中,直接淡入
|
||||
s.img.src = origSrc;
|
||||
const onReady = (): void => {
|
||||
applyGeometry();
|
||||
fade();
|
||||
};
|
||||
// 先换 src 再判 complete:换之前判的是旧图(必命中),会按旧图尺寸算几何。
|
||||
s.img.src = origSrc;
|
||||
if (s.img.complete && s.img.naturalWidth) {
|
||||
onReady(); // 缓存命中,新图尺寸同步可用
|
||||
} else {
|
||||
s.img.addEventListener('load', fade, { once: true });
|
||||
s.img.src = origSrc;
|
||||
s.img.addEventListener('load', onReady, { once: true });
|
||||
}
|
||||
s.caption.textContent = altText;
|
||||
s.caption.style.display = altText ? '' : 'none';
|
||||
|
||||
@ -63,6 +63,13 @@ function getCounter(): HTMLElement | null {
|
||||
return document.querySelector('.lightbox-counter');
|
||||
}
|
||||
|
||||
/** stub 灯箱图的 natural 尺寸与 complete(happy-dom 不加载真实图片)。 */
|
||||
function stubNatural(img: HTMLImageElement, w: number, h: number): void {
|
||||
Object.defineProperty(img, 'naturalWidth', { configurable: true, value: w });
|
||||
Object.defineProperty(img, 'naturalHeight', { configurable: true, value: h });
|
||||
Object.defineProperty(img, 'complete', { configurable: true, value: true });
|
||||
}
|
||||
|
||||
/** 模拟元素 click(真实事件派发,触发 addEventListener('click'))。 */
|
||||
function clickEl(el: Element): void {
|
||||
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
||||
@ -235,6 +242,33 @@ describe('lightbox 黑盒行为', () => {
|
||||
const fullB = imgs[1].querySelector('.blur-img-full');
|
||||
expect(document.activeElement).toBe(fullB);
|
||||
});
|
||||
|
||||
it('切换后按新图宽高比重算几何(不沿用第一张的 target/scale)', () => {
|
||||
const imgs = [makeGalleryImage('/a.webp', 'A'), makeGalleryImage('/b.webp', 'B')];
|
||||
mountRoot(imgs);
|
||||
window.__initLightbox('.post-content');
|
||||
|
||||
clickEl(imgs[0]);
|
||||
const lbImg = getLightboxImg()!;
|
||||
// happy-dom 不真实加载图片:手动 stub 第 1 张 natural 尺寸并派发 load,
|
||||
// 让 openLightbox 的 start() 走完(state.target/baseW 按第 1 张建立)。
|
||||
stubNatural(lbImg, 1000, 500);
|
||||
lbImg.dispatchEvent(new Event('load'));
|
||||
vi.advanceTimersByTime(50); // double-rAF 在 fake timers 下同步推进
|
||||
|
||||
// 切到第 2 张前,先把 natural stub 成新图尺寸(竖图),模拟缓存命中
|
||||
pressKey('ArrowRight');
|
||||
stubNatural(lbImg, 500, 1000);
|
||||
vi.advanceTimersByTime(200); // 150ms 淡出后 swap 同步执行
|
||||
|
||||
// 几何必须按 500x1000 重算:布局盒 = fitCentered 目标尺寸(宽高比 0.5),
|
||||
// transform 归位 scale(1,1)。修复前这里仍是第 1 张的 width/height/scale。
|
||||
const w = parseFloat(lbImg.style.width);
|
||||
const h = parseFloat(lbImg.style.height);
|
||||
expect(w).toBeGreaterThan(0);
|
||||
expect(w / h).toBeCloseTo(0.5, 2);
|
||||
expect(lbImg.style.transform).toContain('scale(1,1)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('重复初始化幂等(SPA 数据刷新场景)', () => {
|
||||
|
||||
@ -12,8 +12,11 @@
|
||||
|
||||
/* 灯箱图片:绝对定位,尺寸/位置由 JS 的 transform 控制。
|
||||
transform-origin 为 top left,配合 translate+scale 实现原地缩放。
|
||||
显式 width/height 由 JS 设置为原图尺寸,确保布局尺寸确定,
|
||||
transform 只做视觉缩放,不改变占位。 */
|
||||
显式 width/height 由 JS 设置(打开时 = 缩略图 rect,图集切换后 = 居中目标
|
||||
尺寸),确保布局尺寸确定,transform 只做视觉缩放,不改变占位。
|
||||
注意不可加 object-fit: contain:transformFor 的非均匀 scale 依赖「位图填满
|
||||
布局盒」这一前提才能恰好抵消失真(素材网格的缩略图容器恒为正方形,与原图
|
||||
宽高比不同,contain 的 letterbox 会导致非均匀 scale 把图压扁)。 */
|
||||
.lightbox-img {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@ -21,7 +24,6 @@
|
||||
transform-origin: top left;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
object-fit: contain;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user