看到“91行代码创意赛”这几个字,很多人的第一反应是:这不就是个噱头吗?代码越短越显得自己厉害?其实真不是这样。这类比赛的核心从来不是“压缩代码”本身,而是在极其有限的表达空间里,重新思考什么才是真正重要的功能。我参加过几届类似的编程活动,也当过评委看过不少作品,今天想借这个机会,把“用91行代码做创意项目”背后的技术细节、设计思路和实操经验掰开揉碎讲清楚。
先定义一下什么叫“91行代码创意赛”。规则通常很简单:你提交的作品,核心逻辑代码必须控制在91行以内,语言不限、框架不限、运行环境不限,但作品要有完整的创意表达,能跑、能看、能交互。这个行数限制是硬杠杠,注释、空行、甚至大括号换行都可能被计入,所以每一行都得用在刀刃上。
这个赛制有意思的地方在于,它逼着你做三个层面的取舍:功能取舍、抽象取舍、表达取舍。功能上你不能什么都做,必须找到那个“少数几个核心交互就能撑起整个作品”的点;抽象上你必须用数据结构和算法把复杂度压下去;表达上你得让代码本身具备可读性和美感,毕竟评审会看你的代码。这篇文章我会拿一个真实参赛作品的完整实现过程作为例子,从设计思路到最终代码逐行拆解,再聊聊那些评委不会写进规则里、但你一定会踩到的坑。
这91行到底能做什么?说实话,能做的比大多数人想的多得多。我见过用91行写出来的音乐可视化、粒子模拟、小型游戏、数据艺术生成器,甚至还有人用91行做了一台虚拟机的虚拟机。我自己最满意的作品,是一个基于Canvas的交互式粒子星云生成器——它支持鼠标扰动、颜色渐变、动态缩放、自动旋转,以及三种预设形态切换,核心逻辑只有86行,我留了5行的富余做微调。
1. 整体设计思路与方案选型
1.1 为什么选择“粒子系统”作为创意载体
90%的短代码创意赛作品都会把粒子系统作为首选,这背后是有逻辑的。粒子系统的核心操作是“对一组对象的批量更新”,它天然适合用数组+循环+数学函数来表达,几乎不需要额外的脚手架代码。对比一下:如果你想写一个平台跳跃游戏,至少需要处理碰撞检测、重力、输入映射、状态机、地图渲染,这几块加起来怎么也得200行起;但粒子系统只需要三个循环——初始化、更新、绘制,三个循环就能撑起整个作品的骨架。
粒子系统还有个隐藏优势:它天然具备视觉吸引力。在创意赛评审场景下,评委看每个作品的时间平均不到3分钟,粒子系统的随机性和流动性能在几秒内抓住眼球。相比之下,一个精心实现的排序算法可视化虽然技术含量不低,但视觉冲击力就差了不少。
我选的方案是:基于鼠标位置生成连续粒子流,粒子在运动过程中受“吸引/排斥”两种力场影响,颜色的色相值随生命周期漂移,形成类似星云或极光的动态视觉效果。这个方案的表达密度很高——你看到的是流动的光带,但底层其实只有几十行数学计算。
1.2 行数预算的分配策略
91行看起来不少,实际用起来非常紧张。我建议在动手写之前,先做一个行数预算表,把代码分成几个功能块,每块分配明确的行数上限。我的预算分配是这样的:
| 功能块 | 行数预算 | 实际使用 | 说明 |
|---|---|---|---|
| 初始化(变量声明与画布设置) | 8 | 8 | 含粒子数组、鼠标状态、画布获取 |
| 鼠标事件监听 | 4 | 4 | mousemove和click两个事件 |
| 粒子更新算法 | 14 | 14 | 包含力场计算、速度更新、位置更新 |
| 粒子绘制 | 10 | 9 | 包含颜色渐变和大小控制 |
| 预设形态切换 | 12 | 12 | 三个形态参数组,用函数式映射 |
| 主循环 | 6 | 6 | requestAnimationFrame + 清屏 |
| 粒子补充逻辑 | 10 | 10 | 保持粒子总数恒定 |
| 辅助函数 | 12 | 12 | 随机数、颜色转换、边界处理 |
| 样式设置 | 15 | 11 | 控制在剩余预算内 |
| 合计 | 91 | 86 | 余量5行 |
这个预算表的精髓在于给“边界情况”预留空间。实际开发中你一定会遇到“哎这里需要多一行判断”“这个颜色转换不写就没法渐变”的情况,没有余量就意味着你要回头删功能,那是最被动的状态。
1.3 语言与运行环境的选择逻辑
创意赛里90%的作品会选JavaScript+Canvas,理由非常充分:Canvas是浏览器原生的2D绘制接口,一个getElementById加上getContext('2d')就是整个渲染管线,不需要引入任何外部依赖;而且浏览器天然处理了事件监听、动画帧调度、内存回收这些脏活,你的代码可以纯粹聚焦在“创意”本身。
相比之下,Python的Pygame虽然也很好写,但需要安装依赖、管理窗口生命周期,光pygame.init()和pygame.display.set_mode()就要占3行;C语言的绘图库更是光初始化就吃掉十几行预算。如果你的作品需要发布到网页上让别人直接打开玩,JavaScript+Canvas几乎是不用思考的选择。
这里要特别提醒一点:如果你用ES6的简写语法,可以省很多行,但可读性会断崖式下降。创意赛的评分通常包含“代码美感”这一项,如果评审看不懂你的代码,技术分再高也会被扣掉表达分。我的建议是用适度的简写,保留少量注释,让代码在“短”和“可读”之间找到平衡点。
2. 核心细节解析与实操要点
2.1 粒子系统的物理模型与数学表达
整个作品的核心是一个被力场驱动的粒子系统。每个粒子有四个基础属性:位置(x,y)、速度(vx,vy)、生命周期(life)、以及派生属性——色相值(hue)。在91行的限制下,我把粒子的属性直接用数组存储,而不是定义类:
let particles = [];每帧追加新粒子时,用一个数组对象{x, y, vx, vy, life}表示,生命周期从1开始逐渐衰减到0。这个设计看起来很朴素,但它在行数上极其划算:不需要定义构造函数(省1行),不需要写this前缀(省大量行数),所有属性通过点号访问,压缩工具都不需要额外处理。
粒子运动的数学表达是整个作品的灵魂。我采用的是“涡旋力场模型”:粒子受到一个指向鼠标位置的引力,同时叠加一个与当前速度方向垂直的侧向力,形成旋转效果。计算方式如下:
let dx = mouse.x - p.x; let dy = mouse.y - p.y; let dist = Math.sqrt(dx * dx + dy * dy); let force = 0.03 / (dist * 0.01 + 1); p.vx += dx / (dist + 1) * force; p.vy += dy / (dist + 1) * force; p.vx += -dy / (dist + 1) * 0.05; p.vy += dx / (dist + 1) * 0.05; p.x += p.vx; p.y += p.vy; p.life -= 0.008;这里force的计算是防除零的关键。当鼠标恰好落在某个粒子的位置上时,dist等于0,如果直接用dx / dist就会得到无穷大。加+1是成本最低的防爆保护。侧向力-dy / (dist + 1) * 0.05让粒子沿着轨迹切线方向漂移,叠加后形成螺旋运动,视觉上看起来就像恒星周围的气体云在旋转。
我试过纯引力模型(不加侧向力),效果是粒子直线冲向鼠标然后反弹,有点像飞蛾扑火,虽然也流畅但缺乏层次感;我也试过纯涡旋模型(不加引力),粒子会绕着一个圆心转圈,看起来像龙卷风,但没法交互。最终方案的引力+侧向力叠加,是我调试了十几组系数后找到的平衡点,视觉上既有被“吸”向光标的感觉,又有绕动的轨迹冗余,观感最丰富。
2.2 颜色与视觉风格的实现技巧
创意赛作品的视觉风格往往比逻辑复杂度更能拉开差距。同样是粒子系统,有人做得像Excel散点图,有人做得像电影特效,差别全在颜色处理上。我在这个项目里用的是“生命周期色相漂移”方案:每个粒子的颜色不是固定的,而是随着生命周期的衰减,从暖色向冷色过渡。
Canvas的hsl颜色格式正好支持这种动态颜色表达,一行代码就能搞定:
let hue = (baseHue + p.life * 120) % 360; ctx.fillStyle = `hsla(${hue}, 100%, 60%, ${p.life})`;baseHue是当前的全局色相基准,会随预设形态切换而变化;hsl的饱和度固定100%,明度60%是为了保证荧光感;透明度直接用生命周期数值,粒子越接近消亡越透明,形成拖尾和淡出效果,一行代码同时处理了颜色和透明度,非常划算。
这里有个细节:hsla的透明度参数需要用模板字符串动态拼接,如果直接写hsla(hue, 100%, 60%, life)在Canvas里是无效的,必须把alpha值转成0到1之间的数字放进字符串。很多人在这一行上栽跟头,以为是颜色格式问题,其实是字符串拼接没注意。
为了让粒子看起来更“厚实”,绘制时用ctx.arc画圆,半径随生命周期衰减:
ctx.beginPath(); ctx.arc(p.x, p.y, p.life * 2 + 0.5, 0, Math.PI * 2); ctx.fill();p.life * 2让半径从2衰减到0。+ 0.5是为了保证最小可见性,否则粒子在消亡边缘会变成不可见的点,视觉上闪烁感会很明显。半径随生命周期同步缩小,粒子的“出生-活跃-消亡”过程就是“由小变大再变小”的呼吸感。
2.3 两种关键交互:鼠标引力与点击切换形态
交互是这个作品从“屏幕保护程序”升级为“创意作品”的关键。我设计了两层交互:鼠标移动产生引力场,鼠标点击切换形态预设。
鼠标移动事件一行就能搞定:
canvas.addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; });这里mouse是{x: -999, y: -999}的初始对象,初始值故意设在屏幕外,这样用户还没移动鼠标时,粒子不会起始就朝某个角落聚集,而是保持自由漂移。等到鼠标进入画面,引力场开始作用,粒子群会像被灯塔吸引的飞虫一样蜿蜒流向光标位置,视觉上会形成一个流动的光带。
点击切换形态用click事件监听:
canvas.addEventListener('click', () => { mode = (mode + 1) % presets.length; });模式索引每次点击加1,取模后循环。预设形态存储在presets数组里,每个预设包含baseHue(基础色相)、gravity(引力系数)、swirl(侧向力系数)、speed(速度衰减因子)等参数。点击循环切换时,色相基准漂移、运动模式变化,用户会有“换了一套物理规则”的体验。
这里我踩过一个坑:直接用canvas.addEventListener监听click事件后,如果在Canvas元素上点击,浏览器可能会触发默认行为(比如选中文本)。虽然不影响功能,但评审时如果发现作品被“框选”了会显得很不专业,e.preventDefault()加上就行:
canvas.addEventListener('click', e => { e.preventDefault(); mode = (mode + 1) % presets.length; });2.4 自适应屏幕与性能基线
创意赛的评审往往会在不同设备上看你的作品,如果作品在1920宽的屏幕上正常、在1440的笔记本上粒子飞出了画面,影响会非常糟糕。自适应方案其实只需要一行代码:
canvas.width = innerWidth; canvas.height = innerHeight;这里要注意的是,用innerWidth而不是window.innerWidth可以省几个字符,但可读性差一点。为了代码美感和行数控制,我会加上ctx.fillRect(0, 0, canvas.width, canvas.height)来清屏,确保粒子轨迹不会残留在画布之外的位置。
自适应的性能问题也不容忽视:全屏Canvas在高分辨率设备上每一帧要绘制上千个粒子,粒子数不加限制的话,4K屏幕上帧率会掉到30fps以下。我在代码里做了一个简单的粒子数量上限控制:
if (particles.length > particleLimit) particles.splice(0, particles.length - particleLimit);particleLimit根据屏幕面积动态计算:
let particleLimit = Math.min(window.innerWidth * window.innerHeight / 6000, 2200);这个比例是经验值:6000像素面积对应1个粒子,意味着1920×1080的画面大约有345个粒子,视觉密度适中且保持60fps;4K屏幕上限制在2200个粒子,帧率依然稳定。我测过,粒子数超过3000时,普通办公本开始出现明显掉帧,这个参数值得记下来。
另外一个容易忽略的性能点是ctx.globalCompositeOperation = 'lighter',这行代码能让粒子重叠处产生加法混色的光晕效果,视觉冲击力非常强。但代价是它会强制Canvas走混合管线,性能开销比默认的source-over高不少。所以我在性能敏感的环节(例如粒子数量多的场景)会默认关闭加法混色,等粒子数量降下来或者在全屏效果时再开。
3. 实操过程与核心环节实现
3.1 基础画布与命名空间准备
先把作品的骨架代码铺开:
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>星云粒子流</title> <style> * { margin: 0; padding: 0; } body { overflow: hidden; background: #111; } canvas { display: block; } </style> </head> <body> <canvas id="c"></canvas> <script> // ↓↓↓ 91行代码从这里开始 ↓↓↓HTML框架我控制在固定模板里,CSS部分放在<style>标签中不在JS行数内,这样可以让91行完全用于逻辑实现。值得注意的是,<canvas>要先于<script>存在,否则getElementById返回null就全盘崩溃,初学者经常栽在这。
JS部分的头几行做基础变量声明:
let canvas = document.getElementById('c'); let ctx = canvas.getContext('2d'); canvas.width = innerWidth; canvas.height = innerHeight; let mouse = { x: -999, y: -999 }; let particles = []; let mode = 0;mode变量用于索引预设形态。这里我想强调一个细节:innerWidth是全局变量,在浏览器环境直接可用;用window.innerWidth语义更清晰但多打7个字符,在91行限制下能省就省。
3.2 预设形态参数组的设计
预设形态是整个作品的创意灵魂,我用数组+对象的方式存储了三套差异明显的粒子行为参数:
let presets = [ { hue: 200, gravity: 0.03, swirl: 0.05, speed: 0.98, name: "蓝洞" }, { hue: 340, gravity: 0.05, swirl: 0.02, speed: 0.94, name: "红潮" }, { hue: 120, gravity: 0.02, swirl: 0.08, speed: 0.90, name: "绿蔓" } ];这套设计的精妙之处在于:行为差异通过参数表达,而参数本身是简单数字,代码不需要分支判断。mode索引变化时,粒子受到的重力强度、旋转强度、衰减速度、基准色相全部跟着变,用户点击一下就体验到三种截然不同的“物理世界”。
色相基准hue不是全局变量,而是每次生成新粒子时从presets[mode].hue采样。这样切换模式的瞬间,新生成的粒子会以新色相出现,旧的粒子还保留着旧色相,新旧粒子在画面中混合过渡。有渐变感的切换过程比瞬间重置的体验要高级得多。
3.3 粒子的出生与生命周期管理
粒子系统里最难处理的是“出生”这个动作。你需要不断往数组里追加新粒子,但又不能让粒子总数无限膨胀。我用固定速率追加加数量上限的模式:
function spawn() { let p = { x: mouse.x + (Math.random() - 0.5) * 100, y: mouse.y + (Math.random() - 0.5) * 100, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2, life: 1 }; particles.push(p); if (particles.length > particleLimit) particles.shift(); }初始位置在鼠标周围的正负50像素内随机散布,这样粒子不是从同一个点喷出,而是从一片区域生成,视觉上更柔和。初始速度随机,让粒子出生时就有微小的漂移,不会形成过于整齐的图案。
如果鼠标还没进入画面,mouse.x和mouse.y是-999,粒子会生成在屏幕左上角外,虽然不可见,但数组一直在增长,导致性能白白消耗。我的处理是加了一层守卫判断:
if (mouse.x > 0 && mouse.y > 0) spawn();只有当鼠标确实在画面内时才生成新粒子,避免无效计算。这个判断只需要一行,但能显著降低初始资源的空转。
3.4 完整91行代码呈现
把以上所有碎片拼在一起,加上每5行一条注释标注功能段,形成完整代码:
// 1. 初始化画布与变量 let canvas = document.getElementById('c'); let ctx = canvas.getContext('2d'); canvas.width = innerWidth; canvas.height = innerHeight; let mouse = { x: -999, y: -999 }; let particles = []; let mode = 0; // 2. 预设形态参数(三套物理规则) let presets = [ { hue: 200, gravity: 0.03, swirl: 0.05, speed: 0.98 }, { hue: 340, gravity: 0.05, swirl: 0.02, speed: 0.94 }, { hue: 120, gravity: 0.02, swirl: 0.08, speed: 0.98 } ]; // 3. 自适应屏幕 window.addEventListener('resize', () => { canvas.width = innerWidth; canvas.height = innerHeight; }); // 4. 交互事件:鼠标移动形成引力场,点击切换形态 canvas.addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; }); canvas.addEventListener('click', e => { e.preventDefault(); mode = (mode + 1) % presets.length; }); // 5. 生成粒子(初始位置在鼠标周围随机散布) function spawn() { let p = { x: mouse.x + (Math.random() - 0.5) * 100, y: mouse.y + (Math.random() - 0.5) * 100, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2, life: 1 }; particles.push(p); if (particles.length > particleLimit) particles.shift(); } // 6. 粒子更新:引力+涡旋双力场模型 function update(p) { let dx = mouse.x - p.x; let dy = mouse.y - p.y; let dist = Math.sqrt(dx * dx + dy * dy) + 1; let force = presets[mode].gravity / dist; p.vx += dx / dist * force * 100; p.vy += dy / dist * force * 100; p.vx += -dy / dist * presets[mode].swirl; p.vy += dx / dist * presets[mode].swirl; p.vx *= presets[mode].speed; p.vy *= presets[mode].speed; p.x += p.vx; p.y += p.vy; p.life -= 0.008; } // 7. 粒子绘制:生命周期驱动的色相漂移与透明度 function draw(p) { let hue = (presets[mode].hue + p.life * 120) % 360; ctx.fillStyle = `hsla(${hue}, 100%, 60%, ${p.life})`; ctx.beginPath(); ctx.arc(p.x, p.y, p.life * 2 + 0.5, 0, Math.PI * 2); ctx.fill(); } // 8. 粒子数量控制(按屏幕面积动态计算) let particleLimit = Math.min(innerWidth * innerHeight / 6000, 2200); // 9. 主循环:清屏、生成、更新、绘制 function loop() { ctx.fillStyle = 'rgba(0,0,0,0.1)'; ctx.fillRect(0, 0, canvas.width, canvas.height); if (mouse.x > 0 && mouse.y > 0) spawn(); for (let i = particles.length - 1; i >= 0; i--) { update(particles[i]); draw(particles[i]); if (particles[i].life <= 0) particles.splice(i, 1); } requestAnimationFrame(loop); } loop(); // 91行代码到这里结束数一下核心逻辑的行数,我逐行检查后确认是91行整,一行不多一行不少。这个版本同时实现了:鼠标引力场、涡旋漂移、生命周期色相漂移、透明拖尾、三种形态切换、自适应全屏、动态性能控制。如果把它放在非限制环境下写,实现相同效果大概要150行以上——91行限制强迫我砍掉了角色定义、模块化拆分、半透明缓存层等“加分项”,但保留了作品的核心体验。
这里有一个多数人不会注意到的技巧:主循环里的清屏方式不是完全清空,而是用rgba(0,0,0,0.1)画半透明黑。这样旧帧的画面不会立刻消失,而是逐渐淡出,形成拖尾残影。拖尾的效果在粒子创意作品里是神器,一行代码把视觉质感拉高了两个档次。但注意,拖尾会有累积效应,粒子运动越快拖尾越长,如果拖尾太长画面会显得脏,0.1的透明度是我调试后的平衡点,0.2太浓、0.05太淡。
3.5 清理策略:解决内存泄漏与卡顿隐患
粒子系统的老大难是数组无限增长的隐患。虽然我设置了particleLimit上限,但光靠“出生时超限就shift()”这个策略其实不够优雅。更稳的做法是在更新循环里顺手做收缩:
if (particles[i].life <= 0) { particles.splice(i, 1); continue; }life从1开始每帧减0.008,大约125帧后归零,也就是约2秒的完整生命周期。2秒的粒子生命周期配合1000ms的拖尾,刚好能形成“出生-生长-舞动-淡出”的完整视觉节奏。如果生命值减得太快,粒子还没来得及飞到鼠标附近就消失了,拖尾变成断续的碎片;减得太慢,粒子拥挤在鼠标周围形成大光斑,破坏画面层次。0.008是我反复调试后比较舒适的一个值。
shift()和splice()虽然都能收缩数组,但前者是O(n)操作,后者找到索引后也是O(n)。粒子系统里频繁splice会造成性能抖动,尤其是粒子数量在2000以上时,Chrome的GC会开始频繁触发。优化方案是把消亡的粒子标记后延迟清理,在每帧结束时统一splice一次。创意赛场景下粒子数通常维持在350到800之间,splice的性能开销可以忽略,但如果你打算在项目里扩展到上万粒子,这个优化必须做。
4. 常见问题与排查技巧实录
4.1 粒子堆积在某个角落形成亮点
这是最常见的故障现象:粒子不跟随鼠标运动,而是聚在画布左上角或右下角形成一坨亮斑。原因通常出在mouse初始值上。如果你的mouse = {x:0, y:0}且画布左上角是(0,0),粒子会自发射向左上角;如果初始值设成负坐标但没加防御判断,粒子会先涌向屏幕外,再被鼠标拉回来,产生一段混乱的过渡。
排查思路很简单:在update函数里加一行临时日志console.log(mouse.x, mouse.y),移动鼠标看数值是否在预期范围内变化。如果要修,最直接的办法是初始值设到屏幕中心或者屏幕外很远,同时加上mouse.x > 0 && mouse.y > 0的生成守卫。我发现很多参赛者栽在这个看似不起眼的初始值上,实话说这也是“评审一打开就是一头雾水”的第一大翻车点。
4.2 点击切换形态后毫无变化
有人的作品点击事件绑定后没反应,检查后发现是canvas.addEventListener监听事件时,点击发生在Canvas的透明区域但没触发。更隐蔽的问题是事件绑定在spawn()函数里,而这个函数只在鼠标进入画面后才执行,如果鼠标一直在画面外点击,元素事件根本不会被监听。
我的经验是:把事件绑定放在最外层初始化阶段,不要嵌套在任何条件判断里。另外注意mode = (mode + 1) % presets.length这行,如果你的presets数组长度是0或undefined,mode会变成NaN,后续力场计算全部失效。开发时要先在presets里放至少一个对象再跑主循环,很多人是先写mode = 0再补presets,调试时数组为空,点击就白屏。
4.3 性能崩溃:帧率从60fps掉到20fps
性能问题通常有两个来源:粒子数量失控和拖尾绘制开销。粒子数量失控的原因多半是particleLimit的值设得太大,例如直接设5000,在4K屏幕上配合arc绘制,谁也扛不住。拖尾绘制开销是因为半透明背景填充每次都会覆盖全屏,高分辨率下fillRect本身也有成本。
排查性能问题,我习惯先用performance.now()做一个简单的帧率测量:
let last = performance.now(); function loop() { let now = performance.now(); // console.log(1000 / (now - last)); last = now; // ... 原有逻辑 requestAnimationFrame(loop); }如果帧率低于45,优先检查粒子总数的峰值,再把particleLimit下调;如果帧率还是不够,把arc的绘制改成rect或者减少粒子绘制半径。另外一个性能技巧是关闭ctx.shadowBlur,这个属性特别吃性能,除非画面元素很少,否则尽量别用。
4.4 Canvas画布尺寸为0导致什么都看不见
初学者最容易踩的坑是:HTML里没写<canvas width="600" height="400">,CSS里canvas { display:block; }也没给尺寸,JS里canvas.width = innerWidth生效后,画布有了尺寸但实际渲染区域是0。因为getContext('2d')后的坐标系是画布自身的width/height,如果CSS把画布压缩成0,等于白纸一张,画的内容都在不可见区域。
调试经验是:打开浏览器F12,在Console里敲canvas.width和canvas.height,如果输出正常但画面空白,去Element面板查看Canvas元素的CSS尺寸是否被压缩。我见过很多次,作品逻辑完全正确,就是CSS一行display:block; width: 100%;把Canvas的高压成了0,整个画面消失无踪。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查顺序 | 修复建议 |
|---|---|---|---|
| 粒子不出现 | 画布尺寸为0 / spawn守卫不通过 | 1. 看Canvas元素尺寸 2. 看mouse值是否有效 | 检查HTML和CSS,确认canvas有实际尺寸且可视 |
| 粒子朝固定方向飞 | mouse初始值设置不当 | 1. 打印mouse初值 2. 检查事件绑定是否生效 | 初始值远离有效区域,或加判断排除无效值 |
| 点击无反应 | 事件绑定位置错误 / presets为空 | 1. 检查Console报错 2. 确认presets长度 | 把事件绑定放初始化最外层,优先填充presets |
| 帧率波动大 | 粒子数超限 / 拖尾绘制开销 | 1. 观察屏幕粒子密度 2. 检查particleLimit | 动态计算粒子上限,关闭shadowBlur |
| 颜色显示不对 | hsla字符串拼接错误 | 1. 打印fillStyle值 2. 检查alpha参数 | 确认hsla(${hue}, 100%, 60%, ${life})的书写格式 |
| 切换形态后卡死 | presets数组访问越界 | 1. 打印mode值 2. 检查取模逻辑 | mode = (mode + 1) % presets.length确保边界安全 |
4.6 一个隐藏的“评审友好”技巧
这是我参加多次创意赛才悟到的细节:代码里的注释文字,其实是创意表达的一部分。评审打开你的代码,第一眼看到的不是效果而是代码结构。如果注释写在每个代码块前面,且注释本身像叙事一样串起故事的逻辑,评审会更快地理解你的创作意图和算法思路,这会显著提升评分中的“表达”维度。
我写注释的格式是“序号+一句功能描述”,全篇不超过10行注释,每题一行,比如:
// 引力 + 涡旋双力场:让粒子像恒星风一样流动 // 生命周期色相漂移:从暖色渐变到冷色,模拟诞生到消散这样的注释风格,即便代码不够优雅,也会因为“一眼能看懂”而获得额外的理解分。91行代码本身就很短,其实完全不需要注释,但没有注释也会降低评审对创意的感知度——你的文字是引导评审情绪的线索。
5. 扩展:从91行到更多可能性
作品做完后我发现,91行的限制反而帮我意识到一个更重要的东西:创意不一定靠“功能多”来展现,更多靠“表现力”来打动。粒子交互这个项目,如果我给它500行,我可能会加上音频可视化、光效滤镜、多图层叠加、甚至3D投影。但91行教会我的是:在一个清晰的核心交互上做到极致,比堆砌一堆功能更有感染力。
顺着这个思路,91行代码的边界其实可以拓展到很多方向。比如做一个“文字生长动画”:用户输入文字,粒子从中心向外扩散排列成文字轮廓;或者做一个“生态模拟”:91行内实现几百个细胞的捕食与繁殖;甚至可以做“复古风指令工具”:用91行写一个交互式终端模拟器,用户输入命令能看到对应的图形反馈。
这些方向的一个共同点是:它们都绕不开“行数预算”的抽象能力。如何用函数封装相同逻辑、如何用数据驱动替代硬编码、如何用数学公式生成复杂形态,这些是短代码创意赛真正的价值。写91行代码,学到的东西能反哺到日常大型项目中——我开始写正式项目时,下意识会问自己:“这个功能真的需要一个类吗?还是用一个函数就够?”这种本能,是在普通开发节奏里很难被逼出来的。
如果你也想参与这类比赛,我的建议是先定一个你熟悉技术栈的领域,比如Canvas、P5.js或者终端动画,然后把91行当成“约束练习”,先写一个完成版(不管行数),再逐步删减和重构,直到控制在91行内。这个“删减+重构”的过程,比直接写91行要轻松得多,而且你会更清楚每一行到底承担了什么职责。
我自己的流程是:先放开写150行,然后从头到尾读一遍,找出重复代码和过度设计,用函数替换、用参数收敛、用公式替代硬编码,逐步压到100行以内。这个过程我称之为“代码的精装修”,常常会发现一批意想不到的简洁表达方式。比如“用%360来循环色相”和“用一个三元表达式来做边界处理”,这些在写长篇代码时通常不会那么在意,但在91行的压力下必须想尽办法。
另外,强烈建议在提交前做一次“读代码”演练:请一个非技术朋友(或者直接给自己录音)从第一行开始读你的代码,看看他能不能理解你每一步在做什么。如果读的时候卡住,说明代码可读性还不够,需要调整注释或变量名——即便行数已经达标,也不值得为了省1行把意思搞混。
最后再分享一个小技巧:把91行代码当成一个“一行一故事”的叙事。变量名用有画面感的词,比如life、swirl、vel,比a、b、c高级很多;关键公式旁边保留简短注释;把交互设计的逻辑写在代码开头注释里,像这样:
// “星云粒子流” — 鼠标移动吸引粒子,点击切换三种物理形态 // 核心循环:生成(x,y) → 力场更新(vx,vy) → 生命周期着色 → 拖尾绘制你会在实际参赛中发现,当你能把自己的作品用清晰、简洁的文字讲给评审听时,代码本身的感染力会放大很多倍。如果你碰到过更刁钻的行数限制、或者用91行写出了更惊艳的作品,欢迎在评论区聊聊你的设计思路,我很期待看到不同的解法。