Ctrl K
ComunidadGuías…

frontend/test/card-art/face-frame.test.ts

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/* 头像的人脸放大:倍数夹在「够看清」「摆得正」「不切头」「不上采样」之间。
 *
 * 期望值照 `faceZoom` 的算式逐步写,不写小数字面量:手抄一个四舍五入过的数只会把断言变成
 * 「和上次跑出来的一样」,看不出哪一条上限在说话。 */
import { describe, expect, it } from 'vitest';

import { faceFrame, faceZoom, hasFaceBox } from '../../src/card-art/face-frame';

/** 全库最小的那张脸:`performer-8711` 是 640×960 的全身站姿照,脸框只有 67 px 宽。
 *  拿它当基准,因为「放大多少」只在这一档上才会同时撞到三条上限。 */
const SMALL_FACE = { cx: 0.439, cy: 0.224, faceW: 67, imgW: 640, imgH: 960 };
/** 539 张里四分之三长这样:脸框已占框宽四成以上,一放大就切头顶。 */
const CLOSE_UP = { cx: 0.5, cy: 0.42, faceW: 280, imgW: 640, imgH: 896 };
/** 关注页题材圆标的代表图,`/work-icon` 落盘那一份:512 px 见方,脸框 170 px。
 *  `Final Fantasy` 那枚,脸在画面上方 0.211 处——半张图都在脸下面。 */
const WORK_ICON = { cx: 0.508, cy: 0.211, faceW: 170, imgW: 512, imgH: 512 };
/** `Tekken` 那枚:脸挤在右上角,两根轴都离边缘很近。 */
const WORK_ICON_CORNER = { cx: 0.889, cy: 0.134, faceW: 110, imgW: 512, imgH: 512 };

const ring = (side: number) => ({ w: side, h: side });

describe('faceZoom', () => {
  it('28 px 的圆标按像素下限要更大的占比,48 px 的作者头像仍走比例那一档', () => {
    // 两张脸都在画面正中,「摆正」那一档算出来正好是 1 倍:只剩下限和比例在竞争。
    const icon = { ...WORK_ICON, cx: 0.5, cy: 0.5 };
    const mid = { cx: 0.5, cy: 0.5, faceW: 187, imgW: 640, imgH: 960 };
    expect(faceZoom(icon, ring(28), 2)).toBeCloseTo((13 / 28) * 28 / (170 * Math.max(28 / 512, 28 / 512)), 10);
    expect(faceZoom(mid, ring(48), 2)).toBeCloseTo(0.32 * 48 / (187 * Math.max(48 / 640, 48 / 960)), 10);
  });

  it('脸偏在一侧时放大到够得着框心,再由构图上限刹住', () => {
    const ceiling = (face: typeof WORK_ICON) => 0.6 * 28 / (face.faceW * Math.max(28 / 512, 28 / 512));
    expect(faceZoom(WORK_ICON, ring(28), 2)).toBeCloseTo(ceiling(WORK_ICON), 10);
    // 两根轴都靠边时取更紧的那根。这一枚要 4.5 倍才摆得正,构图上限先到。
    expect(faceZoom(WORK_ICON_CORNER, ring(28), 2)).toBeCloseTo(ceiling(WORK_ICON_CORNER), 10);
    // 已经在正中的特写一个像素都不动。
    expect(faceZoom(CLOSE_UP, ring(160), 2)).toBe(1);
  });

  it('先撞到哪条上限就由哪条说话', () => {
    const wanted = (side: number) => 0.32 * side / (67 * Math.max(side / 640, side / 960));
    // 资料页 160 px 圆框、2 倍屏:无损上限先到,脸 67 px 正好还得起 2 倍。
    expect(faceZoom(SMALL_FACE, ring(160), 2)).toBeCloseTo(2, 10);
    // 同一张图换到顶栏 64 px 圆框:无损上限升到 5 倍,改由目标占比说话。
    expect(faceZoom(SMALL_FACE, ring(64), 2)).toBeCloseTo(wanted(64), 10);
    // 1 倍屏要的设备像素少一半,无损上限翻倍到 4,仍由目标占比压住。
    expect(faceZoom(SMALL_FACE, ring(160), 1)).toBeCloseTo(wanted(160), 10);
    expect(faceZoom(CLOSE_UP, ring(160), 2)).toBe(1);
    expect(faceFrame(CLOSE_UP, ring(160), 2)).toBeNull();
  });

  it('远景全身图放到五倍开外,压住它的只有无损上限', () => {
    // `Clair Obscur: Expedition 33` 那张 16:9 封面,脸占画面宽的 4.5%。
    const far = { cx: 0.532, cy: 0.196, faceW: 0.045 * 512, imgW: 512, imgH: 288 };
    const lossless = far.faceW / (far.faceW * Math.max(28 / 512, 28 / 288) * 2);
    expect(faceZoom(far, ring(28), 2)).toBeCloseTo(lossless, 10);
  });
});

describe('faceFrame', () => {
  it('放大后的图盖满框,脸心拉向框心', () => {
    // 脸在画面上方 0.181 处:放大到脸心刚好够得着框心,`top` 正正好是 0。
    expect(faceFrame({ cx: 0.736, cy: 0.181, faceW: 234, imgW: 2184, imgH: 1468 }, ring(160), 2))
      .toEqual({ zoom: 2.762, width: 410.98, height: 276.24, left: -252.48, top: 0 });
    expect(faceFrame(SMALL_FACE, ring(160), 2))
      .toEqual({ zoom: 2, width: 200, height: 300, left: -37.8, top: -17.2 });
    // 方图放大之后比框大,纵向终于挪得动。
    expect(faceFrame({ cx: 0.5, cy: 0.3, faceW: 150, imgW: 1000, imgH: 1000 }, ring(160), 2))
      .toEqual({ zoom: 2.133, width: 213.33, height: 213.33, left: -56.67, top: -14 });
    // 3:4 的框:cover 的基础缩放按更紧的那一边算,两个轴各自夹持。
    expect(faceFrame(SMALL_FACE, { w: 150, h: 200 }, 2))
      .toEqual({ zoom: 2.133, width: 213.33, height: 240, left: -43.65, top: -3.76 });
  });
});

describe('缺数据时退回只挪不放大', () => {
  const noPixels = { cx: 0.4, cy: 0.2 };
  it('缺脸框像素、宽为 0 或整个缺席都不算有脸框;脸心为 0 是真实构图', () => {
    expect(hasFaceBox(SMALL_FACE)).toBe(true);
    expect(hasFaceBox(noPixels)).toBe(false);
    expect(hasFaceBox({ cx: 0.4, cy: 0.2, faceW: 0, imgW: 640, imgH: 960 })).toBe(false);
    expect(hasFaceBox(null)).toBe(false);
    expect(hasFaceBox({ cx: 0, cy: 0, faceW: 67, imgW: 640, imgH: 960 })).toBe(true);
  });

  it('算不出倍数时是 1 倍、没有取景结果', () => {
    expect(faceZoom(noPixels, ring(160), 2)).toBe(1);
    expect(faceFrame(noPixels, ring(160), 2)).toBeNull();
    // 框还没布局(骨架屏、display:none)时宽高是 0。
    expect(faceZoom(SMALL_FACE, { w: 0, h: 0 }, 2)).toBe(1);
  });
});