Browse Source

feat: [Cropper] add preview API for realtime preivew croppered image (#2782)

YyumeiZhang 6 months ago
parent
commit
16294a73ef

+ 126 - 26
content/show/cropper/index-en-US.md

@@ -27,6 +27,7 @@ Use `sr` to set the cropped image; use `shape` to set the shape of the cropping
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -36,13 +37,12 @@ const containerStyle = {
 
 function Demo() {
     const ref = useRef(null);
-  const [shape, setShape] = useState('rect');
+    const [shape, setShape] = useState('rect');
+    const [cropperUrl, setCropperUrl] = useState('');
 
     const onButtonClick = useCallback(() => {
-        const value = ref.current.getCropperCanvas();
-        const previewContainer = document.getElementById('previewContainer');
-        previewContainer.innerHTML = '';
-        previewContainer.appendChild(value);
+        const canvas = ref.current.getCropperCanvas();
+        setCropperUrl(canvas.toDataURL());
     }, []);
 
     const onShapeChange = useCallback((e) => {
@@ -62,7 +62,8 @@ function Demo() {
             shape={shape}
         />
         <Button onClick={onButtonClick}>Get Cropped Image</Button>
-        <div id='previewContainer'/>
+         <br/><br/>
+        {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
     </>;
 }
 
@@ -79,6 +80,7 @@ When setting `aspectRatio`, the crop box ratio is fixed, and the crop box will c
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -88,13 +90,12 @@ const containerStyle = {
 
 function Demo() {
     const ref = useRef(null);
-    const shape = useState('rect');
+    const [cropperUrl, setCropperUrl] = useState('');
 
     const onButtonClick = useCallback(() => {
-        const value = ref.current.getCropperCanvas();
-        const previewContainer = document.getElementById('previewContainer-aspect');
-        previewContainer.innerHTML = '';
-        previewContainer.appendChild(value);
+      const canvas = ref.current.getCropperCanvas();
+      const url = canvas.toDataURL();
+      setCropperUrl(url);
     }, []);
 
     return <>
@@ -105,7 +106,8 @@ function Demo() {
             style={containerStyle}
         />
         <Button onClick={onButtonClick}>Get Cropped Image</Button>
-        <div id='previewContainer-aspect' />
+         <br /><br />
+        {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
     </>;
 }
 
@@ -118,6 +120,7 @@ Control image rotation and zoom through `rotate` and `zoom`, and get the latest
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, Slider } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -137,6 +140,7 @@ function Demo() {
   const [rotate, setRotate] = useState(0);
   const [zoom, setZoom] = useState(1);
   const ref = useRef();
+  const [cropperUrl, setCropperUrl] = useState('');
 
   const onZoomChange = useCallback((value) => {
     setZoom(value);
@@ -147,10 +151,8 @@ function Demo() {
   }, []);
 
   const onButtonClick = useCallback(() => {
-    const value = ref.current.getCropperCanvas();
-    const previewContainer = document.getElementById('previewContainer-control');
-    previewContainer.innerHTML = '';
-    previewContainer.appendChild(value);
+    const canvas = ref.current.getCropperCanvas();
+    setCropperUrl(canvas.toDataURL());
   }, []);
 
   return (
@@ -187,11 +189,8 @@ function Demo() {
            </div>
            <br />
            <Button onClick={onButtonClick}>Get Cropped Image</Button>
-           <br />
-           <div >
-            <div id='previewContainer-control'
-            />
-          </div>
+           <br /><br />
+          {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
       </div>
   );
 };
@@ -205,6 +204,7 @@ The crop box style can be customized through `cropperBoxStyle`, `cropperBoxClass
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, Switch } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -221,12 +221,11 @@ const centerStyle = {
 
 function Demo() {
     const ref = useRef(null);
+    const [cropperUrl, setCropperUrl] = useState('');
 
     const onButtonClick = useCallback(() => {
-        const value = ref.current.getCropperCanvas();
-        const previewContainer = document.getElementById('previewContainer-cropperBox');
-        previewContainer.innerHTML = '';
-        previewContainer.appendChild(value);
+        const canvas = ref.current.getCropperCanvas();
+        setCropperUrl(canvas.toDataURL());
     }, []);
 
     return <>
@@ -239,13 +238,113 @@ function Demo() {
             showResizeBox={false}
         />
         <Button onClick={onButtonClick}>Get Cropped Image</Button>
-        <div id='previewContainer-cropperBox'/>
+        <br /><br />
+        {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
     </>;
 }
 
 render(<Demo />)
 ```
 
+### 实时预览裁切效果
+
+通过 `preview` 指定预览容器,实时预览裁切效果。
+
+```jsx live=true dir=column noInline=true
+import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
+
+const containerStyle = {
+  width: 550,
+  height: 300,
+  margin: 20,
+}
+
+const actionStyle = {
+  marginTop: 20,
+  display: 'flex',
+  alignItems: 'center',
+  justifyContent: 'center',
+  width: 'fit-content'
+}
+
+function Demo() {
+  const [rotate, setRotate] = useState(0);
+  const [zoom, setZoom] = useState(1);
+  const [cropperData, setCropperUrl ] = useState('');
+  const ref = useRef();
+
+  const onZoomChange = useCallback((value) => {
+    setZoom(value);
+  })
+
+  const onSliderChange = useCallback((value) => {
+    setRotate(value);
+  }, []);
+
+  const onButtonClick = useCallback(() => {
+    const canvas = ref.current.getCropperCanvas();
+    const url = canvas.toDataURL();
+    setCropperUrl(url);
+  }, []);
+
+  const preview = useCallback(() => {
+    const previewContainer = document.getElementById('previewWrapper');
+    return previewContainer;
+  }, []);
+
+  return (
+      <div >
+           <Cropper 
+              ref={ref} 
+              src={"https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg"}
+              style={containerStyle}
+              rotate={rotate}
+              zoom={zoom}
+              onZoomChange={onZoomChange}
+              preview={preview}
+           />
+           <div style={actionStyle} >
+            <span>旋转</span>
+            <Slider
+              style={{ width: 500}}
+              value={rotate}
+              step={1}
+              min={-360}
+              max={360}
+              onChange={onSliderChange}
+            />
+           </div>
+           <div style={actionStyle} >
+            <span>缩放</span>
+            <Slider
+              style={{ width: 500}}
+              value={zoom}
+              step={0.1}
+              min={0.1}
+              max={3}
+              onChange={onZoomChange}
+            />
+           </div>
+           <br />
+           <div style={{ display: 'flex', }}>
+              <div style={{ width: '50%', flexGrow: 1}}>
+                <strong>实时预览</strong>
+                <div id='previewWrapper' style={{height: 300, marginTop: 8}}/>
+              </div>
+              <div style={{width: '50%', flexGrow: 1, paddingLeft: 10 }}>
+                <Button onClick={onButtonClick}>裁切</Button>
+                <br /><br />
+                <img src={cropperData} style={{ width: '90%'}} />
+              </div>
+           </div>
+      </div>
+  );
+};
+
+render(<Demo />)
+```
+
 ### API
 
 | PROPERTIES | INSTRUCTIONS | TYPE | DEFAULT |
@@ -260,6 +359,7 @@ render(<Demo />)
 | maxZoom | Maximum zoom factor | number | 3 |
 | minZoom | Minimum zoom factor | number | 0.1 |
 | onZoomChange | Callback during zoom transformation | (zoom: number) => void | - |
+| preview | The container of the preview image | () => HTMLElement | - |
 | rotate | rotation angle | number | - |
 | shape | Crop box shape | 'rect' \| 'round' \| 'roundRect' | 'rect' |
 | src | The address of the cropped image | string | - |

+ 127 - 30
content/show/cropper/index.md

@@ -30,6 +30,7 @@ import { Cropper } from '@douyinfe/semi-ui';
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -39,13 +40,12 @@ const containerStyle = {
 
 function Demo() {
     const ref = useRef(null);
-  const [shape, setShape] = useState('rect');
+    const [shape, setShape] = useState('rect');
+    const [cropperUrl, setCropperUrl] = useState('');
 
     const onButtonClick = useCallback(() => {
-        const value = ref.current.getCropperCanvas();
-        const previewContainer = document.getElementById('previewContainer');
-        previewContainer.innerHTML = '';
-        previewContainer.appendChild(value);
+        const canvas = ref.current.getCropperCanvas();
+        setCropperUrl(canvas.toDataURL());
     }, []);
 
     const onShapeChange = useCallback((e) => {
@@ -65,7 +65,8 @@ function Demo() {
             shape={shape}
         />
         <Button onClick={onButtonClick}>裁切</Button>
-        <div id='previewContainer'/>
+        <br/><br/>
+        {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
     </>;
 }
 
@@ -82,6 +83,7 @@ render(<Demo />)
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -91,13 +93,11 @@ const containerStyle = {
 
 function Demo() {
     const ref = useRef(null);
-    const shape = useState('rect');
+    const [cropperUrl, setCropperUrl] = useState('');
 
     const onButtonClick = useCallback(() => {
-        const value = ref.current.getCropperCanvas();
-        const previewContainer = document.getElementById('previewContainer-aspect');
-        previewContainer.innerHTML = '';
-        previewContainer.appendChild(value);
+        const canvas = ref.current.getCropperCanvas();
+        setCropperUrl(canvas.toDataURL());
     }, []);
 
     return <>
@@ -108,7 +108,8 @@ function Demo() {
             style={containerStyle}
         />
         <Button onClick={onButtonClick}>裁切</Button>
-        <div id='previewContainer-aspect' />
+        <br /><br />
+        {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
     </>;
 }
 
@@ -121,6 +122,7 @@ render(<Demo />)
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, Slider } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -140,6 +142,7 @@ function Demo() {
   const [rotate, setRotate] = useState(0);
   const [zoom, setZoom] = useState(1);
   const ref = useRef();
+  const [cropperUrl, setCropperUrl] = useState('');
 
   const onZoomChange = useCallback((value) => {
     setZoom(value);
@@ -150,10 +153,8 @@ function Demo() {
   }, []);
 
   const onButtonClick = useCallback(() => {
-    const value = ref.current.getCropperCanvas();
-    const previewContainer = document.getElementById('previewContainer-control');
-    previewContainer.innerHTML = '';
-    previewContainer.appendChild(value);
+    const canvas = ref.current.getCropperCanvas();
+    setCropperUrl(canvas.toDataURL());
   }, []);
 
   return (
@@ -167,7 +168,7 @@ function Demo() {
               onZoomChange={onZoomChange}
            />
            <div style={actionStyle} >
-            <span>Rotate</span>
+            <span>旋转</span>
             <Slider
               style={{ width: 500}}
               value={rotate}
@@ -178,7 +179,7 @@ function Demo() {
             />
            </div>
            <div style={actionStyle} >
-            <span>Zoom</span>
+            <span>缩放</span>
             <Slider
               style={{ width: 500}}
               value={zoom}
@@ -190,13 +191,8 @@ function Demo() {
            </div>
            <br />
            <Button onClick={onButtonClick}>裁切</Button>
-           <br />
-           <div 
-            // style={{ background: 'pink' }} 
-           >
-            <div id='previewContainer-control'
-            />
-          </div>
+          <br /><br />
+          {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
       </div>
   );
 };
@@ -210,6 +206,7 @@ render(<Demo />)
 
 ```jsx live=true dir=column noInline=true
 import { Cropper, Button, Switch } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
 
 const containerStyle = {
   width: 550,
@@ -226,12 +223,11 @@ const centerStyle = {
 
 function Demo() {
     const ref = useRef(null);
+    const [cropperUrl, setCropperUrl] = useState('');
 
     const onButtonClick = useCallback(() => {
-        const value = ref.current.getCropperCanvas();
-        const previewContainer = document.getElementById('previewContainer-cropperBox');
-        previewContainer.innerHTML = '';
-        previewContainer.appendChild(value);
+        const canvas = ref.current.getCropperCanvas();
+        setCropperUrl(canvas.toDataURL());
     }, []);
 
     return <>
@@ -244,13 +240,113 @@ function Demo() {
             showResizeBox={false}
         />
         <Button onClick={onButtonClick}>裁切</Button>
-        <div id='previewContainer-cropperBox'/>
+        <br /><br />
+        {cropperUrl && <img src={cropperUrl} style={{height: 400}}/>}
     </>;
 }
 
 render(<Demo />)
 ```
 
+### 实时预览裁切效果
+
+通过 `preview` 指定预览容器,实时预览裁切效果。
+
+```jsx live=true dir=column noInline=true
+import { Cropper, Button, RadioGroup, Radio } from '@douyinfe/semi-ui';
+import React, { useState, useRef, useCallback } from 'react';
+
+const containerStyle = {
+  width: 550,
+  height: 300,
+  margin: 20,
+}
+
+const actionStyle = {
+  marginTop: 20,
+  display: 'flex',
+  alignItems: 'center',
+  justifyContent: 'center',
+  width: 'fit-content'
+}
+
+function Demo() {
+  const [rotate, setRotate] = useState(0);
+  const [zoom, setZoom] = useState(1);
+  const [cropperUrl, setCropperUrl ] = useState('');
+  const ref = useRef();
+
+  const onZoomChange = useCallback((value) => {
+    setZoom(value);
+  })
+
+  const onSliderChange = useCallback((value) => {
+    setRotate(value);
+  }, []);
+
+  const onButtonClick = useCallback(() => {
+    const canvas = ref.current.getCropperCanvas();
+    const url = canvas.toDataURL();
+    setCropperUrl(url);
+  }, []);
+
+  const preview = useCallback(() => {
+    const previewContainer = document.getElementById('previewWrapper');
+    return previewContainer;
+  }, []);
+
+  return (
+      <div >
+           <Cropper 
+              ref={ref} 
+              src={"https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg"}
+              style={containerStyle}
+              rotate={rotate}
+              zoom={zoom}
+              onZoomChange={onZoomChange}
+              preview={preview}
+           />
+           <div style={actionStyle} >
+            <span>旋转</span>
+            <Slider
+              style={{ width: 500}}
+              value={rotate}
+              step={1}
+              min={-360}
+              max={360}
+              onChange={onSliderChange}
+            />
+           </div>
+           <div style={actionStyle} >
+            <span>缩放</span>
+            <Slider
+              style={{ width: 500}}
+              value={zoom}
+              step={0.1}
+              min={0.1}
+              max={3}
+              onChange={onZoomChange}
+            />
+           </div>
+           <br />
+           <div style={{ display: 'flex', }}>
+              <div style={{ width: '50%', flexGrow: 1}}>
+                <strong>实时预览</strong>
+                <div id='previewWrapper' style={{height: 300, marginTop: 8}}/>
+              </div>
+              <div style={{width: '50%', flexGrow: 1, paddingLeft: 10 }}>
+                <Button onClick={onButtonClick}>裁切</Button>
+                <br /><br />
+                <img src={cropperUrl} style={{ width: '90%'}} />
+              </div>
+           </div>
+      </div>
+  );
+};
+
+render(<Demo />)
+```
+
 ### API
 
 | 属性 | 说明 | 类型 | 默认值 |
@@ -265,6 +361,7 @@ render(<Demo />)
 | maxZoom | 最大缩放倍数 | number | 3 |
 | minZoom | 最小缩放倍数 | number | 0.1 |
 | onZoomChange | 缩放回调 | (zoom: number) => void | - |
+| preview | 指定预览容器 | () => HTMLElement | - |
 | rotate | 旋转角度 | number | - |
 | shape | 裁切框形状 | 'rect' \| 'round' \| 'roundRect' | 'rect' |
 | src | 图片地址 | string | - |

+ 83 - 0
packages/semi-foundation/cropper/foundation.ts

@@ -60,6 +60,12 @@ export default class CropperFoundation <P = Record<string, any>, S = Record<stri
     rangeX: [number, number];
     rangeY: [number, number];
     initial: boolean;
+    previewImg: HTMLImageElement;
+    previewContainer: HTMLElement;
+    previewContainerInitSize: {
+        width: number;
+        height: number
+    };
     
     constructor(adapter: CropperAdapter<P, S>) {
         super({ ...adapter });
@@ -74,6 +80,8 @@ export default class CropperFoundation <P = Record<string, any>, S = Record<stri
         this.rangeX = null;
         this.rangeY = null;
         this.initial = false;
+        this.previewImg = null;
+        this.previewContainer = null;
     }
 
     init() {
@@ -88,6 +96,7 @@ export default class CropperFoundation <P = Record<string, any>, S = Record<stri
     destroy() {
         this.unBindMoveEvent();
         this.unBindResizeEvent();
+        this.removePreview();
     }
 
     getImgDataWhenResize = (ratio: number) => {
@@ -228,6 +237,80 @@ export default class CropperFoundation <P = Record<string, any>, S = Record<stri
             cropperBox: newCropperBoxState,
             loaded: true,
         } as any);
+
+        this.renderPreview();
+    }
+
+    renderPreview = () => {
+        const { preview, src } = this.getProps();
+        const previewNode = preview?.();
+        if (!previewNode) {
+            return;
+        }
+        const img = document.createElement('img');
+        this.previewImg = img;
+        this.previewContainer = previewNode;
+        img.src = src;
+        previewNode.appendChild(img);
+        this.previewContainer.style.overflow = 'hidden';
+        // 记录预览容器初始宽高
+        const { width: previewWidth, height: previewHeight } = previewNode.getBoundingClientRect();
+        this.previewContainerInitSize = {
+            width: previewWidth,
+            height: previewHeight,
+        };
+    }
+
+    updatePreview = (props: {
+        width: number;
+        height: number;
+        translateX: number;
+        translateY: number;
+        rotate: number
+    }) => {
+        if (!this.previewImg) {
+            return;
+        }
+        const { cropperBox } = this.getStates();
+        let zoom = 1;
+        const { width: containerWidth, height: containerHeight } = this.previewContainerInitSize;
+        let previewWidth = containerWidth;
+        let previewHeight = containerHeight;
+        if (previewWidth < previewHeight) {
+            zoom = containerWidth / cropperBox.width;
+            let tempHeight = zoom * cropperBox.height;
+            if (tempHeight > containerHeight) {
+                zoom = containerHeight / cropperBox.height;
+                previewWidth = zoom * cropperBox.width;
+            } else {
+                previewHeight = tempHeight;
+            }
+        } else {
+            zoom = containerHeight / cropperBox.height;
+            let tempWidth = zoom * cropperBox.width;
+            if (tempWidth > containerWidth) {
+                zoom = containerWidth / cropperBox.width;
+                previewHeight = zoom * cropperBox.height;
+            } else {
+                previewWidth = tempWidth;
+            }
+        }
+        const { width, height, translateX, translateY, rotate } = props;
+        // Set the image style
+        this.previewImg.style.width = `${width * zoom}px`;
+        this.previewImg.style.height = `${height * zoom}px`;
+        this.previewImg.style.transform = `translate(${translateX * zoom}px, ${translateY * zoom}px) rotate(${rotate}deg)`;
+        this.previewImg.style.transformOrigin = 'center';
+        // set preview container size
+        this.previewContainer.style.width = `${previewWidth}px`;
+        this.previewContainer.style.height = `${previewHeight}px`;
+    }
+
+    removePreview = () => {
+        if (this.previewImg && this.previewContainer) {
+            this.previewContainer.removeChild(this.previewImg);
+            this.previewImg = null;
+        }
     }
 
     handleWheel = (e: any) => {

+ 75 - 0
packages/semi-ui/cropper/_story/cropper.stories.jsx

@@ -317,4 +317,79 @@ export const NoResizeBox = () => {
   );
 }
 
+export const RealTimePreview  = () => {
+  const [rotate, setRotate] = useState(0);
+  const [zoom, setZoom] = useState(1);
+  const [cropperUrl, setCropperUrl ] = useState('');
+  const ref = useRef();
+
+  const onZoomChange = useCallback((value) => {
+    setZoom(value);
+  })
+
+  const onSliderChange = useCallback((value) => {
+    setRotate(value);
+  }, []);
+
+  const onButtonClick = useCallback(() => {
+    const canvas = ref.current.getCropperCanvas();
+    const url = canvas.toDataURL();
+    setCropperUrl(url);
+  }, []);
+
+  const preview = useCallback(() => {
+    const previewContainer = document.getElementById('previewContainer');
+    return previewContainer;
+  }, []);
+
+  return (
+      <div id='cropper-container'>
+           <Cropper 
+              ref={ref} 
+              src={"https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg"}
+              // src={'https://lf3-static.bytednsdoc.com/obj/eden-cn/9130eh7pltbfnuhog/16.jpeg'}
+              style={containerStyle}
+              rotate={rotate}
+              zoom={zoom}
+              onZoomChange={onZoomChange}
+              preview={preview}
+           />
+           <div style={actionStyle} >
+            <span>Rotate</span>
+            <Slider
+              style={{ width: 720}}
+              value={rotate}
+              step={1}
+              min={-360}
+              max={360}
+              onChange={onSliderChange}
+            />
+           </div>
+           <div style={actionStyle} >
+            <span>Zoom</span>
+            <Slider
+              style={{ width: 720}}
+              value={zoom}
+              step={0.1}
+              min={0.1}
+              max={3}
+              onChange={onZoomChange}
+            />
+           </div>
+           <br />
+           <div style={{ display: 'flex', columnGap: 10}}>
+              <div style={{width: '50%', flexGrow: 1 }}>
+                <Button onClick={onButtonClick}>Cropper</Button>
+                <br /><br />
+                <img src={cropperUrl} style={{ width: '100%'}} />
+              </div>
+              <div style={{width: '50%', flexGrow: 1 }}>
+                <span style={{marginBottom: 4}}>Preview</span>
+                <div id='previewContainer' style={{height: 300, marginTop: 8}}/>
+              </div>
+           </div>
+      </div>
+  );
+};
+
 

+ 12 - 3
packages/semi-ui/cropper/index.tsx

@@ -36,9 +36,10 @@ interface CropperProps {
     cropperBoxCls?: string;
     /* The fill color of the non-picture parts in the cut result */
     fill?: string;
-    maxZoom: number;
-    minZoom: number;
-    zoomStep: number
+    maxZoom?: number;
+    minZoom?: number;
+    zoomStep?: number;
+    preview?: () => HTMLElement
 }
 
 interface CropperState {
@@ -219,6 +220,14 @@ class Cropper extends BaseComponent<CropperProps, CropperState> {
         const cropperImgX = imgX - cropperBoxX;
         const cropperImgY = imgY - cropperBoxY;
 
+        this.foundation.updatePreview({
+            width: imgData.width,
+            height: imgData.height,
+            translateX: cropperImgX, 
+            translateY: cropperImgY,
+            rotate: rotate,
+        });
+
         return (<ResizeObserver 
             onResize={this.foundation.handleResize} 
             observerProperty={ObserverProperty.Width}