<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>画图工具 - 绘制/擦除/选中</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', Arial, sans-serif; background: #1e1e2e; }
.toolbar {
background: #181825;
padding: 12px 18px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
border-bottom: 3px solid #313244;
}
.toolbar button {
padding: 10px 18px;
font-size: 14px;
font-weight: 600;
border: 2px solid #45475a;
border-radius: 8px;
background: #313244;
color: #cdd6f4;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
transition: all 0.15s;
}
.toolbar button:hover {
background: #585b70;
color: #fff;
}
.toolbar button.active {
background: #89b4fa;
border-color: #89b4fa;
color: #1e1e2e;
}
.toolbar .sep {
width: 2px;
height: 28px;
background: #45475a;
margin: 0 4px;
}
.toolbar label {
color: #a6adc8;
font-size: 13px;
margin-left: 6px;
display: flex;
align-items: center;
gap: 5px;
}
.toolbar input[type="color"] {
width: 40px;
height: 34px;
border: 2px solid #45475a;
border-radius: 6px;
cursor: pointer;
padding: 2px;
background: #313244;
}
.toolbar input[type="range"] {
width: 110px;
margin-left: 5px;
accent-color: #89b4fa;
}
#status {
color: #a6adc8;
font-size: 13px;
margin-left: auto;
padding: 4px 10px;
background: #313244;
border-radius: 6px;
}
canvas {
display: block;
cursor: crosshair;
}
</style>
</head>
<body>
<div class="toolbar">
<button id="btn-pen">✏️ 画笔</button>
<button id="btn-line">📏 直线</button>
<button id="btn-erase">🧽 橡皮擦</button>
<button id="btn-select">👆 选中</button>
<div class="sep"></div>
<button id="btn-demo">➕ 示例线</button>
<button id="btn-clear">🗑 清空</button>
<div class="sep"></div>
<label>颜色<input type="color" id="colorPicker" value="#e74c3c"></label>
<label>粗细<input type="range" id="sizeSlider" min="1" max="20" value="3"></label>
<span id="status">就绪</span>
</div>
<canvas id="c" width="1000" height="600"></canvas>
<script>
// ============================================================
// 获取 DOM 元素(全部用 addEventListener 绑定,不依赖任何库)
// ============================================================
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var statusEl = document.getElementById('status');
var btnPen = document.getElementById('btn-pen');
var btnLine = document.getElementById('btn-line');
var btnErase = document.getElementById('btn-erase');
var btnSelect = document.getElementById('btn-select');
var btnDemo = document.getElementById('btn-demo');
var btnClear = document.getElementById('btn-clear');
var colorPicker = document.getElementById('colorPicker');
var sizeSlider = document.getElementById('sizeSlider');
// ============================================================
// 全局状态
// ============================================================
var mode = 'pen'; // pen | line | erase | select
var isDrawing = false;
var startX, startY;
var snapshot = null; // 直线预览用
var selectedIndex = -1; // 当前选中对象下标
// 每个"对象" = 一条独立直线/笔画,有自己独立的离屏 canvas
var objects = [];
// ============================================================
// 工具函数
// ============================================================
function setStatus(msg) { statusEl.textContent = msg; }
function setActive(btn) {
btnPen.classList.remove('active');
btnLine.classList.remove('active');
btnErase.classList.remove('active');
btnSelect.classList.remove('active');
btn.classList.add('active');
}
function getColor() { return colorPicker.value; }
function getSize() { return parseInt(sizeSlider.value, 10); }
function getPos(e) {
var rect = canvas.getBoundingClientRect();
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
}
// 重绘主画布 = 把所有对象画上去 + 选中框
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < objects.length; i++) {
ctx.drawImage(objects[i].canvas, 0, 0);
}
if (selectedIndex >= 0 && selectedIndex < objects.length) {
drawSelection(objects[selectedIndex]);
}
}
// 创建一个新对象(独立离屏 canvas)
function createObject() {
var off = document.createElement('canvas');
off.width = canvas.width;
off.height = canvas.height;
var offCtx = off.getContext('2d');
var obj = { canvas: off, ctx: offCtx, hasContent: false };
objects.push(obj);
return obj;
}
// ============================================================
// 命中检测:点 (x,y) 是否在该对象上(含容差)
// ============================================================
function hitTest(obj, x, y, tolerance) {
var tol = tolerance || 6;
var w = obj.canvas.width, h = obj.canvas.height;
var x0 = Math.max(0, x - tol), y0 = Math.max(0, y - tol);
var x1 = Math.min(w, x + tol), y1 = Math.min(h, y + tol);
var imgData = obj.ctx.getImageData(x0, y0, x1 - x0, y1 - y0).data;
for (var i = 3; i < imgData.length; i += 4) {
if (imgData[i] > 30) return true;
}
return false;
}
// 找对象的精确包围盒(用于选中框)
function getBounds(obj) {
var w = obj.canvas.width, h = obj.canvas.height;
var data = obj.ctx.getImageData(0, 0, w, h).data;
var minX = w, minY = h, maxX = 0, maxY = 0, found = false;
for (var y = 0; y < h; y += 2) {
for (var x = 0; x < w; x += 2) {
if (data[(y * w + x) * 4 + 3] > 30) {
found = true;
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
}
}
return found ? { minX: minX, minY: minY, maxX: maxX, maxY: maxY } : null;
}
// 画选中框(蓝色虚线矩形,紧贴可见像素)
function drawSelection(obj) {
var b = getBounds(obj);
if (!b) return;
ctx.save();
ctx.strokeStyle = '#89b4fa';
ctx.lineWidth = 2;
ctx.setLineDash([8, 5]);
ctx.strokeRect(b.minX - 5, b.minY - 5, b.maxX - b.minX + 10, b.maxY - b.minY + 10);
ctx.setLineDash([]);
ctx.restore();
}
// ============================================================
// 橡皮擦:在所有对象的离屏 canvas 上擦 + 主画布也擦
// ============================================================
function eraseAt(x, y) {
var r = getSize() + 4;
// 主画布
ctx.save();
ctx.globalCompositeOperation = 'destination-out';
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// 每个对象
for (var i = 0; i < objects.length; i++) {
var obj = objects[i];
if (!obj.hasContent) continue;
obj.ctx.save();
obj.ctx.globalCompositeOperation = 'destination-out';
obj.ctx.beginPath();
obj.ctx.arc(x, y, r, 0, Math.PI * 2);
obj.ctx.fill();
obj.ctx.restore();
}
}
// 删除全透明的空对象
function cleanupEmpty() {
var kept = [];
for (var i = 0; i < objects.length; i++) {
var obj = objects[i];
if (!obj.hasContent) { kept.push(obj); continue; }
var w = obj.canvas.width, h = obj.canvas.height;
var data = obj.ctx.getImageData(0, 0, w, h).data;
var hasPixel = false;
// 采样(步长100像素,够快够准)
for (var j = 3; j < data.length; j += 400) {
if (data[j] > 30) { hasPixel = true; break; }
}
if (hasPixel) {
kept.push(obj);
}
}
objects = kept;
if (selectedIndex >= objects.length) selectedIndex = -1;
}
// ============================================================
// 鼠标事件(全部用 addEventListener,不依赖 Fabric)
// ============================================================
canvas.addEventListener('mousedown', onDown);
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseup', onUp);
canvas.addEventListener('mouseleave', function() { if (isDrawing) onUp(); }); // 防止卡住
function onDown(e) {
var p = getPos(e);
// ---- 画笔 ----
if (mode === 'pen') {
isDrawing = true;
var obj = createObject();
obj.hasContent = true;
obj.ctx.strokeStyle = getColor();
obj.ctx.lineWidth = getSize();
obj.ctx.lineCap = 'round';
obj.ctx.lineJoin = 'round';
obj.ctx.beginPath();
obj.ctx.moveTo(p.x, p.y);
obj._lx = p.x;
obj._ly = p.y;
setStatus('✏️ 画笔:绘制中...');
}
// ---- 直线 ----
else if (mode === 'line') {
isDrawing = true;
startX = p.x;
startY = p.y;
snapshot = ctx.getImageData(0, 0, canvas.width, canvas.height);
setStatus('📏 直线:拖拽中...');
}
// ---- 橡皮擦 ----
else if (mode === 'erase') {
isDrawing = true;
eraseAt(p.x, p.y);
setStatus('🧽 橡皮擦:擦除中...');
}
// ---- 选中 ----
else if (mode === 'select') {
// 从顶层(最后画的)往下找
selectedIndex = -1;
for (var i = objects.length - 1; i >= 0; i--) {
if (hitTest(objects[i], p.x, p.y)) {
selectedIndex = i;
redraw();
setStatus('👆 选中了对象 #' + (i + 1) + '(共 ' + objects.length + ' 个)--- 按 Delete 删除');
return;
}
}
redraw();
setStatus('👆 未选中(共 ' + objects.length + ' 个对象)');
}
}
function onMove(e) {
var p = getPos(e);
// ---- 画笔 ----
if (mode === 'pen' && isDrawing) {
var obj = objects[objects.length - 1];
// 画到离屏
obj.ctx.lineTo(p.x, p.y);
obj.ctx.stroke();
// 画到主画布(实时显示)
ctx.strokeStyle = getColor();
ctx.lineWidth = getSize();
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(obj._lx, obj._ly);
ctx.lineTo(p.x, p.y);
ctx.stroke();
obj._lx = p.x;
obj._ly = p.y;
}
// ---- 直线预览 ----
else if (mode === 'line' && isDrawing) {
ctx.putImageData(snapshot, 0, 0);
ctx.strokeStyle = getColor();
ctx.lineWidth = getSize();
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(p.x, p.y);
ctx.stroke();
}
// ---- 橡皮擦 ----
else if (mode === 'erase' && isDrawing) {
eraseAt(p.x, p.y);
}
}
function onUp(e) {
if (!isDrawing) return;
isDrawing = false;
// ---- 直线完成 ----
if (mode === 'line') {
var p = getPos(e);
var len = Math.sqrt((p.x - startX) * (p.x - startX) + (p.y - startY) * (p.y - startY));
if (len > 3) {
var obj = createObject();
obj.hasContent = true;
obj.ctx.strokeStyle = getColor();
obj.ctx.lineWidth = getSize();
obj.ctx.lineCap = 'round';
obj.ctx.beginPath();
obj.ctx.moveTo(startX, startY);
obj.ctx.lineTo(p.x, p.y);
obj.ctx.stroke();
redraw();
setStatus('✅ 直线完成,长 ' + Math.round(len) + 'px(共 ' + objects.length + ' 个对象)');
} else {
redraw();
setStatus('直线太短,已取消');
}
snapshot = null;
}
// ---- 画笔完成 ----
else if (mode === 'pen') {
cleanupEmpty();
redraw();
setStatus('✅ 画笔完成(共 ' + objects.length + ' 个对象)');
}
// ---- 橡皮擦完成 ----
else if (mode === 'erase') {
cleanupEmpty();
redraw();
setStatus('✅ 橡皮擦完成(剩余 ' + objects.length + ' 个对象)');
}
}
// ============================================================
// 按钮事件(全部用 addEventListener,绝不用 onclick=)
// ============================================================
btnPen.addEventListener('click', function() {
mode = 'pen';
selectedIndex = -1;
setActive(btnPen);
setStatus('模式:✏️ 画笔 --- 在画布上拖动即可绘制');
redraw();
});
btnLine.addEventListener('click', function() {
mode = 'line';
selectedIndex = -1;
setActive(btnLine);
setStatus('模式:📏 直线 --- 按住拖拽画直线');
redraw();
});
btnErase.addEventListener('click', function() {
mode = 'erase';
selectedIndex = -1;
setActive(btnErase);
setStatus('模式:🧽 橡皮擦 --- 在线条上拖动即可擦除(粗细可调)');
redraw();
});
btnSelect.addEventListener('click', function() {
mode = 'select';
setActive(btnSelect);
setStatus('模式:👆 选中 --- 点击对象可选中,按 Delete 删除,拖动看效果');
redraw();
});
btnDemo.addEventListener('click', function() {
// 3 条示例线,每条独立对象
var demos = [
{ x1: 150, y1: 200, x2: 450, y2: 200, color: '#e74c3c', w: 4 }, // 红线 300px
{ x1: 500, y1: 150, x2: 750, y2: 350, color: '#27ae60', w: 4 }, // 绿斜线
{ x1: 800, y1: 100, x2: 800, y2: 450, color: '#2980b9', w: 4 }, // 蓝竖线
];
for (var i = 0; i < demos.length; i++) {
var d = demos[i];
var obj = createObject();
obj.hasContent = true;
obj.ctx.strokeStyle = d.color;
obj.ctx.lineWidth = d.w;
obj.ctx.lineCap = 'round';
obj.ctx.beginPath();
obj.ctx.moveTo(d.x1, d.y1);
obj.ctx.lineTo(d.x2, d.y2);
obj.ctx.stroke();
}
redraw();
setStatus('✅ 已生成 3 条示例线(红300px / 绿斜线 / 蓝竖线)--- 共 ' + objects.length + ' 个对象');
});
btnClear.addEventListener('click', function() {
objects = [];
selectedIndex = -1;
ctx.clearRect(0, 0, canvas.width, canvas.height);
setStatus('🗑 画布已清空');
});
// ============================================================
// 键盘快捷键
// ============================================================
document.addEventListener('keydown', function(e) {
// Delete / Backspace:删除选中对象
if ((e.key === 'Delete' || e.key === 'Backspace') && selectedIndex >= 0) {
objects.splice(selectedIndex, 1);
selectedIndex = -1;
redraw();
setStatus('🗑 已删除选中对象(剩余 ' + objects.length + ' 个)');
e.preventDefault();
}
// 快捷键切模式
if (e.key === '1') btnPen.click();
if (e.key === '2') btnLine.click();
if (e.key === '3') btnErase.click();
if (e.key === '4') btnSelect.click();
});
// ============================================================
// 初始状态
// ============================================================
setActive(btnPen);
setStatus('点击「➕ 示例线」快速体验,或直接在画布上画');
</script>
</body>
</html>