相关文章推荐
烦恼的上铺  ·  prometheus之面试题 - ...·  8 月前    · 
忐忑的柚子  ·  超详细 Docker ...·  1 年前    · 

一.  技术背景

cropper 是一款基于 JavaScript 的图片裁剪插件,通过 Canvas 技术实现了图片的裁剪和截图功能。Canvas 是 HTML5 中新增的一个标签,用于在网页上绘制图形、动画和游戏等交互式内容。它的历史背景可以追溯到2004年,当时苹果公司推出了一款名为 WebKit 的浏览器引擎,它支持使用 JavaScript 和 CSS 来创建动态效果。随着Web技术的不断发展,人们对于在网页上实现更加复杂的图形和动画效果的需求也越来越高,于是 HTML5 标准化组织开始研发Canvas标签,以满足这一需求。Canvas 标签成为了Web开发中不可或缺的一部分,被广泛应用于游戏、数据可视化、图形编辑等领域。

二.  技术依赖

cropper 依赖于 canvas 标签的原生API,其中包含  drawImage(),strokeRect(), getImageData(), toDataUrl() 方法等。

1. getContext()

返回 canvas 的上下文,如果没有定义则返回 null。

var ctx = canvas.getContext(contextType); var ctx = canvas.getContext(contextType, contextAttributes);

参数: 这里我们只介绍 " 2d " :建立一个 CanvasRenderingContext2D 二维渲染上下文。

2. drawImage()

提供多种在画布(canvas)上绘制图像的方式。

drawImage(image, dx, dy)

drawImage(image, dx, dy, dWidth, dHeight)

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) |

这里我们介绍 drawImage(image, dx, dy, dWidth, dHeight) 的参数,

 image: 绘制到上下文的元素。允许任何的画布图像源;
 dx:image的左上角在目标画布上 X 轴坐标。
 dy:image的左上角在目标画布上 Y 轴坐标。
 dWidth:image在目标画布上绘制的宽度。
 dHeight:image在目标画布上绘制的高度。

3. strokeRect()

   使用当前的绘画样式,描绘一个起点在(x, y)、宽度为 w、高度为 h 的矩形方法。

   语法:void ctx.strokeRect(x, y, width, height);

x:矩形起点的 x 轴坐标。
y:矩形起点的 y 轴坐标。
width:矩形的宽度。正值在右侧,负值在左侧。
height:矩形的高度。正值在下,负值在上。

4. getImageData()

   返回一个 ImageData 对象,用来描述 canvas 区域隐含的像素数据,这个区域通过矩形表示,起始点为*(sx, sy)、宽为sw、高为sh。

   语法:ImageData ctx.getImageData(sx, sy, sw, sh);

sx:将要被提取的图像数据矩形区域的左上角 x 坐标。
sy:将要被提取的图像数据矩形区域的左上角 y 坐标。
sw:将要被提取的图像数据矩形区域的宽度。
sh:将要被提取的图像数据矩形区域的高度。

5. toDataUrl()

   方法返回一个包含图片展示的 data url。 

   语法:canvas.toDataURL(type, encoderOptions);

type 可选:图片格式,默认为 image/png。
encoderOptions` 可选:在指定图片格式为 `image/jpeg` 或 `image/webp` 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 `0.92`。其他参数会被忽略。

三.  实现流程

    具体来说,cropper 主要是利用 Canvas 技术实现了以下几个方面的功能:

1. Canvas 绘制图片

    cropper 可以将要裁剪的图片渲染到canvas中,通过 Canvas API  中的 drawImage() 方法实现。这样就可以在 Canvas 中显示图片,方便用户进行裁剪操作。

    举个🌰:canvas 渲染图片的过程。

export default function Canvas(){ 
    const canvasRef = useRef<HTMLCanvasElement>(null);
    const loadImg = () => { const elem = canvasRef.current;
    const canvas = elem?.getContext('2d');
    const img = document.createElement('img');
    console.log(elem?.height,elem?.width);
    // 加载背景图片
    img.onload = () => {
        elem?.height!= 200
        elem?.width!= 300
        canvas?.drawImage(img,0,0) } img.src = src;
    useEffect(() => {
        loadImg();
    return (
        <Group position='center'>
            <Stack spacing={0}>
                <Text>要使用的图片:</Text>
                <Image id="scream" src={src} />
            </Stack>
            <Stack spacing={0} mt={20}>
                <Text>画布:</Text>
                <canvas ref={canvasRef} width={300} height={300} style={{border:'1px solid #d3d3d3'}} />
            </Stack>
        </Group>

     渲染到画布上的效果图:

2. 裁剪区域的计算

     实现裁剪选择框,并自由移动的流程图如下:

     比较常用的方式大概是这个样子:

     如何将这几层图像按照需求正确的叠在一起呢? 这里我们用到的是 canvas API 中的 globalCompositeOperation() 方法。利用这个 API 我们也可以实现刮刮卡抽奖的效果。

     当我们点下鼠标,就能够通过 event 事件对象获取鼠标点击位置,e.clientX 和 e.clientY ;  当鼠标进行移动的时候,也能通过 event 获取鼠标的位置,通过两次鼠标位置的改变,就能够获取鼠标移动的距离。即:初始的x轴位置为 initX = e.clientX,initY = e.clientY;

     移动到某个点的位置为:endX = e.clientX,endY = e.clientY;

     因此裁剪区域的宽Tx:endX - initX;高Ty:endY - initY;

     举个🌰 :这个实例中,我们使用canvas API 绘制了一个裁剪框,并计算出了裁剪区域的坐标和大小。具体来说,我们在鼠标的移动时间中计算出了裁剪框的宽度和高度,然后使用 strokeRect() 方法绘制了一个红色的矩形框。最后,我们根据裁剪框的位置和大小计算出了裁剪区域的坐标和大小。

export default function Cropper() {
    const canvasRef = useRef<HTMLCanvasElement>(null);
    const dragStartRef = useRef<any>(null);
    useEffect(() => {
        const canvas = canvasRef.current!;
        const ctx = canvas.getContext('2d');
        const img = new Image();
        img.onload = () => {
            // 将图片绘制在Canvas上
            canvas.width = img.width;
            canvas.height = img.height;
            ctx!.drawImage(img, 0, 0);
    img.src = src;
    // 绑定鼠标事件
    canvas.addEventListener('mousedown', handleMouseDown); 
    canvas.addEventListener('mousemove', handleMouseMove); 
    canvas.addEventListener('mouseup', handleMouseUp);
    return () => { 
    // 解绑鼠标事件
    canvas.removeEventListener('mousedown', handleMouseDown); 
    canvas.removeEventListener('mousemove', handleMouseMove); 
    canvas.removeEventListener('mouseup', handleMouseUp); }; }, [src]);
    const handleMouseDown = (event: any) => {
        dragStartRef.current = { 
            x: event.clientX,
            y: event.clientY,
    const handleMouseMove = (event: any) => {
        if (!dragStartRef.current)  return;
    const canvas = canvasRef.current!;
    const ctx = canvas!.getContext('2d');
    const dragEnd = { x: event.clientX, y: event.clientY, };
    const width = dragEnd.x - dragStartRef.current.x;
    const height = dragEnd.y - dragStartRef.current.y;
    // 清除Canvas上的内容
    ctx!.clearRect(0, 0, canvas.width, canvas.height);
    // 绘制原始图片
    const img = new Image();
    img.onload = () => { 
        canvas.width = img.width;
        canvas.height = img.height;
        ctx!.drawImage(img, 0, 0);
        // 绘制裁剪框
        ctx!.strokeStyle = '#39f';
        ctx!.lineWidth = 2;
        ctx!.strokeRect(dragStartRef.current.x, dragStartRef.current.y, width, height);
        //绘制蒙层
        ctx!.save();
        ctx!.fillStyle = 'rgba(0,0,0,0.6)';// 蒙层颜色
        ctx!.fillRect(0, 0, 500, 500);
        //将蒙层凿开
        ctx!.globalCompositeOperation = 'source-atop'; 
        ctx!.clearRect(dragStartRef.current.x, dragStartRef.current.y, width, height) 
        // 绘制8个边框像素点并保存坐标信息以及事件参数
        ctx!.globalCompositeOperation = 'source-over';
        ctx!.fillStyle = '#fc178f';
        let size = 5;//定义像素点大小
        //逆时针写的点儿 ctx!.fillRect(dragStartRef.current.x, dragStartRef.current.y, size, size);
        ctx!.fillRect(dragStartRef.current.x + width / 2, dragStartRef.current.y, size, size);
        ctx!.fillRect(dragStartRef.current.x + width - size, dragStartRef.current.y, size, size);
        ctx!.fillRect(dragStartRef.current.x, dragStartRef.current.y + height / 2, size, size);
        ctx!.fillRect(dragStartRef.current.x + width - size, dragStartRef.current.y + height / 2, size, size);
        ctx!.fillRect(dragStartRef.current.x, dragStartRef.current.y + height - size, size, size);
        ctx!.fillRect(dragStartRef.current.x + width / 2, dragStartRef.current.y + height - size, size, size);
        ctx!.fillRect(dragStartRef.current.x + width - size, dragStartRef.current.y + height - size, size, size);
        ctx!.restore();
        //再次使用drawImage 将图片绘制到蒙层下方
        ctx!.save();
        ctx!.globalCompositeOperation = 'destination-over';
        ctx!.drawImage(img, 0, 0, canvas.width, canvas.height);
        ctx!.restore();
        // 获取裁剪区域的像素数据
        const imageData = ctx!.getImageData(dragStartRef.current.x, dragStartRef.current.y, width, height);
        console.log(imageData);
            img.src = src;
        const handleMouseUp = () => {
            dragStartRef.current = null;
    return ( 
        <canvas ref={canvasRef} width={500} height={500} />

  计算裁剪区域大小效果图:

3. 像素数据处理

     cropper 可以利用 Canvas API 中的 getImageData() 方法获取裁剪区域的像素数据,然后对该像素数据进行处理,如旋转、缩放、裁剪等操作。这样就可以根据用户的需求对图片进行精细化处理。

     举个🌰:这个实例主要实现点击按钮后,对像素数据进行旋转,缩放处理。

export default function Cropper() {
    const canvasRef = useRef<HTMLCanvasElement>(null);
    const handleClick = (type?: string) => {
    const canvas = canvasRef.current!;
    const ctx = canvas.getContext('2d');
    const img = new Image();
    img.src = src;
    img.onload = () => { 
        // 将图片绘制在Canvas上
        ctx!.drawImage(img, 0, 0);
        // 缩放 Canvas/旋转Canvas
        type === 'rotate' ? ctx!.rotate(Math.PI / 12): ctx!.scale(0.7, 0.7);
        // 获取旋转/缩放后的像素数据
        var imageData = ctx!.getImageData(0, 0, canvas.width, canvas.height); 
        // 将像素数据绘制到 Canvas 上 
        ctx!.putImageData(imageData, 0, 0); };
    return ( 
        <Group position="center">
            <canvas ref={canvasRef} width={400} height={400} style={{border:'1pc solid #333'}} />
            <Button onClick={() => handleClick('rotate')}>
            </Button>
            <Button onClick={() => handleClick('scale')}>
            </Button>
        </Group> 

旋转、缩放效果图:

4. 输出裁剪结果

      利用 Canvas API 中的 toDataUrl() 方法实现裁剪内容输出,toDataUrl() 方法可以将处理后的像素数据转换为 base64 编码的字符串。

      为什么要转换成 base64 位编码呢?

  • 将图片转换成 base64 位后,图片会跟随代码(html、css、js)一起请求加载,不会再单独进行请求加载;
  • 可以防止由于图片路径错误导致加载失败的问题;

      举个🌰:我们通过之前的步骤,可以拿到裁剪后的内容,那么这一步我们要做到的是输出这个内容,并且展示在 img 标签中。主要使用的是自定义的 getCropData 函数 ,里面利用 canvas API 的 toDataUrl() 方法拿到裁剪内容,并赋值给变量 cropData ,拿到 cropData 作为 img 标签的 src,即可展示裁剪内容的图片;

export default function ResultData() {
    const canvasRef = useRef<HTMLCanvasElement>(null);
    const dragStartRef = useRef<any>(null);
    const newCanvasRef = useRef<HTMLCanvasElement>(null);
    const [cropData, setCropData] = useState('#');
    useEffect(() => {
        const canvas = canvasRef.current!;
        const ctx = canvas.getContext('2d');
        const img = new Image();
        img.onload = () => {
            // 将图片绘制在Canvas上
            canvas.width = img.width;
            canvas.height = img.height;
            ctx!.drawImage(img, 0, 0);
        img.src = src;
        // 绑定鼠标事件
        canvas.addEventListener('mousedown', handleMouseDown); 
        canvas.addEventListener('mousemove', handleMouseMove); 
        canvas.addEventListener('mouseup', handleMouseUp);
        return () => { 
        // 解绑鼠标事件
        canvas.removeEventListener('mousedown', handleMouseDown); 
        canvas.removeEventListener('mousemove', handleMouseMove); 
        canvas.removeEventListener('mouseup', handleMouseUp);
    }, [src]);
    const handleMouseDown = (event: any) => {
        dragStartRef.current = { 
            x: event.clientX,
            y: event.clientY,
    const handleMouseMove = (event: any) => { 
        if (!dragStartRef.current) { return; } 
        const canvas = canvasRef.current!;
        const ctx = canvas!.getContext('2d');
        const newCanvas = newCanvasRef.current!;
        const newCtx = newCanvas.getContext('2d');
        const dragEnd = { 
            x: event.clientX,
            y: event.clientY,
        const width = dragEnd.x - dragStartRef.current.x;
        const height = dragEnd.y - dragStartRef.current.y;
        // 清除Canvas上的内容
        ctx!.clearRect(0, 0, canvas.width, canvas.height);
        // 绘制原始图片
        const img = new Image();
        img.onload = () => { 
            canvas.width = img.width;
            canvas.height = img.height;
            ctx!.drawImage(img, 0, 0);
            // 绘制裁剪框
            ctx!.strokeStyle = '#39f';
            //描述画笔(绘制图形)颜色
            ctx!.lineWidth = 2;
            ctx!.strokeRect(dragStartRef.current.x, dragStartRef.current.y, width, height);
            //绘制蒙层
            ctx!.save();
            ctx!.fillStyle = 'rgba(0,0,0,0.6)'; // 蒙层颜色
            ctx!.fillRect(0, 0, 500, 500);
            //将蒙层凿开
            ctx!.globalCompositeOperation = 'source-atop'; ctx!.clearRect(dragStartRef.current.x, dragStartRef.current.y, width, height)
            // 绘制8个边框像素点并保存坐标信息以及事件参数
            ctx!.globalCompositeOperation = 'source-over';
            ctx!.fillStyle = '#fc178f';
            let size = 5;//定义像素点大小 
            //逆时针写的点儿
            ctx!.fillRect(dragStartRef.current.x, dragStartRef.current.y, size, size); 
            ctx!.fillRect(dragStartRef.current.x + width / 2, dragStartRef.current.y, size, size);
            ctx!.fillRect(dragStartRef.current.x + width - size, dragStartRef.current.y, size, size);
            ctx!.fillRect(dragStartRef.current.x, dragStartRef.current.y + height / 2, size, size);
            ctx!.fillRect(dragStartRef.current.x + width - size, dragStartRef.current.y + height / 2, size, size);
            ctx!.fillRect(dragStartRef.current.x, dragStartRef.current.y + height - size, size, size);
            ctx!.fillRect(dragStartRef.current.x + width / 2, dragStartRef.current.y + height - size, size, size);
            ctx!.fillRect(dragStartRef.current.x + width - size, dragStartRef.current.y + height - size, size, size);
            ctx!.restore();
            //再次使用drawImage 将图片绘制到蒙层下方
            ctx!.save();
            ctx!.globalCompositeOperation = 'destination-over';
            ctx!.drawImage(img, 0, 0, canvas.width, canvas.height);
            ctx!.restore();
            // 获取裁剪区域的像素数据
            const imageData = ctx!.getImageData(dragStartRef.current.x, dragStartRef.current.y, width, height);
            //输出在另一个canvas上
            newCtx!.clearRect(0, 0, 300, 300);
            newCtx!.putImageData(imageData, 0, 0);
                img.src = src;
        const handleMouseUp = () => { 
            dragStartRef.current = null;
        const getCropData = () => { 
            const newCanvas = newCanvasRef.current!;
            const dataUrl = newCanvas.toDataURL(); 
            setCropData(dataUrl);
    return ( 
        <canvas ref={canvasRef} />
        <Group> 
            <canvas ref={newCanvasRef} width={300} height={300} style={{ border: '1px solid #d3d3d3' }} />
            <Stack mt={50}>
                <Box sx={{ width:300, height: 300, border: '1px solid #d3d3d3'}}>
                    <img src={cropData} />
                <Button onClick={getCropData}>确认裁剪</Button>
            </Stack> 
        </Group>

      裁剪内容输出效果图:

四.  react-cropper 实现裁剪图片

前面我们了解到,canvas 内部做了的操作,那么现在我们用 react-cropper 实现以上所有步骤。

export default function Cropper() {  
    const [image, setImage] = useState('');  
    const [cropData, setCropData] = useState('#');  
    const cropperRef = useRef<ReactCropperElement>(null);  
    const onChange = (e: any) => {  
        e.preventDefault();  
        let files;  
        if (e.dataTransfer) {  
            files = e.dataTransfer.files;  
        } else if (e.target) {  
            files = e.target.files;  
        const reader = new FileReader();  
            reader.onload = () => {  
            setImage(reader.result as any);  
        reader.readAsDataURL(files[0]);  
    const getCropData = () => {  
        if (typeof cropperRef.current?.cropper !== 'undefined') { 
          setCropData(cropperRef.current?.cropper.getCroppedCanvas().toDataURL()); 
    return (  
            <div style={{ width: '100%' }}>  
            <Input type="file" onChange={onChange} />  
            <Cropper  
                ref={cropperRef}  
                style={{ height: 400, width: '100%' }}  
                zoomTo={0.5}  
                initialAspectRatio={1}  
                aspectRatio={1}  
                preview=".img-preview"  
                src={image}  
                viewMode={1}  
                minCropBoxHeight={10}  
                minCropBoxWidth={10}  
                background={false}  
                responsive={true}  
                autoCropArea={1}  
                checkOrientation={false}  
                guides={true}  
                style={{  
                width: '50%',  
                float: 'right',  
                display: 'inline-block',  
                padding: 10,  
                boxSizing: 'border-box',  
                <h1>Preview</h1>  
                className="img-preview"  
                style={{ width: '100%', float: 'left', height: '300px', overflow: 'hidden' }}  
                <div className="box" style={{ width: '50%', float: 'right', height: '300px' }}>  
                    <span>Crop</span>  
                    <Button style={{ float: 'right' }} onClick={getCropData}>  
                        Crop Image  
                    </Button>  
                <Image style={{ width: '40%' }} src={cropData} alt="cropped" />  

效果图如下:

五.  总结

     以上就是 cropper 基于 canvas API 实现的全部过程啦,cropper 只是将实现流程中的关键四步封装起来,便于使用。这个插件,可以实现上传图片前进行裁剪得到想要的图片,比如上传头像。

cropper 是一款基于 JavaScript 的图片裁剪插件,通过 Canvas 技术实现了图片的裁剪和截图功能。这个插件,可以实现上传图片前进行裁剪得到想要的图片,比如上传头像
react-cropper + antdesign +dva 实现裁剪图片并上传的功能 一.首先安装react-cropper插件 npm install --save react-cropper 执行该命令以后,下载react-cropper依赖信息自动更新到package.json中 在使用该插件的代码中需要进行引入 import "cropperjs/dist/cropper.css" im...
推荐开源项目:React Cropper - 简洁高效的图片裁剪库 项目地址:https://gitcode.com/react-cropper/react-cropper React Cropper 是一个基于 React 的轻量级图片裁剪组件,为前端开发者提供了一种简单、直观的方式来实现图片的自由裁剪功能。这个项目的目的是帮助开发者在 web 应用中轻松集成图片处理功能,无需深入了解图像处理的...
推荐一款强大的React图像裁剪库:react-cropper 项目地址:https://gitcode.com/roadmanfong/react-cropper 在前端开发中,尤其是在涉及用户上传图片和进行个性化裁剪时,一个高效的图像处理工具是必不可少的。今天,我要向大家推荐的是一个专为React框架设计的轻量级、功能丰富的图像裁剪库——react-cropper。 react-cr...
通过Cropper.js裁切图片后,发现图片变大了(原图446k,裁切后图片6.16m)。 操作Cropper.js完成裁切图片后,先通过var canvas = cropper.getCroppedCanvas();得到HTMLCanvasElement。再通过HTMLCanvasElement的toBlob()得到裁切后的图片Cropper.js通过HTMLCanvasElement重绘裁切后的图片,因此,得到的裁切后的图片的数据的大小有可能变 React-saga和React-thunk都是用于处理异步操作的中间件。 React-thunk是Redux官方推荐的中间件之一,它允许我们在Redux中编写异步操作,使得我们可以在Redux中处理异步操作,而不需要在组件中处理异步操作。Thunk是一个函数,它接收dispatch和getState作为参数,并返回一个函数,这个函数接收dispatch作为参数,并在异步操作完成后调用dispatch。 React-saga是另一个处理异步操作的中间件,它使用了ES6的Generator函数来处理异步操作。Saga使用了一种称为Effect的概念,它是一个简单的JavaScript对象,用于描述异步操作。Saga使用了一种称为yield的语法,它允许我们在Generator函数中暂停异步操作,并在异步操作完成后继续执行。 总的来说,React-thunk和React-saga都是用于处理异步操作的中间件,它们的实现方式不同,但都可以让我们在Redux中处理异步操作。选择哪种中间件取决于个人的喜好和项目的需求。 ### 回答2: React-Saga和React-Thunk都是React应用中用于处理异步操作的中间件。它们的主要目的是在Redux应用中,帮助我们管理异步操作。这两个中间件都可以让React应用更加的灵活、健壮和易于维护。 React-Saga的核心理念是利用生成器函数来处理异步操作,Saga通过使用生成器来让异步操作变得像同步操作一样,其中每个异步操作都会被转化为一个迭代器函数,这些函数可以被Saga调用和暂停。 Saga主要有以下几个特点: 1. Saga可以使异步操作更加同步和简单,让异步调用变得更容易。Saga使用了轻量级、高效的生成器函数,从而有效地减少了异步调用中的代码复杂度。 2. Saga可以很好地管理和协调多个异步操作。Saga可以在任意阶段暂停异步操作,等待其他异步操作完成之后再继续执行。 3. Saga可以捕获和控制异步操作的错误、超时和状态。当出现问题时,Saga可以修复错误或者更改异步操作的状态,保证应用程序的稳定性和可靠性。 React-Thunk的核心概念是利用闭包函数来处理异步操作,Thunk将异步操作转化为一个闭包函数,然后通过回调函数将其传递到Redux的异步流中。 Thunk的主要特点有以下几个: 1. Thunk可以轻松处理异步操作,没有复杂的代码逻辑或者概念。 2. Thunk主要使用了闭包函数来捕捉当前异步操作的上下文,使得处理异步操作更加的简单、方便和自然。 3. Thunk可以轻松控制异步操作的状态、结果和错误处理,保证应用程序的稳定性和可靠性。 总之,React-Saga和React-Thunk都是帮助我们管理和处理应用程序的异步操作的中间件。它们都有自己独特的实现方式和特点。我们可以根据自己的项目需求和开发团队的技能水平来选择适合我们的中间件。 ### 回答3: React-saga 和 React-thunk 都是针对 React 应用中异步操作的中间件。它们两个都可以用来控制异步流程,使得我们可以更好的管理 React 应用程序中异步操作的数据和状态。 相较于 react-thunk, react-saga 是一个更加强大的中间件,它基于 generator 函数的概念,可以用来控制非常复杂的异步流程,使得我们可以在操作时更加精细地掌控多个异步操作的执行顺序和状态。 如果说 react-thunk 的核心概念是将异步操作封装进一个函数里,而在需要时调用这个函数即可,那么 redux-saga 的核心概念则是分离出一个独立的 Generator 函数来代表所有的异步业务逻辑。 redux-saga 可以让你从另一个角度处理异步流程,使你能够同步处理异步操作,不同的 Saga 可以用一种集中且易于理解的方式组合起来,组成它们自己的执行序列。 总而言之,React-saga和React-thunk 都是 React 应用程序开发中非常实用的工具,对于管理异步操作和数据状态非常有帮助。但是针对不同的开发需求,我们需要选择相应的中间件,来实现我们最好的业务逻辑。所以我们在使用的时候需要根据实际情况选择适合的中间件进行操作,以达到最好的效果。 CSDN-Ada助手: 非常棒的博客!很高兴看到你分享了关于“react-cropper”的使用体验和技巧。继续写博客是一个很好的习惯,可以帮助你巩固所学知识,同时也能为其他人提供帮助。除了裁剪工具,你还可以探索更多与图片相关的技能,比如图片压缩、图片滤镜、图片水印等等,这些技能都有广泛的应用场景,可以让你的技术水平更上一层楼。期待你的下一篇博客! 如何写出更高质量的博客,请看该博主的分享:https://blog.csdn.net/lmy_520/article/details/128686434?utm_source=csdn_ai_ada_blog_reply2 如果您持续创作,完成第三篇博客,并且质量分达到 80 分以上,在评论区就有机会获得红包奖励哦! 柱状图 显示正负值,点击列表联动柱状图 CSDN-Ada助手: 恭喜您写了第四篇博客,题目听起来很有趣!在这篇博客中,您展示了如何在柱状图中显示正负值,并且实现了点击列表联动柱状图的功能,这对于数据可视化的开发者来说非常有用。接下来,我建议您可以尝试探索如何在柱状图中添加动态效果,例如动画、交互等,让您的可视化图表更加生动、吸引人。期待您的下一篇博客! CSDN 会根据你创作的前四篇博客的质量,给予优秀的博主博客红包奖励。请关注 https://bbs.csdn.net/forums/csdnnews?typeId=116148&utm_source=csdn_ai_ada_blog_reply4 看奖励名单。 简单易懂的Echars案例,实现点击列表联动饼图 CSDN-Ada助手: 恭喜您写出了这篇简单易懂的Echars案例,实现了点击列表联动饼图的功能。您的博客内容清晰明了,让读者能够轻松理解并学习。希望您能继续分享您的经验和技术,让更多人受益。下一步,您可以考虑分享一些实战案例,让读者更好地掌握实际应用技巧。期待您的下一篇作品! CSDN 正在通过评论红包奖励优秀博客,请看红包流:https://bbs.csdn.net/?type=4&header=0&utm_source=csdn_ai_ada_blog_reply3,我们会奖励持续创作和学习的博主,请看:https://bbs.csdn.net/forums/csdnnews?typeId=116148&utm_source=csdn_ai_ada_blog_reply3