- Pretext คือ ไลบรารี JavaScript/TypeScript แบบเพียว สำหรับคำนวณ ความสูงและการจัดวางบรรทัดของข้อความหลายบรรทัด โดยไม่ต้องเข้าถึง DOM และรองรับทั้งสภาพแวดล้อมเบราว์เซอร์และเซิร์ฟเวอร์
- ไม่ใช้ DOM measurement API อย่าง getBoundingClientRect จึง ตัดต้นทุน layout reflow ออกไป และยังคงความแม่นยำด้วย ลอจิกการวัดของตัวเองที่อิงกับ font engine
- ผ่าน API อย่าง prepare() / layout() เพื่อประมวลผลข้อความล่วงหน้า และใช้ข้อมูลความกว้างที่แคชไว้เพื่อ คำนวณความสูงอย่างรวดเร็วด้วยการคำนวณเชิงคณิตศาสตร์ล้วน
- รองรับ อีโมจิ, ข้อความแบบหลายทิศทาง (bidi), และภาษาหลากหลาย พร้อมให้ผลลัพธ์แบบเดียวกันบน Canvas·SVG·WebGL·การเรนเดอร์ฝั่งเซิร์ฟเวอร์
- เป็น เอนจินข้อความประสิทธิภาพสูง ที่นำไปใช้สร้างเลย์เอาต์ UI ที่ต้องการความแม่นยำได้ เช่น virtualized scroll, การตรวจสอบ text overflow, การจัดวางข้อความลอยตัว
ภาพรวม
- Pretext คือ ไลบรารี JavaScript/TypeScript แบบเพียว สำหรับ การวัดและจัดเลย์เอาต์ข้อความหลายบรรทัด รองรับทั้ง DOM, Canvas, SVG และการเรนเดอร์ฝั่งเซิร์ฟเวอร์
- ไม่ใช้ DOM measurement API (
getBoundingClientRect, offsetHeight เป็นต้น) จึง ตัดต้นทุน layout reflow ออกไป
- ให้ ความแม่นยำและความเร็วสูง ผ่าน ลอจิกการวัดของตัวเองที่อ้างอิง font engine ของเบราว์เซอร์
- รองรับ ทุกภาษา, อีโมจิ, และข้อความแบบหลายทิศทาง (bidi) รวมถึงจัดการความแตกต่างระหว่างเบราว์เซอร์ได้
การติดตั้งและเดโม
ฟีเจอร์หลัก
- Pretext มีรูปแบบการใช้งานหลักอยู่ 2 แบบ
-
1. วัดความสูงของย่อหน้าโดยไม่ต้องเข้าถึง DOM
prepare() จะประมวลผลข้อความล่วงหน้า ทำ normalization ของช่องว่าง, แยกเซกเมนต์, ใช้กฎ glue และทำการวัดบนฐานของ canvas เพื่อคืนค่า opaque handle
layout() ใช้ข้อมูลความกว้างที่แคชไว้เพื่อ คำนวณความสูงและจำนวนบรรทัดด้วยการคำนวณเชิงคณิตศาสตร์ล้วน
- เมื่อเป็นข้อความและการตั้งค่าเดิม ไม่จำเป็นต้องเรียก
prepare() ซ้ำ และเมื่อมีการ resize ก็รันแค่ layout() ใหม่
- ใช้ตัวเลือก
{ whiteSpace: 'pre-wrap' } เพื่อคงช่องว่าง, แท็บ(\t) และการขึ้นบรรทัดใหม่(\n) ไว้ตามเดิม
- ผลเบนช์มาร์ก:
prepare() ประมาณ 19ms (อิงจากข้อความ 500 ชุด), layout() ประมาณ 0.09ms
- ค่าความสูงที่ได้สามารถนำไปใช้กับฟังก์ชัน UI เช่น
- การคำนวณความสูงอย่างแม่นยำในการทำ virtualization และ occlusion handling
- ระบบเลย์เอาต์ที่ทำด้วย JS (เช่น masonry, โครงสร้างคล้าย flexbox)
- การตรวจสอบ text overflow ด้วย AI
- คงตำแหน่งการเลื่อนเมื่อโหลดข้อความ
-
2. สร้างเลย์เอาต์ย่อหน้าด้วยตนเอง
- สร้างข้อมูลระดับเซกเมนต์ด้วย
prepareWithSegments()
layoutWithLines() คืนค่าข้อความและข้อมูลความกว้างของแต่ละบรรทัดภายใต้ความกว้างคงที่
walkLineRanges() ใช้วนผ่านความกว้างและช่วงเคอร์เซอร์ของแต่ละบรรทัดโดยไม่ต้องสร้างสตริงข้อความ
- ตัวอย่าง: สามารถทำ การปรับเลย์เอาต์แบบค้นหาเชิงทวิภาค เพื่อทดสอบหลายความกว้างแล้วหาจำนวนบรรทัดและความสูงที่เหมาะสม
layoutNextLine() ใช้จัดเลย์เอาต์ทีละบรรทัดในกรณีที่ ความกว้างของแต่ละบรรทัดแตกต่างกัน
- ตัวอย่าง: การจัดวางข้อความลอยตัว ให้ข้อความไหลรอบภาพ
- แนวทางนี้ใช้ได้เหมือนกันบน Canvas, SVG, WebGL, การเรนเดอร์ฝั่งเซิร์ฟเวอร์
สรุป API
-
API สำหรับการวัดพื้นฐาน
prepare(text, font, options?): วิเคราะห์และวัดข้อความ พร้อมคืน handle สำหรับส่งต่อให้ layout()
layout(prepared, maxWidth, lineHeight): คำนวณความสูงของข้อความและจำนวนบรรทัด ตามความกว้างและ line height ที่กำหนด
-
API สำหรับการจัดเลย์เอาต์แบบกำหนดเอง
prepareWithSegments(text, font, options?): คืนข้อมูลระดับเซกเมนต์
layoutWithLines(prepared, maxWidth, lineHeight): รวมข้อมูลข้อความ, ความกว้าง และเคอร์เซอร์ของแต่ละบรรทัด
walkLineRanges(prepared, maxWidth, onLine): ส่งต่อความกว้างและช่วงเคอร์เซอร์ของแต่ละบรรทัดผ่าน callback
layoutNextLine(prepared, start, maxWidth): จัดเลย์เอาต์ในรูปแบบ iterator ระดับบรรทัด
- มี type definitions สำหรับ
LayoutLine, LayoutLineRange, LayoutCursor
-
ยูทิลิตีอื่น ๆ
clearCache(): ล้างแคชภายใน
setLocale(locale?): ตั้งค่า locale และล้างแคช (ไม่กระทบสถานะเดิมที่มีอยู่)
ข้อจำกัดและข้อควรระวัง
- Pretext ไม่ใช่เอนจินเรนเดอร์ฟอนต์แบบสมบูรณ์
- คุณสมบัติ CSS เป้าหมายพื้นฐาน
white-space: normal
word-break: normal
overflow-wrap: break-word
line-break: auto
- เมื่อใช้
{ whiteSpace: 'pre-wrap' } จะคงช่องว่าง, แท็บ และการขึ้นบรรทัดใหม่ไว้ พร้อมใช้ tab-size: 8
- บน macOS ฟอนต์
system-ui ไม่เหมาะกับความแม่นยำของ layout() ดังนั้น แนะนำให้ใช้ชื่อฟอนต์แบบระบุชัดเจน
- เนื่องจาก
overflow-wrap: break-word จึง สามารถตัดบรรทัดกลางคำได้เมื่อความกว้างแคบมาก แต่จะแยกเฉพาะในระดับ grapheme เท่านั้น
เกี่ยวกับการพัฒนา
- ดูสภาพแวดล้อมการพัฒนาและคำสั่งต่าง ๆ ได้ใน
DEVELOPMENT.md
การมีส่วนร่วมและที่มา
- สืบทอดแนวคิดมาจากโปรเจกต์ text-layout ของ Sebastian Markbage
- พัฒนาต่อยอดจากโครงสร้างที่รับมาจาก การ shaping บนฐานของ canvas
measureText, การจัดการ bidi ของ pdf.js, และการออกแบบ streaming line breaking
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
โปรเจ็กต์นี้ น่าประทับใจมาก
มันแก้ปัญหาการคำนวณความสูงของข้อความที่ตัดบรรทัดได้อย่างมีประสิทธิภาพโดยไม่ต้องเรนเดอร์ข้อความจริงบนเว็บเพจ
มีการ แคชความกว้างและความสูง ของเซกเมนต์ที่แบ่งระดับคำ และลงมืออิมพลีเมนต์อัลกอริทึมการตัดบรรทัดของเบราว์เซอร์เอง
เป็นงานที่ยากมากเพราะต้องรองรับอักขระหลากหลายแบบ เช่น เครื่องหมายยัติภังค์ อีโมจิ ภาษาจีน รวมถึงความต่างของการเรนเดอร์ระหว่างเบราว์เซอร์ (รวม Safari)
ใช้ ชุดข้อมูล corpora และ หน้า accuracy test เพื่อทดสอบเทียบกับเบราว์เซอร์จริง
สำหรับข้อความ ASCII โค้ดของฉันใช้เวลา 80ms ส่วน pretext ใช้ 2200ms
ยังไม่ได้ทดสอบความแม่นยำ แต่ตั้งใจจะลองคืนนี้
ตอนนี้มี PR ปรับปรุงประสิทธิภาพเปิดไว้แล้วใน issue #18
ฉันเองก็เคยลำบากกับการเรนเดอร์ข้อความหลายบรรทัดบน canvas มาก่อน
โปรเจ็กต์นี้มีการเชื่อมกับ DOM โดยตรง เลยมีประโยชน์กว่ามาก
ตัวอย่าง: เดโม Scrawl
อาจช้ากว่า native API และก็รับประกันไม่ได้ว่าจะใช้ลอจิกเดียวกับการเรนเดอร์แบบ non-canvas ของเบราว์เซอร์
แต่มันเป็นการเรนเดอร์ลง canvas แล้ววัดผล และให้ API สำหรับวิเคราะห์เลย์เอาต์ข้อความมากกว่า
นี่คือฟีเจอร์ที่ รอกันมานานจริง ๆ
ก่อนหน้านี้การทำอะไรอย่าง responsive accordion ให้ดีทำได้ยากเสมอ
รูปแบบการพัฒนาเว็บมักเป็น ① มีความต้องการซับซ้อนเกิดขึ้น → ② แก้ด้วย JS/CSS hack → ③ กลายเป็นมาตรฐาน
รอบนี้ผมคิดว่านี่คือขั้นที่ 2 แบบจริงจัง ไม่ใช่แค่แฮ็ก
ดูจาก RESEARCH.md จะเห็นว่ามีการศึกษาอย่างละเอียดถึงขั้นความต่างของการวัดอีโมจิในแต่ละเบราว์เซอร์
การดูแลรักษาอาจลำบาก แต่รู้สึกว่านี่อาจเป็น จุดเปลี่ยน สำคัญของเว็บ
ที่น่าสนใจคือรอบนี้ มีการใช้ AI อย่างจริงจังในกระบวนการพัฒนา ดูเหมือนว่าจะอิมพลีเมนต์ส่วนใหญ่ด้วย Cursor agent
ตามที่ผู้เขียนไลบรารีบอกไว้ เขาให้ Claude Code และ Codex เรียนรู้จากข้อมูล ground truth ของเบราว์เซอร์ แล้ววัดผลซ้ำ ๆ ตลอดหลายสัปดาห์
ดู ทวีตที่เกี่ยวข้อง
ดูเหมือนว่าจะใช้ Autoresearch บางส่วนด้วย
ฉันชอบตัวอย่าง reflow แบบอิง shape เป็นพิเศษ
อยากลองเอาไปใช้กับ Ensō(enso.sonnet.io) แต่ก็ยั้งไว้เพื่อคงความเรียบง่าย
ตัวอย่าง accordion สามารถทำได้ด้วย CSS
interpolate-sizeเช่นกันดู บทความของ Josh Comeau
ตัวอย่าง text bubble ก็ทำอะไรคล้ายกันได้ด้วย
text-wrap: balance | prettybalanceหรือprettyก็ยังแก้ปัญหาได้ไม่ครบหลายครั้งเราไม่ได้ต้องการให้ความยาวแต่ละบรรทัดเท่ากัน
CSSWG issue ที่เกี่ยวข้อง: #191
text-wrapช่วยเรื่องจำนวนคำต่อบรรทัดได้บ้าง แต่ปัญหาพื้นที่ว่างด้านขวาก็ยังคงอยู่pretext ไม่ได้ใช้ canvas.measureText โดยตรง แต่รับข้อความและแอตทริบิวต์ผ่าน JS API แล้วคำนวณเลย์เอาต์ให้อัตโนมัติ
เมื่อก่อนต้องใช้ measureText เองหรือไม่ก็พอร์ต harfbuzz มาไว้บนเบราว์เซอร์
มันอาจไม่ใช่ breakthrough ทางเทคนิคเท่าไร แต่เป็นผลลัพธ์จากการประกอบชิ้นส่วนที่มีอยู่ได้ดี
แต่ก็ยังสงสัยว่ามันต่างจาก Skia-wasm / Canvaskit ยังไง
สิ่งที่ต่างคือ pretext อิมพลีเมนต์การเรนเดอร์ glyph ด้วย Typescript ล้วนโดยใช้ AI
ความรู้สึกคล้ายความต่างระหว่างอิมพลีเมนต์ ffmpeg ในภาษา C เองกับการเรียกใช้จาก Dart
ความพยายามแบบนี้แสดงให้เห็นถึง ความเป็นไปได้ใหม่ ๆ ของ FOSS ฝั่งไคลเอนต์
ปีที่แล้วฉันทำระบบจัดหน้าบรอชัวร์สำหรับพิมพ์ด้วย HTML และต้องคำนวณขอบเขตกล่องซ้ำ ๆ ผ่าน Selection API เพื่อจัดการการตัดบรรทัดและป้องกัน widow
ถึงตอนนี้มันยังทำงานได้ดี แต่มีแฮ็ก off-by-one แบบไม่รู้สาเหตุอยู่
ความสามารถของ pretext ในการ สร้างบรรทัดแบบวนซ้ำ เป็นฟีเจอร์ที่น่ายินดีมาก
บน Fedora + Firefox เดโมทั้งหมดดูพังไปหมด
ตัวอย่าง: ภาพหน้าจอ
ฟีเจอร์แบบนี้จริง ๆ แล้วควร มีให้ในรูปแบบ Browser Standard API
สงสัยว่าถ้าจะยื่น feature request ไปที่ W3C ต้องทำอย่างไร หรือมีระบบโหวตจากชุมชนไหม
แต่ฝั่งผู้ผลิตเบราว์เซอร์ยังไม่ได้ให้ความสำคัญมากนัก
ตอนนี้ Chrome ดูจะโฟกัสกับ API ที่เกี่ยวกับ AI มากกว่า
ในเอนจิน Sciter มีฟีเจอร์ Graphics.Text อยู่แล้ว
มันเป็น องค์ประกอบเรนเดอร์ข้อความบนแคนวาส ที่สามารถใช้สไตล์ CSS เดิมได้เลย
ฟีเจอร์ ค้นหาข้อความของเบราว์เซอร์ (Ctrl+F) ทำงานกับลิสต์แบบ virtual scroll ได้ไม่ดี
ถ้าจะให้แก้ปัญหานี้จริง อาจต้องมี API “Search” แบบใหม่ที่ไม่ใช่ JS
โปรเจ็กต์ที่เกี่ยวข้อง: Display Locking, เอกสาร MDN
รายการที่ถูก virtualize และอยู่นอกจอไม่มีอยู่ใน DOM จึงค้นหาไม่ได้
การแก้เรื่องนี้ต้องมีสัญญาระดับเบราว์เซอร์แบบใหม่ที่รวมทั้ง การเลือก, โฟกัส, ตำแหน่งสกอลล์, และการไล่ดูผลลัพธ์ที่ตรงกัน เข้าด้วยกัน
แต่ก็มีโอกาสต่ำที่เว็บไซต์ต่าง ๆ จะใช้งานมันอย่างสม่ำเสมอ