mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
enable pitch zoom
This commit is contained in:
parent
73225b166f
commit
2d7999d9dc
3 changed files with 153 additions and 30 deletions
|
@ -1,4 +1,10 @@
|
||||||
import React, { CSSProperties, FC, useMemo, useReducer } from 'react';
|
import React, {
|
||||||
|
CSSProperties,
|
||||||
|
ReactNode,
|
||||||
|
forwardRef,
|
||||||
|
useMemo,
|
||||||
|
useReducer,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
@ -8,17 +14,14 @@ import { DivProps } from '~/utils/types';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
interface ImageLoadingWrapperProps extends Omit<DivProps, 'children'> {
|
interface ImageLoadingWrapperProps extends Omit<DivProps, 'children'> {
|
||||||
children: (props: { loading: boolean; onLoad: () => void }) => void;
|
children: (props: { loading: boolean; onLoad: () => void }) => ReactNode;
|
||||||
preview?: string;
|
preview?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageLoadingWrapper: FC<ImageLoadingWrapperProps> = ({
|
const ImageLoadingWrapper = forwardRef<
|
||||||
className,
|
HTMLDivElement,
|
||||||
children,
|
ImageLoadingWrapperProps
|
||||||
preview,
|
>(({ className, children, preview, color, ...props }, ref) => {
|
||||||
color,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const [loading, onLoad] = useReducer(() => false, true);
|
const [loading, onLoad] = useReducer(() => false, true);
|
||||||
|
|
||||||
const style = useMemo<CSSProperties>(
|
const style = useMemo<CSSProperties>(
|
||||||
|
@ -30,7 +33,7 @@ const ImageLoadingWrapper: FC<ImageLoadingWrapperProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.wrapper, className)} {...props}>
|
<div className={classNames(styles.wrapper, className)} {...props} ref={ref}>
|
||||||
{!!loading && !!preview && (
|
{!!loading && !!preview && (
|
||||||
<div className={styles.preview}>
|
<div className={styles.preview}>
|
||||||
<div className={styles.thumbnail} style={style} />
|
<div className={styles.thumbnail} style={style} />
|
||||||
|
@ -40,6 +43,6 @@ const ImageLoadingWrapper: FC<ImageLoadingWrapperProps> = ({
|
||||||
{children({ loading, onLoad })}
|
{children({ loading, onLoad })}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export { ImageLoadingWrapper };
|
export { ImageLoadingWrapper };
|
||||||
|
|
108
src/components/media/PinchZoom/index.tsx
Normal file
108
src/components/media/PinchZoom/index.tsx
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import {
|
||||||
|
FC,
|
||||||
|
ReactElement,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: (props: {
|
||||||
|
setRef: (ref: HTMLElement | null) => void;
|
||||||
|
}) => ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDistance = (event: TouchEvent) => {
|
||||||
|
return Math.hypot(
|
||||||
|
event.touches[0].pageX - event.touches[1].pageX,
|
||||||
|
event.touches[0].pageY - event.touches[1].pageY,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Start {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
distance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PinchZoom: FC<Props> = ({ children }) => {
|
||||||
|
const start = useRef<Start>({ x: 0, y: 0, distance: 0 });
|
||||||
|
const [ref, setRef] = useState<HTMLElement | null>(null);
|
||||||
|
const imageElementScale = useRef(1);
|
||||||
|
|
||||||
|
const onTouchStart = useCallback((event: TouchEvent) => {
|
||||||
|
if (event.touches.length !== 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault(); // Prevent page scroll
|
||||||
|
|
||||||
|
// Calculate where the fingers have started on the X and Y axis
|
||||||
|
start.current.x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
|
||||||
|
start.current.y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
|
||||||
|
start.current.distance = getDistance(event);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onTouchMove = useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (event.touches.length !== 2 || !ref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault(); // Prevent page scroll
|
||||||
|
|
||||||
|
// Safari provides event.scale as two fingers move on the screen
|
||||||
|
// For other browsers just calculate the scale manually
|
||||||
|
const scale = event.scale ?? getDistance(event) / start.current.distance;
|
||||||
|
imageElementScale.current = Math.min(Math.max(1, scale), 4);
|
||||||
|
|
||||||
|
// Calculate how much the fingers have moved on the X and Y axis
|
||||||
|
const deltaX =
|
||||||
|
((event.touches[0].pageX + event.touches[1].pageX) / 2 -
|
||||||
|
start.current.x) *
|
||||||
|
2; // x2 for accelarated movement
|
||||||
|
const deltaY =
|
||||||
|
((event.touches[0].pageY + event.touches[1].pageY) / 2 -
|
||||||
|
start.current.y) *
|
||||||
|
2; // x2 for accelarated movement
|
||||||
|
|
||||||
|
// Transform the image to make it grow and move with fingers
|
||||||
|
const transform = `translate3d(${deltaX}px, ${deltaY}px, 0) scale(${imageElementScale})`;
|
||||||
|
ref.style.transform = transform;
|
||||||
|
ref.style.zIndex = '9999';
|
||||||
|
},
|
||||||
|
[ref],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onTouchEnd = useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (!ref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset image to it's original format
|
||||||
|
ref.style.transform = '';
|
||||||
|
ref.style.zIndex = '';
|
||||||
|
},
|
||||||
|
[ref],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.addEventListener('touchstart', onTouchStart);
|
||||||
|
ref.addEventListener('touchmove', onTouchMove);
|
||||||
|
ref.addEventListener('touchend', onTouchEnd);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ref.removeEventListener('touchstart', onTouchStart);
|
||||||
|
ref.removeEventListener('touchmove', onTouchMove);
|
||||||
|
ref.removeEventListener('touchend', onTouchEnd);
|
||||||
|
};
|
||||||
|
}, [onTouchEnd, onTouchMove, onTouchStart, ref]);
|
||||||
|
|
||||||
|
return children({ setRef });
|
||||||
|
};
|
||||||
|
|
||||||
|
export { PinchZoom };
|
|
@ -7,6 +7,7 @@ import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
import SwiperClass from 'swiper/types/swiper-class';
|
import SwiperClass from 'swiper/types/swiper-class';
|
||||||
|
|
||||||
import { ImageLoadingWrapper } from '~/components/common/ImageLoadingWrapper/index';
|
import { ImageLoadingWrapper } from '~/components/common/ImageLoadingWrapper/index';
|
||||||
|
import { PinchZoom } from '~/components/media/PinchZoom';
|
||||||
import { NodeComponentProps } from '~/constants/node';
|
import { NodeComponentProps } from '~/constants/node';
|
||||||
import { imagePresets } from '~/constants/urls';
|
import { imagePresets } from '~/constants/urls';
|
||||||
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
||||||
|
@ -100,26 +101,37 @@ const NodeImageSwiperBlock: FC<IProps> = observer(({ node }) => {
|
||||||
>
|
>
|
||||||
{images.map((file, index) => (
|
{images.map((file, index) => (
|
||||||
<SwiperSlide className={styles.slide} key={file.id}>
|
<SwiperSlide className={styles.slide} key={file.id}>
|
||||||
<ImageLoadingWrapper
|
<PinchZoom>
|
||||||
preview={getURL(file, imagePresets['300'])}
|
{({ setRef }) => (
|
||||||
color={file.metadata?.dominant_color}
|
<ImageLoadingWrapper
|
||||||
>
|
preview={getURL(file, imagePresets['300'])}
|
||||||
{({ loading, onLoad }) => (
|
color={file.metadata?.dominant_color}
|
||||||
<NodeImageLazy
|
ref={setRef}
|
||||||
src={getURL(file)}
|
>
|
||||||
width={file.metadata?.width}
|
{({ loading, onLoad }) => (
|
||||||
height={file.metadata?.height}
|
<NodeImageLazy
|
||||||
color={normalizeBrightColor(file?.metadata?.dominant_color)}
|
src={getURL(file)}
|
||||||
onLoad={onLoad}
|
width={file.metadata?.width}
|
||||||
onClick={() => onOpenPhotoSwipe(index)}
|
height={file.metadata?.height}
|
||||||
className={classNames(styles.image, 'swiper-lazy', {
|
color={normalizeBrightColor(
|
||||||
[styles.loading]: loading,
|
file?.metadata?.dominant_color,
|
||||||
})}
|
)}
|
||||||
sizes={getNodeSwiperImageSizes(file, innerWidth, innerHeight)}
|
onLoad={onLoad}
|
||||||
quality={90}
|
onClick={() => onOpenPhotoSwipe(index)}
|
||||||
/>
|
className={classNames(styles.image, 'swiper-lazy', {
|
||||||
|
[styles.loading]: loading,
|
||||||
|
})}
|
||||||
|
sizes={getNodeSwiperImageSizes(
|
||||||
|
file,
|
||||||
|
innerWidth,
|
||||||
|
innerHeight,
|
||||||
|
)}
|
||||||
|
quality={90}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ImageLoadingWrapper>
|
||||||
)}
|
)}
|
||||||
</ImageLoadingWrapper>
|
</PinchZoom>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
</Swiper>
|
</Swiper>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue