Pong ที่รันอยู่ในแท็บเบราว์เซอร์ 240 แท็บ
(eieio.games)- ใช้พื้นที่ว่างบนแถบแท็บที่ไม่ได้ปิดให้เหมือนเป็นหน้าจอ และทำการทดลอง 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 จริง
- main document เปลี่ยน
จัดการการลงทะเบียนและการซิงก์แท็บด้วย 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 ความคิดเห็น
ความคิดเห็นจาก Hacker News
สวัสดีครับ! ผมเป็นคนทำสิ่งนี้เอง สงสัยอยู่เหมือนกันว่าผู้อ่าน HN จะชอบไหม :)
ถ้ามีคำถามก็ยินดีตอบนะครับ ถ้าจะเล่าความคิดกระจัดกระจายสักหน่อย ผมอยากรู้มากว่าถ้าใช้ animated favicon แล้วมันจะออกมาเป็นยังไง Firefox รองรับ animated favicon อยู่แล้ว ดังนั้นถ้าทำ SVG แบบเคลื่อนไหวโดยคาดการณ์ตำแหน่งในอนาคตของลูกบอลไว้ล่วงหน้า ก็น่าจะได้เฟรมเรตที่ดีกว่ามาก
มีเพื่อนบอกผมแบบออฟไลน์ว่าโดยปกติการ rasterize canvas นั้นทำบน GPU เพราะงั้นสัญชาตญาณด้าน ประสิทธิภาพ ที่ผมมีตอนเห็นแอนิเมชันกระตุก ๆ อาจจะผิดไปพอสมควร
ผมค่อนข้างมั่นใจแค่ระดับหนึ่งว่า Chrome จำกัดการอัปเดต favicon ไว้ที่ 4 ครั้งต่อวินาที มีหลายวิธีในการรีเฟรช favicon เลยอาจมีอะไรที่ผมพลาดไปก็ได้
[0] https://jakesgordon.com/writing/javascript-pong/part1/
หลังจาก 1 Million Checkboxes แล้วยังทำสิ่งนี้, global cap locks, bad apple regex, uuid และอีกสารพัดได้อีก แรงขับเคลื่อนของคุณน่าประทับใจมาก
ผมเพิ่งดู 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
ตอนทำสิ่งนี้ ผมมีงาน 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
อีกอย่างในแต่ละเฟรมก็น่าจะมีพื้นที่ของภาพที่เปลี่ยนมากกว่าเดิมมาก ดังนั้นถ้าจะอัปเดต favicon ก็ต้องทำ “แปลง canvas เป็น data URL” มากขึ้นอีกเยอะ ซึ่งใน implementation ปัจจุบันมันค่อนข้างแพง ผมเพิ่งรู้จัก OffscreenCanvas เหมือนกัน และมันอาจช่วยได้
ถ้าปัญหาใน Firefox คือแท็บแคบลงไปอีก คุณยังพอ inspect ตัวเบราว์เซอร์ Firefox เอง เพื่อหารายการกฎ style ของความกว้างแท็บขั้นต่ำ แล้วเอาไปใส่ใน userChrome.css ได้อยู่
มีไกด์เกี่ยวกับเรื่องนี้อยู่หลายอัน และน่าจะมีคนสรุปไว้ให้แล้วสำหรับความกว้างแท็บโดยเฉพาะ
แต่แบบนี้อาจนับว่าโกงก็ได้นะ ยังมี
browser.tabs.tabMinWidthด้วย แต่ถ้าจำไม่ผิดมันก็ยังบังคับค่าต่ำสุดอยู่ดีวง Ok Go เคยมีมิวสิกวิดีโอที่ทำร่วมกับ Google Chrome ซึ่งซิงก์หน้าต่างเบราว์เซอร์กับนักเต้นได้อย่างยอดเยี่ยม และยังมีเอฟเฟกต์แบบกล้องสลับลายด้วย เห็นอันนี้แล้วทำให้นึกถึงสิ่งนั้นเลย
https://www.youtube.com/watch?v=ISL1GfXwr-o