React 实现图片裁剪功能

前言

  写这篇文章的目的是为了记录下图片剪裁功能的实现过程,说实话,这个功能的实现还是有点复杂的,想了好久,也看过一些油管博主的代码,说实话效果总是差强人意。就在我想要放弃的时候,看到了B站up主三石的视频,本片文章canvas渲染的具体代码就是根据他的代码做了一点点的改动所完成的,这里向他表示衷心的感谢。同时本文除了要讲述如何实现功能还是从0开始分析Canvas 渲染图片出现模糊原因,希望我能用图文的形式讲清楚。左边有目录,大家可以跳转到自己需要的地方查看。

技术栈

此文所涉及技术栈

  1. 前端框架:React
  2. 剪切组件:react-image-crop (可以用npm或yarn 安装)
  3. UI 框架: MUI

实现代码

Test.jsx 代码

import { Paper } from '@mui/material'
import React, { useRef, useState } from 'react'
import './test.css'
import ReactCrop from 'react-image-crop'
import 'react-image-crop/dist/ReactCrop.css'


function Test() {
    // 这里设置了一个状态Crop,Crop是剪切框的初始数据,里面保存着XY坐标,宽度和高度
    const [crop, setCrop] = useState({unit: 'px',x:0,y:0,width:200,height:200})
    // canvasRef 是一个引用,用来获取canvas 的真实DOM
    const canvasRef = useRef()
    // imgRef 是一个引用,用来获取img 的真实DOM
    const imgRef = useRef()

    const [url,setUrl] = useState(null)
    /* 
      这是一个回调函数,是在图片初次被加载时调用。
     */
    const onImageLoad = () =>{
        //获取canvas真实dom
        const canvas = canvasRef.current
        //获取img真实dom
        const image = imgRef.current
        image.setAttribute('crossOrigin', 'anonymous')
        // 设置canvas 容器的宽度
        canvas.style.width = '200px';
        // //  // 设置canvas 容器的高度
        canvas.style.height = '200px';
        // 放大我们的画布宽度
        canvas.width = 200 * devicePixelRatio
        // 放大我们的画布高度
        canvas.height = 200 *devicePixelRatio
        //context 可以简单的认为是画笔
        const context = canvas.getContext("2d")
        // width 是我们在真实图片上截取区域的宽度
        let width = (200 / image.width) * image.naturalWidth
        // height 是我们在真实图片上截取区域的高度
        let height = (200 / image.height) * image.naturalHeight
        // 进行渲染
        context.drawImage(image, 0,0,width,height, 0,0,canvas.width,canvas.height)
       
        
    }
    /* 
       这是一个回调函数,是在Crop位置发生位移的时候调用
     */
    const onCropChange = (c) =>{
        setCrop(c)
        const canvas = canvasRef.current
        const image = imgRef.current
        canvas.style.width = '200px';
        canvas.style.height = '200px';
        canvas.width = image.width * devicePixelRatio
        canvas.height = image.height * devicePixelRatio
        const context = canvas.getContext("2d")
        const width = c.width * (image.naturalWidth / image.width);
        const height = c.height * (image.naturalHeight / image.height);
        let x = c.x * (image.naturalWidth / image.width)
        let y = c.y * (image.naturalHeight / image.height)
        context.drawImage(image, x,y,width,height, 0,0,canvas.width,canvas.height)
    }
    return (
    <div>
       
            <Paper className='paper'>
                <ReactCrop crop={crop} onChange={onCropChange} className='cropper'>
                    <img ref={imgRef} src= "https://cdn.pixabay.com/photo/2014/05/13/16/19/porto-343487_1280.jpg" onLoad={onImageLoad}/>
                </ReactCrop>
                <div>
                    <canvas ref={canvasRef} ></canvas>
                </div>  
            </Paper>
       
    </div>
  )
}

export default Test

Scss 代码

.paper {
    width: 600px;
    height: 500px;
    outline: none;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-50%);
    .cropper {
        width: 300px;
        object-fit: cover;
        margin-bottom: 5rem;
    }
}

Test 组件里定义了一个图片剪辑区,以及预览区。可以看到下面的图片,上面是剪切图片的区域,下面是用Canvas 渲染的区域。

我在写这个功能的时候,出现canvas 渲染图片不清晰

这里 可以看出上面那张原图明显比下面canvas 渲染图要来的清晰。原因就是我们的图片其实是被“放大”了。我这里要从头说起。

基本概念

像素  

像素是数字图像的最基本单位。生活中谈到一个图片有多大,我们会用用 XX px * XX px来表示。

我们可以看到下面这张像素图(像素图是一种绘画风格),是由一个个有颜色的方格子组成,每个格子就是1个像素

这两张图都是由1个个像素组成的。

物理像素

在CSDN 上你会经常看到大家在说物理像素这个名词,这是什么意思呢?物理像素就是我们电脑、手机的分辨率,比如我的电脑屏幕分辨率是1920*1080 ,你可以理解为横向有1920个格子,纵向有1080个格子,总共有207.36万个格子,每个格子就是一个像素点。

逻辑像素

逻辑分辨率就是我们前端CSS 里的px了,逻辑像素并没有固定的物理长度。你不能说1px 的长度是几毫米,还是几厘米。这只是一个逻辑概念。

逻辑像素和物理像素之间的关系

上图表示有一个屏幕,物理分辨率为 5 * 6 ,屏幕中有一个Logo 图片。图片分辨率为3 * 4

现在我们换一个分辨率为 10 * 12 的屏幕,我们发现它变小了,这是为什么呢?所谓的逻辑分辨率为3*4是指在屏幕上占用 3 * 4个 物理像素点,刚才说了逻辑像素点并不表示物理大小,在分辨率高的屏幕中,由于每个物理像素点变小,那么显示的图片也会随之变小。但是我们生活中发现我用华为手机看屏幕上的字和OPPO,小米手机是一样的,屏幕的分辨率是有差异的,为什么看不到上面显示的字有什么差异呢?-------这就要引出第三个概念了DPR 

DPR

为了使得同一个逻辑像素能在不同分辨率的设备上保持一样的效果,设备制造商指定了一个设备像素比(dpr)。意思就是1px 的逻辑像素对应多少的物理像素,这有什么用呢?

原来在分辨率5 * 6 设备上展示的图片在 10* 12 设备上变小的原因是单个像素点变小了,这个时候,我们只要使得1个px的逻辑像素对应的物理像素由1 变成 2 ,也就是DPR为2 ,这样就能完美解决这一问题了

 位图像素

 位图(Bitmap)我们常见的很多图片都是位图。比如这张。位图的基本组成单位是像素,我们就把这些像素叫作位图像素。这张图片就是由千千万万个位图像素组成的,一个位图像素中包含了很多的二进制数据

我们看一下上面这张图的信息,分辨率1000* 420 指的是宽度上由1000个位图像素组成,高度上由420个位图像素组成,宽度指的是宽度上可以占1000个逻辑像素,高度指的是高度上可以占420个逻辑像素。位深度指的是单个元素能显示多少种颜色,这张图片是32位,可以显示2的32次方种颜色

位图像素和逻辑像素

位图像素可以理解为就是逻辑像素,位图像素是专门用来描述位图的,1px 位图像素和1px 逻辑像素并没有什么差别,但是要注意,图片的位图像素和页面的展示的宽高是不同的概念。就拿上个图片来说,位图像素是1000 *420 ,说明图片可以在页面宽度上占1000个逻辑像素,但实际的展示宽度却并不一定是1000px,

请看这里的css宽高,是640 *324.6  这是个响应式页面,页面缩小,元素也跟着缩小

随着页面的缩小,css 的尺寸也在缩小。可见图片自身的分辨率和在页面的展示宽高完全是两个不同的概念。 

开始分析canvas绘制图片模糊的原因

前面说过DPR 能够解决逻辑像素在不同分辨率的设备上以相同效果显示的问题,但我们接下来遇到的问题也是由它造成的。

请看下图,左图是标准屏幕,右图是高清屏幕

当DPR 为1 时,1个位图(Bitmap) 像素 = 1个物理像素,此时图像显示出来是清晰的。

当DPR 为2时, 1个位图像素(Bitmap) = 2 * 2 个物理像素,此时图像显示出来是模糊的。这是为什么呢?因为在位图中像素已经是最小的单位了,不可以拿一个位图像素的去填充4个物理像素,剩余的3个位图像素会使用类似的颜色填充

为什么不使用原色填充呢?,这是因为在高清屏幕中使用原色填充,图案锯齿感非常明显,图像明显缺乏了一丝顺化。 

那么怎么解决呢?这这个例子中,要想让图像显示我们就要想办法让 1个 位图像素 = 1 个物理像素

有一个好的办法,就是放大图片,那么放大多少呢?

在这个例子中, 我们要把宽度,和高度都放大2倍

1px * 2 * 1px * 2 = 4px  这样 左侧的1个逻辑像素就等于右侧的1个物理像素

在实际的编码中我们不可以用常量来进行放大,而要使用devicePixelRatio (缩写DPR)进行动态的放大,这里我在Edge 控制台里面打印了当前的devicePixelRatio。

最终解决方案

这里我把canvas 画布的width, height 都放大DPR倍。这时会有小伙伴有疑问了,你把原来的图片放大了,图片不就是我想要的尺寸了么? 没关系,我们可用以设置canvas.style.width和canvas.style.height  ,这样就会canvas 展示在页面上的尺寸就是canvas.style.width和canvas.style.height 了,具体实现可以看我的实现代码

感言:

第一次在CSDN 上写这么长的文章,在实现图像剪裁的功能中遇到很多的困难,基于这种情况,我认为非常有必要把这些知识记录下来以供我和诸位一起学习,本人前端知识确实有限,在写这篇文章的时候也许会出现很多的错误,请各位前端大佬们不吝指正!

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
React Native中进行图片裁剪可以使用第三方库来实现。一个常用的库是react-native-image-crop-picker。下面是一个简单的示例代码,演示如何在React Native中使用该库进行图片裁剪: 首先,你需要安装该库。打开终端,进入项目目录,并执行以下命令: ``` npm install react-native-image-crop-picker ``` 然后,根据你的平台,在iOS或Android项目中执行必要的配置步骤。你可以在该库的官方文档中找到详细的配置说明。 接下来,在你要使用图片裁剪功能的组件中,导入并使用react-native-image-crop-picker库。例如: ```javascript import React, { useState } from 'react'; import { View, Image, Button } from 'react-native'; import ImageCropPicker from 'react-native-image-crop-picker'; const MyComponent = () => { const [image, setImage] = useState(null); const handleImageSelection = () => { ImageCropPicker.openPicker({ width: 300, height: 400, cropping: true, }).then((response) => { if (!response.didCancel) { setImage(response.path); } }); }; return ( <View> {image && <Image source={{ uri: image }} style={{ width: 300, height: 400 }} />} <Button title="Select Image" onPress={handleImageSelection} /> </View> ); }; export default MyComponent; ``` 在上述示例代码中,我们首先导入了需要的React Native组件和react-native-image-crop-picker库。然后,在MyComponent组件中,我们使用useState来管理选择的图片,并在handleImageSelection函数中调用ImageCropPicker.openPicker方法来打开图片选择器并进行裁剪。最后,我们通过判断response对象的didCancel属性,来决定是否将裁剪后的图片路径设置到image状态中,并在界面上显示出来。 请注意,上述示例代码中的裁剪参数(宽度、高度和cropping属性)是可选的,你可以根据自己的需求进行调整。 希望这个示例能帮助到你!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值