diff --git a/libs/lightbox/src/geometry.test.ts b/libs/lightbox/src/geometry.test.ts new file mode 100644 index 0000000..82e19d2 --- /dev/null +++ b/libs/lightbox/src/geometry.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect } from 'vitest'; +import { fitCentered, transformFor, originalUrl, type Rect } from './geometry'; + +describe('fitCentered', () => { + // 大图受 maxW=vw*0.92 / maxH=vh*0.88 约束,scale 取较小者 + it('宽图被视口宽度卡住(横向受限)', () => { + // 2000x1000 in 1000x800: maxW=920, maxH=704 + // scale = min(920/2000, 704/1000, 1) = min(0.46, 0.704, 1) = 0.46 + const r = fitCentered(2000, 1000, 1000, 800); + expect(r.w).toBeCloseTo(920, 5); + expect(r.h).toBeCloseTo(460, 5); + expect(r.x).toBeCloseTo((1000 - 920) / 2, 5); // 40 + expect(r.y).toBeCloseTo((800 - 460) / 2, 5); // 170 + }); + + it('高图被视口高度卡住(纵向受限)', () => { + // 1000x2000 in 1000x800: maxW=920, maxH=704 + // scale = min(920/1000, 704/2000, 1) = min(0.92, 0.352, 1) = 0.352 + const r = fitCentered(1000, 2000, 1000, 800); + expect(r.w).toBeCloseTo(352, 5); + expect(r.h).toBeCloseTo(704, 5); + }); + + it('小图不被放大(scale 钳到 1)', () => { + // 300x200 in 1000x800: scale = min(920/300, 704/200, 1) = 1 + const r = fitCentered(300, 200, 1000, 800); + expect(r.w).toBe(300); + expect(r.h).toBe(200); + expect(r.x).toBeCloseTo((1000 - 300) / 2, 5); // 350 + expect(r.y).toBeCloseTo((800 - 200) / 2, 5); // 300 + }); + + it('正方形图按 maxW/maxH 中较小者缩放', () => { + // 1000x1000 in 1000x800: maxW=920, maxH=704 + // scale = min(0.92, 0.704, 1) = 0.704 + const r = fitCentered(1000, 1000, 1000, 800); + expect(r.w).toBeCloseTo(704, 5); + expect(r.h).toBeCloseTo(704, 5); + }); +}); + +describe('transformFor', () => { + it('居中态(base=target):scale=1,translate 到目标左上角', () => { + const target: Rect = { x: 40, y: 170, w: 920, h: 460 }; + const t = transformFor(target, 920, 460); + expect(t).toBe('translate(40px,170px) scale(1,1)'); + }); + + it('缩小态(rect 比 base 小):scale<1', () => { + // 原图位置小,base=居中尺寸 920x460 + const origin: Rect = { x: 100, y: 500, w: 400, h: 200 }; + const t = transformFor(origin, 920, 460); + // 400/920 = 0.43478260869565216(JS 浮点完整精度) + expect(t).toBe('translate(100px,500px) scale(0.43478260869565216,0.43478260869565216)'); + }); + + it('baseW=0 守卫:scale 守卫为 1(不产生 NaN/Infinity)', () => { + const rect: Rect = { x: 0, y: 0, w: 0, h: 0 }; + const t = transformFor(rect, 0, 0); + expect(t).toBe('translate(0px,0px) scale(1,1)'); + }); + + it('字符串格式为 translate(Xpx,Ypx) scale(SX,SY)', () => { + const rect: Rect = { x: 10, y: 20, w: 100, h: 50 }; + const t = transformFor(rect, 200, 100); + expect(t).toMatch(/^translate\(\d+(\.\d+)?px,\d+(\.\d+)?px\) scale\(\d+(\.\d+)?,\d+(\.\d+)?\)$/); + }); +}); + +describe('originalUrl', () => { + it('去 query string', () => { + expect(originalUrl('/uploads/x.webp?w=800')).toBe('/uploads/x.webp'); + }); + + it('无 query 原样返回', () => { + expect(originalUrl('/uploads/x.webp')).toBe('/uploads/x.webp'); + }); + + it('null 输入返回空串', () => { + expect(originalUrl(null)).toBe(''); + }); + + it('空串输入返回空串', () => { + expect(originalUrl('')).toBe(''); + }); +}); diff --git a/libs/lightbox/src/geometry.ts b/libs/lightbox/src/geometry.ts new file mode 100644 index 0000000..7c1c64c --- /dev/null +++ b/libs/lightbox/src/geometry.ts @@ -0,0 +1,48 @@ +export interface Rect { + x: number; + y: number; + w: number; + h: number; +} + +// 计算图片在视口居中、contain 适配后的目标 rect。 +// naturalW/H: 图片真实像素尺寸;vw/vh: 视口尺寸。 +export function fitCentered(naturalW: number, naturalH: number, vw: number, vh: number): Rect { + var maxW = vw * 0.92; + var maxH = vh * 0.88; + var scale = Math.min(maxW / naturalW, maxH / naturalH, 1); + var w = naturalW * scale; + var h = naturalH * scale; + return { + x: (vw - w) / 2, + y: (vh - h) / 2, + w: w, + h: h, + }; +} + +// 把目标 rect 转成 transform 字符串。 +// baseW/baseH 是 img 元素的布局尺寸(=居中目标尺寸),scale 相对它缩放。 +// transform-origin 为 top left(见 CSS),translate 到 rect 左上角后 scale。 +// - 居中态:scale=1(base 就是居中尺寸) +// - originRect 态:scale = originRect.w / base.w(缩小) +export function transformFor(rect: Rect, baseW: number, baseH: number): string { + var sx = baseW > 0 ? rect.w / baseW : 1; + var sy = baseH > 0 ? rect.h / baseH : 1; + return ( + "translate(" + + rect.x + + "px," + + rect.y + + "px) scale(" + + sx + + "," + + sy + + ")" + ); +} + +// 原图 URL = data-src 去 query。data-src 形如 "/uploads/x.webp?w=800"。 +export function originalUrl(dataSrc: string | null): string { + return (dataSrc || "").split("?")[0]; +}