1 điểm bởi GN⁺ 2025-02-22 | 1 bình luận | Chia sẻ qua WhatsApp
  • Triển khai một thử nghiệm Pong tận dụng khoảng trống trên thanh tab chưa đóng như một màn hình, dùng favicon của 240 tab Chrome làm pixel
  • Lưới tab 8×30 được tạo bằng AppleScript, và mỗi tab tính vị trí của mình bằng windowIndextabIndex trong query URL
  • setInterval ở các tab nền bị chậm xuống khoảng 1 giây một lần, nhưng khi chuyển timer sang Web Worker thì việc cập nhật favicon trở nên đủ nhanh
  • Thay vì WebSocket phía server, dùng Broadcast Channel API để đồng bộ các tab cùng domain; sau khi tất cả tab đăng ký xong, tab main bắt đầu animation
  • Nút thắt lớn nhất là việc tạo lại hàng trăm favicon ở mỗi frame; khi chỉ cập nhật những tab thực sự thay đổi, animation mượt hơn nhiều

Cách dùng 240 tab như một màn hình duy nhất

  • Dự án là một thử nghiệm chạy Pong bằng cách tận dụng không gian bị chiếm bởi các tab trình duyệt chưa đóng
  • Màn hình được cấu thành từ lưới tab 8×30, gồm 8 cửa sổ Chrome và 30 tab cho mỗi cửa sổ
  • Quả bóng và các paddle di chuyển liền mạch giữa HTML canvas của cửa sổ phía trước và vùng favicon trên thanh tab phía trên
  • Mã nguồn được công khai trên GitHub tại faviconic, kèm ghi chú rằng đây là code “awful” ở trạng thái prototype

Ý tưởng bắt đầu từ Flappy Favi

  • Nguồn cảm hứng đến từ Flappy Favi, phiên bản Flappy Bird chạy bên trong favicon do Tru tạo ra
  • Favicon là hình ảnh nhỏ hiển thị trên thanh tab trình duyệt, nhưng nếu chỉ có một favicon thì quá nhỏ và khó nhìn
  • Để vẽ hình ảnh trên nhiều tab, trước hết cần giải quyết ba vấn đề
    • Cách sắp xếp các tab thành một lưới dễ nhìn
    • Cách cập nhật favicon nhanh ngay cả ở tab nền
    • Cách đồng bộ màn hình của nhiều tab với nhau

Tạo lưới tab Chrome bằng AppleScript

  • Ban đầu, tác giả kiểm chứng khả năng bằng cách liên tục bấm nút tab mới của Chrome để làm các tab nhỏ lại, rồi căn cửa sổ thứ hai để tạo hàng thứ hai
  • Việc tạo một lưới lớn thủ công khá phiền phức nên được tự động hóa bằng AppleScript
  • Script mở 8 cửa sổ Chrome, tạo 30 tab trong mỗi cửa sổ, và đặt vị trí các cửa sổ sao cho chúng chồng lên nhau
  • Chrome có vấn đề cố mở lại các tab đã đóng, nên cần dọn phần này khi script bắt đầu
  • URL của mỗi tab chứa windowIndextabIndex
    • Tab được mở cuối cùng có thêm các tham số như isMain=true, numWindows, numTabs, fullWidth

Cập nhật favicon nhanh trong tab nền

  • Trình duyệt sẽ tìm URL favicon mặc định, nhưng có thể đổi icon bằng cách thêm vào HTML head một phần tử chỉ vị trí favicon rồi cập nhật phần tử đó
  • Tốc độ cập nhật favicon của Chrome theo cảm nhận là khoảng 4 lần mỗi giây
  • Ở tab nền, việc dùng tài nguyên bị hạn chế, nên cả một vòng lặp nhỏ đổi favicon mỗi 250 ms cũng chỉ chạy khoảng 1 lần mỗi giây
  • Tác giả đã thử dùng callback âm thanh nền của Web Audio API như một cách đi đường vòng, nhưng không làm cho nó hoạt động đúng được
  • Sau đó, khi chuyển timer sang Web Worker và để worker gửi message tới main document để cập nhật favicon, vấn đề được giải quyết
  • Cấu trúc ví dụ là worker vẽ emoji lên OffscreenCanvas, chuyển thành data URL, rồi gửi cho main document
    • Main document thay đổi href của link rel="shortcut icon" để thực sự cập nhật favicon

Xử lý đăng ký và đồng bộ tab bằng Broadcast Channel

  • Giao tiếp giữa các tab có hai bài toán
    • Mỗi tab phải biết mình là tab thứ mấy trong cửa sổ thứ mấy
    • Phải quyết định dùng kênh giao tiếp nào để truyền các cập nhật tab
  • Việc xác định vị trí được giải quyết bằng cách đọc windowIndextabIndex từ các query parameter trong URL do AppleScript thêm vào
  • Proof of concept ban đầu có cấu trúc mỗi tab kết nối tới một server WebSocket, còn server gửi hình ảnh xuống
  • Cách dùng WebSocket không phù hợp lắm với mục tiêu của dự án
    • Nó xung đột với lý do mang tính thẩm mỹ: muốn triển khai trong các tab trình duyệt trên toàn thế giới mà không cần server
    • Thời điểm kết nối của mỗi tab khác nhau, nên các cập nhật không được đồng bộ
  • Sau đó dự án chuyển sang Broadcast Channel API, dùng để truyền thông tin tới nhiều tab cùng domain
  • Tab nền gửi message registration chứa chỉ số tab/cửa sổ của nó qua broadcast channel, còn tab main nhận message này và trả lại ack
  • Tab main bắt đầu animation sau khi nhận đăng ký từ tất cả các tab nền

Căn chỉnh canvas với favicon

  • Để vật thể vẽ trong cửa sổ phía trước trông như “đi vào” thanh tab, canvas thật và các favicon phía trên được xem như một canvas mở rộng duy nhất
  • Việc triển khai cần trực tiếp đo kích thước thực tế của cửa sổ Chrome
    • 92 pixel từ mép trái cửa sổ Chrome đến favicon đầu tiên
    • 58 pixel từ mép dưới favicon đến mép trên cửa sổ thực tế
    • Kích thước favicon là 16×16
  • Dùng các số đo này cùng với số tab/cửa sổ để tính ba giá trị
    • Chiều rộng canvas sẽ hiển thị sao cho căn với favicon
    • Chiều rộng của mỗi tab
    • Chiều rộng và chiều cao của toàn bộ “canvas”, bao gồm hàng favicon, thanh URL và canvas thật
  • Nếu hình chữ nhật nằm dưới thanh URL thì vẽ lên canvas thật; nếu nằm phía trên thanh URL thì broadcast thông tin vùng tương ứng tới các tab khác
  • Mỗi tab tính tọa độ pixel của mình rồi cập nhật favicon thành pixel trắng/đen

Nút thắt nằm ở việc tạo favicon, không phải truyền dữ liệu

  • Bản triển khai ban đầu chạy được, nhưng animation trên canvas bị khựng và không mượt ngay cả khi vẽ trong requestAnimationFrame
  • Lúc đầu tác giả nghi ngờ cấu trúc main thread tính toàn bộ trạng thái pixel favicon, đưa vào broadcast channel, rồi hàng trăm tab đọc dữ liệu đó đã sao chép quá nhiều dữ liệu
  • Tác giả chuyển sang chỉ broadcast vị trí của hình chữ nhật và để mỗi tab tự tính xem có giao nhau hay không, nhưng hiệu năng không cải thiện
  • Vấn đề thật sự là mỗi tab tạo lại ảnh favicon đen trắng 4×4 ở mỗi frame và sinh URL bằng toDataURL("image/png")
  • Ngay cả khi canvas kết quả không thay đổi, hàng trăm tab vẫn liên tục tạo lại các favicon trắng nhỏ nhiều lần mỗi giây
  • Sau khi sửa để chỉ cập nhật khi favicon thực sự thay đổi, hiệu năng cải thiện đáng kể
  • Tuy vậy, tác giả vẫn chưa hiểu rõ vì sao công việc ở các tab khác lại làm animation của tab phía trước chậm đến vậy

Lý do chọn Pong thay vì Snake

  • Sau thử nghiệm di chuyển hình chữ nhật, tác giả dọn lại “engine” nhỏ và chọn game sẽ xây trên đó
  • Ứng viên đầu tiên là Snake, vì dựa trên block nên có vẻ hợp với favicon
  • Nhưng tác giả cho rằng Snake quá thiên về block, khiến hiệu ứng chuyển động liên tục trên canvas phía trước biến thành pixel rời rạc trên thanh tab không nổi bật bằng
  • Cuối cùng tác giả chọn Pong
    • Quả bóng và paddle phải thường xuyên đi qua lại giữa canvas và thanh tab
    • Chuyển động này rất hợp với hiệu ứng thị giác của dự án

Ghi chú triển khai Pong

  • Sau khi API vẽ favicon đã sẵn sàng, phần triển khai Pong tương đối đơn giản
  • Người chơi máy tính bên phải luôn cố căn tâm paddle với tâm quả bóng
  • Góc nảy của bóng khi chạm paddle được tính bằng lượng giác đơn giản dựa trên vị trí tương đối so với tâm paddle
  • Cách này không thực tế, nhưng giúp tránh vấn đề quả bóng luôn giữ cùng một góc
  • Phần phát hiện va chạm dùng một hàm kiểm tra hai hình chữ nhật có giao nhau hay không
  • Để nhấn mạnh hiệu ứng bóng và paddle đi mượt vào favicon, tác giả thêm trail cho quả bóng

Trò chơi thứ tư được làm tại Recurse Center

  • Dự án được viết trong một batch tại Recurse Center
  • Recurse Center được giới thiệu như một kiểu writer’s retreat miễn phí dành cho lập trình
  • Đây là trò chơi thứ 4 được tạo trong khoảng 40 ngày
  • Tác giả gợi ý độc giả quan tâm cân nhắc ứng tuyển Recurse Center

1 bình luận

 
GN⁺ 2025-02-22
Ý kiến trên Hacker News
  • Xin chào! Cái này là do tôi làm. Tôi tò mò không biết độc giả HN có thích không :)
    Nếu có câu hỏi thì tôi sẽ trả lời. Ghi lại vài suy nghĩ rời rạc: tôi thật sự rất tò mò không biết nếu dùng favicon động thì trông sẽ thế nào. Firefox hỗ trợ favicon động, nên chẳng hạn nếu dự đoán vị trí tương lai của quả bóng rồi tạo SVG động, có lẽ có thể đạt tốc độ khung hình tốt hơn nhiều.
    Một người bạn nói với tôi ngoài đời rằng việc raster hóa canvas thường được xử lý trên GPU, nên có vẻ trực giác về hiệu năng của tôi khi nhìn animation giật cục đã sai khá nhiều.
    Tôi chỉ tương đối chắc rằng Chrome giới hạn việc cập nhật favicon ở 4 lần mỗi giây. Có nhiều cách để làm mới favicon, nên có thể tôi đã bỏ sót điều gì đó.

    • Với tư cách là một người cũng từng làm một Pong vô lý, đây là một tác phẩm thật sự tuyệt vời. Loại nghệ thuật trình diễn kỹ thuật này vừa thú vị để làm, vừa thú vị để xem. Có lẽ nó cũng liên quan đến lý do tôi chơi tung hứng: “Chắc chắn phải có cách làm chuyện này khó hơn nữa.”
    • Bạn đã giải quyết phần đó chưa? Nếu cần tài liệu thiết kế game JavaScript đơn giản thì blog của Jake Gordon khá hay. Ông ấy dùng vòng lặp game đệm kép với canvas trước/sau.
      [0] https://jakesgordon.com/writing/javascript-pong/part1/
    • Quá hay, và tốc độ làm việc thật sự đáng nể!
      Sau 1 Million Checkboxes mà còn làm được cái này, global cap locks, regex Bad Apple, uuid, v.v.; động lực thật ấn tượng.
    • Giờ tôi muốn xem Doom chạy trong các tab trình duyệt.
  • Tôi đã xem bài trình bày gần đây của Nolen tại Recurse, và thật sự rất thích những trò chơi dùng một lần vừa hoàn toàn điên rồ, vừa về cơ bản là hài hước và ngầu như thế này.
    Nó gợi tôi nhớ đến Internet ngày xưa. Thời mà người ta làm ra thứ gì đó chỉ để ngớ ngẩn và vui vẻ. Tối qua, được truyền cảm hứng từ những thứ anh ấy đăng, tôi đã thử làm một gần-Quine rất buồn cười: trang tự in ra mã nguồn của chính nó bằng BEAM decompile và vài thủ thuật.
    Tôi cũng ước mình có thời gian để liên tục tạo ra những thứ như vậy, và chỉ riêng việc vẫn có những người tiếp tục làm ra những điều thú vị theo cách này cũng đủ khiến tôi bật cười.
    Nếu ai muốn cười, phần sort-of quine ở đây: https://github.com/notactuallytreyanastasio/blog/blob/main/l...

  • Bất cứ thứ gì Nolen làm tôi đều thích. Với tôi, có vẻ anh ấy đánh trúng đúng sweet spot của những ứng dụng/trang web đơn mục đích, đem lại cảm giác hoài niệm về Internet ngày xưa.

  • Matthew Rayfield cũng có một thử nghiệm tương tự, dùng thanh địa chỉ thay vì favicon của tab: https://www.youtube.com/watch?v=q7GtCLwTmV4

    • Công trình của Matt là nguồn cảm hứng lớn với tôi!
      Khi làm cái này chắc chắn tôi có nghĩ đến các thử nghiệm với URL của anh ấy, nhưng đã khá lâu rồi tôi chưa đọc blog hay xem video của anh ấy. Đến giữa dự án tôi xem lại và nhớ ra rằng anh ấy cũng đi theo hướng “xếp các cửa sổ để tạo thành 2D”, thật sự rất thú vị.
  • Cái này làm tôi nhớ đến:
    “Show HN: I saw this mind-blowing experiment, so I made a simple version of it” (25.11.2023) https://news.ycombinator.com/item?id=38413660
    “Synchronize a 3D scene across multiple windows using Three.js and localStorage” (27.11.2023) https://news.ycombinator.com/item?id=38437773

  • Tôi dám khẳng định tiếp theo sẽ là Doom.

    • Doom thì hơi phiền. Theo tôi biết có giới hạn 4 khung hình mỗi giây, và đó là tần suất có thể cập nhật tab.
      Ngoài ra, ở mỗi khung hình rất có thể nhiều phần hơn của màn hình sẽ thay đổi, nên để làm mới favicon sẽ cần nhiều thao tác “chuyển canvas thành data URL” hơn rất nhiều. Trong triển khai hiện tại thì việc này tốn kém. Dù vậy tôi trước đó chưa biết đến OffscreenCanvas, và nó có thể sẽ giúp ích.
    • Nếu bạn thích kiểu đó, tôi nhớ Nolen đã từng chạy Doom trong iPhoto rồi.
  • Nếu vấn đề là tab trong Firefox bị thu hẹp hơn, bạn vẫn có thể inspect chính trình duyệt Firefox, tìm quy tắc style về chiều rộng tối thiểu của tab rồi đưa vào userChrome.css.
    Có một vài hướng dẫn liên quan, và chắc hẳn đã có ai đó tổng hợp sẵn cho chiều rộng tab.
    Dù vậy, cách này có lẽ là gian lận. Cũng có browser.tabs.tabMinWidth, nhưng theo trí nhớ của tôi thì nó vẫn có vẻ áp đặt một giá trị tối thiểu.

  • Ban nhạc Ok Go từng có một video âm nhạc hợp tác với Google Chrome, trong đó các cửa sổ trình duyệt và vũ công được đồng bộ rất ấn tượng, còn có cả hiệu ứng kiểu kính vạn hoa. Xem cái này làm tôi nhớ đến nó.