feat(lightbox): extract geometry pure fns with vitest coverage
This commit is contained in:
parent
c5af4457e2
commit
44fc641aaa
86
libs/lightbox/src/geometry.test.ts
Normal file
86
libs/lightbox/src/geometry.test.ts
Normal file
@ -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('');
|
||||
});
|
||||
});
|
||||
48
libs/lightbox/src/geometry.ts
Normal file
48
libs/lightbox/src/geometry.ts
Normal file
@ -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];
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user