| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- import { type TBBox, util, type XY } from 'fabric';
- const { makeBoundingBoxFromPoints } = util;
- /**
- * 计算将 `bounds` 平移多少 `{ x, y }` 后,能按选定策略与 `container` 对齐。
- *
- * - **cover**:在宽、高均不小于容器时,避免出现「容器内侧露空」——保持内容仍盖住整个容器(类似 cover)。
- * - **contain**:将 `bounds` 完全限制在容器矩形内,防止越界拖出可视区域。
- * - **auto**:根据 `bounds` 与 `container` 的尺寸关系自动选择:若宽高均不小于容器(在 EPS 容差内)则按 cover,否则按 contain。
- *
- * @param bounds - 待调整的轴对齐矩形(如内容、选区等)。
- * @param container - 轴对齐的裁剪或视口矩形。
- * @param mode - 夹紧策略。`auto` 为默认,按尺寸推断;`cover` / `contain` 为强制策略。
- * @returns 需要施加的平移 `{ x, y }`;若无需移动则返回 `undefined`。
- */
- export function calcTranslationDelta(bounds: TBBox, container: TBBox, mode: 'auto' | 'cover' | 'contain' = 'auto'): XY | void {
- const EPS = 1e-6;
- const covers = mode === 'auto' ? bounds.width >= container.width - EPS && bounds.height >= container.height - EPS : mode === 'cover';
- const bl = bounds.left;
- const bt = bounds.top;
- const br = bounds.left + bounds.width;
- const bb = bounds.top + bounds.height;
- const cl = container.left;
- const ct = container.top;
- const cr = container.left + container.width;
- const cb = container.top + container.height;
- const delta = { x: 0, y: 0 };
- if (covers) {
- if (bl > cl + EPS) delta.x -= bl - cl;
- if (bt > ct + EPS) delta.y -= bt - ct;
- if (br < cr - EPS) delta.x += cr - br;
- if (bb < cb - EPS) delta.y += cb - bb;
- } else {
- if (bl < cl - EPS) delta.x += cl - bl;
- if (bt < ct - EPS) delta.y += ct - bt;
- if (br > cr + EPS) delta.x -= br - cr;
- if (bb > cb + EPS) delta.y -= bb - cb;
- }
- return delta.x === 0 && delta.y === 0 ? void 0 : delta;
- }
- /**
- * 计算将主体四边形约束在容器四边形(带 UV 内边)内所需的一系列修正步骤。
- *
- * 在最多 `refineIters` 次迭代中,依次尝试:按轴对齐包围盒做 contain 平移、将越界角点沿容器四边形 UV 拉回(平移有上限)、仍不满足时相对重心等比缩小。
- * 返回的步骤需按顺序应用;`translate` 的 `x/y` 为平移增量,`scale` 的 `x/y` 为相对重心的缩放系数(当前实现中二者相等)。
- *
- * @param corners - 主体四边形顶点(至少 4 点,顺序与 `container` 一致,按平行四边形处理)。
- * @param container - 容器/视口四边形顶点(至少 4 点)。
- * @param options - 预留配置(当前未使用)。
- * @returns 修正步骤数组;若 `corners` 非法(非数组或点数不足)则返回 `undefined`;已满足约束时可能为空数组。
- */
- export function calcSubjectContainerClampSteps(corners: XY[], container: XY[], options?: {step?: number}) {
- if (!Array.isArray(corners) || corners.length < 4) return;
- if (!Array.isArray(container) || container.length < 4) return;
- const result: Array<{ kind: 'translate' | 'scale' } & XY> = [];
- const containerInset = 0.35;
- const containerNudgeCap = 48;
- const refineIters = options?.step ?? 16;
- const shrink = 0.97;
- const { insetU, insetV } = parallelogramWithInset(container, containerInset);
- for (let step = 0; step < refineIters; step++) {
- if (!subjectViolations(corners, container, insetU, insetV)) break;
- const delta = calcTranslationDelta(makeBoundingBoxFromPoints(corners), makeBoundingBoxFromPoints(container), 'contain');
- if (delta) {
- corners = corners.map((p) => ({ x: p.x + delta.x, y: p.y + delta.y }));
- result.push({ kind: 'translate', ...delta });
- }
- const pull = subjectPullIntoBackdropQuad(corners, container, { cap: containerNudgeCap, inset: containerInset });
- if (pull) {
- corners = corners.map((p) => ({ x: p.x + pull.x, y: p.y + pull.y }));
- result.push({ kind: 'translate', ...pull });
- }
- if (!subjectViolations(corners, container, insetU, insetV)) break;
- const centroid = corners.reduce((c, p) => ({ x: c.x + p.x, y: c.y + p.y }), { x: 0, y: 0 });
- const cx = centroid.x / corners.length;
- const cy = centroid.y / corners.length;
- corners = corners.map((p) => ({ x: cx + shrink * (p.x - cx), y: cy + shrink * (p.y - cy) }));
- result.push({ kind: 'scale', x: shrink, y: shrink });
- }
- return result;
- }
- function parallelogramWithInset(corners: XY[], inset = 0.35) {
- const [tl, tr, br, bl] = corners;
- const e1x = tr.x - tl.x;
- const e1y = tr.y - tl.y;
- const e2x = bl.x - tl.x;
- const e2y = bl.y - tl.y;
- const len1 = Math.hypot(e1x, e1y) || 1;
- const len2 = Math.hypot(e2x, e2y) || 1;
- return {
- tl,
- tr,
- br,
- bl,
- e1x,
- e1y,
- e2x,
- e2y,
- insetU: inset / len1,
- insetV: inset / len2,
- };
- }
- function subjectPullIntoBackdropQuad(corners: XY[], container: XY[], options?: { cap?: number; inset?: number }) {
- const cap = options?.cap ?? 48;
- const { e1x, e1y, e2x, e2y, insetV, insetU } = parallelogramWithInset(container, options?.inset);
- let sx = 0;
- let sy = 0;
- let n = 0;
- for (const p of corners) {
- if (pointInParallelogramInset(container, p, insetU, insetV)) continue;
- const { u, v } = parallelogramUV(container, p);
- const u0 = insetU;
- const u1 = 1 - insetU;
- const v0 = insetV;
- const v1 = 1 - insetV;
- const uc = Math.min(u1, Math.max(u0, u));
- const vc = Math.min(v1, Math.max(v0, v));
- const du = uc - u;
- const dv = vc - v;
- if (du === 0 && dv === 0) continue;
- sx += du * e1x + dv * e2x;
- sy += du * e1y + dv * e2y;
- n++;
- }
- const delta = { x: n === 0 ? 0 : sx / n, y: n === 0 ? 0 : sy / n };
- const mag = Math.hypot(delta.x, delta.y);
- if (mag > cap) {
- const s = cap / mag;
- delta.x *= s;
- delta.y *= s;
- }
- return delta.x === 0 && delta.y === 0 ? void 0 : delta;
- }
- function parallelogramUV(corners: XY[], point: XY): { u: number; v: number } {
- const [tl, tr, br, bl] = corners;
- const e1x = tr.x - tl.x;
- const e1y = tr.y - tl.y;
- const e2x = bl.x - tl.x;
- const e2y = bl.y - tl.y;
- const px = point.x - tl.x;
- const py = point.y - tl.y;
- const det = e1x * e2y - e1y * e2x;
- if (Math.abs(det) < 1e-12) return { u: 0, v: 0 };
- const u = (px * e2y - py * e2x) / det;
- const v = (e1x * py - e1y * px) / det;
- return { u, v };
- }
- function pointInParallelogramInset(corners: XY[], point: XY, insetU: number, insetV: number): boolean {
- const { u, v } = parallelogramUV(corners, point);
- const u0 = insetU;
- const u1 = 1 - insetU;
- const v0 = insetV;
- const v1 = 1 - insetV;
- if (!(u0 <= u1 && v0 <= v1)) return u >= 0 && u <= 1 && v >= 0 && v <= 1;
- return u >= u0 - 1e-9 && u <= u1 + 1e-9 && v >= v0 - 1e-9 && v <= v1 + 1e-9;
- }
- function subjectViolations(corners: XY[], container: XY[], backdropInsetU: number, backdropInsetV: number): boolean {
- for (const p of corners) {
- if (!pointInParallelogramInset(container, p, backdropInsetU, backdropInsetV)) return true;
- }
- return false;
- }
|