创建基于画布的绘图应用程序。
绘图应用程序没有那么多。当按钮在鼠标位置向下绘制时,听鼠标。
如果你想拥有一个响应式画布并且还包含undos等等,那么你需要从更复杂的层面开始。
绘图和显示。
首先,您应该将图形与显示区分开。这是通过创建保存绘图的屏幕外画布来完成的。它的大小是恒定的,可以由用户平移和缩放(甚至旋转)。
如果要创建线条或方框,使用离屏画布来保存绘图还可以在绘图上绘制。
有助于创建画布的一些功能
function createCanvas(width, height) {
const c = document.createElement("canvas");
c.width = width;
c.height = height;
c.ctx = c.getContext("2d");
return c;
}
const drawing = createCanvas(512,512);
您可以使用将该画布绘制到显示画布
ctx.drawImage(drawing,0,0);
该片段使用阴影和边框在画布中心绘制绘图,看起来不错。
鼠标处理程序
正确执行鼠标界面非常重要。从画布中聆听鼠标事件有一些问题。当用户拖离画布时,您不再获得任何鼠标事件。这意味着当鼠标离开画布时,您需要停止绘图,因为您不知道鼠标在返回时是否仍然处于停机状态。
要解决此问题,请聆听文档的鼠标事件。这将在按钮关闭时捕获鼠标,允许用户在您仍然获得事件时将鼠标移动到屏幕上的任何位置。如果鼠标在离开画布时上升,你仍然会得到那个事件。
注意堆栈溢出代码段窗口会阻止鼠标捕获ATM(最近的更改),因此上述鼠标行为仅限于包含该代码段的iFrame。
您也不应该从鼠标事件中进行任何渲染。某些渲染操作可能很慢,比鼠标的更新速度慢得多。如果从鼠标事件渲染,则会丢失鼠标事件。始终在鼠标事件侦听器中执行尽可能少的代码。
在代码段中,鼠标事件仅记录当前鼠标状态,如果鼠标已关闭并且绘制它将记录鼠标创建的路径。通过函数调用requestAnimationFrame同步到显示刷新率的单独循环负责呈现内容。它以大约60fps的速度运行。为了在没有任何事情发生时停止绘图,会使用一个标志来指示显示需要更新updateDisplay当有更改时,您将其设置为true updateDisplay=true;,并在下次显示硬件准备好显示时一个框架,它将绘制所有更新的内容。
画一条线
一条线只是一组连接点。在代码片段中,我创建了一个线对象。它包含构成线条的点以及线宽和颜色。
当鼠标停止时,我创建一个新的线对象并开始向其添加点。我标记显示需要更新,并在显示循环中通过其绘制方法在显示画布上绘制线条。
当鼠标向上移动时,我在绘图画布上绘制线条。这样做可以让你将一些智能应用到线上(片段很简单,对线条没有任何作用),例如使它沿着它的长度淡出。只有在鼠标启动时才会知道它的长度。
在代码片段中,当鼠标按下时我会丢弃该行,但如果您想要撤消,则会保存在数组中绘制的每一行。要撤消您,只需清除图形并重新绘制除撤消线之外的所有线条。
行对象和相关代码。
// a point object creates point from x,y coords or object that has x,y
const point = (x, y = x.y + ((x = x.x) * 0)) => ({ x, y });
// function to add a point to the line
function addPoint(x, y) { this.points.push(point(x, y)); }
// draw a line on context ctx and adds offset.x, offset.y
function drawLine(ctx, offset) {
ctx.strokeStyle = this.color;
ctx.lineWidth = this.width;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.beginPath();
var i = 0;
while (i < this.points.length) {
const p = this.points[i++];
ctx.lineTo(p.x + offset.x, p.y + offset.y);
}
ctx.stroke();
}
// creates a new line object
function createLine(color, width) {
return {
points: [], // the points making up the line
color, // colour of the line
width, // width of the line
add: addPoint, // function to add a point
draw: drawLine, // function to draw the whole line
};
}
示例代码段
该片段的结果比我想要的要长一点。我已经为相关代码添加了评论,但如果您有任何疑问,请在评论中提问,我会更新答案并提供更多信息。
&#13;
&#13;
// size of drawing and its starting background colour
const drawingInfo = {
width: 384 ,
height: 160,
bgColor: "white",
}
const brushSizes = [1, 2, 3, 4, 5, 6, 7, 8];
const colors = "red,orange,yellow,green,cyan,blue,purple,white,gray,black".split(",");
var currentColor = "blue";
var currentWidth = 2;
var currentSelectBrush;
var currentSelectColor;
const colorSel = document.getElementById("colorSel");
colors.forEach((color, i) => {
var swatch = document.createElement("span");
swatch.className = "swatch";
swatch.style.backgroundColor = color;
if (currentColor === color) {
swatch.className = "swatch highlight";
currentSelectColor = swatch;
} else {
swatch.className = "swatch";
}
swatch.addEventListener("click", (e) => {
currentSelectColor.className = "swatch";
currentColor = e.target.style.backgroundColor;
currentSelectColor = e.target;
currentSelectColor.className = "swatch highlight";
});
colorSel.appendChild(swatch);
})
brushSizes.forEach((brushSize, i) => {
var brush = document.createElement("canvas");
brush.width = 16;
brush.height = 16;
brush.ctx = brush.getContext("2d");
brush.ctx.beginPath();
brush.ctx.arc(8, 8, brushSize / 2, 0, Math.PI * 2);
brush.ctx.fill();
brush.brushSize = brushSize;
if (currentWidth === brushSize) {
brush.className = "swatch highlight";
currentSelectBrush = brush;
} else {
brush.className = "swatch";
}
brush.addEventListener("click", (e) => {
currentSelectBrush.className = "swatch";
currentSelectBrush = e.target;
currentSelectBrush.className = "swatch highlight";
currentWidth = e.target.brushSize;
});
colorSel.appendChild(brush);
})
const canvas = document.getElementById("can");
const mouse = createMouse().start(canvas, true);
const ctx = canvas.getContext("2d");
var updateDisplay = true; // when true the display needs updating
var ch, cw, w, h; // global canvas size vars
var currentLine;
var displayOffset = {
x: 0,
y: 0
};
// a point object creates point from x,y coords or object that has x,y
const point = (x, y = x.y + ((x = x.x) * 0)) => ({
x,
y
});
// function to add a point to the line
function addPoint(x, y) {
this.points.push(point(x, y));
}
function drawLine(ctx, offset) { // draws a line
ctx.strokeStyle = this.color;
ctx.lineWidth = this.width;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.beginPath();
var i = 0;
while (i < this.points.length) {
const p = this.points[i++];
ctx.lineTo(p.x + offset.x, p.y + offset.y);
}
ctx.stroke();
}
function createLine(color, width) {
return {
points: [],
color,
width,
add: addPoint,
draw: drawLine,
};
}
// creates a canvas
function createCanvas(width, height) {
const c = document.createElement("canvas");
c.width = width;
c.height = height;
c.ctx = c.getContext("2d");
return c;
}
// resize main display canvas and set global size vars
function resizeCanvas() {
ch = ((h = canvas.height = innerHeight - 32) / 2) | 0;
cw = ((w = canvas.width = innerWidth) / 2) | 0;
updateDisplay = true;
}
function createMouse() {
function preventDefault(e) { e.preventDefault() }
const mouse = {
x: 0,
y: 0,
buttonRaw: 0,
prevButton: 0
};
const bm = [1, 2, 4, 6, 5, 3]; // bit masks for mouse buttons
const mouseEvents = "mousemove,mousedown,mouseup".split(",");
const m = mouse;
// one mouse handler
function mouseMove(e) {
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
if (e.type === "mousedown") {
m.buttonRaw |= bm[e.which - 1];
} else if (e.type === "mouseup") {
m.buttonRaw &= bm[e.which + 2];
}
// check if there should be a display update
if (m.buttonRaw || m.buttonRaw !== m.prevButton) {
updateDisplay = true;
}
// if the mouse is down and the prev mouse is up then start a new line
if (m.buttonRaw !== 0 && m.prevButton === 0) { // starting new line
currentLine = createLine(currentColor, currentWidth);
currentLine.add(m); // add current mouse position
} else if (m.buttonRaw !== 0 && m.prevButton !== 0) { // while mouse is down
currentLine.add(m); // add current mouse position
}
m.prevButton = m.buttonRaw; // remember the previous mouse state
e.preventDefault();
}
// starts the mouse
m.start = function(element, blockContextMenu) {
m.element = element;
mouseEvents.forEach(n => document.addEventListener(n, mouseMove));
if (blockContextMenu === true) {
document.addEventListener("contextmenu", preventDefault)
}
return m
}
return m;
}
var cursor = "crosshair";
function update(timer) { // Main update loop
cursor = "crosshair";
globalTime = timer;
// if the window size has changed resize the canvas
if (w !== innerWidth || h !== innerHeight) {
resizeCanvas()
}
if (updateDisplay) {
updateDisplay = false;
display(); // call demo code
}
ctx.canvas.style.cursor = cursor;
requestAnimationFrame(update);
}
// create a drawing canvas.
const drawing = createCanvas(drawingInfo.width, drawingInfo.height);
// fill with white
drawing.ctx.fillStyle = drawingInfo.bgColor;
drawing.ctx.fillRect(0, 0, drawing.width, drawing.height);
// function to display drawing
function display() {
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "rgba(0,0,0,0.25)";
const imgX = cw - (drawing.width / 2) | 0;
const imgY = ch - (drawing.height / 2) | 0;
// add a shadow to make it look nice
ctx.fillRect(imgX + 5, imgY + 5, drawing.width, drawing.height);
// add outline
ctx.strokeStyle = "black";
ctx.lineWidth = "2";
ctx.strokeRect(imgX, imgY, drawing.width, drawing.height);
// draw the image
ctx.drawImage(drawing, imgX, imgY);
if (mouse.buttonRaw !== 0) {
if (currentLine !== undefined) {
currentLine.draw(ctx, displayOffset); // draw line on display canvas
cursor = "none";
updateDisplay = true; // keep updating
}
} else if (mouse.buttonRaw === 0) {
if (currentLine !== undefined) {
currentLine.draw(drawing.ctx, {x: -imgX, y: -imgY }); // draw line on drawing
currentLine = undefined;
updateDisplay = true;
// next line is a quick fix to stop a slight flicker due to the current frame not showing the line
ctx.drawImage(drawing, imgX, imgY);
}
}
}
requestAnimationFrame(update);&#13;
#can {
position: absolute;
top: 32px;
left: 0px;
background-color: #AAA;
}
.colors {
border: 1px solid black;
display: inline-flex;
}
.swatch {
min-width: 16px;
min-height: 16px;
max-width: 16px;
border: 1px solid black;
display: inline-block;
margin: 2px;
cursor: pointer;
}
.highlight {
border: 1px solid red;
}&#13;
更新为了回应OP的评论,我添加了一个HTML版本,您应该能够复制和粘贴(包括在内的所有内容