Canvas渲染管线解析:从API调用到像素落地的全过程
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');
// 初始化Canvas
function 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和putImageData
const 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) {
// 创建对象URL
link.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>