客户端
游戏
无障碍

14

评论

22

13

手机看

微信扫一扫,随时随地看

240个标签页打乒乓?开发者用Chrome浏览器“整活”,网友:再玩内存要炸了!

AI划重点 · 全文约8058字,阅读需23分钟

1.开发者Nolen Royalty将240个未关闭的标签页用于运行乒乓球游戏《Pong》,实现了前所未有的视觉效果。

2.通过使用AppleScript工具,Nolen在macOS系统的Chrome浏览器上实现了这一创新。

3.为了保持标签页的同步更新,Nolen采用了BroadcastChannel技术,让所有标签页在后台协同工作。

4.经过优化,Nolen成功提升了游戏性能,使其在标签页上运行更加流畅。

5.除了《Pong》游戏,Nolen还计划在未来为这个项目添加更多有趣的功能。

以上内容由腾讯混元大模型生成,仅供参考

编译 | 苏宓
出品 | CSDN(ID:CSDNnews)

在浏览器网页里面玩游戏不稀奇,但用没关闭的浏览器标签页来玩,那可就太少见了!

最近,有位极具想象力的开发者 Nolen Royalty 看着自己一堆没关的标签页,觉得它们不仅“扎眼”,还占了太多屏幕空间。于是,秉持着程序员“能动手就绝不忍着”的精神,他在这堆没有关闭的标签页上复现了一个乒乓球游戏《Pong》,而且是 240 个标签页一起玩!

不仅如此,Nolen Royalty 还直接在 GitHub 上开源了他的相关实现代码,让这些标签页真正“有用”起来:https://github.com/nolenroyalty/faviconic。需要注意的是,因为其在开发过程中使用了一款 AppleScript 工具,这是苹果系统独有的,所以这个游戏只能在 macOS 系统下的 Chrome 浏览器上抢先体验~

正如下面视频所示,Nolen 把 240 个浏览器标签页排成了一个 8x30 的紧密网格,然后让它们一起运行《Pong》!游戏的球和挡板可以顺畅地在前端窗口的画布和所有标签页之间移动。

Thumbplayer Poster Plugin Image
播放
下一个
打开循环播放
00:00
/
00:00
倍速
3.0X
2.0X
1.5X
1.25X
1.0X
0.75X
0.5X
语言
多音轨
AirPlay
0
静音播放中,点击 恢复音量
画中画
网页全屏
全屏
error-background
你可以 刷新 试试
视频信息
1.33.6
播放信息 上传日志
视频ID
VID
-
播放流水
Flowid
-
播放内核
Kernel
-
显示器信息
Res
-
帧数
-
缓冲健康度
-
网络活动
net
-
视频分辨率
-
编码
Codec
-
mystery
mystery
-

按住画面移动小窗

X

那么他是如何做到的,我们将通过其发布的博文一探究竟。


图片

灵感来源

Nolen 称,这个项目的灵感来自他的朋友 Tru。同样身为开发者的 Tru 于上周用浏览器的小图标(favicon)做了一个能运行《Flappy Bird》的版本,叫做 Flappy Favi(https://mewtru.com/flappyfavi)。

注:favicon 是网站的小图标,通常出现在浏览器标签页的左上角。它的作用是帮助用户快速识别网站,通常是一个 16x16 或 32x32 像素的小图像,比如网站的 logo 或代表该网站的符号。每个网站都有自己独特的 favicon,用来提升用户体验,让浏览网页时更具辨识度。

图片

把 FlappyFavi 放在标签页上玩,也是众人没想到的,不过,虽然这个很有创意,但是由于 favicon 实在太小了,游戏画面几乎看不清,玩起来简直是在挑战眼力的极限。

Thumbplayer Poster Plugin Image
播放
下一个
打开循环播放
00:00
/
00:00
倍速
3.0X
2.0X
1.5X
1.25X
1.0X
0.75X
0.5X
语言
多音轨
AirPlay
0
静音播放中,点击 恢复音量
画中画
网页全屏
全屏
error-background
你可以 刷新 试试
视频信息
1.33.6
播放信息 上传日志
视频ID
VID
-
播放流水
Flowid
-
播放内核
Kernel
-
显示器信息
Res
-
帧数
-
缓冲健康度
-
网络活动
net
-
视频分辨率
-
编码
Codec
-
mystery
mystery
-

按住画面移动小窗

X

于是,Nolen 想能不能有个更好方法解决这个问题?

当然最为直接的方式就是让界面变大一些!Nolen 所能想到的最佳方案,就是把画面拆分到多个标签页上显示。但这带来了几个难题:

1. 怎么创建一个整齐的标签页网格来展示画面?

2. 这些标签页在后台时,如何保持更新?

3. 这些标签页如何协调同步?


图片

试验过程

针对上述的第一个问题,即如何创建标签页网格。Nolen 最开始的方法很简单,首先打开 Chrome 浏览器,疯狂点“新建标签页”按钮,直到标签页变得足够小。最终,它看起来像这样:

图片

一排小小的标签页,favicon 形成了一个网格

其实 Nolen 只需要再打开一个 Chrome 窗口,调整好位置,就能再增加一行标签页。

不过,Nolen 的最终目标是创建一个超大的标签页网格,当前这种手动操作起来太麻烦了。所以,他决定借助一款工具——AppleScript 来帮助实现。AppleScript 是 macOS 上的一款强大的工具,能用接近英语的语法控制程序。虽然它的语法有点冗长,写起来像是“加了很多废话的 Python”,但在这种情况下,它非常管用。

Nolen 写了一个脚本,让它自动打开 8 个 Chrome 窗口,每个窗口里有 30 个标签页,而且还会把这些窗口精确地叠放在一起,形成一个整齐的标签页网格,就像这样:

Thumbplayer Poster Plugin Image
播放
下一个
打开循环播放
00:00
/
00:00
倍速
3.0X
2.0X
1.5X
1.25X
1.0X
0.75X
0.5X
语言
多音轨
AirPlay
0
静音播放中,点击 恢复音量
画中画
网页全屏
全屏
error-background
你可以 刷新 试试
视频信息
1.33.6
播放信息 上传日志
视频ID
VID
-
播放流水
Flowid
-
播放内核
Kernel
-
显示器信息
Res
-
帧数
-
缓冲健康度
-
网络活动
net
-
视频分辨率
-
编码
Codec
-
mystery
mystery
-

按住画面移动小窗

X

在尝试过程中,Nolen 也遇到了一些烦人的小问题,比如 Chrome 会尝试恢复上次关闭的标签页,所以脚本需要在一开始时就得清除这些标签页。

不过,最终代码其实挺简单的。核心部分是这样的:

-- Set the window bounds (x, y, width, height)set bounds of newWindow to {x, y, x + width, y + height}global tabCountset tabCount to 0
tell newWindow set URL of active tab to baseURL & "windowIndex=" & windowNum & "&tabIndex=" & tabCountend tell
set tabCount to (tabCount + 1)
-- Create the specified number of tabsrepeat (numTabs - 1) times tell newWindow if windowNum is (maxWindows - 1) and tabCount is (numTabs - 1) then make new tab with properties {URL:baseUrl & "windowIndex=" & windowNum & "&tabIndex=" & tabCount & "&isMain=true&numWindows=" & maxWindows & "&numTabs=" & numTabs & "&fullWidth=" & fullWidth} else make new tab with properties {URL:baseURL & "windowIndex=" & windowNum & "&tabIndex=" & tabCount} end if end tell set tabCount to (tabCount + 1)end repeat

如何快速更新 favicon(网站小图标)

接下来的问题是:怎么更新 favicon?

通常,浏览器会从某些固定的 URL 读取 favicon,但我们也可以在 HTML 代码的 <head> 里指定一个 favicon 位置。只要更新这个元素,浏览器就会刷新图标。FlappyFavi 就是靠这个原理实现的。

“根据我的观察,Chrome 大约每秒能刷新 favicon 4 次”,Nolen 说道。

但他似乎也不确定这个方法在后台标签页里还能不能用。为了节省资源,浏览器会对后台标签页做限制,而 Nolen 的大部分标签页都是在后台!

为此,他用了一个简单的测试方法——每 250 毫秒更新一次 favicon,看看后台标签页的表现。结果发现:后台标签页的更新频率被降到了大约 1 次/秒!

图片

后台标签页太慢了!

Nolen 发现,他的 setInterval 定时器被浏览器限制了!于是,他开始思考有没有办法绕过这个限制。

他的第一个想法是利用 Web Audio API(https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)。因为音频在后台依然可以流畅播放,而且可以绑定一些回调函数。Nolen 尝试播放一个人耳听不到的音频,然后把更新代码放进音频线程里,希望能骗过浏览器。但失败了。

然后他试了 Web Worker(https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers)。Web Worker 允许开发者把计算任务放到浏览器的后台线程里运行,这样就不会影响页面渲染。起初,Nolen 猜测 Web Worker 可能不会被限制得那么严重。

于是,他把定时器代码移到了 Web Worker 里,让它定时向主页面发送消息,触发 favicon 更新。结果——成功了!

图片

所有标签页的 favicon 都能同步更新了!

这部分的代码有些长,但逻辑很简单:Web Worker 生成一组 emoji,把它们转换成数据 URL,并传回主页面更新 favicon。

Web Worker 图标代码:

// worker
let intervalId = null;let counter = 0;const emojis = ["🌞", "🌜", "⭐", "🌎", "🚀"];let currentIndex = 0;
function drawEmoji(emoji) { // Create an OffscreenCanvas (supported in workers) const canvas = new OffscreenCanvas(32, 32); const ctx = canvas.getContext("2d");
ctx.font = "28px serif"; ctx.fillText(emoji, 2, 24);
// Convert to blob and send back canvas.convertToBlob().then((blob) => { const reader = new FileReader(); reader.onloadend = () => { counter++; postMessage({ type: "update", dataUrl: reader.result, counter: counter, }); }; reader.readAsDataURL(blob); });}
self.onmessage = function (e) { if (e.data.command === "start") { const interval = e.data.interval; if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => { drawEmoji(emojis[currentIndex]); currentIndex = (currentIndex + 1) % emojis.length; }, interval);
// Initial update drawEmoji(emojis[currentIndex]); } else if (e.data.command === "stop") { if (intervalId) { clearInterval(intervalId); intervalId = null; } }};
// main documentworker.onmessage = function (e) { if (e.data.type === "update") { let link = document.querySelector("link[rel*='icon']") || document.createElement("link"); link.type = "image/x-icon"; link.rel = "shortcut icon"; link.href = e.data.dataUrl; document.head.appendChild(link); }};

如何让所有标签页同步?

但问题来了:如果要让所有标签页配合一起运行,我们需要它们互相通信。

这涉及两个问题:

  • 每个标签页怎么知道自己的位置?(比如:我是第二个窗口的第三个标签页。)

  • 标签页之间用什么方式通信?

第一个问题比较简单。此前,Nolen 在 AppleScript 代码里已经做了处理——脚本会把窗口编号和标签页编号作为 URL 参数传进去。这样,每个标签页只需要解析自己的 URL,就能知道自己的 X 和 Y 坐标。

针对第二个问题,Nolen 最初的想法是使用 WebSocket,让所有标签页连接到一个服务器,由服务器统一指挥它们该做什么。

为此,他做了个简单的概念验证。加载时,每个标签页在 Web Worker 里创建 WebSocket 连接,然后服务器给每个标签页发送数据,告诉它更新 favicon。服务器会区分奇数和偶数编号的标签页,让它们显示不同的图像。

图片

这基本可行,但有两个问题:

1. 就 Nolen 个人而言,他不想用服务器!他更希望这个方案可以在任何浏览器里运行,而不需要额外搭建服务器。

2. 同步问题:由于每个标签页的加载速度不同,导致它们的更新不同步。

为了处理第一个问题,Nolen 选择使用 BroadcastChannel 让所有标签页同步——这是一种在同一域名下在不同标签页之间分发信息的方式。与 WebSockets 不同,后者是点对点通信,而  BroadcastChannel 是“一对多”的方式。对 Nolen 来说,这似乎更符合他的需求。

至于第二个问题,Nolen 让后台标签页通过 BroadcastChannel 发送注册消息,消息中包含它们的标签和窗口索引。主标签页(即前台标签页,未被限制)则监听这些消息,并在收到消息后发送确认,之后后台标签页就不再尝试注册。等到主标签页接收到所有后台标签页的注册事件后,动画才开始运行。

这样,所有标签页都能同时更新 favicon,不会有不同步的问题了!

// workerbc = new BroadcastChannel("bc");bc.addEventListener("message", (event) => {  const msg = event.data;  if (!msg) return;  else if (    msg.type === "ack" &&    msg.tabIndex === tabIndex &&    msg.windowIndex === windowIndex  ) {    clearInterval(regInterval);    registrationDone = true;    postMessage({ type: "registration-ack" });  }});
regInterval = setInterval(() => { bc.postMessage({ type: "register", tabIndex, windowIndex });}, 1000);
// main tabconst bc = new BroadcastChannel("bc");const registrations = {};if (data && data.type === "register") { const key = `tab_${data.tabIndex}_${data.windowIndex}`; console.log(`Registered: ${key}`); registrations[key] = true; bc.postMessage({ type: "ack", tabIndex: data.tabIndex, windowIndex: data.windowIndex, });
const expected = numTabs * numWindows; if (Object.keys(registrations).length === expected) { console.log("All tabs registered. Beginning..."); runLoopGeneric({ bc, worker, numTabs, numWindows, fullWidth, impl: "pong", }); }}

图片

从 Canvas 到标签页

当 Nolen 成功控制所有标签页后,他开始思考:到底应该在这些标签页上显示什么内容?

他觉得,如果能在主标签页(前台窗口)里绘制一个图形,并让它从主窗口“移动”到标签栏上,那将会非常酷!

于是,他决定从一个简单的矩形开始做实验……

图片

为了实现这个目标,他需要想象有一个“画布”(canvas),它从前台窗口延伸到上面的所有标签页图标(favicon),然后根据物体的位置,在这些图标和主画布上同时进行绘制。

在做这个尝试时,Nolen 花了很多时间进行测量。

图片

例如,他发现从 Chrome 窗口的左侧到第一个 favicon 之间的距离是 92px(至少在这种标签页数量下是这样)。从 favicon 底部到窗口顶部的距离是 58 px。favicon 的大小是 16x16 像素,类似这样的测量数据,还有很多……

他的代码利用这些测量值,以及当前打开的窗口和标签页数量,以此来:

  • 计算画布的宽度,使其能够与上方的 favicons 完美对齐。

  • 计算每个标签页的宽度。

  • 确定整个“画布”的宽度和高度,包括 favicon 区域、地址栏以及窗口内部的实际画布。

图片

然后,他模拟一个矩形在整个“画布”上移动。当矩形位于 URL 栏以下时,他在“真实画布”上绘制它。同时,他计算出矩形在 URL 栏以上的部分,并将这些信息广播给其他标签页。每个标签页根据之前计算的像素坐标,更新自己的 favicon,改变对应的像素为黑色或白色,以同步动画效果。

矩形的代码:

// main tabfunction transmitSquareCoords() {  const copied = { ...square };  updateMap = {};  for (let t = 0; t < numTabs - 1; t++) {    for (let w = 0; w < numWindows; w++) {      pixels = [];      const PIXEL_COUNT = 4;      const FAVICON_SIZE = 16;      const MULT = FAVICON_SIZE / PIXEL_COUNT;      for (let yy = 0; yy < PIXEL_COUNT; yy++) {        for (let xx = 0; xx < PIXEL_COUNT; xx++) {          let x = tabSingle * t + (tabSingle - 16) / 2 + xx * MULT;          let y = TOP_TO_FAVICON + HARDCODED_WINDOW_DIFF * w + yy * MULT;          let thisSquare = {            x,            y,            w: MULT,            h: MULT,          };          if (intersects(thisSquare, copied)) {            pixels.push(1);          } else {            pixels.push(0);          }        }      }      const key = `tab_${t}_${w}`;      updateMap[key] = pixels;    }  }  bc.postMessage({ type: "update", pixels: updateMap });}
// background tabfunction updateFavicon(pixels) { const canvas = document.createElement("canvas"); canvas.width = 4; canvas.height = 4; const ctx = canvas.getContext("2d"); for (let i = 0; i < 16; i++) { const x = i % 4, y = Math.floor(i / 4); ctx.fillStyle = pixels[i] ? "#000" : "#fff"; ctx.fillRect(x, y, 1, 1); } const faviconURL = canvas.toDataURL("image/png"); let link = document.querySelector("link[rel='icon']"); if (!link) { link = document.createElement("link"); link.rel = "icon"; document.head.appendChild(link); } link.href = faviconURL;}

提升性能

这个方案基本能跑起来,但它的资源占用超乎预期——动画在画布上时不时会卡顿,它是用 requestAnimationFrame 来绘制的,按理说应该是很平滑的。

卡顿意味着前台标签页的线程占用了太多资源。Nolen 检查了一下,发现前台页面的逻辑并不复杂,所以他猜测可能是数据传输的问题。

最开始,他的主线程会计算每个 favicon 像素的状态,然后把完整的数据广播给所有标签页。这样可能会导致大量的复制操作——每个标签页都得单独拷贝数据,这可能会带来很大的负担。虽然他对此有所怀疑,但也没有更好的猜测。

于是,他修改了代码,直接广播方块的位置,让每个标签页自己计算是否需要更新自己的 favicon。但是效果并没有改善,问题依然存在。

最后,他决定采用经典的调试方法,逐步禁用代码,看看是哪一部分导致了卡顿。最终,他发现问题出在了生成 favicon 上:他的代码每秒钟生成上百个 favicon,导致性能大幅下降。

基本上,Nolen 编写的代码会在每个标签页里做了类似这样的事情,用来创建一个 4x4 的黑白图像,并将其转化为一个 URL,以便指向 favicon。

const ctx = bwCanvas.getContext("2d");for (let i = 0; i < len; i++) {  const x = i % width;  const y = Math.floor(i / width);  const index = (y * width + x) * 4;  ctx.fillStyle = pixels[i] ? BLACK : WHITE;  ctx.fillRect(x, y, 1, 1);}return bwCanvas.toDataURL("image/png");

这段代码会在每一帧重新执行,无论画布是否发生变化。结果就是,每秒钟有上百个标签页都在重新生成几乎相同的白色小方块 favicon,导致 CPU 占用率过高。

于是,他修改了代码,只有当 favicon 实际发生变化时才更新,性能也立刻得到了大幅提升。

我更新了代码,只在 favicon 发生变化时才更新,这样性能得到了显著提升。

图片

图片

做点有趣的东西

优化完动画后,Nolen 把代码整理了一下,相当于做了一个小型的“引擎”,然后开始琢磨着可以用它做点什么有趣的东西。

最开始,Nolen 想到了贪吃蛇(Snake)。因为贪吃蛇的格子化运动跟 favicon 的像素特点很搭,而且游戏逻辑也不复杂,于是他很快就写了一个简单的版本。

但很快,Nolen 遇到一个问题——你可能也能猜到。

图片

贪吃蛇的格子式移动方式太“离散”了。之前那个矩形动画从“连续”的画布平滑移动到“离散”的 favicon 逐步点亮,看起来很自然。而贪吃蛇本身就是基于固定格子的,格子里移动的感觉让整个游戏看起来有点不对劲。

于是,Nolen 再想了想,最后决定做个《Pong》游戏(乒乓球)。因为《Pong》里的球和挡板需要在画布和标签页之间来回移动,这种视觉效果应该挺酷的。

实现 Pong 游戏

然后,Nolen 就做了个 Pong 游戏!

这个过程其实没什么复杂的,因为他已经很熟悉做游戏的流程,而且《Pong》本身也挺简单,特别是当他已经有了一套成熟的绘制 API。

不过,Nolen 在博客中还是提到了一些实现的细节:

  • 电脑玩家(右边的挡板)会努力让自己的挡板中心对齐球的中心。

  • 他用了一些简单的三角函数来计算球反弹的角度,让球的反弹不像真实物理那样死板,而是有点变化,这样游戏就不会变得太无聊。

  • 他又写了一次“判断两个方块是否相交”的函数——估计这是他第 20 次写这种逻辑了。

  • 他还对球的移动做了平滑处理,并加了个拖尾效果,试图让球看起来更流畅、更有动感。

Nolen 还开玩笑说:“说实话,我盯着这个拖尾效果看了太久,现在搞不清楚它到底是酷还是丑了……”

Thumbplayer Poster Plugin Image
播放
下一个
打开循环播放
00:00
/
00:00
倍速
3.0X
2.0X
1.5X
1.25X
1.0X
0.75X
0.5X
语言
多音轨
AirPlay
0
静音播放中,点击 恢复音量
画中画
网页全屏
全屏
error-background
你可以 刷新 试试
视频信息
1.33.6
播放信息 上传日志
视频ID
VID
-
播放流水
Flowid
-
播放内核
Kernel
-
显示器信息
Res
-
帧数
-
缓冲健康度
-
网络活动
net
-
视频分辨率
-
编码
Codec
-
mystery
mystery
-

按住画面移动小窗

X

时下这部分代码已经开源了,但 Nolen 也坦言:“说实话,代码质量一般,因为一直没从‘原型模式’中脱离,没做什么精细优化。如果你感兴趣,可以去 GitHub 看看(https://github.com/nolenroyalty/faviconic),但别嫌弃代码写得乱。”

在 Nolen 的创意发布后,大家都纷纷表示:“一如既往的令人印象深刻!”不仅是因为他成功把标签页变成了一个可以玩游戏的地方,更因为这个项目展现了他新奇的创意。当然,还有不少人开玩笑说:“内存:游戏很好,下次别再玩了!”

来源:

https://eieio.games/blog/running-pong-in-240-browser-tabs/#toc:from-canvas-to-tab-bar

https://news.ycombinator.com/item?id=43119086

图片

免责声明:本内容来自腾讯平台创作者,不代表腾讯新闻或腾讯网的观点和立场。
举报
评论 0文明上网理性发言,请遵守《新闻评论服务协议》
请先登录后发表评论~
查看全部0条评论
首页
刷新
反馈
顶部