用Claude生成单文件赛博城市:Canvas动画与断电自救状态机实现
2026/9/22 0:31:15 网站建设 项目流程

最近在折腾 AI 编程时,我发现一个特别上头的玩法:让 Claude 只靠一个 HTML 文件,现场造出一座会断电自救的赛博城市。不需要 React,不需要 Node,不需要任何构建工具,只要一个浏览器和一段精心设计的提示词,就能在页面里看到霓虹闪烁的楼宇、流动的车辆光轨,甚至在“全城断电”后自动启动备用电源、派出维修无人机,让城市重新亮起来。

这篇文章就把这个实验完整拆开:从 Claude 生成思路、提示词设计,到 Canvas 城市绘制、断电状态机实现,再到完整可运行的参考代码和踩坑排查。适合对 AI 辅助编程感兴趣的前端开发者,也适合想用 Claude 做交互原型但不知道从哪里入手的同学。看完之后,你不仅能跑起一个赛博城市,还能掌握“如何把清晰需求交给 AI,再快速改造成自己想要的版本”这套工作流。

1. 背景与核心概念

1.1 什么是“会断电自救的赛博城市”

从字面上看,这是一个用 HTML + CSS + JavaScript 在浏览器里绘制的赛博朋克风格城市夜景。它和普通静态城市图最大的区别,是包含了一套完整的“电力系统模拟”:

  • 城市有电量数值,页面右上角会显示电量条。
  • 点击“模拟断电”后,城市灯光会逐渐熄灭,窗户随机变黑。
  • 电量降到 0 时,备用电源自动启动,画面中出现无人机粒子。
  • 无人机在城市上方飞行一段时间后,电力逐渐恢复,窗户灯光重新点亮。
  • 全部恢复后,城市进入正常的“电网供电”状态。

简单说,所谓“断电自救”,不是真的去控制硬件电路,而是用 JavaScript 写了一个状态机,把断电、救援、恢复这个完整过程可视化。它很适合用来演示 Canvas 动画、状态机设计、粒子系统,以及 AI 编程在单个文件内完成复杂交互的能力。

1.2 为什么选择“单 HTML 文件”

很多前端项目一上来就是脚手架、依赖、构建,但做这种创意原型时,单 HTML 文件反而有不可替代的优势:

  • 零依赖,双击就能运行。
  • 所有 HTML、CSS、JavaScript 都集中在一个文件里,方便 Claude 一次生成。
  • 易于分享,直接把文件发给别人就能看效果。
  • 方便调试,打开浏览器控制台就能定位问题。

单 HTML 文件特别适合“让 AI 从零生成一个完整交互页面”的场景。Claude 在生成代码时,如果要求它“输出可运行的单文件”,它会倾向于把所有逻辑集中在同一作用域内,结构也更清晰。

1.3 为什么要用 Claude 来写

Claude 这类 AI 编程助手擅长根据自然语言需求生成完整代码。对于赛博城市这种“视觉 + 交互 + 状态管理”都有的小项目,人工手写可能需要一个多小时,而 Claude 通常几十秒就能给出初版。

不过要注意,AI 生成的代码不是开箱即用的黄金代码。你需要:

  • 把需求写清楚,尤其是状态流转、交互按钮、视觉风格。
  • 生成之后逐段审查代码,确认逻辑符合预期。
  • 根据运行效果反复调整提示词,再做局部修改。

本文就是一套完整的实验路径:先给出提示词,让 Claude 生成页面;再给出一份我整理好的参考实现,方便你对照理解;最后讲解常见问题,减少试错成本。

2. 实验环境与准备工作

2.1 工具清单

这个实验对环境要求非常低,基本只需要三样东西:

工具说明
现代浏览器Chrome、Edge、Firefox 均可,推荐 Chrome
Claude 账号可以在 Claude 官网或支持 Claude 的编程工具中使用
文本编辑器保存和修改 HTML 文件,VS Code、记事本都行

不需要安装 Node.js,不需要安装 npm 包,也不需要配置构建工具。整个项目就是一个index.html文件,浏览器直接打开即可运行。

版本说明:Claude 模型更新比较快,不同版本生成代码的能力会有差异。本文以“Claude 生成前端单文件项目”的通用思路为例,不依赖某个特定版本。如果你使用的版本生成结果不理想,可以通过调整提示词或手动修改代码来兜底。

2.2 Claude 的使用入口

你可以根据手头条件选择使用方式:

  • 在 Claude 网页端直接对话,让它生成代码。
  • 在支持 Claude 的代码编辑器或终端工具中,让它直接创建文件。
  • 也可以把生成的代码复制到本地,手动保存为 HTML 文件。

需要注意,如果对话窗口一次生成的内容太长,可能会被截断。遇到这种情况,可以分两步让 Claude 完成:

  1. 先让它生成完整的 HTML 结构、CSS 样式和 JavaScript 骨架。
  2. 再让它补充或修复某个具体模块,例如“优化无人机的粒子效果”“把状态机拆得更清晰”。

2.3 项目文件结构

本项目的文件结构非常简洁:

cyber-city/ └── index.html

后续所有代码都写在index.html中,包括:

  • <style>标签:负责页面布局、HUD 面板、按钮样式。
  • <canvas>元素:负责绘制城市背景、建筑、窗户、霓虹灯、道路和无人机。
  • <script>标签:负责生成城市数据、运行状态机、处理用户交互、执行动画循环。

这种结构下,代码之间的依赖关系很直观:先创建 Canvas 画布,再生成城市数据,然后进入动画循环,在循环里根据状态更新电量和灯光,最后绘制到画布。

3. 核心设计拆解:Canvas 城市与电力状态机

在让 Claude 写代码之前,最好先理解这个系统由哪几个模块组成。下面拆成三层来看。

3.1 可视化层:Canvas 绘制思路

城市夜景的核心是 Canvas 2D。它不依赖任何外部图片,所有建筑、窗户、霓虹灯牌、路面车灯都是通过绘图 API 画出来的。

整体绘制顺序如下:

  1. 绘制夜空渐变背景和闪烁星星。
  2. 绘制城市建筑,建筑主体使用深色填充。
  3. 在建筑表面绘制密集的窗户小矩形。
  4. 在建筑周围绘制霓虹灯招牌,文字带发光效果。
  5. 绘制道路和流动的车辆光轨。
  6. 在特殊状态下绘制无人机粒子。

窗户是城市灯光是否“活着”的关键。每个窗户在生成城市时会被分配一个初始状态:lit = 1表示点亮,lit = 0表示熄灭。断电时,动态把部分窗户的lit改成 0;恢复时,再把lit改成 1。这样比整栋建筑直接变暗更有层次感。

3.2 状态层:电力状态机

“断电自救”的核心是状态机。如果直接用临时变量控制灯光,很容易出现逻辑混乱。定义一个清晰的枚举结构会让代码更好读:

const STATE = { NORMAL: 'normal', // 电网正常 BLACKOUT: 'blackout', // 发生断电 RESCUE: 'rescue', // 备用电源启动 RECOVER: 'recover' // 正在恢复 };

状态流转方向是固定的:

normal → blackout → rescue → recover → normal

每个状态对应不同的电量变化速度和视觉效果:

状态电量变化视觉表现
normal电量维持或缓慢回满城市灯光正常,亮度最高
blackout电量快速下降灯光渐暗,窗户随机熄灭
rescue电量缓慢恢复备用电源运行,无人机粒子出现
recover电量快速回升灯光逐渐重新点亮

如果用户点击“恢复供电”按钮,则无论当前在 blackout 还是 rescue,都可以直接跳到 recover,实现手动干预。

3.3 交互层:按钮与自动循环

页面需要提供三种交互:

  • 模拟断电:立即进入 blackout 状态,触发自救流程。
  • 恢复供电:手动结束断电状态,进入恢复流程。
  • 自动循环演示:每过一段时间自动执行一次“断电 → 自救 → 恢复”的完整流程,方便展示。

按钮只负责修改状态机的输入,不直接去改城市灯光。这样状态流转逻辑集中在update函数中,避免多个地方同时操作状态造成冲突。

4. 用 Claude 生成赛博城市的完整提示词

4.1 提示词设计技巧

想让 Claude 生成靠谱的单文件项目,提示词不能只写“给我一个赛博城市”。你需要把以下信息说清楚:

  • 技术边界:必须用原生 HTML + CSS + JavaScript + Canvas,不依赖第三方库。
  • 功能需求:城市夜景、断电、备用电源、无人机、灯光恢复。
  • 交互要求:需要哪些按钮,点击后触发什么效果。
  • 状态逻辑:明确状态流转过程。
  • 代码风格:给中文注释,函数拆分清晰。
  • 输出要求:必须是完整单文件,不要省略。

Claude 对“结构清晰的需求”响应质量明显更高。你可以把需求拆成“做什么 + 怎么做 + 边界条件”三部分,它生成出来的代码可维护性会好很多。

4.2 本文使用的提示词模板

下面这份提示词是我实验中效果比较稳定的版本,可以直接复制使用。你也可以在此基础上增加更多细节,例如“屋顶要有天线”“无人机会发射光束”等。

你是资深前端动画工程师。请在 index.html 一个文件中实现“赛博朋克城市断电自救模拟器”。 要求: 1. 使用 HTML + CSS + 原生 JavaScript + Canvas 2D,不依赖任何第三方库、图片和外部字体。 2. 用 Canvas 绘制城市天际线,包含建筑、窗户灯光、霓虹灯招牌、道路和流动车灯。 3. 实现城市电力状态机,流转顺序为: normal(正常)→ blackout(断电)→ rescue(备用电源启动)→ recover(恢复)→ normal(正常)。 4. 断电时,城市灯光逐渐变暗,窗户随机熄灭;电量降到 0 后,备用电源启动,画面出现维修无人机粒子; 恢复供电时,窗户随机重新点亮,城市亮度逐渐恢复正常。 5. 页面右上角显示电量条和当前状态文字。 6. 页面提供三个按钮:模拟断电、恢复供电、自动循环演示。 7. 代码要有中文注释,变量命名直观,逻辑拆分清晰。 8. 控制无人机数量,避免粒子过多导致卡顿。 9. 保证输出完整可运行的单 HTML 文件,不要省略代码。

你可以直接把这整段贴给 Claude。如果它生成的版本有偏差,比如无人机没有出现、按钮不生效,就在原基础上追加一句“请保留原有功能,只修复 XX 部分”,迭代速度很快。

4.3 生成后的文件保存方式

Claude 生成代码后,把完整内容复制到本地,保存为index.html,然后双击用浏览器打开。如果 Claude 的界面提供了代码预览或下载按钮,也可以直接下载。

保存时建议把文件放到独立目录,例如cyber-city/index.html。这样后续如果要加图片、音频或其他静态资源,目录结构仍然清晰。

5. 完整参考代码与关键逻辑讲解

下面给出一个完整的可运行版本。它和 Claude 生成的可能不完全一样,但结构和逻辑是相通的,你可以把它作为调试基线,对照理解每一层在做什么。

5.1 完整单文件代码 index.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> * { margin: 0; padding: 0; box-sizing: border-box; } body { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #050510; font-family: "Courier New", monospace; overflow: hidden; } #app { position: relative; width: 1280px; max-width: 96vw; aspect-ratio: 16 / 9; border: 1px solid rgba(0, 255, 255, 0.4); box-shadow: 0 0 40px rgba(0, 200, 255, 0.15); overflow: hidden; } canvas { display: block; width: 100%; height: 100%; } #hud { position: absolute; top: 18px; left: 18px; z-index: 10; color: #bffcff; text-shadow: 0 0 8px rgba(0, 255, 255, 0.7); } #title { font-size: 22px; font-weight: bold; letter-spacing: 4px; } #powerBar { width: 260px; height: 12px; border: 1px solid #67f2ff; margin-top: 8px; background: rgba(0, 0, 0, 0.4); overflow: hidden; } #powerInner { display: block; height: 100%; width: 100%; background: linear-gradient(90deg, #00d5ff, #00ffa2); transition: width 0.2s linear; } #statusText { margin-top: 6px; font-size: 14px; } #buttons { margin-top: 14px; display: flex; gap: 8px; flex-wrap: wrap; } #buttons button { background: rgba(0, 30, 40, 0.75); color: #a9f8ff; border: 1px solid rgba(0, 200, 255, 0.6); padding: 6px 14px; font-family: inherit; font-size: 13px; cursor: pointer; transition: all 0.2s ease; } #buttons button:hover { background: rgba(0, 120, 160, 0.6); color: #ffffff; box-shadow: 0 0 12px rgba(0, 255, 255, 0.5); } #toast { position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%); padding: 8px 24px; background: rgba(0, 20, 30, 0.8); border: 1px solid #00e0ff; color: #cffcff; font-size: 14px; letter-spacing: 2px; opacity: 0; transition: opacity 0.4s ease; pointer-events: none; text-shadow: 0 0 6px rgba(0, 255, 255, 0.6); z-index: 20; } #toast.show { opacity: 1; } </style> </head> <body> <div id="app"> <canvas id="cityCanvas"></canvas> <div id="hud"> <div id="title">CYBER CITY // 赛博城市</div> <div id="powerBar"><span id="powerInner"></span></div> <div id="statusText">状态:电网正常</div> <div id="buttons"> <button id="blackoutBtn">模拟断电</button> <button id="restoreBtn">恢复供电</button> <button id="autoBtn">自动循环演示</button> </div> </div> <div id="toast"></div> </div> <script> (() => { // ========== 基础配置 ========== const canvas = document.getElementById('cityCanvas'); const ctx = canvas.getContext('2d'); const DPR = Math.min(window.devicePixelRatio || 1, 1.5); const W = 1280; const H = 720; canvas.width = W * DPR; canvas.height = H * DPR; ctx.scale(DPR, DPR); const powerInner = document.getElementById('powerInner'); const statusText = document.getElementById('statusText'); const toast = document.getElementById('toast'); // ========== 状态机 ========== const STATE = { NORMAL: 'normal', BLACKOUT: 'blackout', RESCUE: 'rescue', RECOVER: 'recover' }; let state = STATE.NORMAL; let power = 100; let blackoutTimer = 0; let rescueTimer = 0; let autoMode = false; let autoTimer = 0; let frame = 0; // ========== 城市数据生成 ========== const buildings = []; const signs = []; const drones = []; const rand = (a, b) => a + Math.random() * (b - a); function generateCity() { buildings.length = 0; signs.length = 0; let x = 0; while (x < W - 60) { const w = rand(50, 120); const h = rand(90, 280); const y = H - 120 - h; const bodyColor = `hsl(${rand(220, 260)}, 40%, ${rand(12, 25)}%)`; const windows = []; const rows = Math.floor(h / 18); const cols = Math.floor((w - 14) / 12); for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { windows.push({ x: x + 12 + c * 12, y: y + 14 + r * 18, w: 6, h: 9, lit: Math.random() > 0.3 ? 1 : 0 }); } } buildings.push({ x, y, w, h, color: bodyColor, windows }); if (Math.random() > 0.5) { const texts = ['NEO', 'PIXEL', '电', 'NET', 'CYBER', 'FLUX', '夜']; signs.push({ x: x + w / 2, y: y + rand(10, 50), text: texts[Math.floor(Math.random() * texts.length)], color: `hsl(${rand(0, 360)}, 90%, 60%)`, size: rand(18, 34) }); } x += w + rand(4, 18); } window.__groundLights = Array.from({ length: 20 }, (_, i) => ({ x: rand(0, W), y: H - rand(0, 30), len: rand(30, 90), speed: rand(0.5, 1.8) })); } // ========== 无人机粒子 ========== function spawnDrones(count) { for (let i = 0; i < count; i++) { drones.push({ x: rand(0, W), y: H - rand(20, 60), vx: rand(-1, 1), vy: rand(-0.8, -0.2), life: rand(120, 240), color: `hsl(${rand(160, 200)}, 100%, 70%)` }); } } function updateDrones() { for (let i = drones.length - 1; i >= 0; i--) { const d = drones[i]; d.x += d.vx; d.y += d.vy; d.life--; if (d.life <= 0 || d.y < 50) { drones.splice(i, 1); } } } // ========== 状态切换 ========== function showToast(msg) { toast.textContent = msg; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 2000); } function triggerBlackout() { if (state !== STATE.NORMAL && state !== STATE.RECOVER) return; state = STATE.BLACKOUT; blackoutTimer = 0; showToast('城市遭遇断电!备用系统启动中…'); } function restorePower(manual = true) { if (state === STATE.BLACKOUT || state === STATE.RESCUE || state === STATE.RECOVER) { state = STATE.RECOVER; power = Math.max(power, 10); if (manual) showToast('恢复供电指令已发送'); } } function toggleAuto() { autoMode = !autoMode; autoTimer = 0; showToast(autoMode ? '自动循环演示:开' : '自动循环演示:关'); } // ========== 主更新逻辑 ========== function update() { frame++; if (autoMode) { autoTimer++; if (autoTimer === 30 || autoTimer === 500) { triggerBlackout(); autoTimer = 0; } } switch (state) { case STATE.NORMAL: power = Math.min(100, power + 2); buildings.forEach(b => { b.windows.forEach(win => { if (win.lit === 0 && Math.random() < 0.003) win.lit = 1; }); }); break; case STATE.BLACKOUT: power = Math.max(0, power - 1.2); blackoutTimer++; buildings.forEach(b => { b.windows.forEach(win => { if (Math.random() < 0.02) win.lit = 0; }); }); if (power <= 0) { state = STATE.RESCUE; rescueTimer = 0; spawnDrones(8); showToast('备用电源启动,维修无人机出发!'); } break; case STATE.RESCUE: power = Math.min(60, power + 0.8); rescueTimer++; if (rescueTimer % 30 === 0) spawnDrones(1); if (rescueTimer > 150 && power >= 50) { state = STATE.RECOVER; showToast('电力恢复中,城市正在重新点亮'); } break; case STATE.RECOVER: power = Math.min(100, power + 1.5); buildings.forEach(b => { b.windows.forEach(win => { if (win.lit === 0 && Math.random() < 0.1) win.lit = 1; }); }); if (power >= 100) { state = STATE.NORMAL; showToast('供电已恢复,城市运行正常'); } break; } updateDrones(); powerInner.style.width = power + '%'; const stateText = { normal: '电网正常', blackout: '发生断电!', rescue: '备用电源运行中',

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询