import React, { useCallback, useEffect, useRef, useState } from "react"; import { findNodeHandle, ImageBackground, LayoutChangeEvent, } from "react-native"; import { Button, View } from "mazlo-ui"; import checkerboard from "mazlo-ui/images/checkerboard.svg"; import { getScaleRange, getDefaultCrop } from "../../utils/image"; import styles from "./styles"; export type Crop = { x: number; y: number; scale: number; }; type Image = { height: number; width: number; url: string; }; type Props = { crop?: Crop; image: Image; height: number; width: number; frameHeight?: number; onClear?: () => void; onCrop: (crop: Crop) => void; onStartAdjust?: () => void; }; function useWheel(view, handler, params) { const callback = useCallback(handler, params); const ref = useRef(callback); useEffect(() => { ref.current = callback; }, [callback]); useEffect(() => { const elem = findNodeHandle(view) as any; if (!elem) return; const listener = (event) => ref.current(event); elem.addEventListener("wheel", listener, { passive: false }); return () => { elem.removeEventListener("wheel", listener, { passive: false }); }; }, [view]); } const noOffset = { scale: 0, x: 0, y: 0 }; const ImageCrop = ({ crop, image, height, width, onClear, onCrop, onStartAdjust, }: Props) => { const { x, y, scale } = crop || getDefaultCrop(image, height, width); const { min: minScale, max: maxScale } = getScaleRange(image, height, width); const [frameWidth, setFrameWidth] = useState(500); const [adjusting, setAdjusting] = useState(false); const [dragging, setDragging] = useState(false); const [{ scale: dScale, x: dx, y: dy }, setOffset] = useState(noOffset); const frameScale = Math.min(1, frameWidth / width, 400 / height); const offsetScale = scale + dScale; const imageScale = offsetScale * frameScale; const translateX = (dx - x) / offsetScale; const translateY = (dy - y) / offsetScale; const clampOffset = useCallback( (fn) => { setOffset((current) => { const offset = { ...current, ...fn(current) }; const { min, max } = getScaleRange(image, height, width); const clampedScale = Math.max(Math.min(scale + offset.scale, max), min); const minOffsetX = x - image.width * clampedScale + width; const minOffsetY = y - image.height * clampedScale + height; return { scale: clampedScale - scale, x: Math.max(Math.min(offset.x, x), minOffsetX), y: Math.max(Math.min(offset.y, y), minOffsetY), }; }); }, [image, scale, x, y, width, height] ); const container = useRef(); useWheel( container.current, (e: MouseWheelEvent) => { if (!adjusting) return; e.preventDefault(); clampOffset((offset) => ({ scale: offset.scale + e.deltaY / 100 })); }, [adjusting, clampOffset] ); const onMouseDown = useCallback(() => { if (!adjusting) return; setDragging(true); const onMouseMove = (e: MouseEvent) => { e.preventDefault(); clampOffset((offset) => ({ x: offset.x + e.movementX / frameScale, y: offset.y + e.movementY / frameScale, })); }; const onMouseUp = () => { setDragging(false); window.removeEventListener("mousemove", onMouseMove); window.removeEventListener("mouseup", onMouseUp); }; window.addEventListener("mousemove", onMouseMove); window.addEventListener("mouseup", onMouseUp); }, [adjusting, clampOffset]); const onZoom = useCallback( (e) => { const { value } = e.target; clampOffset(() => ({ scale: +value - scale, })); }, [clampOffset, scale] ); const toggleAdjusting = useCallback(() => { setAdjusting((active) => { if (active) { setOffset((offset) => { onCrop({ scale: offsetScale, x: x - offset.x, y: y - offset.y, }); return noOffset; }); } else if (onStartAdjust) { onStartAdjust(); } return !active; }); }, [x, y, offsetScale, onStartAdjust]); const onLayout = useCallback((e: LayoutChangeEvent) => { setFrameWidth(e.nativeEvent.layout.width); }, []); return ( {adjusting && maxScale > minScale && ( )} {!adjusting && !!onClear && (