react-easy-crop
Version:
A React component to crop images/videos with easy interactions
1 lines • 86.5 kB
Source Map (JSON)
{"version":3,"file":"index.module.mjs","sources":["../src/helpers.ts","../src/Cropper.tsx"],"sourcesContent":["import { Area, MediaSize, Point, Size } from './types'\n\n/**\n * Compute the dimension of the crop area based on media size,\n * aspect ratio and optionally rotation\n */\nexport function getCropSize(\n mediaWidth: number,\n mediaHeight: number,\n containerWidth: number,\n containerHeight: number,\n aspect: number,\n rotation = 0\n): Size {\n const { width, height } = rotateSize(mediaWidth, mediaHeight, rotation)\n const fittingWidth = Math.min(width, containerWidth)\n const fittingHeight = Math.min(height, containerHeight)\n\n if (fittingWidth > fittingHeight * aspect) {\n return {\n width: fittingHeight * aspect,\n height: fittingHeight,\n }\n }\n\n return {\n width: fittingWidth,\n height: fittingWidth / aspect,\n }\n}\n\n/**\n * Compute media zoom.\n * We fit the media into the container with \"max-width: 100%; max-height: 100%;\"\n */\nexport function getMediaZoom(mediaSize: MediaSize) {\n // Take the axis with more pixels to improve accuracy\n return mediaSize.width > mediaSize.height\n ? mediaSize.width / mediaSize.naturalWidth\n : mediaSize.height / mediaSize.naturalHeight\n}\n\n/**\n * Ensure a new media position stays in the crop area.\n */\nexport function restrictPosition(\n position: Point,\n mediaSize: Size,\n cropSize: Size,\n zoom: number,\n rotation = 0\n): Point {\n const { width, height } = rotateSize(mediaSize.width, mediaSize.height, rotation)\n\n return {\n x: restrictPositionCoord(position.x, width, cropSize.width, zoom),\n y: restrictPositionCoord(position.y, height, cropSize.height, zoom),\n }\n}\n\nfunction restrictPositionCoord(\n position: number,\n mediaSize: number,\n cropSize: number,\n zoom: number\n): number {\n const maxPosition = (mediaSize * zoom) / 2 - cropSize / 2\n\n return clamp(position, -maxPosition, maxPosition)\n}\n\nexport function getDistanceBetweenPoints(pointA: Point, pointB: Point) {\n return Math.sqrt(Math.pow(pointA.y - pointB.y, 2) + Math.pow(pointA.x - pointB.x, 2))\n}\n\nexport function getRotationBetweenPoints(pointA: Point, pointB: Point) {\n return (Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180) / Math.PI\n}\n\n/**\n * Compute the output cropped area of the media in percentages and pixels.\n * x/y are the top-left coordinates on the src media\n */\nexport function computeCroppedArea(\n crop: Point,\n mediaSize: MediaSize,\n cropSize: Size,\n aspect: number,\n zoom: number,\n rotation = 0,\n restrictPosition = true\n): { croppedAreaPercentages: Area; croppedAreaPixels: Area } {\n // if the media is rotated by the user, we cannot limit the position anymore\n // as it might need to be negative.\n const limitAreaFn = restrictPosition ? limitArea : noOp\n\n const mediaBBoxSize = rotateSize(mediaSize.width, mediaSize.height, rotation)\n const mediaNaturalBBoxSize = rotateSize(mediaSize.naturalWidth, mediaSize.naturalHeight, rotation)\n\n // calculate the crop area in percentages\n // in the rotated space\n const croppedAreaPercentages = {\n x: limitAreaFn(\n 100,\n (((mediaBBoxSize.width - cropSize.width / zoom) / 2 - crop.x / zoom) / mediaBBoxSize.width) *\n 100\n ),\n y: limitAreaFn(\n 100,\n (((mediaBBoxSize.height - cropSize.height / zoom) / 2 - crop.y / zoom) /\n mediaBBoxSize.height) *\n 100\n ),\n width: limitAreaFn(100, ((cropSize.width / mediaBBoxSize.width) * 100) / zoom),\n height: limitAreaFn(100, ((cropSize.height / mediaBBoxSize.height) * 100) / zoom),\n }\n\n // we compute the pixels size naively\n const widthInPixels = Math.round(\n limitAreaFn(\n mediaNaturalBBoxSize.width,\n (croppedAreaPercentages.width * mediaNaturalBBoxSize.width) / 100\n )\n )\n const heightInPixels = Math.round(\n limitAreaFn(\n mediaNaturalBBoxSize.height,\n (croppedAreaPercentages.height * mediaNaturalBBoxSize.height) / 100\n )\n )\n const isImgWiderThanHigh = mediaNaturalBBoxSize.width >= mediaNaturalBBoxSize.height * aspect\n\n // then we ensure the width and height exactly match the aspect (to avoid rounding approximations)\n // if the media is wider than high, when zoom is 0, the crop height will be equals to image height\n // thus we want to compute the width from the height and aspect for accuracy.\n // Otherwise, we compute the height from width and aspect.\n const sizePixels = isImgWiderThanHigh\n ? {\n width: Math.round(heightInPixels * aspect),\n height: heightInPixels,\n }\n : {\n width: widthInPixels,\n height: Math.round(widthInPixels / aspect),\n }\n\n const croppedAreaPixels = {\n ...sizePixels,\n x: Math.round(\n limitAreaFn(\n mediaNaturalBBoxSize.width - sizePixels.width,\n (croppedAreaPercentages.x * mediaNaturalBBoxSize.width) / 100\n )\n ),\n y: Math.round(\n limitAreaFn(\n mediaNaturalBBoxSize.height - sizePixels.height,\n (croppedAreaPercentages.y * mediaNaturalBBoxSize.height) / 100\n )\n ),\n }\n\n return { croppedAreaPercentages, croppedAreaPixels }\n}\n\n/**\n * Ensure the returned value is between 0 and max\n */\nfunction limitArea(max: number, value: number): number {\n return Math.min(max, Math.max(0, value))\n}\n\nfunction noOp(_max: number, value: number) {\n return value\n}\n\n/**\n * Compute crop and zoom from the croppedAreaPercentages.\n */\nexport function getInitialCropFromCroppedAreaPercentages(\n croppedAreaPercentages: Area,\n mediaSize: MediaSize,\n rotation: number,\n cropSize: Size,\n minZoom: number,\n maxZoom: number\n) {\n const mediaBBoxSize = rotateSize(mediaSize.width, mediaSize.height, rotation)\n\n // This is the inverse process of computeCroppedArea\n const zoom = clamp(\n (cropSize.width / mediaBBoxSize.width) * (100 / croppedAreaPercentages.width),\n minZoom,\n maxZoom\n )\n\n const crop = {\n x:\n (zoom * mediaBBoxSize.width) / 2 -\n cropSize.width / 2 -\n mediaBBoxSize.width * zoom * (croppedAreaPercentages.x / 100),\n y:\n (zoom * mediaBBoxSize.height) / 2 -\n cropSize.height / 2 -\n mediaBBoxSize.height * zoom * (croppedAreaPercentages.y / 100),\n }\n\n return { crop, zoom }\n}\n\n/**\n * Compute zoom from the croppedAreaPixels\n */\nfunction getZoomFromCroppedAreaPixels(\n croppedAreaPixels: Area,\n mediaSize: MediaSize,\n cropSize: Size\n): number {\n const mediaZoom = getMediaZoom(mediaSize)\n\n return cropSize.height > cropSize.width\n ? cropSize.height / (croppedAreaPixels.height * mediaZoom)\n : cropSize.width / (croppedAreaPixels.width * mediaZoom)\n}\n\n/**\n * Compute crop and zoom from the croppedAreaPixels\n */\nexport function getInitialCropFromCroppedAreaPixels(\n croppedAreaPixels: Area,\n mediaSize: MediaSize,\n rotation = 0,\n cropSize: Size,\n minZoom: number,\n maxZoom: number\n): { crop: Point; zoom: number } {\n const mediaNaturalBBoxSize = rotateSize(mediaSize.naturalWidth, mediaSize.naturalHeight, rotation)\n\n const zoom = clamp(\n getZoomFromCroppedAreaPixels(croppedAreaPixels, mediaSize, cropSize),\n minZoom,\n maxZoom\n )\n\n const cropZoom =\n cropSize.height > cropSize.width\n ? cropSize.height / croppedAreaPixels.height\n : cropSize.width / croppedAreaPixels.width\n\n const crop = {\n x:\n ((mediaNaturalBBoxSize.width - croppedAreaPixels.width) / 2 - croppedAreaPixels.x) * cropZoom,\n y:\n ((mediaNaturalBBoxSize.height - croppedAreaPixels.height) / 2 - croppedAreaPixels.y) *\n cropZoom,\n }\n return { crop, zoom }\n}\n\n/**\n * Return the point that is the center of point a and b\n */\nexport function getCenter(a: Point, b: Point): Point {\n return {\n x: (b.x + a.x) / 2,\n y: (b.y + a.y) / 2,\n }\n}\n\nexport function getRadianAngle(degreeValue: number) {\n return (degreeValue * Math.PI) / 180\n}\n\n/**\n * Returns the new bounding area of a rotated rectangle.\n */\nexport function rotateSize(width: number, height: number, rotation: number): Size {\n const rotRad = getRadianAngle(rotation)\n\n return {\n width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),\n height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),\n }\n}\n\n/**\n * Clamp value between min and max\n */\nexport function clamp(value: number, min: number, max: number) {\n return Math.min(Math.max(value, min), max)\n}\n\n/**\n * Combine multiple class names into a single string.\n */\nexport function classNames(...args: (boolean | string | number | undefined | void | null)[]) {\n return args\n .filter((value) => {\n if (typeof value === 'string' && value.length > 0) {\n return true\n }\n\n return false\n })\n .join(' ')\n .trim()\n}\n","import * as React from 'react'\nimport normalizeWheel from 'normalize-wheel'\nimport { Area, MediaSize, Point, Size, VideoSrc } from './types'\nimport {\n getCropSize,\n restrictPosition,\n getDistanceBetweenPoints,\n getRotationBetweenPoints,\n computeCroppedArea,\n getCenter,\n getInitialCropFromCroppedAreaPixels,\n getInitialCropFromCroppedAreaPercentages,\n classNames,\n clamp,\n} from './helpers'\nimport cssStyles from './styles.css'\n\nexport type CropperProps = {\n image?: string\n video?: string | VideoSrc[]\n transform?: string\n crop: Point\n zoom: number\n rotation: number\n aspect: number\n minZoom: number\n maxZoom: number\n cropShape: 'rect' | 'round'\n cropSize?: Size\n objectFit?: 'contain' | 'cover' | 'horizontal-cover' | 'vertical-cover'\n showGrid?: boolean\n zoomSpeed: number\n zoomWithScroll?: boolean\n roundCropAreaPixels?: boolean\n onCropChange: (location: Point) => void\n onZoomChange?: (zoom: number) => void\n onRotationChange?: (rotation: number) => void\n onCropComplete?: (croppedArea: Area, croppedAreaPixels: Area) => void\n onCropAreaChange?: (croppedArea: Area, croppedAreaPixels: Area) => void\n onCropSizeChange?: (cropSize: Size) => void\n onInteractionStart?: () => void\n onInteractionEnd?: () => void\n onMediaLoaded?: (mediaSize: MediaSize) => void\n style: {\n containerStyle?: React.CSSProperties\n mediaStyle?: React.CSSProperties\n cropAreaStyle?: React.CSSProperties\n }\n classes: {\n containerClassName?: string\n mediaClassName?: string\n cropAreaClassName?: string\n }\n restrictPosition: boolean\n mediaProps: React.ImgHTMLAttributes<HTMLElement> | React.VideoHTMLAttributes<HTMLElement>\n cropperProps: React.HTMLAttributes<HTMLDivElement>\n disableAutomaticStylesInjection?: boolean\n initialCroppedAreaPixels?: Area\n initialCroppedAreaPercentages?: Area\n onTouchRequest?: (e: React.TouchEvent<HTMLDivElement>) => boolean\n onWheelRequest?: (e: WheelEvent) => boolean\n setCropperRef?: (ref: React.RefObject<HTMLDivElement>) => void\n setImageRef?: (ref: React.RefObject<HTMLImageElement>) => void\n setVideoRef?: (ref: React.RefObject<HTMLVideoElement>) => void\n setMediaSize?: (size: MediaSize) => void\n setCropSize?: (size: Size) => void\n nonce?: string\n keyboardStep: number\n}\n\ntype State = {\n cropSize: Size | null\n hasWheelJustStarted: boolean\n mediaObjectFit: String | undefined\n}\n\nconst MIN_ZOOM = 1\nconst MAX_ZOOM = 3\nconst KEYBOARD_STEP = 1\n\ntype GestureEvent = UIEvent & {\n rotation: number\n scale: number\n clientX: number\n clientY: number\n}\n\nclass Cropper extends React.Component<CropperProps, State> {\n static defaultProps = {\n zoom: 1,\n rotation: 0,\n aspect: 4 / 3,\n maxZoom: MAX_ZOOM,\n minZoom: MIN_ZOOM,\n cropShape: 'rect' as const,\n objectFit: 'contain' as const,\n showGrid: true,\n style: {},\n classes: {},\n mediaProps: {},\n cropperProps: {},\n zoomSpeed: 1,\n restrictPosition: true,\n zoomWithScroll: true,\n keyboardStep: KEYBOARD_STEP,\n }\n\n cropperRef: React.RefObject<HTMLDivElement> = React.createRef()\n imageRef: React.RefObject<HTMLImageElement> = React.createRef()\n videoRef: React.RefObject<HTMLVideoElement> = React.createRef()\n containerPosition: Point = { x: 0, y: 0 }\n containerRef: HTMLDivElement | null = null\n styleRef: HTMLStyleElement | null = null\n containerRect: DOMRect | null = null\n mediaSize: MediaSize = { width: 0, height: 0, naturalWidth: 0, naturalHeight: 0 }\n dragStartPosition: Point = { x: 0, y: 0 }\n dragStartCrop: Point = { x: 0, y: 0 }\n gestureZoomStart = 0\n gestureRotationStart = 0\n isTouching = false\n lastPinchDistance = 0\n lastPinchRotation = 0\n rafDragTimeout: number | null = null\n rafPinchTimeout: number | null = null\n wheelTimer: number | null = null\n currentDoc: Document | null = typeof document !== 'undefined' ? document : null\n currentWindow: Window | null = typeof window !== 'undefined' ? window : null\n resizeObserver: ResizeObserver | null = null\n previousCropSize: Size | null = null\n isInitialized = false\n\n state: State = {\n cropSize: null,\n hasWheelJustStarted: false,\n mediaObjectFit: undefined,\n }\n\n componentDidMount() {\n if (!this.currentDoc || !this.currentWindow) return\n if (this.containerRef) {\n if (this.containerRef.ownerDocument) {\n this.currentDoc = this.containerRef.ownerDocument\n }\n if (this.currentDoc.defaultView) {\n this.currentWindow = this.currentDoc.defaultView\n }\n\n this.initResizeObserver()\n // only add window resize listener if ResizeObserver is not supported. Otherwise, it would be redundant\n if (typeof window.ResizeObserver === 'undefined') {\n this.currentWindow.addEventListener('resize', this.computeSizes)\n }\n this.props.zoomWithScroll &&\n this.containerRef.addEventListener('wheel', this.onWheel, { passive: false })\n this.containerRef.addEventListener('gesturestart', this.onGestureStart as EventListener)\n }\n\n this.currentDoc.addEventListener('scroll', this.onScroll)\n\n if (!this.props.disableAutomaticStylesInjection) {\n this.styleRef = this.currentDoc.createElement('style')\n this.styleRef.setAttribute('type', 'text/css')\n if (this.props.nonce) {\n this.styleRef.setAttribute('nonce', this.props.nonce)\n }\n this.styleRef.innerHTML = cssStyles\n this.currentDoc.head.appendChild(this.styleRef)\n }\n\n // when rendered via SSR, the image can already be loaded and its onLoad callback will never be called\n if (this.imageRef.current && this.imageRef.current.complete) {\n this.onMediaLoad()\n }\n\n // set image and video refs in the parent if the callbacks exist\n if (this.props.setImageRef) {\n this.props.setImageRef(this.imageRef)\n }\n\n if (this.props.setVideoRef) {\n this.props.setVideoRef(this.videoRef)\n }\n\n if (this.props.setCropperRef) {\n this.props.setCropperRef(this.cropperRef)\n }\n }\n\n componentWillUnmount() {\n if (!this.currentDoc || !this.currentWindow) return\n if (typeof window.ResizeObserver === 'undefined') {\n this.currentWindow.removeEventListener('resize', this.computeSizes)\n }\n this.resizeObserver?.disconnect()\n if (this.containerRef) {\n this.containerRef.removeEventListener('gesturestart', this.preventZoomSafari)\n }\n\n if (this.styleRef) {\n this.styleRef.parentNode?.removeChild(this.styleRef)\n }\n\n this.cleanEvents()\n this.props.zoomWithScroll && this.clearScrollEvent()\n }\n\n componentDidUpdate(prevProps: CropperProps) {\n if (prevProps.rotation !== this.props.rotation) {\n this.computeSizes()\n this.recomputeCropPosition()\n } else if (prevProps.aspect !== this.props.aspect) {\n this.computeSizes()\n } else if (prevProps.objectFit !== this.props.objectFit) {\n this.computeSizes()\n } else if (prevProps.zoom !== this.props.zoom) {\n this.recomputeCropPosition()\n } else if (\n prevProps.cropSize?.height !== this.props.cropSize?.height ||\n prevProps.cropSize?.width !== this.props.cropSize?.width\n ) {\n this.computeSizes()\n } else if (\n prevProps.crop?.x !== this.props.crop?.x ||\n prevProps.crop?.y !== this.props.crop?.y\n ) {\n this.emitCropAreaChange()\n }\n if (prevProps.zoomWithScroll !== this.props.zoomWithScroll && this.containerRef) {\n this.props.zoomWithScroll\n ? this.containerRef.addEventListener('wheel', this.onWheel, { passive: false })\n : this.clearScrollEvent()\n }\n if (prevProps.video !== this.props.video) {\n this.videoRef.current?.load()\n }\n\n const objectFit = this.getObjectFit()\n if (objectFit !== this.state.mediaObjectFit) {\n this.setState({ mediaObjectFit: objectFit }, this.computeSizes)\n }\n }\n\n initResizeObserver = () => {\n if (typeof window.ResizeObserver === 'undefined' || !this.containerRef) {\n return\n }\n let isFirstResize = true\n this.resizeObserver = new window.ResizeObserver((entries) => {\n if (isFirstResize) {\n isFirstResize = false // observe() is called on mount, we don't want to trigger a recompute on mount\n return\n }\n this.computeSizes()\n })\n this.resizeObserver.observe(this.containerRef)\n }\n\n // this is to prevent Safari on iOS >= 10 to zoom the page\n preventZoomSafari = (e: Event) => e.preventDefault()\n\n cleanEvents = () => {\n if (!this.currentDoc) return\n this.currentDoc.removeEventListener('mousemove', this.onMouseMove)\n this.currentDoc.removeEventListener('mouseup', this.onDragStopped)\n this.currentDoc.removeEventListener('touchmove', this.onTouchMove)\n this.currentDoc.removeEventListener('touchend', this.onDragStopped)\n this.currentDoc.removeEventListener('gesturechange', this.onGestureChange as EventListener)\n this.currentDoc.removeEventListener('gestureend', this.onGestureEnd as EventListener)\n this.currentDoc.removeEventListener('scroll', this.onScroll)\n }\n\n clearScrollEvent = () => {\n if (this.containerRef) this.containerRef.removeEventListener('wheel', this.onWheel)\n if (this.wheelTimer) {\n clearTimeout(this.wheelTimer)\n }\n }\n\n onMediaLoad = () => {\n const cropSize = this.computeSizes()\n\n if (cropSize) {\n this.previousCropSize = cropSize\n this.emitCropData()\n this.setInitialCrop(cropSize)\n this.isInitialized = true\n }\n\n if (this.props.onMediaLoaded) {\n this.props.onMediaLoaded(this.mediaSize)\n }\n }\n\n setInitialCrop = (cropSize: Size) => {\n if (this.props.initialCroppedAreaPercentages) {\n const { crop, zoom } = getInitialCropFromCroppedAreaPercentages(\n this.props.initialCroppedAreaPercentages,\n this.mediaSize,\n this.props.rotation,\n cropSize,\n this.props.minZoom,\n this.props.maxZoom\n )\n\n this.props.onCropChange(crop)\n this.props.onZoomChange && this.props.onZoomChange(zoom)\n } else if (this.props.initialCroppedAreaPixels) {\n const { crop, zoom } = getInitialCropFromCroppedAreaPixels(\n this.props.initialCroppedAreaPixels,\n this.mediaSize,\n this.props.rotation,\n cropSize,\n this.props.minZoom,\n this.props.maxZoom\n )\n\n this.props.onCropChange(crop)\n this.props.onZoomChange && this.props.onZoomChange(zoom)\n }\n }\n\n getAspect() {\n const { cropSize, aspect } = this.props\n if (cropSize) {\n return cropSize.width / cropSize.height\n }\n return aspect\n }\n\n getObjectFit() {\n if (this.props.objectFit === 'cover') {\n const mediaRef = this.imageRef.current || this.videoRef.current\n\n if (mediaRef && this.containerRef) {\n this.containerRect = this.containerRef.getBoundingClientRect()\n const containerAspect = this.containerRect.width / this.containerRect.height\n const naturalWidth =\n this.imageRef.current?.naturalWidth || this.videoRef.current?.videoWidth || 0\n const naturalHeight =\n this.imageRef.current?.naturalHeight || this.videoRef.current?.videoHeight || 0\n const mediaAspect = naturalWidth / naturalHeight\n\n return mediaAspect < containerAspect ? 'horizontal-cover' : 'vertical-cover'\n }\n return 'horizontal-cover'\n }\n\n return this.props.objectFit\n }\n\n computeSizes = () => {\n const mediaRef = this.imageRef.current || this.videoRef.current\n\n if (mediaRef && this.containerRef) {\n this.containerRect = this.containerRef.getBoundingClientRect()\n this.saveContainerPosition()\n const containerAspect = this.containerRect.width / this.containerRect.height\n const naturalWidth =\n this.imageRef.current?.naturalWidth || this.videoRef.current?.videoWidth || 0\n const naturalHeight =\n this.imageRef.current?.naturalHeight || this.videoRef.current?.videoHeight || 0\n const isMediaScaledDown =\n mediaRef.offsetWidth < naturalWidth || mediaRef.offsetHeight < naturalHeight\n const mediaAspect = naturalWidth / naturalHeight\n\n // We do not rely on the offsetWidth/offsetHeight if the media is scaled down\n // as the values they report are rounded. That will result in precision losses\n // when calculating zoom. We use the fact that the media is positionned relative\n // to the container. That allows us to use the container's dimensions\n // and natural aspect ratio of the media to calculate accurate media size.\n // However, for this to work, the container should not be rotated\n let renderedMediaSize: Size\n\n if (isMediaScaledDown) {\n switch (this.state.mediaObjectFit) {\n default:\n case 'contain':\n renderedMediaSize =\n containerAspect > mediaAspect\n ? {\n width: this.containerRect.height * mediaAspect,\n height: this.containerRect.height,\n }\n : {\n width: this.containerRect.width,\n height: this.containerRect.width / mediaAspect,\n }\n break\n case 'horizontal-cover':\n renderedMediaSize = {\n width: this.containerRect.width,\n height: this.containerRect.width / mediaAspect,\n }\n break\n case 'vertical-cover':\n renderedMediaSize = {\n width: this.containerRect.height * mediaAspect,\n height: this.containerRect.height,\n }\n break\n }\n } else {\n renderedMediaSize = {\n width: mediaRef.offsetWidth,\n height: mediaRef.offsetHeight,\n }\n }\n\n this.mediaSize = {\n ...renderedMediaSize,\n naturalWidth,\n naturalHeight,\n }\n\n // set media size in the parent\n if (this.props.setMediaSize) {\n this.props.setMediaSize(this.mediaSize)\n }\n\n const cropSize = this.props.cropSize\n ? this.props.cropSize\n : getCropSize(\n this.mediaSize.width,\n this.mediaSize.height,\n this.containerRect.width,\n this.containerRect.height,\n this.props.aspect,\n this.props.rotation\n )\n\n if (\n this.state.cropSize?.height !== cropSize.height ||\n this.state.cropSize?.width !== cropSize.width\n ) {\n this.props.onCropSizeChange && this.props.onCropSizeChange(cropSize)\n }\n\n this.setState({ cropSize }, this.recomputeCropPosition)\n\n // pass crop size to parent\n if (this.props.setCropSize) {\n this.props.setCropSize(cropSize)\n }\n\n return cropSize\n }\n }\n\n saveContainerPosition = () => {\n if (this.containerRef) {\n const bounds = this.containerRef.getBoundingClientRect()\n this.containerPosition = { x: bounds.left, y: bounds.top }\n }\n }\n\n static getMousePoint = (e: MouseEvent | React.MouseEvent | GestureEvent) => ({\n x: Number(e.clientX),\n y: Number(e.clientY),\n })\n\n static getTouchPoint = (touch: Touch | React.Touch) => ({\n x: Number(touch.clientX),\n y: Number(touch.clientY),\n })\n\n onMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {\n if (!this.currentDoc) return\n e.preventDefault()\n this.currentDoc.addEventListener('mousemove', this.onMouseMove)\n this.currentDoc.addEventListener('mouseup', this.onDragStopped)\n this.saveContainerPosition()\n this.onDragStart(Cropper.getMousePoint(e))\n }\n\n onMouseMove = (e: MouseEvent) => this.onDrag(Cropper.getMousePoint(e))\n\n onScroll = (e: Event) => {\n if (!this.currentDoc) return\n e.preventDefault()\n this.saveContainerPosition()\n }\n\n onTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {\n if (!this.currentDoc) return\n this.isTouching = true\n if (this.props.onTouchRequest && !this.props.onTouchRequest(e)) {\n return\n }\n\n this.currentDoc.addEventListener('touchmove', this.onTouchMove, { passive: false }) // iOS 11 now defaults to passive: true\n this.currentDoc.addEventListener('touchend', this.onDragStopped)\n\n this.saveContainerPosition()\n\n if (e.touches.length === 2) {\n this.onPinchStart(e)\n } else if (e.touches.length === 1) {\n this.onDragStart(Cropper.getTouchPoint(e.touches[0]))\n }\n }\n\n onTouchMove = (e: TouchEvent) => {\n // Prevent whole page from scrolling on iOS.\n e.preventDefault()\n if (e.touches.length === 2) {\n this.onPinchMove(e)\n } else if (e.touches.length === 1) {\n this.onDrag(Cropper.getTouchPoint(e.touches[0]))\n }\n }\n\n onGestureStart = (e: GestureEvent) => {\n if (!this.currentDoc) return\n e.preventDefault()\n this.currentDoc.addEventListener('gesturechange', this.onGestureChange as EventListener)\n this.currentDoc.addEventListener('gestureend', this.onGestureEnd as EventListener)\n this.gestureZoomStart = this.props.zoom\n this.gestureRotationStart = this.props.rotation\n }\n\n onGestureChange = (e: GestureEvent) => {\n e.preventDefault()\n if (this.isTouching) {\n // this is to avoid conflict between gesture and touch events\n return\n }\n\n const point = Cropper.getMousePoint(e)\n const newZoom = this.gestureZoomStart - 1 + e.scale\n this.setNewZoom(newZoom, point, { shouldUpdatePosition: true })\n if (this.props.onRotationChange) {\n const newRotation = this.gestureRotationStart + e.rotation\n this.props.onRotationChange(newRotation)\n }\n }\n\n onGestureEnd = (e: GestureEvent) => {\n this.cleanEvents()\n }\n\n onDragStart = ({ x, y }: Point) => {\n this.dragStartPosition = { x, y }\n this.dragStartCrop = { ...this.props.crop }\n this.props.onInteractionStart?.()\n }\n\n onDrag = ({ x, y }: Point) => {\n if (!this.currentWindow) return\n if (this.rafDragTimeout) this.currentWindow.cancelAnimationFrame(this.rafDragTimeout)\n\n this.rafDragTimeout = this.currentWindow.requestAnimationFrame(() => {\n if (!this.state.cropSize) return\n if (x === undefined || y === undefined) return\n const offsetX = x - this.dragStartPosition.x\n const offsetY = y - this.dragStartPosition.y\n const requestedPosition = {\n x: this.dragStartCrop.x + offsetX,\n y: this.dragStartCrop.y + offsetY,\n }\n\n const newPosition = this.props.restrictPosition\n ? restrictPosition(\n requestedPosition,\n this.mediaSize,\n this.state.cropSize,\n this.props.zoom,\n this.props.rotation\n )\n : requestedPosition\n this.props.onCropChange(newPosition)\n })\n }\n\n onDragStopped = () => {\n this.isTouching = false\n this.cleanEvents()\n this.emitCropData()\n this.props.onInteractionEnd?.()\n }\n\n onPinchStart(e: React.TouchEvent<HTMLDivElement>) {\n const pointA = Cropper.getTouchPoint(e.touches[0])\n const pointB = Cropper.getTouchPoint(e.touches[1])\n this.lastPinchDistance = getDistanceBetweenPoints(pointA, pointB)\n this.lastPinchRotation = getRotationBetweenPoints(pointA, pointB)\n this.onDragStart(getCenter(pointA, pointB))\n }\n\n onPinchMove(e: TouchEvent) {\n if (!this.currentDoc || !this.currentWindow) return\n const pointA = Cropper.getTouchPoint(e.touches[0])\n const pointB = Cropper.getTouchPoint(e.touches[1])\n const center = getCenter(pointA, pointB)\n this.onDrag(center)\n\n if (this.rafPinchTimeout) this.currentWindow.cancelAnimationFrame(this.rafPinchTimeout)\n this.rafPinchTimeout = this.currentWindow.requestAnimationFrame(() => {\n const distance = getDistanceBetweenPoints(pointA, pointB)\n const newZoom = this.props.zoom * (distance / this.lastPinchDistance)\n this.setNewZoom(newZoom, center, { shouldUpdatePosition: false })\n this.lastPinchDistance = distance\n\n const rotation = getRotationBetweenPoints(pointA, pointB)\n const newRotation = this.props.rotation + (rotation - this.lastPinchRotation)\n this.props.onRotationChange && this.props.onRotationChange(newRotation)\n this.lastPinchRotation = rotation\n })\n }\n\n onWheel = (e: WheelEvent) => {\n if (!this.currentWindow) return\n if (this.props.onWheelRequest && !this.props.onWheelRequest(e)) {\n return\n }\n\n e.preventDefault()\n const point = Cropper.getMousePoint(e)\n const { pixelY } = normalizeWheel(e)\n const newZoom = this.props.zoom - (pixelY * this.props.zoomSpeed) / 200\n this.setNewZoom(newZoom, point, { shouldUpdatePosition: true })\n\n if (!this.state.hasWheelJustStarted) {\n this.setState({ hasWheelJustStarted: true }, () => this.props.onInteractionStart?.())\n }\n\n if (this.wheelTimer) {\n clearTimeout(this.wheelTimer)\n }\n this.wheelTimer = this.currentWindow.setTimeout(\n () => this.setState({ hasWheelJustStarted: false }, () => this.props.onInteractionEnd?.()),\n 250\n )\n }\n\n getPointOnContainer = ({ x, y }: Point, containerTopLeft: Point): Point => {\n if (!this.containerRect) {\n throw new Error('The Cropper is not mounted')\n }\n return {\n x: this.containerRect.width / 2 - (x - containerTopLeft.x),\n y: this.containerRect.height / 2 - (y - containerTopLeft.y),\n }\n }\n\n getPointOnMedia = ({ x, y }: Point) => {\n const { crop, zoom } = this.props\n return {\n x: (x + crop.x) / zoom,\n y: (y + crop.y) / zoom,\n }\n }\n\n setNewZoom = (zoom: number, point: Point, { shouldUpdatePosition = true } = {}) => {\n if (!this.state.cropSize || !this.props.onZoomChange) return\n\n const newZoom = clamp(zoom, this.props.minZoom, this.props.maxZoom)\n\n if (shouldUpdatePosition) {\n const zoomPoint = this.getPointOnContainer(point, this.containerPosition)\n const zoomTarget = this.getPointOnMedia(zoomPoint)\n const requestedPosition = {\n x: zoomTarget.x * newZoom - zoomPoint.x,\n y: zoomTarget.y * newZoom - zoomPoint.y,\n }\n\n const newPosition = this.props.restrictPosition\n ? restrictPosition(\n requestedPosition,\n this.mediaSize,\n this.state.cropSize,\n newZoom,\n this.props.rotation\n )\n : requestedPosition\n\n this.props.onCropChange(newPosition)\n }\n this.props.onZoomChange(newZoom)\n }\n\n getCropData = () => {\n if (!this.state.cropSize) {\n return null\n }\n\n // this is to ensure the crop is correctly restricted after a zoom back (https://github.com/ValentinH/react-easy-crop/issues/6)\n const restrictedPosition = this.props.restrictPosition\n ? restrictPosition(\n this.props.crop,\n this.mediaSize,\n this.state.cropSize,\n this.props.zoom,\n this.props.rotation\n )\n : this.props.crop\n return computeCroppedArea(\n restrictedPosition,\n this.mediaSize,\n this.state.cropSize,\n this.getAspect(),\n this.props.zoom,\n this.props.rotation,\n this.props.restrictPosition\n )\n }\n\n emitCropData = () => {\n const cropData = this.getCropData()\n if (!cropData) return\n\n const { croppedAreaPercentages, croppedAreaPixels } = cropData\n if (this.props.onCropComplete) {\n this.props.onCropComplete(croppedAreaPercentages, croppedAreaPixels)\n }\n\n if (this.props.onCropAreaChange) {\n this.props.onCropAreaChange(croppedAreaPercentages, croppedAreaPixels)\n }\n }\n\n emitCropAreaChange = () => {\n const cropData = this.getCropData()\n if (!cropData) return\n\n const { croppedAreaPercentages, croppedAreaPixels } = cropData\n if (this.props.onCropAreaChange) {\n this.props.onCropAreaChange(croppedAreaPercentages, croppedAreaPixels)\n }\n }\n\n recomputeCropPosition = () => {\n if (!this.state.cropSize) return\n\n let adjustedCrop = this.props.crop\n\n // Only scale if we're initialized and this is a legitimate resize\n if (this.isInitialized && this.previousCropSize?.width && this.previousCropSize?.height) {\n const sizeChanged =\n Math.abs(this.previousCropSize.width - this.state.cropSize.width) > 1e-6 ||\n Math.abs(this.previousCropSize.height - this.state.cropSize.height) > 1e-6\n\n if (sizeChanged) {\n const scaleX = this.state.cropSize.width / this.previousCropSize.width\n const scaleY = this.state.cropSize.height / this.previousCropSize.height\n\n adjustedCrop = {\n x: this.props.crop.x * scaleX,\n y: this.props.crop.y * scaleY,\n }\n }\n }\n\n const newPosition = this.props.restrictPosition\n ? restrictPosition(\n adjustedCrop,\n this.mediaSize,\n this.state.cropSize,\n this.props.zoom,\n this.props.rotation\n )\n : adjustedCrop\n\n this.previousCropSize = this.state.cropSize\n\n this.props.onCropChange(newPosition)\n this.emitCropData()\n }\n\n onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n const { crop, onCropChange, keyboardStep, zoom, rotation } = this.props\n let step = keyboardStep\n\n if (!this.state.cropSize) return\n\n // if the shift key is pressed, reduce the step to allow finer control\n if (event.shiftKey) {\n step *= 0.2\n }\n\n let newCrop = { ...crop }\n\n switch (event.key) {\n case 'ArrowUp':\n newCrop.y -= step\n event.preventDefault()\n break\n case 'ArrowDown':\n newCrop.y += step\n event.preventDefault()\n break\n case 'ArrowLeft':\n newCrop.x -= step\n event.preventDefault()\n break\n case 'ArrowRight':\n newCrop.x += step\n event.preventDefault()\n break\n default:\n return\n }\n\n if (this.props.restrictPosition) {\n newCrop = restrictPosition(newCrop, this.mediaSize, this.state.cropSize, zoom, rotation)\n }\n\n if (!event.repeat) {\n this.props.onInteractionStart?.()\n }\n\n onCropChange(newCrop)\n }\n\n onKeyUp = (event: React.KeyboardEvent<HTMLDivElement>) => {\n switch (event.key) {\n case 'ArrowUp':\n case 'ArrowDown':\n case 'ArrowLeft':\n case 'ArrowRight':\n event.preventDefault()\n break\n default:\n return\n }\n this.emitCropData()\n this.props.onInteractionEnd?.()\n }\n\n render() {\n const {\n image,\n video,\n mediaProps,\n cropperProps,\n transform,\n crop: { x, y },\n rotation,\n zoom,\n cropShape,\n showGrid,\n roundCropAreaPixels,\n style: { containerStyle, cropAreaStyle, mediaStyle },\n classes: { containerClassName, cropAreaClassName, mediaClassName },\n } = this.props\n\n const objectFit = this.state.mediaObjectFit ?? this.getObjectFit()\n\n return (\n <div\n onMouseDown={this.onMouseDown}\n onTouchStart={this.onTouchStart}\n ref={(el) => (this.containerRef = el)}\n data-testid=\"container\"\n style={containerStyle}\n className={classNames('reactEasyCrop_Container', containerClassName)}\n >\n {image ? (\n <img\n alt=\"\"\n className={classNames(\n 'reactEasyCrop_Image',\n objectFit === 'contain' && 'reactEasyCrop_Contain',\n objectFit === 'horizontal-cover' && 'reactEasyCrop_Cover_Horizontal',\n objectFit === 'vertical-cover' && 'reactEasyCrop_Cover_Vertical',\n mediaClassName\n )}\n {...(mediaProps as React.ImgHTMLAttributes<HTMLElement>)}\n src={image}\n ref={this.imageRef}\n style={{\n ...mediaStyle,\n transform:\n transform || `translate(${x}px, ${y}px) rotate(${rotation}deg) scale(${zoom})`,\n }}\n onLoad={this.onMediaLoad}\n />\n ) : (\n video && (\n <video\n autoPlay\n playsInline\n loop\n muted={true}\n className={classNames(\n 'reactEasyCrop_Video',\n objectFit === 'contain' && 'reactEasyCrop_Contain',\n objectFit === 'horizontal-cover' && 'reactEasyCrop_Cover_Horizontal',\n objectFit === 'vertical-cover' && 'reactEasyCrop_Cover_Vertical',\n mediaClassName\n )}\n {...mediaProps}\n ref={this.videoRef}\n onLoadedMetadata={this.onMediaLoad}\n style={{\n ...mediaStyle,\n transform:\n transform || `translate(${x}px, ${y}px) rotate(${rotation}deg) scale(${zoom})`,\n }}\n controls={false}\n >\n {(Array.isArray(video) ? video : [{ src: video }]).map((item) => (\n <source key={item.src} {...item} />\n ))}\n </video>\n )\n )}\n {this.state.cropSize && (\n <div\n ref={this.cropperRef}\n style={{\n ...cropAreaStyle,\n width: roundCropAreaPixels\n ? Math.round(this.state.cropSize.width)\n : this.state.cropSize.width,\n height: roundCropAreaPixels\n ? Math.round(this.state.cropSize.height)\n : this.state.cropSize.height,\n }}\n tabIndex={0}\n onKeyDown={this.onKeyDown}\n onKeyUp={this.onKeyUp}\n data-testid=\"cropper\"\n className={classNames(\n 'reactEasyCrop_CropArea',\n cropShape === 'round' && 'reactEasyCrop_CropAreaRound',\n showGrid && 'reactEasyCrop_CropAreaGrid',\n cropAreaClassName\n )}\n {...cropperProps}\n />\n )}\n </div>\n )\n }\n}\n\nexport default Cropper\n"],"names":["getCropSize","mediaWidth","mediaHeight","containerWidth","containerHeight","aspect","rotation","_a","rotateSize","width","height","fittingWidth","Math","min","fittingHeight","getMediaZoom","mediaSize","naturalWidth","naturalHeight","restrictPosition","position","cropSize","zoom","x","restrictPositionCoord","y","maxPosition","clamp","getDistanceBetweenPoints","pointA","pointB","sqrt","pow","getRotationBetweenPoints","atan2","PI","computeCroppedArea","crop","limitAreaFn","limitArea","noOp","mediaBBoxSize","mediaNaturalBBoxSize","croppedAreaPercentages","widthInPixels","round","heightInPixels","isImgWiderThanHigh","sizePixels","croppedAreaPixels","__assign","max","value","_max","getInitialCropFromCroppedAreaPercentages","minZoom","maxZoom","getZoomFromCroppedAreaPixels","mediaZoom","getInitialCropFromCroppedAreaPixels","cropZoom","getCenter","a","b","getRadianAngle","degreeValue","rotRad","abs","cos","sin","classNames","args","_i","arguments","length","filter","join","trim","MIN_ZOOM","MAX_ZOOM","KEYBOARD_STEP","Cropper","_super","__extends","_this","apply","cropperRef","React","createRef","imageRef","videoRef","containerPosition","containerRef","styleRef","containerRect","dragStartPosition","dragStartCrop","gestureZoomStart","gestureRotationStart","isTouching","lastPinchDistance","lastPinchRotation","rafDragTimeout","rafPinchTimeout","wheelTimer","currentDoc","document","currentWindow","window","resizeObserver","previousCropSize","isInitialized","state","hasWheelJustStarted","mediaObjectFit","undefined","initResizeObserver","ResizeObserver","isFirstResize","entries","computeSizes","observe","preventZoomSafari","e","preventDefault","cleanEvents","removeEventListener","onMouseMove","onDragStopped","onTouchMove","onGestureChange","onGestureEnd","onScroll","clearScrollEvent","onWheel","clearTimeout","onMediaLoad","emitCropData","setInitialCrop","props","onMediaLoaded","initialCroppedAreaPercentages","onCropChange","onZoomChange","initialCroppedAreaPixels","_b","mediaRef","current","getBoundingClientRect","saveContainerPosition","containerAspect","videoWidth","_c","_d","videoHeight","isMediaScaledDown","offsetWidth","offsetHeight","mediaAspect","renderedMediaSize","setMediaSize","_e","_f","onCropSizeChange","setState","recomputeCropPosition","setCropSize","bounds","left","top","onMouseDown","addEventListener","onDragStart","getMousePoint","onDrag","onTouchStart","onTouchRequest","passive","touches","onPinchStart","getTouchPoint","onPinchMove","onGestureStart","point","newZoom","scale","setNewZoom","shouldUpdatePosition","onRotationChange","newRotation","onInteractionStart","cancelAnimationFrame","requestAnimationFrame","offsetX","offsetY","requestedPosition","newPosition","onInteractionEnd","onWheelRequest","pixelY","normalizeWheel","zoomSpeed","call","setTimeout","getPointOnContainer","containerTopLeft","Error","getPointOnMedia","zoomPoint","zoomTarget","getCropData","restrictedPosition","getAspect","cropData","onCropComplete","onCropAreaChange","emitCropAreaChange","adjustedCrop","sizeChanged","scaleX","scaleY","onKeyDown","event","keyboardStep","step","shiftKey","newCrop","key","repeat","onKeyUp","prototype","componentDidMount","ownerDocument","defaultView","zoomWithScroll","disableAutomaticStylesInjection","createElement","setAttribute","nonce","innerHTML","cssStyles","head","appendChild","complete","setImageRef","setVideoRef","setCropperRef","componentWillUnmount","disconnect","parentNode","removeChild","componentDidUpdate","prevProps","objectFit","_g","_h","video","_j","load","getObjectFit","center","distance","render","image","mediaProps","cropperProps","transform","cropShape","showGrid","roundCropAreaPixels","style","containerStyle","cropAreaStyle","mediaStyle","classes","containerClassName","cropAreaClassName","mediaClassName","ref","el","className","alt","src","concat","onLoad","autoPlay","playsInline","loop","muted","onLoadedMetadata","controls","Array","isArray","map","item","tabIndex","defaultProps","Number","clientX","clientY","touch","Component"],"mappings":"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