pin_drop当前位置:知识文库 ❯ 图文
多媒体:Canvas阴影 - 完整教程与代码示例
一、教程简介
Canvas 阴影效果可以为图形添加深度感和立体感,是提升视觉品质的重要手段。通过 shadowColor、shadowBlur、shadowOffsetX 和 shadowOffsetY 四个属性,可以为任何绘制的图形(包括矩形、路径、文本和图像)添加阴影。本教程将详细介绍阴影属性的用法、效果控制以及实际应用技巧。
二、核心概念
阴影的四个属性
Canvas 阴影由四个属性共同控制:
阴影的工作原理
阴影是基于图形的轮廓生成的:
- 取图形的轮廓形状
- 根据
shadowOffsetX和shadowOffsetY偏移轮廓 - 使用
shadowColor作为阴影颜色 - 根据
shadowBlur对阴影边缘进行高斯模糊
阴影的应用范围
阴影会应用于所有后续的绘制操作,包括:
-
fillRect()/strokeRect()- 矩形 -
fill()/stroke()- 路径 -
fillText()/strokeText()- 文本 -
drawImage()- 图像
三、语法与用法
属性列表
阴影效果对照
四、代码示例
阴影属性演示
代码示例
<!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>
body {
margin: 0;
padding: 20px;
background: #0d1117;
font-family: "Microsoft YaHei", sans-serif;
color: #c9d1d9;
}
h1 { text-align: center; color: #f0883e; }
canvas {
display: block;
margin: 20px auto;
border: 1px solid #30363d;
border-radius: 8px;
}
</style>
</head>
<body>
<h1>Canvas 阴影属性演示</h1>
<canvas id="shadowCanvas" width="750" height="550"></canvas>
<script>
const canvas = document.getElementById('shadowCanvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#161b22';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// === shadowBlur 对比 ===
ctx.fillStyle = '#8b949e';
ctx.font = 'bold 16px "Microsoft YaHei"';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.shadowColor = 'transparent';
ctx.fillText('shadowBlur 模糊程度对比', 375, 15);
const blurValues = [0, 5, 10, 20, 30, 50];
const boxW = 90;
const boxH = 70;
const startX = 30;
const gap = 15;
blurValues.forEach((blur, i) => {
const x = startX + i * (boxW + gap);
const y = 50;
ctx.save();
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = blur;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillStyle = '#f0883e';
ctx.beginPath();
ctx.roundRect(x, y, boxW, boxH, 8);
ctx.fill();
ctx.restore();
ctx.fillStyle = '#8b949e';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(`blur: ${blur}`, x + boxW / 2, y + boxH + 10);
});
// === shadowOffset 对比 ===
ctx.fillStyle = '#8b949e';
ctx.font = 'bold 16px "Microsoft YaHei"';
ctx.fillText('shadowOffset 偏移方向对比', 375, 175);
const offsets = [
{ x: 0, y: 0, label: '0, 0' },
{ x: 10, y: 0, label: '10, 0' },
{ x: 0, y: 10, label: '0, 10' },
{ x: 10, y: 10, label: '10, 10' },
{ x: -10, y: -10, label: '-10, -10' },
{ x: 10, y: -10, label: '10, -10' }
];
offsets.forEach((offset, i) => {
const x = startX + i * (boxW + gap);
const y = 210;
ctx.save();
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = offset.x;
ctx.shadowOffsetY = offset.y;
ctx.fillStyle = '#539bf5';
ctx.beginPath();
ctx.roundRect(x, y, boxW, boxH, 8);
ctx.fill();
ctx.restore();
ctx.fillStyle = '#8b949e';
ctx.font = '11px Arial';
ctx.textAlign = 'center';
ctx.fillText(`offset: ${offset.label}`, x + boxW / 2, y + boxH + 10);
});
// === shadowColor 对比 ===
ctx.fillStyle = '#8b949e';
ctx.font = 'bold 16px "Microsoft YaHei"';
ctx.fillText('shadowColor 阴影颜色对比', 375, 335);
const colors = [
{ color: 'rgba(0, 0, 0, 0.5)', label: '黑色半透明' },
{ color: 'rgba(255, 0, 0, 0.5)', label: '红色半透明' },
{ color: 'rgba(0, 255, 0, 0.5)', label: '绿色半透明' },
{ color: 'rgba(0, 100, 255, 0.5)', label: '蓝色半透明' },
{ color: 'rgba(255, 200, 0, 0.5)', label: '黄色半透明' },
{ color: 'rgba(200, 0, 255, 0.5)', label: '紫色半透明' }
];
colors.forEach((item, i) => {
const x = startX + i * (boxW + gap);
const y = 370;
ctx.save();
ctx.shadowColor = item.color;
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillStyle = '#c9d1d9';
ctx.beginPath();
ctx.roundRect(x, y, boxW, boxH, 8);
ctx.fill();
ctx.restore();
ctx.fillStyle = '#8b949e';
ctx.font = '11px "Microsoft YaHei"';
ctx.textAlign = 'center';
ctx.fillText(item.label, x + boxW / 2, y + boxH + 10);
});
// === 发光效果 ===
ctx.fillStyle = '#8b949e';
ctx.font = 'bold 16px "Microsoft YaHei"';
ctx.textAlign = 'center';
ctx.fillText('发光效果 (offset=0, blur>0)', 375, 490);
const glowColors = ['#f47067', '#57ab5a', '#539bf5', '#c69026', '#b083f0'];
const glowLabels = ['红色发光', '绿色发光', '蓝色发光', '黄色发光', '紫色发光'];
glowColors.forEach((color, i) => {
const x = 75 + i * 140;
const y = 530;
ctx.save();
ctx.shadowColor = color;
ctx.shadowBlur = 20;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, 15, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.fillStyle = '#8b949e';
ctx.font = '11px "Microsoft YaHei"';
ctx.textAlign = 'center';
ctx.fillText(glowLabels[i], x, y + 30);
});
</script>
</body>
</html>阴影实战 - 卡片与按钮效果
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>阴影实战 - 卡片与按钮</title>
<style>
body {
margin: 0;
padding: 20px;
background: #e8eaf6;
font-family: "Microsoft YaHei", sans-serif;
}
h1 { text-align: center; color: #333; }
canvas {
display: block;
margin: 20px auto;
border-radius: 8px;
}
</style>
</head>
<body>
<h1>阴影实战 - UI 组件效果</h1>
<canvas id="uiCanvas" width="700" height="400"></canvas>
<script>
const canvas = document.getElementById('uiCanvas');
const ctx = canvas.getContext('2d');
// 背景
ctx.fillStyle = '#e8eaf6';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// === 卡片组件 ===
function drawCard(x, y, w, h, elevation) {
ctx.save();
// 多层阴影模拟不同高度
const shadowAlpha = 0.1 + elevation * 0.03;
const shadowBlur = 5 + elevation * 5;
const shadowOffsetY = 2 + elevation * 2;
ctx.shadowColor = `rgba(0, 0, 0, ${shadowAlpha})`;
ctx.shadowBlur = shadowBlur;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = shadowOffsetY;
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.roundRect(x, y, w, h, 12);
ctx.fill();
ctx.restore();
// 卡片内容
ctx.fillStyle = '#333';
ctx.font = 'bold 16px "Microsoft YaHei"';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(`Elevation ${elevation}`, x + 20, y + 20);
ctx.fillStyle = '#888';
ctx.font = '13px "Microsoft YaHei"';
ctx.fillText(`shadowBlur: ${shadowBlur}`, x + 20, y + 48);
ctx.fillText(`shadowOffsetY: ${shadowOffsetY}`, x + 20, y + 68);
ctx.fillText(`shadowAlpha: ${shadowAlpha.toFixed(2)}`, x + 20, y + 88);
}
drawCard(30, 30, 200, 120, 1);
drawCard(260, 30, 200, 120, 3);
drawCard(490, 30, 200, 120, 5);
// === 按钮组件 ===
function drawButton(x, y, w, h, options = {}) {
const {
bgColor = '#6200ee',
textColor = '#fff',
text = '按钮',
pressed = false,
radius = h / 2
} = options;
ctx.save();
if (pressed) {
// 按下状态:阴影缩小
ctx.shadowColor = 'rgba(98, 0, 238, 0.2)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 1;
} else {
// 正常状态:较大阴影
ctx.shadowColor = 'rgba(98, 0, 238, 0.3)';
ctx.shadowBlur = 8;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 4;
}
ctx.fillStyle = bgColor;
ctx.beginPath();
ctx.roundRect(x, y, w, h, radius);
ctx.fill();
ctx.restore();
// 文字
ctx.fillStyle = textColor;
ctx.font = 'bold 14px "Microsoft YaHei"';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(text, x + w / 2, y + h / 2);
}
drawButton(30, 200, 120, 40, { text: '正常状态' });
drawButton(170, 200, 120, 40, { text: '按下状态', pressed: true });
drawButton(310, 200, 120, 40, { bgColor: '#03dac6', text: '次要按钮' });
drawButton(450, 200, 120, 40, { bgColor: '#fff', textColor: '#6200ee', text: '轮廓按钮' });
// === 文本阴影 ===
ctx.fillStyle = '#333';
ctx.font = 'bold 16px "Microsoft YaHei"';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText('文本阴影效果', 30, 280);
// 普通文本阴影
ctx.save();
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.font = 'bold 28px "Microsoft YaHei"';
ctx.fillStyle = '#333';
ctx.fillText('普通文本阴影', 30, 310);
ctx.restore();
// 发光文本
ctx.save();
ctx.shadowColor = '#6200ee';
ctx.shadowBlur = 15;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.font = 'bold 28px "Microsoft YaHei"';
ctx.fillStyle = '#6200ee';
ctx.fillText('发光文本效果', 280, 310);
ctx.restore();
// 多层发光
ctx.save();
ctx.shadowColor = '#ff6f00';
ctx.shadowBlur = 20;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.font = 'bold 28px "Microsoft YaHei"';
ctx.fillStyle = '#fff';
ctx.fillText('多层发光', 530, 310);
// 再画一层增强发光
ctx.fillText('多层发光', 530, 310);
ctx.restore();
// === 内阴影模拟 ===
ctx.fillStyle = '#333';
ctx.font = 'bold 16px "Microsoft YaHei"';
ctx.fillText('内阴影模拟(使用渐变)', 30, 370);
// 使用渐变模拟内阴影
const innerX = 250, innerY = 360, innerW = 200, innerH = 30;
ctx.save();
ctx.beginPath();
ctx.roundRect(innerX, innerY, innerW, innerH, 6);
ctx.clip();
// 先填充底色
ctx.fillStyle = '#fff';
ctx.fillRect(innerX, innerY, innerW, innerH);
// 顶部内阴影
const topShadow = ctx.createLinearGradient(innerX, innerY, innerX, innerY + 15);
topShadow.addColorStop(0, 'rgba(0, 0, 0, 0.15)');
topShadow.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = topShadow;
ctx.fillRect(innerX, innerY, innerW, 15);
// 左侧内阴影
const leftShadow = ctx.createLinearGradient(innerX, innerY, innerX + 10, innerY);
leftShadow.addColorStop(0, 'rgba(0, 0, 0, 0.1)');
leftShadow.addColorStop(1, 'rgba(0, 0, 0, 0)');
ctx.fillStyle = leftShadow;
ctx.fillRect(innerX, innerY, 10, innerH);
ctx.restore();
// 边框
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.roundRect(innerX, innerY, innerW, innerH, 6);
ctx.stroke();
</script>
</body>
</html>五、浏览器兼容性
注意
不同浏览器的阴影渲染可能略有差异,特别是 shadowBlur 的模糊算法可能不同。
六、注意事项与最佳实践
1. 阴影会影响所有后续绘制
设置阴影属性后,所有后续的绘制操作都会带有阴影,直到重置阴影:
代码示例
// 重置阴影
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
// 或者使用 save/restore
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.5)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
// ... 绘制带阴影的图形
ctx.restore(); // 阴影设置被恢复2. 阴影性能问题
阴影(特别是大 shadowBlur 值)会显著影响绘制性能:
代码示例
// 性能差:大模糊值
ctx.shadowBlur = 50; // 模糊计算量大
// 性能好:小模糊值
ctx.shadowBlur = 10;
// 优化:对静态内容使用离屏 Canvas 缓存
const offscreen = document.createElement('canvas');
// ... 在离屏 Canvas 上绘制带阴影的内容
// 然后用 drawImage 绘制到主 Canvas3. shadowBlur 值的理解
shadowBlur 的值并不直接等于模糊的像素范围,它是一个高斯模糊的参数。实际模糊范围大约是 shadowBlur 值的 2-3 倍。
4. Canvas 不支持内阴影
Canvas 原生不支持内阴影效果,但可以通过以下方式模拟:
代码示例
// 方法一:使用渐变模拟
function drawInnerShadow(ctx, x, y, w, h, r) {
ctx.save();
ctx.beginPath();
ctx.roundRect(x, y, w, h, r);
ctx.clip();
// 顶部阴影
const topGrad = ctx.createLinearGradient(x, y, x, y + 10);
topGrad.addColorStop(0, 'rgba(0,0,0,0.2)');
topGrad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = topGrad;
ctx.fillRect(x, y, w, 10);
ctx.restore();
}
// 方法二:绘制一个稍大的阴影图形,然后用裁剪限制范围5. 多层阴影效果
通过多次绘制同一图形,可以创建多层阴影效果:
代码示例
// 外层柔和阴影
ctx.save();
ctx.shadowColor = 'rgba(0, 0, 0, 0.1)';
ctx.shadowBlur = 30;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 10;
ctx.fillStyle = '#fff';
ctx.fillRect(50, 50, 200, 100);
ctx.restore();
// 内层锐利阴影
ctx.save();
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 3;
ctx.fillStyle = '#fff';
ctx.fillRect(50, 50, 200, 100);
ctx.restore();6. 阴影颜色使用半透明
阴影颜色通常使用半透明黑色,这样更自然:
代码示例
// 自然:半透明黑色
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
// 不自然:不透明黑色
ctx.shadowColor = '#000000';七、代码规范示例
代码示例
/**
* 阴影工具类
* 封装常用阴影效果的创建方法
*/
class ShadowHelper {
constructor(ctx) {
this.ctx = ctx;
}
/**
* 设置阴影
* @param {Object} options - 阴影选项
*/
set(options = {}) {
const {
color = 'rgba(0, 0, 0, 0.3)',
blur = 10,
offsetX = 0,
offsetY = 4
} = options;
const ctx = this.ctx;
ctx.shadowColor = color;
ctx.shadowBlur = blur;
ctx.shadowOffsetX = offsetX;
ctx.shadowOffsetY = offsetY;
}
/**
* 清除阴影
*/
clear() {
const ctx = this.ctx;
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
/**
* 绘制带阴影的图形
* @param {Function} drawFn - 绘制函数
* @param {Object} shadowOptions - 阴影选项
*/
drawWithShadow(drawFn, shadowOptions = {}) {
const ctx = this.ctx;
ctx.save();
this.set(shadowOptions);
drawFn(ctx);
ctx.restore();
}
/**
* Material Design 风格阴影
* @param {number} elevation - 高度等级 (1-24)
* @returns {Object} 阴影选项
*/
static materialElevation(elevation) {
const alpha = Math.min(0.3, 0.05 + elevation * 0.02);
const blur = 5 + elevation * 5;
const offsetY = 2 + elevation * 2;
return {
color: `rgba(0, 0, 0, ${alpha})`,
blur: blur,
offsetX: 0,
offsetY: offsetY
};
}
/**
* 发光效果
* @param {string} color - 发光颜色
* @param {number} intensity - 发光强度
* @returns {Object} 阴影选项
*/
static glow(color, intensity = 15) {
return {
color: color,
blur: intensity,
offsetX: 0,
offsetY: 0
};
}
/**
* 多层阴影
* @param {Function} drawFn - 绘制函数
* @param {Array<Object>} shadowLayers - 阴影层配置
*/
drawWithMultiShadow(drawFn, shadowLayers = []) {
const ctx = this.ctx;
shadowLayers.forEach(shadow => {
ctx.save();
this.set(shadow);
drawFn(ctx);
ctx.restore();
});
// 最后绘制无阴影的图形本身
ctx.save();
this.clear();
drawFn(ctx);
ctx.restore();
}
}
// 使用示例
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const shadow = new ShadowHelper(ctx);
// Material Design 阴影
shadow.drawWithShadow(
(ctx) => {
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.roundRect(50, 50, 200, 100, 8);
ctx.fill();
},
ShadowHelper.materialElevation(4)
);
// 发光效果
shadow.drawWithShadow(
(ctx) => {
ctx.fillStyle = '#6200ee';
ctx.beginPath();
ctx.arc(400, 100, 30, 0, Math.PI * 2);
ctx.fill();
},
ShadowHelper.glow('#6200ee', 20)
);
// 多层阴影
shadow.drawWithMultiShadow(
(ctx) => {
ctx.fillStyle = '#fff';
ctx.fillRect(50, 200, 200, 100);
},
[
{ color: 'rgba(0,0,0,0.05)', blur: 30, offsetY: 15 },
{ color: 'rgba(0,0,0,0.1)', blur: 10, offsetY: 5 }
]
);八、常见问题与解决方案
常见问题
阴影为什么影响后续所有绘制?
阴影属性是状态性的,设置后持续生效。解决方案是使用save()/restore()配对管理阴影状态,或在不需要阴影时手动重置shadowColor为transparent、shadowBlur为0。
Canvas阴影性能差怎么优化?
大shadowBlur值导致高斯模糊计算量大。优化方案:减小shadowBlur值,对静态内容使用离屏Canvas缓存带阴影的图形,然后在主Canvas上用drawImage绘制缓存。
阴影在图形边缘被裁切怎么办?
阴影偏移或模糊范围超出了Canvas边界。解决方案是留出足够的边距,或增大Canvas尺寸,确保阴影范围在画布内。
Canvas如何实现内阴影效果?
Canvas不原生支持内阴影,但可以通过渐变模拟:先clip裁剪到图形区域,然后用线性渐变从半透明黑色过渡到透明,绘制在图形边缘模拟内阴影。
如何创建多层阴影效果?
通过多次绘制同一图形,每次使用不同的阴影参数(如外层柔和阴影shadowBlur=30加内层锐利阴影shadowBlur=10),叠加出更真实的多层阴影效果。
九、总结
Canvas 阴影是增强视觉层次感的重要工具:
-
四个属性:
shadowColor、shadowBlur、shadowOffsetX、shadowOffsetY共同控制阴影效果 -
阴影颜色:默认完全透明(无阴影),需显式设置才能看到效果
-
发光效果:偏移为 0、模糊值大时产生发光效果
-
状态性:阴影设置会影响所有后续绘制,需用
save/restore管理 -
性能:大
shadowBlur值影响性能,静态内容应缓存 -
内阴影:Canvas 不原生支持,需用渐变等技巧模拟
-
多层阴影:多次绘制同一图形叠加不同阴影,可创建更真实的效果
-
实战应用:卡片阴影、按钮效果、文本发光、Material Design 等
本文涉及AI创作
内容由AI创作,请仔细甄别