1
0
Fork 0
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:
Fedor Katurov 2023-11-04 20:33:33 +06:00
parent 73225b166f
commit 2d7999d9dc
3 changed files with 153 additions and 30 deletions

View file

@ -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 };

View 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 };

View file

@ -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>