木地板背景上摆着一张绿色台球桌,六个球袋、一颗白球和摆成三角的十五颗彩球。按住白球往反方向拖,会拉出一根指示方向和力度的瞄准杆——拉得越远杆越长,松手球就沿那个方向冲出去。球会一路减速滑行,撞到桌边按入射角等于反射角弹回,撞到别的球则把一部分动能传过去,被撞的球随之滚动。任何球滚到球袋附近就算落袋,自动从台面移到下方的球袋区。点“重新开局”,所有球回到初始位置。
它有意思的地方在于:这里没有物理引擎。每颗球只是一个绝对定位的 div,位置、角度、速度全靠几行三角函数在动画循环里推。副标题“三角函数有点难”几乎是作者的自嘲,但也正好说明了这份源码的价值——它是一份看得懂的、最小可用的二维碰撞实现。
这张台球桌的四个组成
瞄准杆是实时算出来的角度与长度。鼠标按下后跟随移动,用反正切求出鼠标与白球连线的角度赋给旋转,用两点距离求出力度并限幅到一个上限,松手时把这两个值直接存进球的数据里,动画循环下一帧就开始搬运它。
移动是极坐标转直角坐标。每帧用角度的余弦与正弦分别乘以速度,得到横纵位移量累加到当前位置,同时速度按固定值递减,模拟摩擦。
撞库用反射公式。一个通用的反射函数接收入射角与台面角度,上下库按 0 度、左右库按 90 度调用,同时把越界的位置夹回边界内,避免球卡在墙里。
球球碰撞按圆心距判定。每帧两两比较圆心距离,小于两倍半径即视为相撞,按连线方向分配速度:撞人的球留下小部分动能并反弹,被撞的球拿走大部分并沿连线方向滚开。
三段核心计算
其一,角度换算要注意坐标系方向。网页的纵轴向下为正,与数学里的直角坐标相反,所以源码里的反正切写成了这样:
return (Math.atan2(y1 - y2, x1 - x2) * 180) / Math.PI + 180;
加 180 度是为了把结果从“指向鼠标”翻转成“背离鼠标”,正好对应台球里“往后拉、向前打”的直觉。这一步搞反的话,球会朝着鼠标的反方向飞出去。
其二,碰撞后把位置退回上一帧。检测到相撞时,脚本不是让球停在重叠位置,而是把坐标还原成移动前的值再改变角度。这么做是为了避免两颗球在重叠状态下连续判定相撞、互相锁死——这是自己写碰撞时最容易遇到的问题,处理办法要么退回位置,要么加一个碰撞冷却,源码选了前者。
其三,动能分配是简化的。真实台球的碰撞要按连线方向分解速度分量,这里直接按固定比例把速度分给两颗球(撞人的留小部分、被撞的拿大部分),角度则用反射函数近似。这是精度与代码量之间的取舍:观感上足够像,代码只有十几行,非常适合作为学习起点,也方便按需替换成更严谨的弹性碰撞公式。
拿到源码后能改什么
手感参数集中在几处字面量:每帧的速度衰减值(越大摩擦越强、球停得越快)、拉杆力度的上限、碰撞后的动能分配比例。想做成“冰面”就把衰减调到很小,想做成“地毯”则调大。
台面尺寸、球的直径、球袋位置都在样式里,注意脚本中判定用的半径与球袋容差是写死的数值,改球径时需要同步调整这几处,否则会出现球穿过袋口或提前落袋。
要扩展成完整游戏也有清晰的切入点:落袋时脚本把球移进球袋容器,在那里加计分与犯规判断即可;白球落袋后可以自动复位;再加一个回合切换,就是一个可玩的两人对战。彩球的花色是纯 CSS 画的编号球,改配色不需要图片。
运行提示:页面依赖 jQuery(从 CDN 加载),离线环境请先下载到本地。台面与地板使用了本地纹理图,建议用本地服务器打开。当前的瞄准交互基于鼠标事件,触摸设备需要改用指针事件才能操作。建议使用近两年版本的 Chrome、Edge、Firefox 或 Safari。详细运行方法请参见压缩包内的帮助文档。
关于这个网页台球的常见问题
手机上能玩吗?
当前监听的是鼠标按下、移动、抬起三个事件,触摸屏不会触发。改成指针事件即可支持触摸,改动只有三处事件名,其余逻辑不用动。
球有时候会重叠或卡在一起?
这是简化碰撞的典型表现,尤其在开球时多球密集的情况下。可以把碰撞后退回位置改成“沿连线方向推开一个最小间距”,或者给每对球加一个短暂的碰撞冷却。
可以做成斯诺克或桌球九球吗?
可以。球的数量、摆位、花色都在标签与样式里,改成对应的排列即可;规则部分需要自己在落袋回调里补上判定逻辑。
能加上球杆和音效吗?
能。瞄准杆已经是一个独立元素,替换成球杆造型即可;音效在撞库与撞球两处判定里各触发一次短音频,注意加冷却避免密集碰撞时声音重叠。
它适合用在什么地方
作为可玩的小游戏,它适合品牌活动页的互动环节、H5 抽奖的前置玩法、体育或桌游站点的趣味模块;作为教学素材,它是一份很好的“不用引擎写二维碰撞”的完整范例。角度换算、反射公式、碰撞判定与落袋逻辑的完整实现都在源码里,代码不到两百行,下载后可查看。