diff --git a/js/image-viewer/transform.ts b/js/image-viewer/transform.ts new file mode 100644 index 0000000000..afee7fe4a6 --- /dev/null +++ b/js/image-viewer/transform.ts @@ -0,0 +1,137 @@ +import { positiveAdd, positiveSubtract } from '../input-number/number'; +import type { ZoomOptions, ZoomResult, TranslateOffset, ImageScale } from './types'; + +export type { ZoomOptions, ZoomResult, TranslateOffset, ImageScale }; + +/** ImageScale 的默认值,所有使用处应引用此常量,避免多处声明不一致 */ +export const DEFAULT_IMAGE_SCALE: ImageScale = { + max: 2, + min: 0.5, + step: 0.2, + defaultScale: 1, +}; + +/** + * 检测图片是否超出视口(容器)边界 + * @param container 外层容器元素 + * @param modalBox 图片包裹元素 + */ +export const isImageExceedsViewport = (container: HTMLElement, modalBox: HTMLElement): boolean => { + const containerRect = container.getBoundingClientRect(); + const modalRect = modalBox.getBoundingClientRect(); + return ( + modalRect.left < containerRect.left || + modalRect.right > containerRect.right || + modalRect.top < containerRect.top || + modalRect.bottom > containerRect.bottom + ); +}; + +/** 镜像默认值(未镜像) */ +export const MIRROR_DEFAULT = 1; + +/** 切换镜像状态:1 → -1,-1 → 1 */ +export const toggleMirror = (current: number): number => (current > 0 ? -1 : 1); + +/** 每次旋转的角度(逆时针 90°) */ +export const ROTATE_DEG = -90; + +/** + * 计算最短路径归零的旋转补偿值 + * 用于 resetRotate 场景:避免 CSS transition 时图片"倒转一大圈" + * + * @param currentDeg 当前累计旋转角度 + * @returns 需要减去的补偿值(rotate.value -= 返回值 即可归零) + * + * @example + * // currentDeg = -270 → return 90(-270 - 90 = -360 ≡ 0°) + * // currentDeg = -180 → return -180(-180 - (-180) = 0) + * // currentDeg = 0 → return 0(无需旋转) + */ +export function calcResetRotation(currentDeg: number): number { + const degreeToRotate = currentDeg % 360; + if (degreeToRotate === 0) return 0; + // 找最短方向旋转回 0° + return Math.abs(degreeToRotate) > 180 ? (degreeToRotate + 360) % 360 : degreeToRotate; +} + +/** 将缩放值限制在 [min, max] 范围内 */ +export function clampScale(value: number, min: number, max: number): number { + return Math.max(min, Math.min(max, value)); +} + +/** + * 计算放大后的新 scale 值(使用精确浮点数加法) + * @returns clamp 后的新 scale 值 + */ +export function calcZoomInScale(oldScale: number, step: number, min: number, max: number): number { + return clampScale(positiveAdd(oldScale, step), min, max); +} + +/** + * 计算缩小后的新 scale 值(使用精确浮点数减法) + * @returns clamp 后的新 scale 值 + */ +export function calcZoomOutScale(oldScale: number, step: number, min: number, max: number): number { + return clampScale(positiveSubtract(oldScale, step), min, max); +} + +/** + * 计算缩放后的位移补偿 + * 公式:newTranslate = scaleRatio * T + (1 - scaleRatio) * Z + * 其中 Z 为缩放中心,T 为当前位移,scaleRatio = newScale / oldScale + */ +export function calculateTranslateOffset( + oldScale: number, + newScale: number, + options?: ZoomOptions +): TranslateOffset | undefined { + if (options?.mouseOffsetX == null || options?.mouseOffsetY == null) { + return undefined; + } + + const scaleRatio = newScale / oldScale; + const { translateX = 0, translateY = 0 } = options?.currentTranslate ?? {}; + const { mouseOffsetX, mouseOffsetY } = options; + + return { + translateX: scaleRatio * translateX + (1 - scaleRatio) * mouseOffsetX, + translateY: scaleRatio * translateY + (1 - scaleRatio) * mouseOffsetY, + }; +} + +/** + * 执行一次 zoom in 并计算位移补偿 + * @returns { newScale, zoomResult } + */ +export function zoomIn( + oldScale: number, + step: number, + min: number, + max: number, + options?: ZoomOptions +): { newScale: number; zoomResult: ZoomResult } { + const newScale = calcZoomInScale(oldScale, step, min, max); + return { + newScale, + zoomResult: { newTranslate: calculateTranslateOffset(oldScale, newScale, options) }, + }; +} + +/** + * 执行一次 zoom out 并计算位移补偿 + * @returns { newScale, zoomResult } + */ +export function zoomOut( + oldScale: number, + step: number, + min: number, + max: number, + options?: ZoomOptions +): { newScale: number; zoomResult: ZoomResult } { + const newScale = calcZoomOutScale(oldScale, step, min, max); + return { + newScale, + zoomResult: { newTranslate: calculateTranslateOffset(oldScale, newScale, options) }, + }; +} diff --git a/js/image-viewer/types.d.ts b/js/image-viewer/types.d.ts deleted file mode 100644 index daef641437..0000000000 --- a/js/image-viewer/types.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface ImageInfo { - mainImage: string | File; - thumbnail?: string | File; - download?: boolean; - isSvg?: boolean; -} - -export type Images = Array; diff --git a/js/image-viewer/types.ts b/js/image-viewer/types.ts new file mode 100644 index 0000000000..f8a77f473e --- /dev/null +++ b/js/image-viewer/types.ts @@ -0,0 +1,42 @@ +export interface ImageInfo { + mainImage: string | File; + thumbnail?: string | File; + download?: boolean; + isSvg?: boolean; +} + +export type Images = Array; + +/** 位移偏移量 */ +export interface TranslateOffset { + translateX: number; + translateY: number; +} + +/** 缩放选项 */ +export interface ZoomOptions { + /** 缩放中心点 X 坐标(相对于预览图片容器中心的偏移量) */ + mouseOffsetX?: number; + /** 缩放中心点 Y 坐标(相对于预览图片容器中心的偏移量) */ + mouseOffsetY?: number; + /** 当前位移 */ + currentTranslate?: TranslateOffset; +} + +/** 缩放结果 */ +export interface ZoomResult { + /** 缩放后的新位移 */ + newTranslate?: TranslateOffset; +} + +/** 图片缩放配置 */ +export interface ImageScale { + /** 缩放的最大比例 */ + max: number; + /** 缩放的最小比例 */ + min: number; + /** 缩放的步长速度 */ + step: number; + /** 默认的缩放比例 */ + defaultScale: number; +}