Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions packages/components/image-viewer/base/ImageItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@ export default defineComponent({
imageReferrerpolicy: String as PropType<TdImageViewerProps['imageReferrerpolicy']>,
},

setup(props) {
setup(props, { expose }) {
const { src, placementSrc, isSvg } = toRefs(props);
const classPrefix = usePrefixClass();
const error = ref(false);
const loaded = ref(false);
const { transform, mouseDownHandler } = useDrag({ translateX: 0, translateY: 0 });
const { transform, mouseDownHandler, resetTransform } = useDrag({ translateX: 0, translateY: 0 });
const { globalConfig } = useConfig('imageViewer');
const errorText = globalConfig.value.errorText;
const svgElRef = ref<HTMLDivElement>();
const modalBoxRef = ref<HTMLDivElement>();

// 暴露内部状态,供父组件在缩放时读写 transform
expose({ modalBoxRef, transform, resetTransform });

const imgStyle = computed(() => ({
transform: `rotate(${props.rotate}deg)`,
Expand Down Expand Up @@ -121,7 +125,7 @@ export default defineComponent({

return () => (
<div class={`${classPrefix.value}-image-viewer__modal-pic`}>
<div class={`${classPrefix.value}-image-viewer__modal-box`} style={boxStyle.value}>
<div ref={modalBoxRef} class={`${classPrefix.value}-image-viewer__modal-box`} style={boxStyle.value}>
{error.value && (
<div class={`${classPrefix.value}-image-viewer__img-error`}>
{/* 脱离文档流 */}
Expand Down
78 changes: 61 additions & 17 deletions packages/components/image-viewer/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { positiveSubtract, positiveAdd } from '@tdesign/common-js/input-number/number';
import { ref, watch } from 'vue';
import { ImageScale } from '../type';
import { throttle } from 'lodash-es';

interface InitTransform {
translateX: number;
Expand All @@ -17,6 +16,7 @@ export function useDrag(initTransform: InitTransform) {

const { pageX: startX, pageY: startY } = e;
const { translateX, translateY } = transform.value;

const mouseMoveHandler = (e: MouseEvent) => {
const { pageX, pageY } = e;
transform.value = {
Expand Down Expand Up @@ -58,34 +58,78 @@ export function useMirror() {
return { mirror, onMirror, resetMirror };
}

export interface ZoomOptions {
/** 缩放中心点 X 坐标(相对于预览图片容器中心的偏移量) */
mouseOffsetX?: number;
/** 缩放中心点 Y 坐标(相对于预览图片容器中心的偏移量) */
mouseOffsetY?: number;
/** 当前位移 */
currentTranslate?: { translateX: number; translateY: number };
}

export interface ZoomResult {
/** 缩放后的新位移 */
newTranslate?: { translateX: number; translateY: number };
}

export function useScale(imageScale: ImageScale) {
const params = { max: 2, min: 0.5, step: 0.2, defaultScale: 1, ...imageScale };
const { max, min, step, defaultScale } = params;
const scale = ref(defaultScale);

const onZoomIn = throttle(() => {
const result = positiveAdd(scale.value, step);
setScale(result);
}, 50);
/**
* 计算缩放后的位移补偿
* 公式:newTranslate = scaleRatio * T + (1 - scaleRatio) * Z
* 其中 Z 为缩放中心,T 为当前位移,scaleRatio = newScale / oldScale
*/
const calculateTranslateOffset = (
oldScale: number,
newScale: number,
options?: ZoomOptions,
): { translateX: number; translateY: number } | 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,
};
};

const onZoomOut = throttle(() => {
const result = positiveSubtract(scale.value, step);
setScale(result);
}, 50);
const onZoomIn = (options?: ZoomOptions): ZoomResult => {
const oldScale = scale.value;
const result = positiveAdd(oldScale, step);
const newScale = Math.min(result, max);
setScale(newScale);

return {
newTranslate: calculateTranslateOffset(oldScale, newScale, options),
};
};

const onZoomOut = (options?: ZoomOptions): ZoomResult => {
const oldScale = scale.value;
const result = positiveSubtract(oldScale, step);
const newScale = Math.max(result, min);
setScale(newScale);

return {
newTranslate: calculateTranslateOffset(oldScale, newScale, options),
};
};

const resetScale = () => {
scale.value = defaultScale;
};

const setScale = (newScale: number) => {
let value = newScale;
if (newScale < min) {
value = min;
}
if (newScale > max) {
value = max;
}
scale.value = value;
scale.value = Math.max(min, Math.min(max, newScale));
};

watch(
Expand Down
52 changes: 49 additions & 3 deletions packages/components/image-viewer/image-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,12 @@ export default defineComponent({
const { mirror, onMirror, resetMirror } = useMirror();
const { scale, onZoomIn, onZoomOut, resetScale } = useScale(props.imageScale as ImageScale);
const { rotate, onRotate, resetRotate } = useRotate();

const onRest = () => {
resetMirror();
resetScale();
resetRotate();
imageItemRef.value?.resetTransform?.();
};

const images = computed(() => formatImages(props.images));
Expand Down Expand Up @@ -117,7 +119,6 @@ export default defineComponent({
onClose({ e, trigger: 'overlay' });
}
};

const keydownHandler = (e: KeyboardEvent) => {
e.stopPropagation();

Expand Down Expand Up @@ -145,6 +146,12 @@ export default defineComponent({
};

const divRef = ref<HTMLDivElement>();
const imageItemRef = ref<{
modalBoxRef?: HTMLDivElement;
transform?: { translateX: number; translateY: number };
resetTransform?: () => void;
}>();

watch(
() => visibleValue.value,
(val) => {
Expand All @@ -169,10 +176,48 @@ export default defineComponent({
clearTimeout(animationTimer.value);
});

// 检测图片是否超出视口
const isImageExceedsViewport = (container: HTMLDivElement, modalBox: HTMLDivElement): 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
);
};

// 滚轮缩放
const onWheel = (e: WheelEvent) => {
e.preventDefault();
const { deltaY } = e;
deltaY > 0 ? onZoomOut() : onZoomIn();
const isZoomOut = e.deltaY > 0;

const container = divRef.value;
const modalBox = imageItemRef.value?.modalBoxRef;

// 无视口信息时,直接缩放
if (!container || !modalBox) {
isZoomOut ? onZoomOut() : onZoomIn();
return;
}

// 缩小且图片超出视口:以视口中心为基准,向视口中心收敛
if (isZoomOut && isImageExceedsViewport(container, modalBox)) {
const currentTranslate = imageItemRef.value?.transform ?? { translateX: 0, translateY: 0 };

const result = onZoomOut({
mouseOffsetX: 0,
mouseOffsetY: 0,
currentTranslate,
});
if (result?.newTranslate) {
imageItemRef.value.transform = result.newTranslate;
}
} else {
// 正常缩放
isZoomOut ? onZoomOut() : onZoomIn();
}
};

const transStyle = computed(() => ({
Expand Down Expand Up @@ -340,6 +385,7 @@ export default defineComponent({
currentImage={currentImage.value}
/>
<TImageItem
ref={imageItemRef}
scale={scale.value}
rotate={rotate.value}
mirror={mirror.value}
Expand Down
Loading