diff --git a/libs/lightbox/src/index.ts b/libs/lightbox/src/index.ts index b6e3768..e1f635a 100644 --- a/libs/lightbox/src/index.ts +++ b/libs/lightbox/src/index.ts @@ -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'; diff --git a/libs/lightbox/src/lightbox.test.ts b/libs/lightbox/src/lightbox.test.ts index c0c1a02..41fd538 100644 --- a/libs/lightbox/src/lightbox.test.ts +++ b/libs/lightbox/src/lightbox.test.ts @@ -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 数据刷新场景)', () => { diff --git a/libs/lightbox/src/style.css b/libs/lightbox/src/style.css index 5e1615a..af80e94 100644 --- a/libs/lightbox/src/style.css +++ b/libs/lightbox/src/style.css @@ -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; }