一、引言
在前端开发中,用户对选择的图片进行处理是一个比较常见的功能。对用户选择的图片进行缩放、旋转、移动和裁剪等操作,不仅能够提升用户体验,还能为网页增添更多的互动性和视觉效果。
本文将介绍两种不同的方法来实现这一功能:一种是利用 CSS3 实现图片处理;另一种则是通过Canvas 中的 Transform来完成变换操作。
通过对这两种方案的探讨,读者将能够了解不同方法的优势与特点,并根据实际需求选择最适合的实现方式。
二、实现方案
方案一:CSS3 实现
1.实现思路
利用CSS3 强大的transform 属性对图片进行缩放、旋转和移动操作,通过直观的CSS 属性设置来实现图片变换效果。然后使用html2canvas 库则用于将包含处理后图片的DOM元素转换为Canvas,进而导出为图片,实现对图片的裁剪和最终保存。
2.实现效果
3.实现步骤
初始化图片信息
首先是创建一个静态的图片展示。用来获取用户上传的图片,通过FileReader将图片渲染到页面中。并且绘制一个正方形在图片上方,用于展示图片输出效果
jsx
export default () => {
const getBase64 = (img: RcFile, callback: (url: string) => void) => {
const reader = new FileReader();
reader.addEventListener('load', () => callback(reader.result as string));
reader.readAsDataURL(img);
};
const handleChange: UploadProps['onChange'] = (info) => {
if (info.file.status === 'uploading') {
return;
}
if (info.file.status === 'done') {
getBase64(info.file.originFileObj as RcFile, (url) => {
setImgUrl(url);
setVisible(true);
});
}
};
return <div>
<Upload
name="avatar"
listType="picture-card"
className="avatar-uploader"
showUploadList={false}
onChange={handleChange}
action={(file) =>
new Promise((resolve) => {
getBase64(file as RcFile, (url) => {
resolve(url);
});
})
}
>
<button style={{ border: 0, background: 'none' }} type="button">
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</button>
</Upload>
<img
src={imgUrl}
className="image-container-img"
style={{
transform: `translate(${moveConfig.moveX}px, ${moveConfig.moveY}px) scale(${scale}) rotate(${rotate}deg)`,
}}
/>
}
</div>
}
监听图片移动事件
监听图片的鼠标按下事件、鼠标移动事件、鼠标释放事件。通过对比用户初始鼠标位置与鼠标移动位置计算出来图片移动位置,然后设置元素的transform属性,用来移动图片位置。
javascript
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
// 阻止默认事件,防止触发浏览器默认行为
e.preventDefault();
setMoveConfig({
moveX: moveConfig.moveX,
moveY: moveConfig.moveY,
startX: e.clientX - moveConfig.moveX,
startY: e.clientY - moveConfig.moveY,
startMove: true,
});
};
// 使用节流减少state更新次数
const { run: handleMouseMove } = useThrottleFn(
(e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
if (!moveConfig.startMove) {
return;
}
setMoveConfig({
moveX: e.clientX - moveConfig.startX,
moveY: e.clientY - moveConfig.startY,
startX: moveConfig.startX,
startY: moveConfig.startY,
startMove: true,
});
},
{ wait: 16.7 }
);
const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
setMoveConfig({ ...moveConfig, startMove: false });
};
注意:需要使用preventDefault阻止默认动作,否则会引起mouseup事件无法触发,图片会在鼠标弹起后跟上鼠标移动
图片旋转与放大
前面已经监听了鼠标的按下与移动事件,旋转图片相关操作就不能再使用鼠标事件了,我们可以在图片下方加两个渲染按钮,用来记录图片的旋转角度。然后设置图片的transform 属性中的rotate来设置图片的旋转角度。
jsx
<Button icon={<RotateLeftOutlined />} onClick={() => setRotate(rotate - 11.25)} />
<Button icon={<RotateRightOutlined />} onClick={() => setRotate(rotate + 11.25)} />
图片放大
图片方案采用和旋转相似的方式实现,通过定义一个按钮来控制图片的放大缩小倍数。然后设置图片的transform 中的scale属性来设置图片缩放倍数
jsx
<Button
icon={<MinusOutlined />}
disabled={scale <= 0.2}
onClick={() => {
setScale(scale - 0.1);
}}
/>
<Button
icon={<PlusOutlined />}
disabled={scale >= 3}
onClick={() => {
setScale(scale + 0.1);
}}
/>
保存新图片
用户所有的操作都可以通过css3中的transform属性来实现,但是保存图片还是需要用到canvas 。我们可以采用html2canvas库来实现对用户操作之后的图片转换为canvas对象,然后转换为base64 ,再将base64 转换为File对象,用来上传到服务器
javascript
const generateFile = () => {
const container = 图片容器
const canvas = await html2canvas(container, {
useCORS: true,
logging: true,
width: 300,
height: 300,
// 表示开始裁剪的x轴坐标
x: container.offsetLeft + 150,
// 表示开始裁剪的y轴坐标
y: container.offsetTop + 60,
});
const dataurl = canvas.toDataURL("image/png");
const arr = dataurl.split(",");
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const file = new File([u8arr], fileName, { type: 'png' });
}
方案二:Canvas Transform实现
使用css3的方案中,主要是直接对图片进行各种处理。相关操作都可以直接用canvas直接实现。不过本文针对canvas的实现方式,将采用对图片上方的形状进行移动、缩放操作、旋转则继续操作图像内容
1.实现思路
直接利用Canvas 的变换矩阵来实现图片的缩放、旋转和移动。通过Canvas 提供的translate 、rotate 、scale 等方法,对绘制图片时的变换矩阵进行精确控制,从而实现各种图像变换效果。裁剪操作则通过drawImage方法的参数设置来指定裁剪区域和目标区域。
2.实现效果
3.实现步骤
初始化图片
这一步和上面实现基本一致,先获取用户上传的图片,然后通过Canvas 的drawImage将图片渲染到画布中。
jsx
const getBase64 = (img: RcFile, callback: (url: string) => void) => {
const reader = new FileReader();
reader.addEventListener('load', () => callback(reader.result as string));
reader.readAsDataURL(img);
};
export default () => {
const [imgDOM, setImgDOM] = useState<HTMLImageElement>();
const handleChange: UploadProps['onChange'] = (info) => {
if (info.file.status === 'uploading') {
return;
}
if (info.file.status === 'done') {
setVisible(true);
getBase64(info.file.originFileObj as RcFile, (url) => {
const img = new Image();
img.src = url;
img.onload = () => {
// 这里缓存一个imgDom,canvas绘制图片需要img元素
setImgDOM(img);
// 绘制图片
ctx.drawImage(imgDOM, 0, 0, img.width,img.height);
};
});
}
};
return (
<Upload onChange={handleChange}>
</Upload>
);
};
将图片绘制在canvas中心位置
上面的步骤中,我们将图片绘制到Canvas 中,但是图片是在Canvas 左上角进行绘制的,没有在整个Canvas 中间区域展示。对于将图片居中,我们可以使用Canvas 中的translate方法,将画布的原点定位到画布中间,然后图片渲染的时候起始位置可以设置图片宽高的一半,这样就可以将图片居中了。
因为每次都要重新设置原点,所以推荐每次绘制前先保存Canvas绘制状态,然后绘制完成后恢复上一次状态,可以将画布恢复为初始化状态,避免每次绘制前需要计算初始位置等属性
jsx
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2);
const { width, height } = imgDOM;
ctx.drawImage(imgDOM, -width / 2, -height / 2, width, height);
// 恢复上一次状态
ctx.restore();
移动可视窗口
示例图中间位置的白色可选区域,通过绘制一个新的Canvas 来展示,在移动的过程中,通过改变小Canvas的位置以及原点位置,来实现图片的渲染。图片渲染位置也是通更新原点位置来实现图片的截取显示
javascript
const ctx = canvasTop.getContext('2d')!;
ctx.clearRect(0, 0, canvasTop.width, canvasTop.height);
ctx.save();
ctx.translate(canvasTop.width / 2 - moveConfig.moveX, canvasTop.height / 2 - moveConfig.moveY);
const { width, height } = imgDOM;
ctx.drawImage(imgDOM, -width / 2, -height / 2, width, height);
ctx.restore();
旋转与缩放Canvas中的图片
旋转直接采用Canvas 中的rotate方法来进行旋转图片,缩放在采用在绘制的时候,放大图片绘制的比例来进行展示
jsx
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
// 新增旋转相关
ctx.rotate((rotate * Math.PI) / 180);
const { width, height } = imgDOM;
// 实际宽度
const realWidth = width * scale;
// 实际高度
const realHeight = height * scale;
ctx.drawImage(imgDOM, -realWidth / 2, -realHeight / 2, realWidth, realHeight);
ctx.restore();
预览图片
和移动窗口图片一样,直接使用一个Canvas来展示
保存图片
保存图片时,可以直接使用右侧效果图直接进行toDataURL保存图片。
javascript
ctx.toDataURL()
三、完善与不足
本文只是简单的实现了两用方案来处理图片,有很多细节点没有着重实现。比如CSS3方案中图片移动限制,取景框可能没有图片;鼠标再离开图片后抬起,导致无法关闭图片移动;Canvas中取景框不能调整大小;缩放,旋转不能自定义大小与角度。未完待续。
四、总结
本文使用了两种方案来实现图片裁切、旋转、缩放操作。一种是操作DOM 元素的方式,一种通过Canvas操作方式。 两种方案各有优劣,开发者可根据实现难度、性能表现、跨域处理需求、输出质量以及移动端支持等多方面因素综合考虑,选择合适的方案。