1.Canvas 基本定义 ★ 了解
Canvas 是 HTML5 提供的一个通过 JavaScript 来绘制图形的元素。它提供了一个空白的绘图区域,开发者可以使用 JavaScript 脚本在其中绘制各种图形、动画、游戏画面等。
2.Canvas 使用场景 ★ 了解
-
数据可视化:绘制图表、图形等
-
游戏开发:HTML5 游戏
-
图像处理:滤镜、像素操作
-
动画效果:创建动态视觉效果
-
交互式图形:绘图应用、设计工具
-
教育演示:数学函数可视化等
3.Canvas 示例代码 ★ 基础
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Canvas 示例</title><style>body {display: flex;flex-direction: column;align-items: center;font-family: Arial, sans-serif;}canvas {border: 1px solid #ccc;margin: 10px;}</style>
</head>
<body><h1>Canvas 使用示例</h1><!-- 示例1:基本形状绘制 --><h2>1. 基本形状绘制</h2><canvas id="basicShapes" width="400" height="200"></canvas><!-- 示例2:路径绘制 --><h2>2. 路径绘制</h2><canvas id="pathDrawing" width="400" height="200"></canvas><!-- 示例3:文本绘制 --><h2>3. 文本绘制</h2><canvas id="textDrawing" width="400" height="100"></canvas><!-- 示例4:简单动画 --><h2>4. 简单动画</h2><canvas id="animation" width="400" height="200"></canvas><script>// 示例1:基本形状绘制const basicCanvas = document.getElementById('basicShapes');const basicCtx = basicCanvas.getContext('2d');// 绘制填充矩形basicCtx.fillStyle = 'rgba(255, 0, 0, 0.5)';basicCtx.fillRect(50, 50, 100, 80);// 绘制描边矩形basicCtx.strokeStyle = 'blue';basicCtx.lineWidth = 3;basicCtx.strokeRect(200, 50, 100, 80);// 绘制圆形basicCtx.beginPath();basicCtx.fillStyle = 'green';basicCtx.arc(350, 90, 40, 0, Math.PI * 2);basicCtx.fill();// 示例2:路径绘制const pathCanvas = document.getElementById('pathDrawing');const pathCtx = pathCanvas.getContext('2d');// 绘制三角形pathCtx.beginPath();pathCtx.moveTo(50, 50);pathCtx.lineTo(150, 50);pathCtx.lineTo(100, 150);pathCtx.closePath();pathCtx.fillStyle = 'purple';pathCtx.fill();// 绘制复杂路径pathCtx.beginPath();pathCtx.moveTo(200, 50);pathCtx.lineTo(250, 150);pathCtx.lineTo(300, 50);pathCtx.lineTo(350, 150);pathCtx.lineTo(400, 50);pathCtx.strokeStyle = 'orange';pathCtx.lineWidth = 2;pathCtx.stroke();// 示例3:文本绘制const textCanvas = document.getElementById('textDrawing');const textCtx = textCanvas.getContext('2d');textCtx.font = '30px Arial';textCtx.fillStyle = 'red';textCtx.fillText('填充文本', 50, 50);textCtx.font = '25px Verdana';textCtx.strokeStyle = 'blue';textCtx.lineWidth = 1;textCtx.strokeText('描边文本', 50, 90);// 示例4:简单动画const animCanvas = document.getElementById('animation');const animCtx = animCanvas.getContext('2d');let x = 0;let y = 100;let dx = 2;function animate() {// 清除画布animCtx.clearRect(0, 0, animCanvas.width, animCanvas.height);// 绘制小球animCtx.beginPath();animCtx.arc(x, y, 20, 0, Math.PI * 2);animCtx.fillStyle = 'red';animCtx.fill();// 更新位置x += dx;// 边界检测if (x > animCanvas.width - 20 || x < 20) {dx = -dx;}requestAnimationFrame(animate);}animate();</script>
</body>
</html>
4.Canvas 2D API 完整列表与示例
4.1 Canvas 2D API 完整列表 ★ 重点
1. 绘图状态属性
属性 | 类型 | 描述 | 默认值 |
---|---|---|---|
fillStyle | String/Gradient/Pattern | 填充颜色或样式 | '#000000' |
strokeStyle | String/Gradient/Pattern | 描边颜色或样式 | '#000000' |
lineWidth | Number | 线条宽度(像素) | 1.0 |
lineCap | String | 线条末端样式('butt' , 'round' , 'square' ) | 'butt' |
lineJoin | String | 线条连接样式('round' , 'bevel' , 'miter' ) | 'miter' |
miterLimit | Number | 斜接长度限制 | 10.0 |
lineDashOffset | Number | 虚线偏移量 | 0.0 |
shadowBlur | Number | 阴影模糊程度 | 0 |
shadowColor | String | 阴影颜色 | 'rgba(0, 0, 0, 0)' |
shadowOffsetX | Number | 阴影水平偏移 | 0 |
shadowOffsetY | Number | 阴影垂直偏移 | 0 |
globalAlpha | Number | 全局透明度(0.0-1.0) | 1.0 |
globalCompositeOperation | String | 图形合成方式 | 'source-over' |
font | String | 文本字体样式 | '10px sans-serif' |
textAlign | String | 文本水平对齐('start' , 'end' , 'left' , 'right' , 'center' ) | 'start' |
textBaseline | String | 文本垂直对齐('top' , 'hanging' , 'middle' , 'alphabetic' , 'ideographic' , 'bottom' ) | 'alphabetic' |
2. 路径绘制方法
方法 | 参数 | 描述 |
---|---|---|
beginPath() | - | 开始新路径 |
closePath() | - | 闭合当前路径 |
moveTo(x, y) | x, y | 移动绘制起点 |
lineTo(x, y) | x, y | 绘制直线到点 |
arc(x, y, radius, startAngle, endAngle, anticlockwise) | x, y, radius, startAngle, endAngle, anticlockwise | 绘制圆弧 |
arcTo(x1, y1, x2, y2, radius) | x1, y1, x2, y2, radius | 绘制弧线 |
quadraticCurveTo(cpx, cpy, x, y) | cpx, cpy, x, y | 二次贝塞尔曲线 |
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) | cp1x, cp1y, cp2x, cp2y, x, y | 三次贝塞尔曲线 |
rect(x, y, width, height) | x, y, width, height | 添加矩形路径 |
fill() | [fillRule] | 填充当前路径 |
stroke() | - | 描边当前路径 |
clip() | [fillRule] | 创建裁剪区域 |
isPointInPath(x, y) | x, y | 判断点是否在路径内 |
3. 绘图方法
方法 | 参数 | 描述 |
---|---|---|
fillRect(x, y, width, height) | x, y, width, height | 绘制填充矩形 |
strokeRect(x, y, width, height) | x, y, width, height | 绘制描边矩形 |
clearRect(x, y, width, height) | x, y, width, height | 清除矩形区域 |
4. 文本方法
方法 | 参数 | 描述 |
---|---|---|
fillText(text, x, y [, maxWidth]) | text, x, y, maxWidth | 绘制填充文本 |
strokeText(text, x, y [, maxWidth]) | text, x, y, maxWidth | 绘制描边文本 |
measureText(text) | text | 测量文本宽度 |
5. 图像方法
方法 | 参数 | 描述 |
---|---|---|
drawImage(image, dx, dy) | image, dx, dy | 绘制图像 |
drawImage(image, dx, dy, dWidth, dHeight) | image, dx, dy, dWidth, dHeight | 绘制缩放图像 |
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) | image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight | 绘制裁剪图像 |
6. 像素操作
方法 | 参数 | 描述 |
---|---|---|
createImageData(width, height) | width, height | 创建空白ImageData |
createImageData(imagedata) | imagedata | 复制ImageData |
getImageData(sx, sy, sw, sh) | sx, sy, sw, sh | 获取像素数据 |
putImageData(imagedata, dx, dy) | imagedata, dx, dy | 放置像素数据 |
putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) | imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight | 放置部分像素数据 |
7. 变换方法
方法 | 参数 | 描述 |
---|---|---|
scale(x, y) | x, y | 缩放绘图 |
rotate(angle) | angle | 旋转绘图 |
translate(x, y) | x, y | 移动原点 |
transform(a, b, c, d, e, f) | a, b, c, d, e, f | 矩阵变换 |
setTransform(a, b, c, d, e, f) | a, b, c, d, e, f | 重置并变换 |
resetTransform() | - | 重置变换 |
8. 渐变和图案
方法 | 参数 | 描述 |
---|---|---|
createLinearGradient(x0, y0, x1, y1) | x0, y0, x1, y1 | 创建线性渐变 |
createRadialGradient(x0, y0, r0, x1, y1, r1) | x0, y0, r0, x1, y1, r1 | 创建径向渐变 |
createPattern(image, repetition) | image, repetition | 创建图案 |
addColorStop(position, color) | position, color | 渐变添加色标 |
9. 状态管理
方法 | 参数 | 描述 |
---|---|---|
save() | - | 保存当前状态 |
restore() | - | 恢复之前状态 |
10. 其他方法
方法 | 参数 | 描述 |
---|---|---|
setLineDash(segments) | segments | 设置虚线样式 |
getLineDash() | - | 获取虚线样式 |
4.2 Canvas 2D完整HTML示例 ★ 了解
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Canvas 2D API 完整示例</title><style>body {font-family: Arial, sans-serif;margin: 0;padding: 20px;display: flex;flex-direction: column;align-items: center;background-color: #f5f5f5;}h1 {color: #333;margin-bottom: 20px;}.canvas-container {margin: 20px 0;border: 1px solid #ddd;background-color: white;box-shadow: 0 2px 10px rgba(0,0,0,0.1);}canvas {display: block;}.controls {margin: 20px 0;display: flex;gap: 10px;}button {padding: 8px 16px;background-color: #4CAF50;color: white;border: none;border-radius: 4px;cursor: pointer;}button:hover {background-color: #45a049;}.code-block {background-color: #f8f8f8;padding: 15px;border-radius: 5px;font-family: monospace;white-space: pre-wrap;margin: 20px 0;border: 1px solid #ddd;}</style>
</head>
<body><h1>Canvas 2D API 演示</h1><div class="canvas-container"><canvas id="demoCanvas" width="500" height="300"></canvas></div><div class="controls"><button id="drawBtn">绘制图形</button><button id="clearBtn">清除画布</button></div><div class="code-block">// 获取Canvas元素和上下文const canvas = document.getElementById('demoCanvas');const ctx = canvas.getContext('2d');// 绘制矩形ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';ctx.fillRect(50, 50, 100, 80);// 绘制圆形ctx.beginPath();ctx.arc(300, 100, 50, 0, Math.PI * 2);ctx.fillStyle = 'blue';ctx.fill();</div><script>// 等待DOM完全加载document.addEventListener('DOMContentLoaded', function() {// 获取Canvas元素和上下文const canvas = document.getElementById('demoCanvas');const ctx = canvas.getContext('2d');const drawBtn = document.getElementById('drawBtn');const clearBtn = document.getElementById('clearBtn');// 初始化Canvasfunction initCanvas() {// 设置默认样式ctx.fillStyle = '#f8f8f8';ctx.fillRect(0, 0, canvas.width, canvas.height);// 设置绘制样式ctx.strokeStyle = '#333';ctx.lineWidth = 1;ctx.font = '14px Arial';ctx.fillStyle = '#333';// 绘制网格背景drawGrid();}// 绘制网格背景function drawGrid() {ctx.save(); // 保存当前状态ctx.strokeStyle = '#eee';// 绘制垂直线for (let x = 0; x <= canvas.width; x += 20) {ctx.beginPath();ctx.moveTo(x, 0);ctx.lineTo(x, canvas.height);ctx.stroke();}// 绘制水平线for (let y = 0; y <= canvas.height; y += 20) {ctx.beginPath();ctx.moveTo(0, y);ctx.lineTo(canvas.width, y);ctx.stroke();}ctx.restore(); // 恢复之前状态}// 绘制示例图形function drawShapes() {// 1. 绘制基本形状ctx.save();ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';ctx.fillRect(50, 50, 100, 80);ctx.strokeStyle = 'blue';ctx.lineWidth = 3;ctx.strokeRect(50, 150, 100, 80);// 2. 绘制路径(三角形)ctx.beginPath();ctx.moveTo(200, 50);ctx.lineTo(250, 150);ctx.lineTo(150, 150);ctx.closePath();ctx.fillStyle = 'green';ctx.fill();// 3. 绘制圆形ctx.beginPath();ctx.arc(350, 100, 50, 0, Math.PI * 2);ctx.fillStyle = 'purple';ctx.fill();// 4. 绘制贝塞尔曲线ctx.beginPath();ctx.moveTo(200, 200);ctx.bezierCurveTo(250, 150, 300, 250, 350, 200);ctx.strokeStyle = 'orange';ctx.lineWidth = 2;ctx.stroke();// 5. 绘制文本ctx.font = '20px Arial';ctx.fillStyle = 'black';ctx.fillText('Canvas 2D API 示例', 150, 250);// 6. 使用渐变const gradient = ctx.createLinearGradient(0, 0, 0, 300);gradient.addColorStop(0, 'red');gradient.addColorStop(0.5, 'yellow');gradient.addColorStop(1, 'blue');ctx.fillStyle = gradient;ctx.fillRect(400, 0, 100, 300);ctx.restore();}// 清除画布function clearCanvas() {ctx.clearRect(0, 0, canvas.width, canvas.height);initCanvas();}// 事件监听drawBtn.addEventListener('click', drawShapes);clearBtn.addEventListener('click', clearCanvas);// 初始化initCanvas();});</script>
</body>
</html>
5.Canvas 的兼容性分析 ★ 关注
Canvas 作为 HTML5 的核心特性之一,在现代浏览器中有很好的支持,但也存在一些兼容性注意事项。
5.1 浏览器支持情况
主流浏览器支持
-
Chrome:完全支持(包括移动版)
-
Firefox:完全支持(包括移动版)
-
Safari:完全支持(包括 iOS 版)
-
Edge:完全支持(基于 Chromium 的新版)
-
Opera:完全支持
旧版浏览器支持
-
IE 9+:基本支持 Canvas,但某些高级特性可能不支持
-
IE 8 及更早版本:不支持 Canvas
5.2 兼容性注意事项
-
基本 Canvas 支持检测
if (document.createElement('canvas').getContext) {// 支持 Canvas } else {// 不支持 Canvas,提供回退方案 }
-
文本 API 差异
-
某些浏览器对
measureText()
方法的实现略有不同 -
文本渲染质量在不同浏览器/操作系统上可能有差异
-
-
图像导出格式
-
toDataURL()
和toBlob()
方法支持的图像格式可能不同 -
某些浏览器可能不支持 PNG 以外的格式
-
-
性能差异
-
不同浏览器/设备的 Canvas 渲染性能差异较大
-
移动设备上的性能通常低于桌面设备
-
5.3 常见兼容性问题解决方案
1. 针对 IE 8 及更早版本的回退方案
<canvas id="myCanvas">您的浏览器不支持 Canvas,以下是替代内容:<img src="fallback.png" alt="替代图像">
</canvas>
2. 跨浏览器前缀处理
某些 API 可能需要浏览器前缀:
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d') || canvas.getContext('experimental-webgl');
3. 功能检测而非浏览器检测
// 检测特定功能是否可用
function isCanvasSupported() {const elem = document.createElement('canvas');return !!(elem.getContext && elem.getContext('2d'));
}
5.4 实际兼容性数据
根据 Can I Use 网站的最新数据(截至2023年):
浏览器/版本 | 支持情况 |
---|---|
Chrome 4+ | ✅ 完全支持 |
Firefox 2+ | ✅ 完全支持 |
Safari 3.1+ | ✅ 完全支持 |
Edge 12+ | ✅ 完全支持 |
IE 9+ | ✅ 基本支持 |
IE 6-8 | ❌ 不支持 |
Opera 9+ | ✅ 完全支持 |
iOS Safari 3.2+ | ✅ 完全支持 |
Android Browser 2.1+ | ✅ 完全支持 |
5.5 高级特性的兼容性
-
WebGL (3D Canvas)
-
需要额外检测
getContext('webgl')
或getContext('experimental-webgl')
-
移动设备支持有限
-
-
混合模式
-
globalCompositeOperation
的某些模式在不同浏览器中表现可能不同
-
-
滤镜效果
-
非标准滤镜(如
filter: blur()
)支持不一致
-
5.6 最佳实践建议
-
始终进行特性检测:不要假设 Canvas 可用
-
为旧版浏览器提供替代内容:如图片或文字说明
-
性能敏感应用进行能力检测:检测渲染性能
-
考虑使用 polyfill:如 FlashCanvas 为 IE 6-8 提供支持
-
测试不同设备和浏览器:特别是移动设备
5.7 兼容性测试示例代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Canvas兼容性测试</title><style>/* 结果展示区域的样式 */#result {padding: 10px;margin: 10px;border: 1px solid #ccc;background-color: #f8f8f8;}/* 支持的功能样式 */.supported {color: green;font-weight: bold;}/* 不支持的功能样式 */.unsupported {color: red;font-weight: bold;}/* Canvas容器的样式 */#canvasContainer {margin: 20px 0;text-align: center;}/* Canvas元素本身的样式 */#testCanvas {border: 1px solid #000;background-color: #fff;}/* 整体页面样式 */body {font-family: Arial, sans-serif;max-width: 800px;margin: 0 auto;padding: 20px;line-height: 1.6;}h1 {color: #333;text-align: center;}</style>
</head>
<body>
<h1>Canvas兼容性测试</h1><!-- 结果显示区域 -->
<div id="result">正在检测浏览器支持情况...</div><!-- Canvas容器,包含备用内容 -->
<div id="canvasContainer"><canvas id="testCanvas" width="200" height="100">您的浏览器不支持HTML5 Canvas,请升级到现代浏览器如Chrome、Firefox、Safari或Edge。</canvas>
</div><script>// 等待DOM完全加载后执行测试document.addEventListener('DOMContentLoaded', function() {// 获取DOM元素const resultDiv = document.getElementById('result');const canvas = document.getElementById('testCanvas');// 1. 基本支持检测// 检查浏览器是否支持Canvas元素和getContext方法if (!canvas || typeof canvas.getContext !== 'function') {resultDiv.innerHTML = '<span class="unsupported">您的浏览器不支持Canvas。</span>' +'<p>建议升级到最新版本的Chrome、Firefox、Safari或Edge。</p>';return;}// 尝试获取2D绘图上下文let ctx;try {ctx = canvas.getContext('2d');if (!ctx) {throw new Error('无法获取2D上下文');}} catch (e) {resultDiv.innerHTML = '<span class="unsupported">您的浏览器不支持Canvas 2D绘图。</span>';console.error('获取Canvas上下文失败:', e);return;}// 2. 测试基本绘图功能try {// 测试填充矩形ctx.fillStyle = 'rgba(0, 128, 0, 0.7)'; // 半透明绿色ctx.fillRect(10, 10, 50, 50);// 测试描边矩形ctx.strokeStyle = 'blue';ctx.lineWidth = 2;ctx.strokeRect(70, 10, 50, 50);// 测试圆形绘制ctx.fillStyle = 'red';ctx.beginPath();ctx.arc(150, 35, 25, 0, Math.PI * 2);ctx.fill();// 基本功能测试通过resultDiv.innerHTML = '<span class="supported">您的浏览器完全支持Canvas基本功能。</span>';// 3. 检测高级功能detectAdvancedFeatures();} catch (e) {resultDiv.innerHTML = '<span class="unsupported">您的浏览器部分支持Canvas,但某些功能无法使用。</span>' +'<p>错误信息: ' + e.message + '</p>';console.error('Canvas基本功能测试失败:', e);}/*** 检测Canvas高级功能支持情况*/function detectAdvancedFeatures() {// 定义要检测的功能列表const features = {text: {name: '文本绘制',supported: false,test: testTextSupport},shadows: {name: '阴影效果',supported: false,test: testShadowSupport},gradients: {name: '渐变填充',supported: false,test: testGradientSupport},imageData: {name: '像素操作',supported: false,test: testImageDataSupport},lineDash: {name: '虚线样式',supported: false,test: testLineDashSupport},globalAlpha: {name: '全局透明度',supported: false,test: testGlobalAlphaSupport}};// 执行所有功能测试for (const feature in features) {if (features.hasOwnProperty(feature)) {try {features[feature].test();features[feature].supported = true;} catch (e) {console.warn(features[feature].name + '测试失败:', e);}}}// 显示检测结果let featuresHtml = '<h3>高级功能支持情况:</h3><ul>';for (const feature in features) {if (features.hasOwnProperty(feature)) {featuresHtml += `<li>${features[feature].name}: ` +`<span class="${features[feature].supported ? 'supported' : 'unsupported'}">` +`${features[feature].supported ? '✓ 支持' : '✗ 不支持'}</span></li>`;}}featuresHtml += '</ul>';resultDiv.innerHTML += featuresHtml;// 测试文本支持function testTextSupport() {ctx.font = '14px Arial';ctx.fillStyle = 'black';ctx.fillText('文本测试', 10, 80);// 额外检测measureText方法if (typeof ctx.measureText !== 'function') {throw new Error('measureText方法不支持');}const metrics = ctx.measureText('测试');if (typeof metrics.width !== 'number') {throw new Error('文本测量功能异常');}}// 测试阴影支持function testShadowSupport() {ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';ctx.shadowBlur = 5;ctx.shadowOffsetX = 3;ctx.shadowOffsetY = 3;ctx.fillStyle = 'purple';ctx.fillRect(180, 10, 10, 10);// 重置阴影设置ctx.shadowColor = 'transparent';ctx.shadowBlur = 0;ctx.shadowOffsetX = 0;ctx.shadowOffsetY = 0;}// 测试渐变支持function testGradientSupport() {const gradient = ctx.createLinearGradient(0, 0, 100, 0);if (!gradient || typeof gradient.addColorStop !== 'function') {throw new Error('渐变创建失败');}gradient.addColorStop(0, 'red');gradient.addColorStop(1, 'blue');// 测试径向渐变const radialGradient = ctx.createRadialGradient(50, 50, 5, 50, 50, 30);if (!radialGradient) {throw new Error('径向渐变不支持');}}// 测试像素操作支持function testImageDataSupport() {const imageData = ctx.createImageData(1, 1);if (!imageData || !imageData.data || imageData.data.length !== 4) {throw new Error('ImageData创建失败');}// 测试getImageData和putImageDataconst testData = ctx.getImageData(0, 0, 1, 1);ctx.putImageData(testData, 0, 0);}// 测试虚线样式支持function testLineDashSupport() {if (typeof ctx.setLineDash !== 'function' ||typeof ctx.getLineDash !== 'function') {throw new Error('虚线API不支持');}ctx.setLineDash([5, 3]);const dashPattern = ctx.getLineDash();if (!Array.isArray(dashPattern) || dashPattern.length !== 2) {throw new Error('虚线功能异常');}// 重置为实线ctx.setLineDash([]);}// 测试全局透明度支持function testGlobalAlphaSupport() {const originalAlpha = ctx.globalAlpha;ctx.globalAlpha = 0.5;if (ctx.globalAlpha !== 0.5) {throw new Error('全局透明度设置失败');}ctx.globalAlpha = originalAlpha;}}});
</script>
</body>
</html>
6.手划签名的案例 ★ 综合案例
最终效果展示
完整代码如下
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><!-- 移动端视口设置,禁止缩放 --><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"><title>高清手写签名板</title><style>/* 全局样式重置 */* {box-sizing: border-box; /* 盒模型设置为border-box更易控制尺寸 */margin: 0; /* 清除默认外边距 */padding: 0; /* 清除默认内边距 */-webkit-tap-highlight-color: transparent; /* 移除移动端点击高亮效果 */}/* 页面主体样式 */body {font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;margin: 0;padding: 20px;display: flex;flex-direction: column; /* 垂直排列子元素 */align-items: center; /* 水平居中 */touch-action: none; /* 禁用浏览器默认触摸行为 */background-color: #f5f5f5; /* 浅灰色背景 */min-height: 100vh; /* 最小高度为视口高度 */}/* 标题样式 */h1 {color: #333; /* 深灰色文字 */margin-bottom: 20px; /* 底部间距 */font-size: 1.5rem; /* 响应式字体大小 */text-align: center; /* 文字居中 */}/* Canvas容器样式 */.canvas-container {position: relative;margin-bottom: 20px;border: 2px solid #333; /* 深灰色边框 */background-color: #fff; /* 白色背景 */box-shadow: 0 2px 10px rgba(0,0,0,0.1); /* 轻微阴影效果 */width: 100%; /* 宽度100% */max-width: 600px; /* 最大宽度限制 */aspect-ratio: 2/1; /* 固定宽高比为2:1 */}/* Canvas元素样式 */#signatureCanvas {display: block; /* 块级元素 */width: 100%; /* 宽度填满容器 */height: 100%; /* 高度填满容器 */touch-action: none; /* 禁用默认触摸行为 */}/* 控制按钮区域样式 */.controls {margin-bottom: 20px;display: flex; /* 弹性布局 */gap: 10px; /* 按钮间距 */flex-wrap: wrap; /* 允许换行 */justify-content: center; /* 水平居中 */width: 100%; /* 宽度100% */max-width: 600px; /* 最大宽度限制 */}/* 按钮基础样式 */button {padding: 12px 20px; /* 内边距 */background-color: #4CAF50; /* 绿色背景 */color: white; /* 白色文字 */border: none; /* 无边框 */border-radius: 6px; /* 圆角 */cursor: pointer; /* 手型光标 */font-size: 1rem; /* 字体大小 */min-width: 120px; /* 最小宽度 */transition: background-color 0.2s; /* 背景色过渡动画 */}/* 按钮悬停效果 */button:hover {background-color: #45a049; /* 深绿色 */}/* 清除按钮特殊样式 */button#clearBtn {background-color: #f44336; /* 红色背景 */}button#clearBtn:hover {background-color: #d32f2f; /* 深红色 */}/* 预览区域样式 */.preview {margin-top: 20px;text-align: center; /* 文字居中 */width: 100%; /* 宽度100% */max-width: 600px; /* 最大宽度限制 */}/* 预览图片样式 */#signaturePreview {max-width: 100%; /* 最大宽度100% */border: 1px solid #ddd; /* 浅灰色边框 */margin-top: 10px; /* 顶部间距 */background-color: white; /* 白色背景 */box-shadow: 0 1px 3px rgba(0,0,0,0.1); /* 轻微阴影 */}/* 使用说明文本样式 */.instructions {margin-top: 15px;color: #666; /* 灰色文字 */font-size: 0.9rem; /* 较小字体 */text-align: center; /* 文字居中 */padding: 0 10px; /* 左右内边距 */}/* 移动端响应式样式 */@media (max-width: 480px) {body {padding: 15px; /* 较小的内边距 */}h1 {font-size: 1.3rem; /* 较小的字体 */margin-bottom: 15px;}button {padding: 10px 15px; /* 较小的内边距 */font-size: 0.9rem; /* 较小的字体 */min-width: 100px; /* 较小的最小宽度 */}}</style>
</head>
<body>
<h1>高清手写签名板</h1><!-- Canvas签名区域容器 -->
<div class="canvas-container"><!-- Canvas绘图区域 --><canvas id="signatureCanvas"></canvas>
</div><!-- 控制按钮区域 -->
<div class="controls"><!-- 保存签名按钮 --><button id="saveBtn">保存签名</button><!-- 清除签名按钮 --><button id="clearBtn">清除签名</button>
</div><!-- 签名预览区域 -->
<div class="preview"><h3>签名预览:</h3><!-- 签名预览图片,初始隐藏 --><img id="signaturePreview" alt="签名预览" style="display: none;">
</div><!-- 使用说明 -->
<p class="instructions">PC端: 使用鼠标按住并拖动来签名 | 移动端: 使用手指触摸并滑动来签名</p><script>/*** 自定义Canvas2Image实现* 解决原生Canvas2Image库在部分浏览器中无法直接保存data URL的问题* 使用toBlob API实现更可靠的图片保存功能*/const CustomCanvas2Image = {/*** 保存Canvas为PNG图片* @param {HTMLCanvasElement} canvas - 要保存的Canvas元素* @param {number} [width] - 可选,输出图片宽度* @param {number} [height] - 可选,输出图片高度* @param {string} [fileName] - 可选,保存的文件名*/saveAsPNG: function(canvas, width, height, fileName) {return this.convertToImage(canvas, width, height, fileName, 'png');},/*** 将Canvas转换为图片并保存* @param {HTMLCanvasElement} canvas - 要转换的Canvas元素* @param {number} [width] - 可选,输出图片宽度* @param {number} [height] - 可选,输出图片高度* @param {string} [fileName] - 可选,保存的文件名* @param {string} [type] - 图片类型,默认'png'*/convertToImage: function(canvas, width, height, fileName, type) {// 设置默认文件名和图片类型fileName = fileName || 'untitled';type = type || 'png';// 创建隐藏的下载链接const link = document.createElement('a');link.download = fileName + '.' + type;/*** 使用Canvas的toBlob方法获取图片二进制数据* 这是比toDataURL更高效的方案,且不会遇到data URL长度限制问题*/canvas.toBlob(function(blob) {// 创建对象URLlink.href = URL.createObjectURL(blob);// 模拟点击下载const event = new MouseEvent('click');link.dispatchEvent(event);// 释放对象URL内存setTimeout(function() {URL.revokeObjectURL(link.href);}, 100);}, 'image/' + type, 1.0); // 1.0表示最高质量}};/*** 主应用程序逻辑* 使用立即执行函数封装,避免污染全局命名空间*/(function() {'use strict'; // 启用严格模式// ==================== DOM元素获取 ====================// Canvas容器元素const container = document.querySelector('.canvas-container');// Canvas绘图元素const canvas = document.getElementById('signatureCanvas');// Canvas 2D绘图上下文const ctx = canvas.getContext('2d');// 保存按钮const saveBtn = document.getElementById('saveBtn');// 清除按钮const clearBtn = document.getElementById('clearBtn');// 预览图片元素const previewImg = document.getElementById('signaturePreview');// ==================== 全局变量 ====================// 设备像素比(用于高清显示)const devicePixelRatio = window.devicePixelRatio || 1;// 是否正在绘制的标志let isDrawing = false;// 上一个点的X坐标let lastX = 0;// 上一个点的Y坐标let lastY = 0;// 存储触摸点的数组(用于平滑处理)let points = [];// ==================== Canvas初始化 ====================/*** 设置Canvas尺寸和样式* 考虑设备像素比以确保高清显示*/function setupCanvas() {// 获取容器的实际显示尺寸const rect = container.getBoundingClientRect();const width = rect.width;const height = rect.height;/*** 设置Canvas的实际像素尺寸* 乘以devicePixelRatio确保在高DPI设备上清晰显示*/canvas.width = Math.floor(width * devicePixelRatio);canvas.height = Math.floor(height * devicePixelRatio);// 设置Canvas的显示尺寸(CSS像素)canvas.style.width = `${width}px`;canvas.style.height = `${height}px`;/*** 缩放绘图上下文以匹配设备像素比* 这样我们在逻辑坐标中绘图时,会自动映射到物理像素*/ctx.scale(devicePixelRatio, devicePixelRatio);// 设置默认绘制样式ctx.strokeStyle = '#000000'; // 黑色线条ctx.lineWidth = 2; // 2像素线宽ctx.lineCap = 'round'; // 圆形线帽ctx.lineJoin = 'round'; // 圆形线连接// 初始化清空画布clearCanvas();}// ==================== 画布操作函数 ====================/*** 清除画布内容* 重置为白色背景*/function clearCanvas() {// 保存当前绘图状态ctx.save();// 重置变换矩阵ctx.setTransform(1, 0, 0, 1, 0, 0);// 填充白色背景ctx.fillStyle = '#ffffff';ctx.fillRect(0, 0, canvas.width, canvas.height);// 恢复之前保存的绘图状态ctx.restore();// 隐藏预览图片previewImg.style.display = 'none';// 清空存储的点数组points = [];}// ==================== 签名保存函数 ====================/*** 保存签名图片* 使用自定义的Canvas2Image实现*/function saveSignature() {try {// 显示预览(使用data URL)previewImg.src = canvas.toDataURL('image/png');previewImg.style.display = 'block';/*** 使用自定义的Canvas2Image保存高清PNG图片* 参数说明:* 1. canvas元素* 2. 宽度(null表示使用原始尺寸)* 3. 高度(null表示使用原始尺寸)* 4. 文件名(包含时间戳确保唯一)*/CustomCanvas2Image.saveAsPNG(canvas,null,null,`signature_${new Date().getTime()}`);console.log('签名保存成功');} catch (error) {console.error('保存签名失败:', error);/*** 备用保存方案:当直接保存失败时* 1. 显示预览图片* 2. 提示用户手动长按保存*/const previewData = canvas.toDataURL('image/png');previewImg.src = previewData;previewImg.style.display = 'block';alert('请长按预览图片并选择"保存图片"');}}// ==================== 绘图相关函数 ====================/*** 获取精确的坐标位置(兼容鼠标和触摸事件)* @param {Event} e - 鼠标或触摸事件对象* @returns {Array} 包含x,y坐标的数组*/function getPosition(e) {// 获取Canvas相对于视口的位置和尺寸const rect = canvas.getBoundingClientRect();let clientX, clientY;// 判断事件类型(触摸事件或鼠标事件)if (e.type.includes('touch')) {// 触摸事件:获取第一个触摸点const touch = e.touches[0] || e.changedTouches[0];clientX = touch.clientX;clientY = touch.clientY;} else {// 鼠标事件:直接获取坐标clientX = e.clientX;clientY = e.clientY;}// 计算缩放比例(考虑设备像素比)const scaleX = canvas.width / rect.width;const scaleY = canvas.height / rect.height;// 返回转换后的坐标(考虑设备像素比和Canvas缩放)return [(clientX - rect.left) * scaleX / devicePixelRatio,(clientY - rect.top) * scaleY / devicePixelRatio];}/*** 开始绘制* @param {Event} e - 鼠标或触摸事件对象*/function startDrawing(e) {e.preventDefault(); // 阻止默认行为(如滚动)isDrawing = true; // 设置绘制标志const pos = getPosition(e); // 获取起始位置lastX = pos[0]; // 记录起始X坐标lastY = pos[1]; // 记录起始Y坐标points = [{x: lastX, y: lastY}]; // 初始化点数组// 开始新路径ctx.beginPath();ctx.moveTo(lastX, lastY);}/*** 绘制过程(包含平滑处理)* @param {Event} e - 鼠标或触摸事件对象*/function draw(e) {if (!isDrawing) return; // 如果不是绘制状态则返回e.preventDefault(); // 阻止默认行为// 获取当前点坐标const pos = getPosition(e);const x = pos[0];const y = pos[1];// 存储当前点用于平滑处理points.push({x, y});// 限制处理点数以提高性能if (points.length > 10) {points.shift(); // 移除最旧的点}// 计算平滑后的点坐标const smoothed = smoothPoints(points);const smoothedX = smoothed.x;const smoothedY = smoothed.y;// 绘制线条到平滑后的点ctx.lineTo(smoothedX, smoothedY);ctx.stroke();// 更新最后位置lastX = smoothedX;lastY = smoothedY;}/*** 平滑处理函数(减少绘制锯齿)* @param {Array} points - 包含点对象的数组* @returns {Object} 平滑后的坐标 {x, y}*/function smoothPoints(points) {// 如果点数不足2个,返回第一个点或默认值if (points.length < 2) return points[0] || {x: 0, y: 0};// 使用加权平均算法平滑坐标let sumX = 0;let sumY = 0;let weightSum = 0;// 遍历所有点,越近的点权重越大for (let i = 0; i < points.length; i++) {const weight = (i + 1) / points.length; // 线性权重sumX += points[i].x * weight;sumY += points[i].y * weight;weightSum += weight;}// 返回加权平均值return {x: sumX / weightSum,y: sumY / weightSum};}/*** 结束绘制*/function stopDrawing() {if (isDrawing) {isDrawing = false; // 重置绘制标志points = []; // 清空点数组}}// ==================== 事件监听 ====================/*** 添加所有事件监听器*/function addEventListeners() {// ===== PC端鼠标事件 =====// 鼠标按下:开始绘制canvas.addEventListener('mousedown', startDrawing);// 鼠标移动:绘制过程canvas.addEventListener('mousemove', draw);// 鼠标释放:结束绘制canvas.addEventListener('mouseup', stopDrawing);// 鼠标离开Canvas:结束绘制canvas.addEventListener('mouseleave', stopDrawing);// ===== 移动端触摸事件 =====// 使用 passive: false 以便能够阻止默认滚动行为// 触摸开始:开始绘制canvas.addEventListener('touchstart', function(e) {e.preventDefault(); // 必须阻止默认行为startDrawing(e);}, {passive: false});// 触摸移动:绘制过程canvas.addEventListener('touchmove', function(e) {e.preventDefault(); // 必须阻止默认行为draw(e);}, {passive: false});// 触摸结束:结束绘制canvas.addEventListener('touchend', function(e) {e.preventDefault(); // 必须阻止默认行为stopDrawing();}, {passive: false});// ===== 按钮事件 =====// 保存按钮点击事件saveBtn.addEventListener('click', saveSignature);// 清除按钮点击事件clearBtn.addEventListener('click', clearCanvas);// ===== 窗口大小变化事件 =====// 使用防抖避免频繁重绘window.addEventListener('resize', function() {clearTimeout(this.resizeTimer);this.resizeTimer = setTimeout(() => {setupCanvas();}, 200); // 200毫秒后执行});// ===== 阻止文档触摸移动事件 =====// 当正在绘制时阻止页面滚动document.addEventListener('touchmove', function(e) {if (isDrawing) {e.preventDefault();}}, {passive: false});}// ==================== 兼容性检测 ====================/*** 检测浏览器兼容性* @returns {boolean} 是否兼容*/function checkCompatibility() {// 检测Canvas支持if (!window.HTMLCanvasElement) {alert('您的浏览器不支持Canvas,请使用现代浏览器如Chrome、Firefox、Safari或Edge');return false;}try {// 测试获取2D上下文const testCanvas = document.createElement('canvas');if (!testCanvas.getContext || !testCanvas.getContext('2d')) {alert('您的浏览器不支持Canvas 2D绘图');return false;}// 测试toBlob方法支持if (!testCanvas.toBlob && !testCanvas.msToBlob) {alert('您的浏览器不支持Canvas toBlob方法,将使用备用保存方案');}return true;} catch (e) {console.error('兼容性检测失败:', e);return false;}}// ==================== 初始化 ====================// 检查兼容性后初始化应用if (checkCompatibility()) {setupCanvas(); // 初始化Canvas设置addEventListeners(); // 添加事件监听}})();
</script>
</body>
</html>