最终的效果
1.点击上传图片(载入原有的头像)
2.在图片范围内等比例裁剪图片
3.重新上传图片
实现最终效果的步骤
1、获取图片内容
2、裁剪图片内容
3、导出图片内容
1、结合antd的Model框
<Modal
key={`${isEdit}`}
width={648}
visible={visibleImg}
title="编辑头像"
footer={null}
onCancel={() => {
this.setState({
isCropperCancel: true,
visibleImg: false,
});
}}
>
<div className="changeHeadImgModal">
<Cropper
isCropperCancel={isCropperCancel}
visible={visibleImg}
src={editImgUrl}
newSrc={newImg}
onUpLoad={(newImg) => {
this.setState({
newImg,
});
}}
/>
<div className="HeadImgModalBtn">
<Button onClick={() => {
this.setState({
isCropperCancel: true,
visibleImg: false,
});
}}
>
取消
</Button>
<Button
type="primary"
onClick={() => {
this.setState({
isCropperCancel: false,
visibleImg: false,
}, () => {
this.addNewHeadImg();
});
}}
>
确定
</Button>
</div>
</div>
</Modal>
Model框可以加 key值是用来重载组件
在Model框里引入我们自己写的组件<Cropper/>,传的参数有:
1.isCropperCancel判断是否取消裁剪 -- bool
2.visible判断是否关闭当前Model框 -- bool
3.src初始传入的图片base64 -- string
4.newSrc确定裁剪后新的图片base64 -- string
5.onUpLoad新的裁剪图片传回的函数 -- function
2、<Cropper/>组件的结构(HTML)
<div>
<div className="modalBody">
<canvas ref="canvas" className="cropperCanvas"/>
<div className="baffle"/>
<img className="headPicture" src={dataURL} alt="头像图片"
style={{
clip: `
rect(
${rect.top}px,
${rect.right}px,
${rect.bottom}px,
${rect.left}px
)`,
width: `${dw}px`,
height: `${dh}px`,
left: `${dx}px`,
top: `${dy}px`,
}}/>
<div className="mark" onMouseDown={this.markMouseDown}
style={{
width: `${markBorder.width}px`,
height: `${markBorder.height}px`,
left: `${markBorder.left}px`,
top: `${markBorder.top}px`,
}}
>
{
point.map((item, index) => {
return (
<div className={`stretch ${item.position}`} onMouseDown={this.stretchMouseDown}
style={{
left: `${item.left}px`,
top: `${item.top}px`,
}}
key={index}
/>
);
})
}
</div>
</div>
<div className="reselectImg">
<label htmlFor="ULImage">
<input type="file" name="ULImage" id="ULImage" accept=".png, .jpg, .jpeg, .gif, .bnp" multiple
style={{ display: "none" }} onChange={this.upLoadImg}/>
<span className="reselectBtn"><Icon type="upload"/>重新选择</span>
</label>
</div>
</div>
1、底部的重新上传按钮,使用input file重写的按钮
2、主题部分的图片区域包括
第一层最底下的canvas
第二层的挡板baffle(用来让底部的canvas图变暗)
第三层img和canvas重合利用clip: rect控制显示区域,这样就有框选区域变亮的效果
第四层mark就是选取层的边框,在mark层内部还有边框上可以放大缩小的点,因为是等比例变化,所有只有四个点可以用来放大缩小
2、<Cropper/>组件的样式(Less)
.changeHeadImgModal{
width: 100%;
.modalBody{
text-align: center;
height: 300px;
line-height: 300px;
padding-top: 1px;
background: url("./media/images/pic_bg.png") no-repeat center 0px;
background-size: cover;
//overflow: hidden;
position: relative;
.mark{
position:absolute;
z-index: 10;
background-color: rgba(0, 0, 0, 0);
border: 1px solid #1991EB;
box-sizing: border-box;
.stretch{
position: absolute;
width: 5px;
height: 5px;
background-color: #1991EB;
}
.T,.L,.R,.B{
border-radius: 50%;
}
}
.baffle{
position:absolute;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 8;
left: 0;
top: 0;
}
.headPicture{
position: absolute;
z-index: 9;
box-sizing: border-box;
}
}
.HeadImgModalBtn{
width: 100%;
text-align: center;
button{
margin: 0 8px;
}
}
.reselectImg{
margin-top: 15px;
.reselectBtn{
font-family: PingFangSC-Regular, PingFang SC;
font-size: 12px;
font-weight: 400;
display: inline-block;
width: 100px;
text-align: center;
cursor: pointer;
height: 32px;
border-radius: 4px;
background: #1991EB;
line-height: 32px;
transition: background 0.2s linear 50ms;
}
.reselectBtn:hover{
background: #52a2dc;
transition: background 0.2s linear 50ms;
}
}
}
样式可以根据具体的UI图
2、<Cropper/>组件的逻辑
state:
this.state = {
visible: false,//是否关闭截取图片
dataURL: "",//文件的URL
dh: 0,//图片在画布的高
dw: 0,//图片在画布的宽
dx: 0,//图片在画布的基准位置x
dy: 0,//图片在画布的基准位置y
ctx: null,//当前画布
isCrop: true,//是否裁剪图片
isMoveMark: false,//是否开始移动mark
isMoveStretch: false,//是否拖拽点
markBorder: {
width: 0,
height: 0,
left: 0,
top: 0,
},//边框大小
rect: {
top: 0,
right: 0,
bottom: 0,
left: 0,
},//明亮的区域
startMark: {
x: 0,
y: 0,
},//移动mark初始基准值
startStretch: {
x: 0,
y: 0,
},//变形点点的初始基准值
};
固定的一些常量:
const canvasWidth = 618;
const canvasHeight = 300;
const aspectRatio = 134 / 110;//设置边框的宽高,高宽比是134/110
(1)读取上传的图片文件(重新上传按钮)利用的input file读取文件转化为base64
upLoadImg = (e) => {
const fileData = e.target.files[0];
const reader = new FileReader();
if (fileData) {
reader.readAsDataURL(fileData);
reader.onload = () => {
this.getImage(reader.result);//读取图片base64
};
}
};
(2)将读取图片文件绘制在canvas和img上下两层,将图片绘制在cavans上,初始化裁剪框也是根据图片的宽高去设定
getImage = (dataURL) => {
this.setState({
dataURL,
}, () => {
const canvas = this.refs["canvas"] || document.querySelector(".modalBody .cropperCanvas");
const image = new Image();
const ctx = canvas.getContext("2d");
//设置宽高用于清空旧画布
canvas.height = canvasHeight;
canvas.width = canvasWidth;
image.src = dataURL;
image.onload = () => {
//获取图片的宽高
const height = image.height;
const width = image.width;
//调整图片的位置
let dx = 0;
let dy = 0;
let dw, dh;
if (height < width && width / height >= canvasWidth / canvasHeight) {
dw = canvasWidth;
dh = height * (canvasWidth / width);
dy = (canvasHeight - dh) / 2;
} else {
dh = canvasHeight;
dw = width * (canvasHeight / height);
dx = (canvasWidth - dw) / 2;
}
ctx.drawImage(image, dx, dy, dw, dh);
this.setState({
dx, dy, dw, dh, ctx,
}, () => {
//根据图片大小和形状初始化框选的区域
const { dx, dy, dw, dh } = this.state;
const initBorder = (dh >= dw && (dh / dw) >= aspectRatio)
? {
width: dw,
height: dw * aspectRatio,
left: dx - 1,
top: (dh - dw * aspectRatio) / 2,
} : {
width: dh / aspectRatio,
height: dh,
left: dx + (dw - dh / aspectRatio) / 2,
top: dy,
};
const initRect = {
top: (dh - initBorder.height) / 2,
right: initBorder.width + (dw - initBorder.width) / 2 - 1,
bottom: initBorder.height + (dh - initBorder.height) / 2,
left: (dw - initBorder.width) / 2,
};
this.setBorder(initBorder, initRect);
});
};
});
};
(2)设置裁剪框和明亮处以及8个点的位置(核心方法)
setBorder = (markBorder, rect) => {
//设置显示区域和明亮区域大小
this.setState({
markBorder, rect,
}, () => {
const { markBorder, isCrop } = this.state;
isCrop && this.cropPicture(markBorder);
});
};
const point = [
{ position: "LT", left: -3, top: -3 },
{ position: "T", left: markBorder.width / 2 - 3, top: -3 },
{ position: "RT", left: markBorder.width - 4, top: -3 },
{ position: "R", left: markBorder.width - 4, top: markBorder.height / 2 - 3 },
{ position: "RB", left: markBorder.width - 4, top: markBorder.height - 4 },
{ position: "B", left: markBorder.width / 2 - 3, top: markBorder.height - 4 },
{ position: "LB", left: -3, top: markBorder.height - 4 },
{ position: "L", left: -3, top: markBorder.height / 2 - 3 },
];
(3)裁剪图片,利用的也是canvas
cropPicture = (markBorder) => {
const data = this.state.ctx.getImageData(markBorder.left, markBorder.top, markBorder.width, markBorder.height);
const cs = document.createElement("canvas");
const ct = cs.getContext("2d");
cs.width = markBorder.width;
cs.height = markBorder.height;
ct.putImageData(data, 0, 0, 0, 0, markBorder.width, markBorder.height);
const newImg = cs.toDataURL("image/png");
this.setNewImg(newImg);
cs.remove();
};
(3)新图片导出
setNewImg = (newImg) => {
this.props.onUpLoad(newImg);
};
(4)移动选取框,核心就是改变框的大小和明亮处的大小,然后调用setBorder这个方法,用到的是鼠标的选取框的点击事件以及全局的鼠标移动和鼠标弹起事件
markMouseDown = (e) => {
e.preventDefault();
//添加在全局的鼠标弹起事件
window.onmouseup = () => {
this.setState({
isCrop: true,
isMoveMark: false,
}, () => {
const { markBorder, rect } = this.state;
this.setBorder(markBorder, rect);
window.onmouseup = null;
window.onmousemove = null;
});
};
this.setState({
startMark: {
x: e.clientX,
y: e.clientY,
},
isCrop: false,
isMoveMark: true,
}, () => {
//添加全局的鼠标移动事件
window.onmousemove = (e) => {
e.preventDefault();
const { isMoveMark, startMark, dx, dy, dw, dh, markBorder } = this.state;
const movingDistance = {
x: 0,
y: 0,
};//移动的距离
if (isMoveMark) {
//横向偏移临界值判断
if (e.clientX - startMark.x <= dx - markBorder.left) {
movingDistance.x = dx - markBorder.left;
} else if (e.clientX - startMark.x >= (dx + dw) - (markBorder.left + markBorder.width)) {
movingDistance.x = (dx + dw) - (markBorder.left + markBorder.width);
} else {
movingDistance.x = e.clientX - startMark.x;//横向偏移差
}
//纵向偏移临界值判断
if (e.clientY - startMark.y <= dy - markBorder.top) {
movingDistance.y = dy - markBorder.top;
} else if (e.clientY - startMark.y >= (dy + dh) - (markBorder.top + markBorder.height)) {
movingDistance.y = (dy + dh) - (markBorder.top + markBorder.height);
} else {
movingDistance.y = e.clientY - startMark.y;//纵向偏移差
}
this.setState({
startMark: {
x: e.clientX,
y: e.clientY,
}
}, () => {
this.moveMark(movingDistance);
});
}
};
});
};
moveMark = (movingDistance) => {
const border = JSON.parse(JSON.stringify(this.state.markBorder));
const rect = JSON.parse(JSON.stringify(this.state.rect));
const { x, y } = movingDistance;//移动的差值
//设置markBorder
border.left = border.left + x;
border.top = border.top + y;
//设置明亮区域
rect.left = rect.left + x;
rect.right = rect.right + x;
rect.top = rect.top + y;
rect.bottom = rect.bottom + y;
this.setBorder(border, rect);
};
(4)裁剪的点,利用裁剪点的点击事件以及全局的鼠标移动和鼠标弹起事件
//变形点等比例变形
stretchMouseDown = (e) => {
e.stopPropagation();
e.preventDefault();
const key = e.target.className.split(" ")[1];
window.onmouseup = () => {
this.setState({
isMoveStretch: false,
isCrop: true,
}, () => {
const { markBorder, rect } = this.state;
this.setBorder(markBorder, rect);
window.onmouseup = null;
window.onmousemove = null;
});
};
this.setState({
startStretch: {
x: e.clientX,
y: e.clientY,
},
isCrop: false,
isMoveStretch: true,
}, () => {
window.onmousemove = (e) => {
e.preventDefault();
const { isMoveStretch, startStretch } = this.state;
if (isMoveStretch) {
const nowStretch = {
x: e.clientX,
y: e.clientY,
};
switch (key) {
case "LT":
this.moveStretchLT(startStretch, nowStretch);
break;
case "RT":
this.moveStretchRT(startStretch, nowStretch);
break;
case "RB":
this.moveStretchRB(startStretch, nowStretch);
break;
case "LB":
this.moveStretchLB(startStretch, nowStretch);
break;
default:
break;
}
}
};
});
};
(4)四个裁剪点的行数,判断是否超出边界
//移动左上角的点
moveStretchLT = (startStretch, nowStretch) => {
const { dx, dy, markBorder } = this.state;
const nts = nowStretch.y - startStretch.y;//鼠标的y距离差
let y = 0;//真实差
//边界值
if (nts <= dy - markBorder.top) {
y = dy - markBorder.top;
} else if (nts >= markBorder.height - 5) {
y = markBorder.height - 5;
} else {
y = nts;
}
this.setState({
startStretch: nowStretch,
}, () => {
const border = JSON.parse(JSON.stringify(this.state.markBorder));
const rect = JSON.parse(JSON.stringify(this.state.rect));
//以竖边为基准,实现等比例
const dex = border.width - (border.height - y) / aspectRatio;
if (dex >= dx - markBorder.left && dex <= markBorder.width - 5) {
border.height = border.height - y;
border.width = border.height / aspectRatio;
border.left = border.left + dex;
border.top = border.top + y;
rect.left = rect.left + dex;
rect.top = rect.top + y;
}
this.setBorder(border, rect);
});
};
//移动右上角的点
moveStretchRT = (startStretch, nowStretch) => {
const { dx, dy, dw, markBorder } = this.state;
const nts = nowStretch.y - startStretch.y;
let y = 0;
if (nts <= dy - markBorder.top) {
y = dy - markBorder.top;
} else if (nts >= markBorder.height - 5) {
y = markBorder.height - 5;
} else {
y = nts;
}
this.setState({
startStretch: nowStretch,
}, () => {
const border = JSON.parse(JSON.stringify(this.state.markBorder));
const rect = JSON.parse(JSON.stringify(this.state.rect));
const dex = (border.height - y) / aspectRatio - border.width;
if (dex <= (dx + dw) - (markBorder.left + markBorder.width) && dex >= -markBorder.width + 5) {
border.height = border.height - y;
border.width = border.height / aspectRatio;
border.top = border.top + y;
rect.right = rect.right + dex;
rect.top = rect.top + y;
}
this.setBorder(border, rect);
});
};
//移动左下角的点
moveStretchLB = (startStretch, nowStretch) => {
const { dx, dy, dh, markBorder } = this.state;
const nts = nowStretch.y - startStretch.y;
let y = 0;
if (nts <= -markBorder.height) {
y = -markBorder.height;
} else if (nts >= (dy + dh) - (markBorder.height + markBorder.top)) {
y = (dy + dh) - (markBorder.height + markBorder.top);
} else {
y = nts;
}
this.setState({
startStretch: nowStretch,
}, () => {
const border = JSON.parse(JSON.stringify(this.state.markBorder));
const rect = JSON.parse(JSON.stringify(this.state.rect));
const dex = (border.height + y) / aspectRatio - border.width;
if (dex >= 5 - markBorder.width && dex <= markBorder.left - dx) {
border.height = border.height + y;
border.width = border.height / aspectRatio;
border.left = border.left - dex;
rect.bottom = rect.bottom + y;
rect.left = rect.left - dex;
}
this.setBorder(border, rect);
});
};
//移动右下角的点
moveStretchRB = (startStretch, nowStretch) => {
const { dx, dy, dh, dw, markBorder } = this.state;
const nts = nowStretch.y - startStretch.y;
let y = 0;
if (nts <= -markBorder.height) {
y = -markBorder.height;
} else if (nts >= (dy + dh) - (markBorder.height + markBorder.top)) {
y = (dy + dh) - (markBorder.height + markBorder.top);
} else {
y = nts;
}
this.setState({
startStretch: nowStretch,
}, () => {
const border = JSON.parse(JSON.stringify(this.state.markBorder));
const rect = JSON.parse(JSON.stringify(this.state.rect));
const dex = (border.height + y) / aspectRatio - border.width;
if (dex >= 5 - markBorder.width && dex <= (dx + dw) - (markBorder.left + markBorder.width)) {
border.height = border.height + y;
border.width = border.height / aspectRatio;
rect.bottom = rect.bottom + y;
rect.right = rect.right + dex;
}
this.setBorder(border, rect);
});
};