
本文详解如何使用 Canvas 的径向渐变与混合模式(如 color-dodge 和 lighter)实现可自定义颜色的聚光灯效果,替代纯黑遮罩,让光照呈现温暖、自然的色彩过渡。
本文详解如何使用 canvas 的径向渐变与混合模式(如 `color-dodge` 和 `lighter`)实现可自定义颜色的聚光灯效果,替代纯黑遮罩,让光照呈现温暖、自然的色彩过渡。
在原实现中,聚光灯仅通过 destination-out 复合模式“挖空”图像形成黑色圆形区域,本质上是遮罩而非真实光照——它无法体现色温、辉光或柔边等视觉特性。要真正模拟彩色光源(如暖黄手电、冷白LED或霓虹灯效),需转向叠加式光照渲染:即先绘制原始图像,再以带颜色的径向渐变作为“光层”,通过加性混合模式叠加其上。
✅ 核心技术要点
-
径向渐变(
createRadialGradient):定义从中心发散的光强衰减,支持多色阶控制色调与明暗过渡; -
加性混合模式:
-
"color-dodge":增强亮度并保留色相,适合主光晕; -
"lighter":简单相加像素值,用于柔化边缘或叠加辅助光;
-
-
动态坐标变换(
setTransform):将渐变原点锚定在鼠标位置,避免重复创建渐变对象,提升性能; -
图像加载保障:必须等待
Image.onload后再设置 canvas 尺寸与启动绘制,否则naturalWidth/Height为 0,导致渲染异常。
? 完整可运行示例
<canvas id="canvas" width="800" height="600"></canvas>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const image = new Image();
// 推荐使用更稳定的图片链接(原链接可能失效)
image.src = "https://picsum.photos/800/600?grayscale"; // 示例灰度图,便于观察光照效果
image.addEventListener("load", () => {
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
// 创建中心在 (0,0)、半径为 150 的径向渐变(后续通过 transform 平移)
const radius = 150;
const lightGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, radius);
lightGrad.addColorStop(0.0, "#ffea00"); // 中心:明亮暖黄
lightGrad.addColorStop(0.3, "#ff9800"); // 中间:橙红过渡
lightGrad.addColorStop(0.7, "#ff5722"); // 外缘:深橙
lightGrad.addColorStop(1.0, "transparent"); // 边界完全透明(非黑色!)
let mouseX = canvas.width / 2;
let mouseY = canvas.height / 2;
function drawImage() {
ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置变换矩阵
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(image, 0, 0);
}
function drawFlashlight() {
drawImage();
// 关键:将画布原点平移到鼠标位置,使渐变中心对齐光斑
ctx.setTransform(1, 0, 0, 1, mouseX, mouseY);
ctx.fillStyle = lightGrad;
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2);
// 第一层:color-dodge 增强高光细节(α=0.8)
ctx.globalAlpha = 0.8;
ctx.globalCompositeOperation = "color-dodge";
ctx.fill();
// 第二层:lighter 柔化边缘并增加光晕体积(α=0.4)
ctx.globalAlpha = 0.4;
ctx.globalCompositeOperation = "lighter";
ctx.fill();
}
canvas.addEventListener("mousemove", (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
});
// 启动动画循环(推荐用 requestAnimationFrame 替代高频 mousemove 触发)
function animate() {
drawFlashlight();
requestAnimationFrame(animate);
}
animate();
});
⚠️ 注意事项与优化建议
-
避免
globalCompositeOperation = "destination-out":该模式会永久擦除像素,无法叠加颜色;彩色光照必须基于source-over+ 加性混合。 -
渐变终点设为
transparent:确保光斑外无硬边或意外遮挡,实现自然衰减。 -
慎用
globalAlpha与混合模式组合:不同globalAlpha值会显著影响最终叠加强度,建议通过调试(如临时改为fillStyle = "red")验证光斑位置与范围。 -
性能优化:
mousemove直接触发重绘易造成卡顿,改用requestAnimationFrame统一调度,并在animate()中读取最新鼠标坐标。 -
响应式适配:若 canvas 需缩放显示,请用
getBoundingClientRect()计算逻辑坐标,并结合devicePixelRatio提升高清屏清晰度(进阶需求)。
通过上述方法,你不仅能为聚光灯赋予真实色彩,还可灵活调整色相、饱和度、衰减曲线与混合强度,轻松实现从复古钨丝灯到科幻激光束的多样化光照风格。











