前言:
写这篇文章的目的是为了记录下图片剪裁功能的实现过程,说实话,这个功能的实现还是有点复杂的,想了好久,也看过一些油管博主的代码,说实话效果总是差强人意。就在我想要放弃的时候,看到了B站up主三石的视频,本片文章canvas渲染的具体代码就是根据他的代码做了一点点的改动所完成的,这里向他表示衷心的感谢。同时本文除了要讲述如何实现功能还是从0开始分析Canvas 渲染图片出现模糊原因,希望我能用图文的形式讲清楚。左边有目录,大家可以跳转到自己需要的地方查看。
技术栈
此文所涉及技术栈
- 前端框架:React
- 剪切组件:react-image-crop (可以用npm或yarn 安装)
- 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 上写这么长的文章,在实现图像剪裁的功能中遇到很多的困难,基于这种情况,我认为非常有必要把这些知识记录下来以供我和诸位一起学习,本人前端知识确实有限,在写这篇文章的时候也许会出现很多的错误,请各位前端大佬们不吝指正!