1 คะแนน โดย GN⁺ 2025-02-22 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ใช้พื้นที่ว่างบนแถบแท็บที่ไม่ได้ปิดให้เหมือนเป็นหน้าจอ และทำการทดลอง Pong ที่ใช้ favicon ของ แท็บ Chrome 240 แท็บ เป็นพิกเซล
  • กริดแท็บ 8×30 สร้างด้วย AppleScript และแต่ละแท็บคำนวณตำแหน่งของตัวเองจาก windowIndex และ tabIndex ใน query ของ URL
  • setInterval ในแท็บเบื้องหลังช้าลงเหลือราวทุก 1 วินาที แต่เมื่อย้ายตัวจับเวลาไปไว้ใน Web Worker การอัปเดต favicon ก็เร็วพอ
  • แทนที่จะใช้ WebSocket ฝั่งเซิร์ฟเวอร์ ใช้ Broadcast Channel API เพื่อซิงก์แท็บในโดเมนเดียวกัน และแท็บ main จะเริ่มแอนิเมชันหลังจากทุกแท็บลงทะเบียนเสร็จ
  • คอขวดที่ใหญ่ที่สุดคือการสร้าง favicon หลายร้อยรายการใหม่ทุกเฟรม และเมื่ออัปเดตเฉพาะแท็บที่เปลี่ยนจริง แอนิเมชันก็ลื่นขึ้นมาก

วิธีใช้แท็บ 240 แท็บเหมือนเป็นหน้าจอเดียว

  • โปรเจกต์นี้เป็นการทดลองรัน Pong โดยใช้พื้นที่ที่แท็บเบราว์เซอร์ที่ไม่ได้ปิดครอบครองอยู่
  • หน้าจอประกอบด้วย กริดแท็บ 8×30 ที่สร้างจากหน้าต่าง Chrome 8 หน้าต่าง และแต่ละหน้าต่างมีแท็บ 30 แท็บ
  • ลูกบอลและแพดเดิลเคลื่อนที่ต่อเนื่องกันระหว่าง HTML canvas ในหน้าต่างด้านหน้าและพื้นที่ favicon บนแถบแท็บด้านบน
  • โค้ดเปิดเผยไว้บน GitHub ที่ faviconic พร้อมหมายเหตุว่าเป็นโค้ด “awful” ในสถานะต้นแบบ

ไอเดียที่เริ่มจาก Flappy Favi

  • แรงบันดาลใจมาจาก Flappy Favi ซึ่งเป็น Flappy Bird เวอร์ชันที่รันอยู่ใน favicon สร้างโดย Tru
  • favicon คือรูปภาพเล็ก ๆ ที่แสดงบนแถบแท็บของเบราว์เซอร์ แต่ถ้ามีเพียงอันเดียวก็เล็กเกินกว่าจะมองเห็นได้ชัด
  • หากต้องการวาดภาพคร่อมหลายแท็บ ต้องแก้ปัญหาสามอย่างก่อน
    • วิธีจัดวางแท็บเป็น กริด ที่ดูดี
    • วิธีอัปเดต favicon ให้รวดเร็วแม้ในแท็บเบื้องหลัง
    • วิธี ซิงก์ หน้าจอของหลายแท็บเข้าด้วยกัน

สร้างกริดแท็บ Chrome ด้วย AppleScript

  • ตอนแรกตรวจสอบความเป็นไปได้ด้วยการกดปุ่มแท็บใหม่ของ Chrome ซ้ำ ๆ เพื่อทำให้แท็บเล็กลง แล้วจัดวางหน้าต่างที่สองให้ตรงกันเพื่อสร้างแถวที่สอง
  • การสร้างกริดขนาดใหญ่ด้วยมือยุ่งยาก จึงทำให้เป็นอัตโนมัติด้วย AppleScript
  • สคริปต์เปิดหน้าต่าง Chrome 8 หน้าต่าง สร้างแท็บ 30 แท็บในแต่ละหน้าต่าง และจัดตำแหน่งให้หน้าต่างซ้อนอยู่เหนือกัน
  • มีปัญหาที่ Chrome พยายามเปิดแท็บที่เคยปิดไปแล้วอีกครั้ง จึงต้องจัดการสิ่งนี้เมื่อเริ่มสคริปต์
  • URL ของแต่ละแท็บมี windowIndex และ tabIndex
    • แท็บที่เปิดเป็นแท็บสุดท้ายจะมีพารามิเตอร์เพิ่มเติม เช่น isMain=true, numWindows, numTabs, fullWidth

อัปเดต favicon อย่างรวดเร็วในแท็บเบื้องหลัง

  • เบราว์เซอร์จะค้นหา URL favicon เริ่มต้น แต่สามารถเปลี่ยนไอคอนได้โดยเพิ่มองค์ประกอบใน HTML head ที่ระบุตำแหน่ง favicon แล้วอัปเดตมัน
  • การอัปเดต favicon ของ Chrome ให้ความรู้สึกว่าอยู่ที่ประมาณ 4 ครั้งต่อวินาที
  • ในแท็บเบื้องหลัง การใช้ทรัพยากรถูกจำกัด แม้แต่ลูปเล็ก ๆ ที่เปลี่ยน favicon ทุก 250ms ก็รันเพียงประมาณวินาทีละครั้ง
  • เคยพยายามใช้คอลแบ็กเสียงเบื้องหลังของ Web Audio API เป็นทางเลี่ยง แต่ทำให้ทำงานได้ไม่สำเร็จ
  • หลังจากนั้น เมื่อย้ายตัวจับเวลาไปไว้ใน Web Worker และให้ worker ส่งข้อความไปยัง main document เพื่ออัปเดต favicon ปัญหาก็คลี่คลาย
  • โครงสร้างตัวอย่างคือ worker วาด emoji ลงบน OffscreenCanvas แปลงเป็น data URL แล้วส่งไปยัง main document
    • main document เปลี่ยน href ของ link rel="shortcut icon" เพื่ออัปเดต favicon จริง

จัดการการลงทะเบียนและการซิงก์แท็บด้วย Broadcast Channel

  • การสื่อสารระหว่างแท็บมีโจทย์สองข้อ
    • แต่ละแท็บต้องรู้ว่าตัวเองเป็นแท็บที่เท่าไรของหน้าต่างที่เท่าไร
    • ต้องตัดสินใจว่าจะใช้ช่องทางสื่อสารใดส่งการอัปเดตแท็บ
  • การระบุตำแหน่งแก้ได้ด้วยการอ่าน windowIndex และ tabIndex จากพารามิเตอร์ query ของ URL ที่ AppleScript ใส่ไว้
  • proof of concept ช่วงแรกมีโครงสร้างที่แต่ละแท็บเชื่อมต่อกับเซิร์ฟเวอร์ WebSocket และเซิร์ฟเวอร์ส่งภาพมาให้
  • วิธี WebSocket ไม่สอดคล้องกับเป้าหมายของโปรเจกต์นัก
    • ขัดกับเหตุผลเชิงสุนทรียะที่อยากให้ deploy ในแท็บเบราว์เซอร์ทั่วโลกได้โดยไม่ต้องมีเซิร์ฟเวอร์
    • เวลาที่แต่ละแท็บเชื่อมต่อไม่เหมือนกัน ทำให้อัปเดตไม่ ซิงก์ กัน
  • ต่อมาจึงเปลี่ยนมาใช้ Broadcast Channel API ซึ่งส่งข้อมูลไปยังหลายแท็บในโดเมนเดียวกันได้
  • แท็บเบื้องหลังส่งข้อความ registration ที่มีดัชนีแท็บ/หน้าต่างของตัวเองผ่าน broadcast channel และแท็บ main รับแล้วส่ง ack กลับ
  • แท็บ main เริ่มแอนิเมชันหลังจากได้รับการลงทะเบียนจากแท็บเบื้องหลังทั้งหมดแล้ว

จัดแนว canvas กับ favicon ให้ตรงกัน

  • เพื่อให้วัตถุที่วาดในหน้าต่างด้านหน้าดูเหมือน “เข้าไป” ในแถบแท็บจริง ๆ จึงปฏิบัติต่อ canvas จริงและ favicon ด้านบนเป็นเหมือน canvas ที่ขยายออกไปผืนเดียว
  • การทำงานนี้ต้องวัดขนาดจริงของหน้าต่าง Chrome ด้วยตนเอง
    • จากด้านซ้ายของหน้าต่าง Chrome ถึง favicon แรก 92 พิกเซล
    • จากด้านล่างของ favicon ถึงด้านบนของหน้าต่างจริง 58 พิกเซล
    • ขนาด favicon คือ 16×16
  • ใช้ค่าที่วัดได้เหล่านี้และจำนวนแท็บ/หน้าต่างเพื่อคำนวณค่าสามอย่าง
    • ความกว้างของ canvas ที่จะแสดงให้ตรงกับ favicon
    • ความกว้างของแต่ละแท็บ
    • ความกว้างและความสูงของ “canvas” ทั้งหมดที่รวมแถว favicon, แถบ URL และ canvas จริง
  • หากสี่เหลี่ยมอยู่ใต้แถบ URL ก็วาดลงบน canvas จริง และหากอยู่เหนือแถบ URL ก็ broadcast ข้อมูลพื้นที่นั้นไปยังแท็บอื่น
  • แต่ละแท็บคำนวณพิกัดพิกเซลของตัวเอง แล้วอัปเดต favicon เป็นพิกเซลสีขาว/ดำ

คอขวดอยู่ที่การสร้าง favicon ไม่ใช่การส่งข้อมูล

  • การติดตั้งช่วงแรกทำงานได้ แต่แอนิเมชันบน canvas มีอาการสะดุด และแม้วาดใน requestAnimationFrame ก็ยังไม่ลื่น
  • ตอนแรกสงสัยว่าโครงสร้างที่ main thread คำนวณสถานะพิกเซล favicon ทั้งหมด ใส่ลงใน broadcast channel และให้แท็บหลายร้อยแท็บอ่านนั้นคัดลอกข้อมูลมากเกินไป
  • เปลี่ยนมา broadcast เฉพาะตำแหน่ง ของสี่เหลี่ยม และให้แต่ละแท็บคำนวณเองว่ามีการตัดกันหรือไม่ แต่ประสิทธิภาพไม่ดีขึ้น
  • ปัญหาจริงคือแต่ละแท็บสร้างภาพ favicon ขาวดำขนาด 4×4 ใหม่ทุกเฟรม แล้วสร้าง URL ด้วย toDataURL("image/png")
  • แม้ canvas ผลลัพธ์จะไม่เปลี่ยน แท็บหลายร้อยแท็บก็ยังสร้าง favicon สีขาวเล็ก ๆ ซ้ำหลายครั้งต่อวินาที
  • เมื่อแก้ให้ update เฉพาะตอนที่ favicon เปลี่ยนจริง ประสิทธิภาพก็ดีขึ้นมาก
  • อย่างไรก็ตาม ยังไม่เข้าใจชัดเจนว่าทำไมงานของแท็บอื่นจึงทำให้แอนิเมชันในแท็บด้านหน้าช้าลงมากเช่นนั้น

เหตุผลที่เลือก Pong แทน Snake

  • หลังการทดลองการเคลื่อนที่ของสี่เหลี่ยม ก็จัดระเบียบ “engine” เล็ก ๆ แล้วเลือกเกมที่จะสร้างบนมัน
  • ตัวเลือกแรกคือ Snake เพราะเป็นแบบ block-based จึงน่าจะเข้ากับ favicon ได้ดี
  • แต่เห็นว่า Snake เป็น block-based มากเกินไป ทำให้เอฟเฟกต์ที่การเคลื่อนที่ต่อเนื่องบน canvas ด้านหน้ากลายเป็นพิกเซลแบบไม่ต่อเนื่องบนแถบแท็บโดดเด่นน้อยลง
  • สุดท้ายจึงเลือก Pong
    • ลูกบอลและแพดเดิลต้องเคลื่อนผ่านไปมาระหว่าง canvas กับแถบแท็บบ่อย ๆ
    • การเคลื่อนที่นี้เข้ากับเอฟเฟกต์ภาพของโปรเจกต์ได้ดี

บันทึกการทำ Pong

  • หลังจาก API วาด favicon พร้อมแล้ว การทำ Pong เองค่อนข้างเรียบง่าย
  • ผู้เล่นคอมพิวเตอร์ฝั่งขวาพยายามให้จุดกึ่งกลางของแพดเดิลตรงกับจุดกึ่งกลางของลูกบอลเสมอ
  • มุมตอนที่ลูกบอลเด้งจากแพดเดิลคำนวณด้วยตรีโกณมิติง่าย ๆ โดยใช้ตำแหน่งสัมพัทธ์กับจุดกึ่งกลางของแพดเดิล
  • วิธีนี้ไม่สมจริง แต่ช่วยป้องกันปัญหาที่ลูกบอลรักษามุมเดิมตลอดเวลา
  • การตรวจจับการชนใช้ฟังก์ชันที่ตรวจว่าสี่เหลี่ยมสองอันตัดกันหรือไม่
  • เพื่อเน้นเอฟเฟกต์ที่ลูกบอลและแพดเดิลเข้าสู่ favicon อย่างลื่นไหล จึงเพิ่ม trail ให้ลูกบอล

เกมที่สี่ที่สร้างที่ Recurse Center

  • โปรเจกต์นี้เขียนขึ้นระหว่าง batch ที่ Recurse Center
  • Recurse Center ถูกแนะนำว่าเป็นสถานที่คล้าย writer’s retreat ฟรีสำหรับการเขียนโปรแกรม
  • โปรเจกต์นี้เป็นเกมที่ 4 ที่สร้างขึ้นในช่วงประมาณ 40 วัน
  • แนะนำให้ผู้อ่านที่สนใจพิจารณาสมัคร Recurse Center

1 ความคิดเห็น

 
GN⁺ 2025-02-22
ความคิดเห็นจาก Hacker News
  • สวัสดีครับ! ผมเป็นคนทำสิ่งนี้เอง สงสัยอยู่เหมือนกันว่าผู้อ่าน HN จะชอบไหม :)
    ถ้ามีคำถามก็ยินดีตอบนะครับ ถ้าจะเล่าความคิดกระจัดกระจายสักหน่อย ผมอยากรู้มากว่าถ้าใช้ animated favicon แล้วมันจะออกมาเป็นยังไง Firefox รองรับ animated favicon อยู่แล้ว ดังนั้นถ้าทำ SVG แบบเคลื่อนไหวโดยคาดการณ์ตำแหน่งในอนาคตของลูกบอลไว้ล่วงหน้า ก็น่าจะได้เฟรมเรตที่ดีกว่ามาก
    มีเพื่อนบอกผมแบบออฟไลน์ว่าโดยปกติการ rasterize canvas นั้นทำบน GPU เพราะงั้นสัญชาตญาณด้าน ประสิทธิภาพ ที่ผมมีตอนเห็นแอนิเมชันกระตุก ๆ อาจจะผิดไปพอสมควร
    ผมค่อนข้างมั่นใจแค่ระดับหนึ่งว่า Chrome จำกัดการอัปเดต favicon ไว้ที่ 4 ครั้งต่อวินาที มีหลายวิธีในการรีเฟรช favicon เลยอาจมีอะไรที่ผมพลาดไปก็ได้

    • ในฐานะคนที่เคยทำ Pong แบบหลุดโลกเหมือนกัน ต้องบอกว่านี่เป็นงานที่ยอดเยี่ยมมาก งานแนว ศิลปะการแสดงเชิงเทคนิค แบบนี้ทั้งทำก็สนุก ทั้งดูก็สนุก อาจจะคล้ายกับเหตุผลที่ผมชอบเล่นโยนของก็ได้: “ต้องมีวิธีทำให้สิ่งนี้ยากขึ้นอีกแน่ ๆ”
    • คุณแก้ส่วนนั้นได้หรือยัง? ถ้าหาเอกสารเรื่องการออกแบบเกม JavaScript แบบง่าย ๆ บล็อกของ Jake Gordon ดีมาก เขาใช้ game loop แบบ double buffer ที่มี canvas หน้า/หลัง
      [0] https://jakesgordon.com/writing/javascript-pong/part1/
    • ดีมากจริง ๆ และความเร็วในการทำงานก็น่าทึ่งสุด ๆ!
      หลังจาก 1 Million Checkboxes แล้วยังทำสิ่งนี้, global cap locks, bad apple regex, uuid และอีกสารพัดได้อีก แรงขับเคลื่อนของคุณน่าประทับใจมาก
    • ตอนนี้ผมอยากเห็น Doom ที่รันอยู่ในแท็บเบราว์เซอร์แล้ว
  • ผมเพิ่งดู Recurse talk ล่าสุดของ Nolen มา เกม ใช้ครั้งเดียวจบ พวกนี้ที่ทั้งบ้าบิ่นสุด ๆ แต่ก็ขำและเท่มากในแก่นของมัน ดูแล้วเพลินจริง ๆ
    มันทำให้นึกถึงอินเทอร์เน็ตยุคเก่า ตอนที่ผู้คนสร้างอะไรบางอย่างขึ้นมาเพียงเพราะอยากเล่นอะไรบ้า ๆ และสนุก ๆ เมื่อคืนผมเองก็ได้แรงบันดาลใจจากสิ่งที่เขาโพสต์ เลยลองทำ pseudo-quine ที่ตลกมากอันหนึ่ง โดยใช้การ decompile ของ BEAM กับทริกเล็ก ๆ น้อย ๆ ให้หน้าเว็บแสดงซอร์สโค้ดของตัวเองออกมา
    ผมเองก็อยากมีเวลาสร้างอะไรแบบนี้ออกมาเรื่อย ๆ และแค่รู้ว่ายังมีคนที่ทำของน่าสนใจต่อไปในแนวนี้อยู่ ก็ทำให้ยิ้มได้แล้ว
    สำหรับคนที่อยากขำ sort-of quine นั้นอยู่ที่นี่: https://github.com/notactuallytreyanastasio/blog/blob/main/l...

  • ไม่ว่า Nolen จะทำอะไรก็ดีหมด สำหรับผมมันเหมือนเขาแตะจุดลงตัวของ แอป/เว็บไซต์ที่มีจุดประสงค์เดียว ได้พอดี ซึ่งชวนให้คิดถึงความหลังว่าอินเทอร์เน็ตสมัยก่อนเคยเป็นยังไง

  • Matthew Rayfield ก็มีงานทดลองคล้ายกันที่ใช้ แถบที่อยู่ แทน favicon ของแท็บ: https://www.youtube.com/watch?v=q7GtCLwTmV4

    • งานของ Matt เป็นแรงบันดาลใจให้ผมมาก!
      ตอนทำสิ่งนี้ ผมมีงาน URL ของเขาอยู่ในหัวแน่นอน แต่ก็ไม่ได้อ่านบล็อกหรือดูวิดีโอของเขามาพักใหญ่แล้ว พอกลับไปดูอีกครั้งระหว่างทำโปรเจกต์ แล้วนึกขึ้นได้ว่าเขาก็ไปในทาง “ซ้อนหน้าต่างเพื่อสร้างสองมิติ” เหมือนกัน มันน่าสนุกจริง ๆ
  • อันนี้ทำให้นึกถึง:
    “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

  • ผมขอฟันธงว่าอย่างต่อไปคือ Doom

    • Doom นี่ค่อนข้างจุกจิกนะ เท่าที่ผมรู้มีข้อจำกัดอยู่ที่ 4 เฟรมต่อวินาที ซึ่งก็คือความถี่ในการอัปเดตแท็บนั่นแหละ
      อีกอย่างในแต่ละเฟรมก็น่าจะมีพื้นที่ของภาพที่เปลี่ยนมากกว่าเดิมมาก ดังนั้นถ้าจะอัปเดต favicon ก็ต้องทำ “แปลง canvas เป็น data URL” มากขึ้นอีกเยอะ ซึ่งใน implementation ปัจจุบันมันค่อนข้างแพง ผมเพิ่งรู้จัก OffscreenCanvas เหมือนกัน และมันอาจช่วยได้
    • ถ้าชอบแนวนั้น เท่าที่ผมรู้ Nolen เคยรัน Doom บน iPhoto มาแล้ว
  • ถ้าปัญหาใน Firefox คือแท็บแคบลงไปอีก คุณยังพอ inspect ตัวเบราว์เซอร์ Firefox เอง เพื่อหารายการกฎ style ของความกว้างแท็บขั้นต่ำ แล้วเอาไปใส่ใน userChrome.css ได้อยู่
    มีไกด์เกี่ยวกับเรื่องนี้อยู่หลายอัน และน่าจะมีคนสรุปไว้ให้แล้วสำหรับความกว้างแท็บโดยเฉพาะ
    แต่แบบนี้อาจนับว่าโกงก็ได้นะ ยังมี browser.tabs.tabMinWidth ด้วย แต่ถ้าจำไม่ผิดมันก็ยังบังคับค่าต่ำสุดอยู่ดี

  • วง Ok Go เคยมีมิวสิกวิดีโอที่ทำร่วมกับ Google Chrome ซึ่งซิงก์หน้าต่างเบราว์เซอร์กับนักเต้นได้อย่างยอดเยี่ยม และยังมีเอฟเฟกต์แบบกล้องสลับลายด้วย เห็นอันนี้แล้วทำให้นึกถึงสิ่งนั้นเลย