用 Canvas 合成引导 AI 实现手电筒光效
通过离屏 Canvas 绘制暗层,再用 destination-out 和径向渐变擦出光照区域,可实现轻量的二维手电筒效果。文章建议在提示词中明确这一实现路径,减少 AI 引入复杂射线计算或 WebGL 的情况。
AI 深度解读
给 AI 一句「做个动态手电筒光照」,它可能交付一大段射线计算,也可能引入复杂的 WebGL 配置。作者给出的替代方案很直接:用 Canvas 2D 先画一层黑色遮罩,再围绕鼠标擦出一个柔和的圆形亮区。
这个技巧适合单文件 HTML5 潜行或恐怖小游戏,也适合作为 AI 编程提示词里的明确约束。不过,标题里的「6 行」和「60 FPS」都要分开看:代码并不只有六行,原文也没有提供性能测试。
不计算光线,也能做出黑暗中的亮区
作者把问题从「模拟光线传播」改成了「控制哪些地方被黑色遮住」。
先正常绘制地板、战利品和敌人,再在离屏画布(offscreen canvas)上铺一层接近不透明的深色。接着,以鼠标或玩家位置为中心,把遮罩擦掉一部分。最后将遮罩覆盖到主画布上,底下的场景就会从亮区里显露出来。
这里的离屏画布,可以理解成一张暂时不直接展示的透明画纸。它专门处理黑暗,主画布负责游戏场景,两者最后叠起来。
两个 API 各司其职:
globalCompositeOperation = 'destination-out'根据新绘制内容的透明度,擦除画布上已有的内容。createRadialGradient创建径向渐变,让擦除效果从中心向外逐渐减弱。
它做的是柔和的可见区域遮罩,没有计算墙壁如何挡住光。
所以,原文虽然用了「flashlight beam」和「flashlight cone」的描述,示例实际画出来的是圆形亮区,并不是有朝向的锥形光束。
把合成方式写进提示词,代码更容易落到预期
下面这段代码解决的是「场景画完后,怎样叠加一个跟随鼠标的黑暗遮罩」。作者要求把它放进主 requestAnimationFrame 渲染循环,位于地板、战利品和敌人绘制之后。
// 1. Draw darkness on an offscreen lighting canvas (lightCtx)
lightCtx.globalCompositeOperation = 'source-over';
lightCtx.fillStyle = 'rgba(5, 5, 12, 0.96)';
lightCtx.fillRect(0, 0, canvas.width, canvas.height);
// 2. Carve out a soft radial flashlight beam at the mouse/player cursor
lightCtx.globalCompositeOperation = 'destination-out';
const beam = lightCtx.createRadialGradient(mouse.x, mouse.y, 15, mouse.x, mouse.y, 220);
beam.addColorStop(0, 'rgba(0, 0, 0, 1)');
beam.addColorStop(1, 'rgba(0, 0, 0, 0)');
lightCtx.fillStyle = beam;
lightCtx.beginPath();
lightCtx.arc(mouse.x, mouse.y, 220, 0, Math.PI * 2);
lightCtx.fill();
// 3. Stamp the darkness mask onto your main game canvas
ctx.drawImage(lightCanvas, 0, 0);
重点看 destination-out 那一行:后面画的渐变会擦除黑暗。渐变内半径是 15,外半径是 220;中心色标的 alpha 为 1,擦除最强,边缘为 0,不再擦除。渐变里的黑色不是手电筒颜色,决定擦除强度的是透明度。
这段代码还依赖已经创建好的 lightCanvas、lightCtx、主画布上下文 ctx,以及 mouse.x、mouse.y。原文没有提供这些初始化代码,因此它是渲染片段,不能直接当完整游戏运行。
作者建议在提示词里明确加入:
「Use an offscreen canvas with globalCompositeOperation = 'destination-out' and createRadialGradient for a 2D Noir flashlight mask」
这句话的价值在于指定实现路径。只描述「恐怖氛围」或「动态光照」,模型还有很大的自由发挥空间;把离屏画布、合成模式和渐变 API 写出来,输出更容易贴近这个简单方案。至于能否因此避免所有错误,原文没有验证。
绘制顺序还能控制叙事效果:先把敌人画进场景,再覆盖黑暗,最后绘制敌人的「发光眼睛」,就能让眼睛在暗处露出来。这是图层顺序产生的效果,不需要额外模拟光源。
我的判断
对只需要探索亮区、黑暗氛围的小游戏,我认可这个取舍。标准 HTML5 Canvas 2D API 就能实现,零外部依赖,代码也容易让前端开发者读懂和调整。
但作者声称它能在每个现代浏览器原生跑到 60 FPS,没有附设备、画布尺寸、场景复杂度或帧耗时数据。这属于未经充分支撑的性能断言。原文对射线方案容易生成「300 行坏掉的向量数学」、重型 WebGL 配置拖慢移动端的描述,同样没有给对照测试。
和射线投射相比,这个方案省去了遮挡计算,也就失去了对应能力。如果玩法要求墙后完全不可见,示例里的圆形遮罩不够用;如果只是给原型加氛围,它很合适。WebGL 也不能仅凭这篇文章就判定为更慢。
标题的「6 行」更像宣传概括。文末还引导下载包含 Three.js 提示词、God-Mode 控制台加速技巧和 FFmpeg 预告片脚本的 PDF,但正文没有展示这些内容,不能据此判断整套资料的质量。
能拿来做什么
- 做 Canvas 游戏原型时,可以先用这段遮罩验证黑暗氛围,再根据玩法决定是否需要墙体遮挡和方向性光束。
- 让 AI 编写同类效果时,明确写出离屏画布、
destination-out和createRadialGradient,并要求补齐初始化与渲染循环,减少实现方向上的偏差。 - 把敌人主体和发光眼睛分开绘制,用遮罩前后的顺序控制可见性,快速验证暗处提示是否有效。
- 准备适配手机时,在目标设备上测量实际帧耗时;将画布尺寸和场景复杂度纳入测试,再判断能否达到
60 FPS。