我们之前实现的画画工具,都是非常简单的功能,按照用户鼠标轨迹显示一条简单的线段。接下来,我们实现一个稍微复杂的功能:用鼠标拖动画出一个矩形。 首先,用户按下鼠标,然后移动鼠标,我们在用户按下鼠标的位置和当前鼠标移动的位置之间显示一个矩形。随着用户不断移动鼠标,矩形的大小也跟随鼠标的位置不断调整,直至用户鼠标松开。 在这里我们的代码,将会和之前的代码完全不同。在用户鼠标移动到新的位置的时候,我们需要将旧的矩形擦除,然后再显示新的矩形。否则,canvas上面将会出现多个矩形。 要实现这个功能,有多种方案:
- 在用户开始画矩形之前,我们先备份以下当前canvas的内容。然后每次画出新的矩形的时候,我们先把备份的内容覆盖在canvas上面,然后再画新的矩形。这样可以保证canvas上面只有一个矩形。
- 在当前canvas上面,覆盖一个透明的canvas,我们在绘制矩形的时候,每次都先把透明的canvas清空,然后再绘制canvas。因为清空的是上面的临时canvas,因此不会影响下面的canvas。
- 采用XOR方式显示矩形。XOR方式比较特殊,第一次绘制矩形的时候可以显示出来,第二次在同样的位置重复XOR方式显示的时候,则类似于擦除效果,会把之前的矩形擦除。这样,我们在新的位置绘制矩形之前,先把旧的矩形擦除。利用这种方式,我们可以避免备份一个canvas内容,也可以避免生成一个临时的canvas。 我们在这里,我们使用第一种方式来实现。另外两种方式,可以自行实现。 要备份一个canvas内容,可以用下面的方式:
- 生成一个临时的canvas
let tempCanvas = document.createElement('canvas');
tempCanvas.width = pad.width;
tempCanvas.height = pad.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(pad, 0, 0);
- 生成一个临时的Image。
function copy() {
var imgData = ctx.getImageData(0, 0, pad.width, pad.height);
ctx.putImageData(imgData, 0, 0);
}
第二种方法要简单一些,我们用第二种方法。 首先,添加一个按钮,然后当点击这个按钮的时候,我们就开始绘制矩形。
<button id='rect' onclick="handleDrawRect()">rect</button>
function handleDrawRect(event) {
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
curTool = 'rect';
}
因为绘制矩形的逻辑和铅笔,荧光笔的方式不同,因此,我们需要增加一个变量,来记住当前选择的类型。同时,也需要修改其他按钮的消息,记住当前选择的工具(铅笔,荧光笔,橡皮擦还是矩形)。 接下来,我们修改鼠标按下的消息:
function cloneCanvas() {
tempImageData = ctx.getImageData(0, 0, pad.width, pad.height);
}
//
function handleMouseDown(event) {
//
if (undoCursor != -1) {
actions = actions.slice(0, undoCursor);
}
undoCursor = -1;
//
//
if (curTool == 'rect') {
cloneCanvas();
orgPoint = {x: event.offsetX, y: event.offsetY};
} else {
//
ctx.beginPath();
ctx.moveTo(event.offsetX, event.offsetY);
//
points.push({x: event.offsetX, y: event.offsetY});
}
//
pad.addEventListener('mousemove', handleMouseMove);
pad.addEventListener('mouseup', handleMouseUp);
}
当前工具如果是矩形的话,我们就备份当前canvas,然后记住鼠标按下的位置坐标。 接下来响应鼠标移动消息:
function handleMouseMove(event) {
//
if (curTool == 'rect') {
if (tempImageData) {
ctx.putImageData(tempImageData, 0, 0);
}
//
let curPoint = {x: event.offsetX, y: event.offsetY};
//
ctx.beginPath();
ctx.rect(orgPoint.x, orgPoint.y, curPoint.x - orgPoint.x, curPoint.y - orgPoint.y);
ctx.stroke();
} else {
//
ctx.lineTo(event.offsetX, event.offsetY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(event.offsetX, event.offsetY);
points.push({x: event.offsetX, y: event.offsetY});
}
}
如果是矩形,我们就在绘制矩形之前,先把之前复制的内容,覆盖在canvas上面。这样就可以擦除之前的矩形,只保留当前位置的矩形。 此时保存,选择矩形工具,就可以绘制矩形了。 不过我们的任务还没有完成。尝试点击以下undo/redo按钮,就会发现问题。因为我们并没有把矩形的操作记录到actions里面,自然undo/redo就会出现问题。 首先,我们需要在鼠标抬起的时候,将当前操作记录到actions里面。
function handleMouseUp(event) {
pad.removeEventListener('mousemove', handleMouseMove);
pad.removeEventListener('mouseup', handleMouseUp);
//
let action = {
tool: curTool,
lineWidth: ctx.lineWidth,
strokeStyle: ctx.strokeStyle,
};
if (curTool == 'rect') {
action.topLeft = orgPoint,
action.bottomRight = {x: event.offsetX, y: event.offsetY};
} else {
action.points = points;
}
//
actions.push(action);
//
points = [];
updateButtonStatus();
}
在这里,我们给action增加了tool,用来标记每一个操作的类型。针对不同的操作,保存的数据也进行了变化。 然后修改repaint函数,在重绘的时候,根据不同的tool,进行不同的绘制。
function repaint() {
ctx.clearRect(0, 0, pad.width, pad.height);
//
let toIndex = undoCursor == -1 ? actions.length : undoCursor;
for (let i = 0; i < toIndex; i++) {
//
let action = actions[i];
ctx.beginPath();
ctx.lineWidth = action.lineWidth;
ctx.strokeStyle = action.strokeStyle;
//
if (action.tool == 'rect') {
//
const pt1 = action.topLeft;
const pt2 = action.bottomRight;
ctx.rect(pt1.x, pt1.y, pt2.x - pt1.x, pt2.y - pt1.y);
ctx.stroke();
} else {
let points = action.points;
if (points.length == 0) {
continue;
}
//
let firstPoint = points[0];
ctx.moveTo(firstPoint.x, firstPoint.y);
for (let j = 1; j < points.length; j++) {
const point = points[j];
ctx.lineTo(point.x, point.y);
}
ctx.stroke();
//
}
//
}
}
同样,绘制的时候,我们也需要根据不同的操作,选择不同的绘制方式。 最后在浏览器里面测试我们的代码,已经可以完美实现矩形的绘制和undo/redo了。 在添加矩形绘制工具后,我们会发现,代码中已经出现太多if/else判断了。我们需要在鼠标消息,操作记录,重绘的地方,根据不同的tool来实现不同的操作。如果我们以后再增加新的tool,例如钢笔,直线,圆形,椭圆等等其他工具,那么可以预见,我们代码中的if/else将会越来越多,代码也会越来越难以维护。 接下来我们将会解决这个问题。 当前完整的代码:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body style='background:lightgrey'>
<div>
<button id='pencil' onclick="handleChoosePencil()">pencil</button>
<button id='highlighter' onclick="handleChooseHighlighter()">highlighter</button>
<button id='eraser' onclick="handleChooseEraser()">eraser</button>
<button id='rect' onclick="handleDrawRect()">rect</button>
<button id='undo' onclick="handleUndo()">undo</button>
<button id='redo' onclick="handleRedo()">redo</button>
</div>
<canvas id='pad' width='800px' height='600px' style='background:white'></canvas>
</body>
<script>
//
let actions = [];
let points = [];
let undoCursor = -1;
let curTool = '';
let orgPoint = {x: 0, y: 0};
let tempImageData = null;
//
const pad = document.getElementById('pad');
const ctx = pad.getContext('2d');
ctx.lineWidth = 2;
ctx.strokeStyle = 'blue';
//
updateButtonStatus();
//
//
pad.addEventListener('mousedown', handleMouseDown);
//
function cloneCanvas() {
tempImageData = ctx.getImageData(0, 0, pad.width, pad.height);
}
//
function handleMouseDown(event) {
//
if (undoCursor != -1) {
actions = actions.slice(0, undoCursor);
}
undoCursor = -1;
//
//
if (curTool == 'rect') {
cloneCanvas();
orgPoint = {x: event.offsetX, y: event.offsetY};
} else {
//
ctx.beginPath();
ctx.moveTo(event.offsetX, event.offsetY);
//
points.push({x: event.offsetX, y: event.offsetY});
}
//
pad.addEventListener('mousemove', handleMouseMove);
pad.addEventListener('mouseup', handleMouseUp);
}
//
function handleMouseMove(event) {
//
if (curTool == 'rect') {
if (tempImageData) {
ctx.putImageData(tempImageData, 0, 0);
}
//
let curPoint = {x: event.offsetX, y: event.offsetY};
//
ctx.beginPath();
ctx.rect(orgPoint.x, orgPoint.y, curPoint.x - orgPoint.x, curPoint.y - orgPoint.y);
ctx.stroke();
} else {
//
ctx.lineTo(event.offsetX, event.offsetY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(event.offsetX, event.offsetY);
points.push({x: event.offsetX, y: event.offsetY});
}
}
//
function handleMouseUp(event) {
pad.removeEventListener('mousemove', handleMouseMove);
pad.removeEventListener('mouseup', handleMouseUp);
//
let action = {
tool: curTool,
lineWidth: ctx.lineWidth,
strokeStyle: ctx.strokeStyle,
};
if (curTool == 'rect') {
action.topLeft = orgPoint,
action.bottomRight = {x: event.offsetX, y: event.offsetY};
} else {
action.points = points;
}
//
actions.push(action);
//
points = [];
updateButtonStatus();
}
//
function handleChoosePencil(event) {
ctx.strokeStyle = 'rgb(0, 0, 255)';
ctx.lineWidth = 2;
curTool = 'pencil';
}
//
function handleChooseHighlighter(event) {
ctx.strokeStyle = 'rgba(255, 255, 0, 0.5)';
ctx.lineWidth = 8;
curTool = 'highlighter';
}
//
function handleChooseEraser(event) {
ctx.strokeStyle = 'white';
ctx.lineWidth = 8;
curTool = 'eraser';
}
//
function handleDrawRect(event) {
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
curTool = 'rect';
}
//
function canUndo() {
if (actions.length == 0) {
return false;
}
//
if (undoCursor == 0) {
return false;
}
//
return true;
}
//
function canRedo() {
//
if (actions.length == 0) {
return false;
}
//
if (undoCursor == -1 || undoCursor == actions.length) {
return false;
}
//
return true;
}
//
function handleUndo(event) {
if (!canUndo()) {
return;
}
//
if (undoCursor == -1) {
undoCursor = actions.length;
}
//
undoCursor--;
//
repaint();
//
updateButtonStatus();
}
//
function handleRedo(event) {
if (!canRedo()) {
return;
}
//
undoCursor++;
//
repaint();
//
updateButtonStatus();
}
//
function updateButtonStatus() {
const undoButton = document.getElementById('undo');
const redoButton = document.getElementById('redo');
undoButton.disabled = !canUndo();
redoButton.disabled = !canRedo();
}
//
function repaint() {
ctx.clearRect(0, 0, pad.width, pad.height);
//
let toIndex = undoCursor == -1 ? actions.length : undoCursor;
for (let i = 0; i < toIndex; i++) {
//
let action = actions[i];
ctx.beginPath();
ctx.lineWidth = action.lineWidth;
ctx.strokeStyle = action.strokeStyle;
//
if (action.tool == 'rect') {
//
const pt1 = action.topLeft;
const pt2 = action.bottomRight;
ctx.rect(pt1.x, pt1.y, pt2.x - pt1.x, pt2.y - pt1.y);
ctx.stroke();
} else {
let points = action.points;
if (points.length == 0) {
continue;
}
//
let firstPoint = points[0];
ctx.moveTo(firstPoint.x, firstPoint.y);
for (let j = 1; j < points.length; j++) {
const point = points[j];
ctx.lineTo(point.x, point.y);
}
ctx.stroke();
//
}
//
}
}
</script>
</html>