在任意网页里"召唤"一个火柴人:一次有趣的 JavaScript Hack
有时候,写点"没什么用但很好玩"的代码,比写业务代码更能提升对浏览器底层的理解。
这次分享的是一个小脚本:
只需要把它保存为书签(Bookmarklet),点击一下,就能在当前任意网页上生成一个可操控的火柴人。
它能跳跃、移动、下落穿透平台,还能拾取道具、回血、加速、增强跳跃甚至短暂无敌。
更重要的是------它不依赖任何框架,不污染页面结构,不影响交互。 先上完整代码(压缩后)
js
javascript:(function(){const I="__stickman_anim__";if(window[I]){window[I].destroy();delete window[I];return}const c=document.createElement("canvas");c.style.cssText="position:fixed;left:0;top:0;pointer-events:none;z-index:999999";document.body.appendChild(c);const x=c.getContext("2d");function r(){c.width=innerWidth;c.height=innerHeight}r();addEventListener("resize",r);let g=.6,f=.85,ms=3,jp=-12,k={},t=0,a,dead=!1;function centerSpawn(){let cx=innerWidth/2,cy=innerHeight/2,el=document.elementFromPoint(cx,cy);if(el){let r=el.getBoundingClientRect();return{x:r.left+r.width/2-10,y:r.top+r.height/2-20}}return{x:cx-10,y:cy-20}}let sp=centerSpawn();const p={x:sp.x,y:sp.y,vx:0,vy:0,w:20,h:40,onGround:!1,dropping:!1,hp:100,maxHp:100,inv:!1},it=[],M=5;let et=0;function spawn(){if(it.length>=M||dead)return;it.push({x:Math.random()*(c.width-30),y:Math.random()*(c.height-200),s:15,t:Math.floor(Math.random()*4)})}const si=setInterval(spawn,4e3);function eff(n){et=300;n==0&&(p.hp=Math.min(p.maxHp,p.hp+30));n==1&&(ms=6);n==2&&(jp=-20);n==3&&(p.inv=!0)}function reset(){ms=3;jp=-12;p.inv=!1}function plats(){return[...document.querySelectorAll("body *")].map(e=>e.getBoundingClientRect()).filter(r=>r.width>60&&r.height>20)}function col(){p.onGround=!1;for(let r of plats())if(p.x+p.w>r.left&&p.x<r.right&&p.y+p.h>r.top&&p.y+p.h<r.top+15&&p.vy>=0&&!p.dropping){p.y=r.top-p.h;p.vy=0;p.onGround=!0}}function pick(){for(let i=it.length-1;i>=0;i--){let o=it[i];if(p.x<o.x+o.s&&p.x+p.w>o.x&&p.y<o.y+o.s&&p.y+p.h>o.y){eff(o.t);it.splice(i,1)}}}function destroy(){dead=!0;cancelAnimationFrame(a);clearInterval(si);removeEventListener("keydown",kd);removeEventListener("keyup",ku);removeEventListener("resize",r);c.remove();delete window[I]}function upd(){if(dead)return;if(!p.inv)p.hp-=.01;if(p.hp<=0)return destroy();k.ArrowLeft&&(p.vx=-ms);k.ArrowRight&&(p.vx=ms);p.vx*=f;p.vy+=g;p.x+=p.vx;p.y+=p.vy;col();pick();k.ArrowDown||(p.dropping=!1);p.y>innerHeight+200&&(p.y=-100,p.vy=0);if(et>0&&!--et)reset();t+=Math.abs(p.vx)*.2}function limb(X,Y,l,a2){x.beginPath();x.moveTo(X,Y);x.lineTo(X+Math.cos(a2)*l,Y+Math.sin(a2)*l);x.stroke()}function draw(){x.clearRect(0,0,c.width,c.height);x.lineWidth=2;for(let o of it){x.fillStyle=o.t==0?"lime":o.t==1?"orange":o.t==2?"cyan":"gold";x.fillRect(o.x,o.y,o.s,o.s)}x.font="12px monospace";x.fillStyle="black";x.fillText(Math.floor(p.hp)+" / "+p.maxHp,c.width-100,c.height-15);let px=p.x,py=p.y;x.beginPath();x.arc(px+10,py+8,6,0,2*Math.PI);x.stroke();x.beginPath();x.moveTo(px+10,py+14);x.lineTo(px+10,py+30);x.stroke();let as=0,ls=0;p.onGround?(as=Math.sin(t)*.8,ls=Math.sin(t)): (as=-.5,ls=.5);limb(px+10,py+18,12,Math.PI/2+as);limb(px+10,py+18,12,Math.PI/2-as);limb(px+10,py+30,14,Math.PI/2+ls);limb(px+10,py+30,14,Math.PI/2-ls)}function loop(){upd();draw();a=requestAnimationFrame(loop)}function kd(e){["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(e.code)&&e.preventDefault();k[e.code]=!0;if(e.code=="ArrowUp"&&p.onGround)p.vy=jp;if(e.code=="ArrowDown"&&p.onGround){p.dropping=!0;p.vy=5}}function ku(e){k[e.code]=!1}addEventListener("keydown",kd,{passive:!1});addEventListener("keyup",ku);a=requestAnimationFrame(loop);window[I]={destroy}})();
我们来拆解一下它背后的设计思路。
一、Bookmarklet:最轻量的"外挂"形式
整个脚本是一个立即执行函数:
js
javascript:(function(){ ... })();
它的运行机制是:
- 以
javascript:开头 - 作为浏览器书签保存
- 点击时在当前页面上下文执行
为了支持"再次点击销毁",脚本挂载了一个全局标记:
js
const I = "__stickman_anim__";
if (window[I]) {
window[I].destroy();
delete window[I];
return;
}
这段设计非常关键,它让这个脚本具备:
- 可重复执行
- 可优雅卸载
- 不会重复创建实例
这是写任何"注入型脚本"时都应该养成的习惯。
二、Canvas 覆盖层:不干扰页面交互的核心技巧
脚本通过创建一个全屏 canvas 作为渲染层:
js
const c = document.createElement("canvas");
c.style.cssText = `
position:fixed;
left:0;
top:0;
pointer-events:none;
z-index:999999
`;
这里有两个关键点:
1.pointer-events: none
这保证:
- 鼠标点击仍然穿透 canvas
- 不影响网页原有交互
这是实现"外挂层"体验的关键。
2.超高 z-index
确保无论什么网站,都能显示在最上层。
三、物理系统:一个极简 2D 引擎
火柴人的运动核心参数:
js
let g = 0.6; // 重力
let f = 0.85; // 摩擦
let ms = 3; // 移动速度
let jp = -12; // 跳跃初速度
每一帧的更新逻辑:
js
p.vx *= f;
p.vy += g;
p.x += p.vx;
p.y += p.vy;
这就是一个最基础的:
- 重力模拟
- 摩擦减速
- 速度积分
虽然简单,但已经足够流畅。
四、网页即平台:
js
function plats(){
return [...document.querySelectorAll("body *")]
.map(e => e.getBoundingClientRect())
.filter(r => r.width > 60 && r.height > 20)
}
把页面中所有尺寸足够大的 DOM 元素当成"平台"。
然后做简单的底部碰撞检测:
js
if (
p.x + p.w > r.left &&
p.x < r.right &&
p.y + p.h > r.top &&
p.y + p.h < r.top + 15 &&
p.vy >= 0 &&
!p.dropping
)
这意味着:
- 页面中的 div
- 卡片
- 图片
- 区块
全部变成可以踩的平台。
整个网页变成关卡。
五、道具系统:状态增强机制
每 4 秒生成一个道具:
js
setInterval(spawn, 4000);
道具类型:
| 类型 | 效果 |
|---|---|
| 0 | 回血 |
| 1 | 加速 |
| 2 | 超级跳跃 |
| 3 | 无敌 |
效果持续时间:
js
et = 300; // 帧计时
然后自动恢复默认参数。
这是一个非常典型的 Buff 状态机。
六、动画:用数学让火柴人活起来
腿部和手臂摆动基于:
js
Math.sin(t)
在地面时:
js
as = Math.sin(t) * 0.8;
ls = Math.sin(t);
在空中时:
js
as = -0.5;
ls = 0.5;
通过正弦函数驱动摆动,几乎零成本实现动态感。
但其实逐帧图片动画效果可能更好。
七、生命周期管理:
destroy 方法做了完整清理:
- cancelAnimationFrame
- clearInterval
- removeEventListener
- remove canvas
- 删除 window 挂载
八、为什么这个小玩意很有价值?
它锻炼了:
- DOM API 熟练度
- 物理运动理解
- 碰撞检测思路
- 游戏循环结构
- 浏览器渲染机制
- 事件管理与清理
九、如果要继续进阶
可以考虑增加一些丰富的功能:
- 攻击系统
- 敌人 AI
- 音效
- 粒子爆炸效果
- 真正的碰撞分离算法
- 使用 QuadTree 优化平台检测
- 使用 requestIdleCallback 优化扫描频率
十、总结
这段代码本质上做了三件事:
- 创建一个不影响页面的渲染层
- 把 DOM 元素当作游戏世界
- 用最小物理模型驱动角色运动
下班。