<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>堆代码 duidaima.com</title> <link rel="stylesheet" href="styles.css"> </head> <body> <div class="signature-container"> <canvas id="signature-pad" width="400" height="200"></canvas> <button id="clear">清除</button> </div> <script src="script.js"></script> </body> </html>我们将采用 <canvas> 标签来构建签名板。选择画布作为工具是因为它为我们提供了以下功能:
4.利用 toDataURL 方法将签名以图像形式导出(如 PNG 或 JPEG),这在保存签名或将其上传至服务器时非常有用。
body { display: flex; justify - content: center; align - items: center; height: 100vh; background - color: #f0f0f0; margin: 0; } .signature - container { display: flex; flex - direction: column; align - items: center; } canvas { border: 1px solid#000; background - color: #fff; } button { margin - top: 10px; padding: 5px 10px; cursor: pointer; }然后向目录中添加一个 script.js 文件:
document.addEventListener('DOMContentLoaded', function() { var canvas = document.getElementById('signature-pad'); var ctx = canvas.getContext('2d'); var drawing = false; canvas.addEventListener('mousedown', function(e) { drawing = true; ctx.beginPath(); ctx.moveTo(e.offsetX, e.offsetY); }); canvas.addEventListener('mousemove', function(e) { if (drawing) { ctx.lineTo(e.offsetX, e.offsetY); ctx.stroke(); } }); canvas.addEventListener('mouseup', function() { drawing = false; }); canvas.addEventListener('mouseout', function() { drawing = false; }); document.getElementById('clear').addEventListener('click', function() { ctx.clearRect(0, 0, canvas.width, canvas.height); }); });让我们详细解释一下这段代码的工作原理:
5.为了结束绘图,我们设置了 mouseup 和 mouseout 两个事件监听器,分别在鼠标按钮释放和鼠标光标移出画布时停止绘图。此外,当用户点击清除按钮时,ctx.clearRect(0, 0, canvas.width, canvas.height); 用于清除整个画布,为新的绘图做准备。
document.addEventListener('DOMContentLoaded', function() { var canvas = document.getElementById('signature-pad'); var ctx = canvas.getContext('2d'); var drawing = false; function startDrawing(e) { drawing = true; ctx.beginPath(); ctx.moveTo(e.offsetX || e.touches[0].clientX - canvas.offsetLeft, e.offsetY || e.touches[0].clientY - canvas.offsetTop); } function draw(e) { if (drawing) { ctx.lineTo(e.offsetX || e.touches[0].clientX - canvas.offsetLeft, e.offsetY || e.touches[0].clientY - canvas.offsetTop); ctx.stroke(); } } function stopDrawing() { drawing = false; } // 鼠标事件 canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseout', stopDrawing); // 触摸事件 canvas.addEventListener('touchstart', startDrawing); canvas.addEventListener('touchmove', draw); canvas.addEventListener('touchend', stopDrawing); canvas.addEventListener('touchcancel', stopDrawing); document.getElementById('clear').addEventListener('click', function() { ctx.clearRect(0, 0, canvas.width, canvas.height); }); });对于触摸事件,使用 e.touches[0].clientX 和 e.touches[0].clientY 来获取触摸坐标。为了考虑画布的位置,使用 canvas.offsetLeft 和 canvas.offsetTop 进行调整:
<div class="controls"> <select id="stroke-style"> <option value="pen"> 钢笔 </option> <option value="brush"> 刷子 </option> </select> <button id="clear"> JavaScript签名板 </button> </div>然后我们更新我们的 CSS:
body { display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f0f0; margin: 0; } .signature-container { display: flex; flex-direction: column; align-items: center; } canvas { border: 1px solid #000; background-color: #fff; } .controls { margin-top: 10px; display: flex; gap: 10px; } button, select { padding: 5px 10px; cursor: pointer; }最后,更新 script.js:
document.addEventListener('DOMContentLoaded', function() { var canvas = document.getElementById('signature-pad'); var ctx = canvas.getContext('2d'); var drawing = false; var strokeStyle = 'pen'; function startDrawing(e) { drawing = true; ctx.beginPath(); ctx.moveTo(e.offsetX || e.touches[0].clientX - canvas.offsetLeft, e.offsetY || e.touches[0].clientY - canvas.offsetTop); } function draw(e) { if (drawing) { ctx.lineTo(e.offsetX || e.touches[0].clientX - canvas.offsetLeft, e.offsetY || e.touches[0].clientY - canvas.offsetTop); ctx.stroke(); } } function stopDrawing() { drawing = false; } // 鼠标事件 canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseout', stopDrawing); // 触摸事件 canvas.addEventListener('touchstart', startDrawing); canvas.addEventListener('touchmove', draw); canvas.addEventListener('touchend', stopDrawing); canvas.addEventListener('touchcancel', stopDrawing); document.getElementById('clear').addEventListener('click', function() { ctx.clearRect(0, 0, canvas.width, canvas.height); }); document.getElementById('stroke-style').addEventListener('change', function(e) { strokeStyle = e.target.value; if (strokeStyle === 'pen') { ctx.lineWidth = 2; ctx.lineCap = 'round'; } else if (strokeStyle === 'brush') { ctx.lineWidth = 5; ctx.lineCap = 'round'; } }); // 设置初始笔画样式 ctx.lineWidth = 2; ctx.lineCap = 'round'; });处理响应性
2.将画布元素的宽度设置为 width: 100%,高度设置为 height: auto,以实现响应式效果。
body { display: flex; justify - content: center; align - items: center; height: 100vh; background - color: #f0f0f0; margin: 0; } .signature - container { display: flex; flex - direction: column; align - items: center; width: 90 % ; max - width: 600px; } canvas { border: 1px solid#000; background - color: #fff; width: 100 % ; height: auto; } .controls { margin - top: 10px; display: flex; gap: 10px; } button, select { padding: 5px 10px; cursor: pointer; }这是在添加之后 script.js 的样子:
document.addEventListener('DOMContentLoaded', function() { var canvas = document.getElementById('signature-pad'); var ctx = canvas.getContext('2d'); var drawing = false; var strokeStyle = 'pen'; function resizeCanvas() { canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; ctx.lineWidth = strokeStyle === 'pen' ? 2 : 5; ctx.lineCap = 'round'; } function startDrawing(e) { drawing = true; ctx.beginPath(); ctx.moveTo(e.offsetX || e.touches[0].clientX - canvas.offsetLeft, e.offsetY || e.touches[0].clientY - canvas.offsetTop); } function draw(e) { if (drawing) { ctx.lineTo(e.offsetX || e.touches[0].clientX - canvas.offsetLeft, e.offsetY || e.touches[0].clientY - canvas.offsetTop); ctx.stroke(); } } function stopDrawing() { drawing = false; } // 鼠标事件 canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseout', stopDrawing); // 触摸事件 canvas.addEventListener('touchstart', startDrawing); canvas.addEventListener('touchmove', draw); canvas.addEventListener('touchend', stopDrawing); canvas.addEventListener('touchcancel', stopDrawing); document.getElementById('clear').addEventListener('click', function() { ctx.clearRect(0, 0, canvas.width, canvas.height); }); document.getElementById('stroke-style').addEventListener('change', function(e) { strokeStyle = e.target.value; ctx.lineWidth = strokeStyle === 'pen' ? 2 : 5; }); // 初始画布设置 resizeCanvas(); window.addEventListener('resize', resizeCanvas); });你可能会观察到,当改变浏览器窗口大小时,画布上的签名会消失,这是因为调整画布尺寸会自动清除其内容。这是 <canvas> 元素的标准行为。不过,我们可以通过一些技巧来避免这个问题。为了在缩放画布时保持签名的完整性,并且能够正确地对其进行缩放,我们需要先保存画布上的签名,然后在调整画布尺寸后再重新绘制它。我们可以通过 toDataURL 方法来保存画布上的签名。
document.addEventListener('DOMContentLoaded', function() { var canvas = document.getElementById('signature-pad'); var ctx = canvas.getContext('2d'); var drawing = false; var strokeStyle = 'pen'; var signatureData = null; function resizeCanvas() { if (signatureData) { var img = new Image(); img.src = signatureData; img.onload = function() { ctx.clearRect(0, 0, canvas.width, canvas.height); canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; ctx.drawImage(img, 0, 0, canvas.width, canvas.height); setStrokeStyle(); }; } else { canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; setStrokeStyle(); } } function setStrokeStyle() { if (strokeStyle === 'pen') { ctx.lineWidth = 2; ctx.lineCap = 'round'; } else if (strokeStyle === 'brush') { ctx.lineWidth = 5; ctx.lineCap = 'round'; } } function startDrawing(e) { drawing = true; ctx.beginPath(); ctx.moveTo(e.offsetX || e.touches[0].clientX - canvas.offsetLeft, e.offsetY || e.touches[0].clientY - canvas.offsetTop); } function draw(e) { if (drawing) { ctx.lineTo(e.offsetX || e.touches[0].clientX - canvas.offsetLeft, e.offsetY || e.touches[0].clientY - canvas.offsetTop); ctx.stroke(); } } function stopDrawing() { drawing = false; signatureData = canvas.toDataURL(); } // 鼠标事件 canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseout', stopDrawing); // 触摸事件 canvas.addEventListener('touchstart', startDrawing); canvas.addEventListener('touchmove', draw); canvas.addEventListener('touchend', stopDrawing); canvas.addEventListener('touchcancel', stopDrawing); document.getElementById('clear').addEventListener('click', function() { ctx.clearRect(0, 0, canvas.width, canvas.height); signatureData = null; }); document.getElementById('stroke-style').addEventListener('change', function(e) { strokeStyle = e.target.value; setStrokeStyle(); }); // 堆代码 duidaima.com // 初始画布设置 resizeCanvas(); window.addEventListener('resize', resizeCanvas); });保存和导出
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title> JavaScript签名板 </title> <link rel="stylesheet" href="styles.css"> </head> <body> <div class="signature-container"> <canvas id="signature-pad" width="400" height="200"> </canvas> <div class="controls"> <button id="clear"> 清除 </button> <button id="export-png"> 导出png格式 </button> <button id="export-jpeg"> 导出jpeg格式 </button> </div> </div> <script src="script.js"> </script> </body> </html>这是在添加之后 JavaScript 文件的样子:
document.addEventListener('DOMContentLoaded', function() { var canvas = document.getElementById('signature-pad'); var ctx = canvas.getContext('2d'); var drawing = false; var strokeStyle = 'pen'; var signatureData = null; function resizeCanvas() { if (signatureData) { var img = new Image(); img.src = signatureData; img.onload = function() { ctx.clearRect(0, 0, canvas.width, canvas.height); canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; ctx.drawImage(img, 0, 0, canvas.width, canvas.height); setStrokeStyle(); }; } else { canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; setStrokeStyle(); } } function setStrokeStyle() { if (strokeStyle === 'pen') { ctx.lineWidth = 2; ctx.lineCap = 'round'; } else if (strokeStyle === 'brush') { ctx.lineWidth = 5; ctx.lineCap = 'round'; } } function startDrawing(e) { drawing = true; ctx.beginPath(); ctx.moveTo(e.offsetX || e.touches[0].clientX - canvas.offsetLeft, e.offsetY || e.touches[0].clientY - canvas.offsetTop); } function draw(e) { if (drawing) { ctx.lineTo(e.offsetX || e.touches[0].clientX - canvas.offsetLeft, e.offsetY || e.touches[0].clientY - canvas.offsetTop); ctx.stroke(); } } function stopDrawing() { drawing = false; signatureData = canvas.toDataURL(); } function exportCanvas(format) { var exportCanvas = document.createElement('canvas'); exportCanvas.width = canvas.width; exportCanvas.height = canvas.height; var exportCtx = exportCanvas.getContext('2d'); // 用白色填充背景 exportCtx.fillStyle = '#fff'; exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); // 绘制签名 exportCtx.drawImage(canvas, 0, 0); // 导出画布 var dataURL = exportCanvas.toDataURL(`image / $ { format }`); var link = document.createElement('a'); link.href = dataURL; link.download = `signature.$ { format }`; link.click(); } // 鼠标事件 canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseout', stopDrawing); // 触摸事件 canvas.addEventListener('touchstart', startDrawing); canvas.addEventListener('touchmove', draw); canvas.addEventListener('touchend', stopDrawing); canvas.addEventListener('touchcancel', stopDrawing); document.getElementById('clear').addEventListener('click', function() { ctx.clearRect(0, 0, canvas.width, canvas.height); signatureData = null; }); document.getElementById('stroke-style').addEventListener('change', function(e) { strokeStyle = e.target.value; setStrokeStyle(); }); document.getElementById('export-png').addEventListener('click', function() { exportCanvas('png'); }); document.getElementById('export-jpeg').addEventListener('click', function() { exportCanvas('jpeg'); }); // 初始画布设置 resizeCanvas(); window.addEventListener('resize', resizeCanvas); });添加更多功能
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"></script>然后添加必要的按钮。这是最终 HTML 的样子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title> JavaScript签名板 </title> <link rel="stylesheet" href="styles.css"> <script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"> </script> </head> <body> <div class="signature-container"> <canvas id="signature-pad" width="600" height="400"> </canvas> <div class="controls"> <button id="undo"> 撤销 </button> <button id="redo"> 重做 </button> <button id="clear"> 清除 </button> <button id="save-png"> 导出png格式 </button> <button id="save-jpeg"> 导出jpeg格式 </button> </div> </div> <script src="script.js"> </script> </body> </html>更新样式:
body { display: flex; justify - content: center; align - items: center; height: 100vh; background - color: #f0f0f0; margin: 0; } .signature - container { display: flex; flex - direction: column; align - items: center; width: 90 % ; max - width: 600px; } canvas { border: 1px solid#000; background - color: #fff; width: 100 % ; height: auto; } .controls { margin - top: 10px; display: flex; gap: 10px; flex - wrap: wrap; } button { padding: 5px 10px; cursor: pointer; }为了进一步完善我们的签名板功能,script.js 脚本需要进行以下更新:
document.addEventListener('DOMContentLoaded', function() { var canvas = document.getElementById('signature-pad'); var signaturePad = new SignaturePad(canvas); var undoStack = []; var redoStack = []; function saveState() { undoStack.push(deepCopy(signaturePad.toData())); redoStack = []; } function undo() { if (undoStack.length > 0) { redoStack.push(deepCopy(signaturePad.toData())); undoStack.pop(); signaturePad.clear(); if (undoStack.length) { var lastStroke = undoStack[undoStack.length - 1]; signaturePad.fromData(lastStroke, { clear: false }); } } } function redo() { if (redoStack.length > 0) { undoStack.push(deepCopy(signaturePad.toData())); var nextState = redoStack.pop(); signaturePad.clear(); if (nextState.length) { signaturePad.fromData(nextState); } } } document.getElementById('undo').addEventListener('click', undo); document.getElementById('redo').addEventListener('click', redo); document.getElementById('clear').addEventListener('click', function() { signaturePad.clear(); undoStack = []; redoStack = []; }); document.getElementById('save-png').addEventListener('click', function() { if (!signaturePad.isEmpty()) { var dataURL = signaturePad.toDataURL('image/png'); var link = document.createElement('a'); link.href = dataURL; link.download = 'signature.png'; link.click(); } }); document.getElementById('save-jpeg').addEventListener('click', function() { if (!signaturePad.isEmpty()) { var dataURL = signaturePad.toDataURL('image/jpeg'); var link = document.createElement('a'); link.href = dataURL; link.download = 'signature.jpeg'; link.click(); } }); // 绘图结束时保存状态 signaturePad.addEventListener("endStroke", () = >{ console.log("Signature end"); saveState(); }); // 初始画布设置 function resizeCanvas() { var ratio = Math.max(window.devicePixelRatio || 1, 1); canvas.width = canvas.offsetWidth * ratio; canvas.height = canvas.offsetHeight * ratio; canvas.getContext('2d').scale(ratio, ratio); signaturePad.clear(); // 否则 isEmpty() 可能会返回错误值 if (undoStack.length > 0) { signaturePad.fromData(undoStack[undoStack.length - 1]); } } function deepCopy(data) { return JSON.parse(JSON.stringify(data)); } window.addEventListener('resize', resizeCanvas); resizeCanvas(); });