4 คะแนน โดย GN⁺ 2026-03-30 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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) รวมถึงจัดการความแตกต่างระหว่างเบราว์เซอร์ได้

การติดตั้งและเดโม

  • ติดตั้ง: npm install @chenglou/pretext
  • รันในเครื่อง: หลัง bun install ให้ใช้ bun start เพื่อเปิดไดเรกทอรี /demos
  • เดโมออนไลน์: chenglou.me/pretext และ somnai-dreams.github.io/pretext-demos

ฟีเจอร์หลัก

  • 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 ความคิดเห็น

 
GN⁺ 2026-03-30
ความคิดเห็นจาก Hacker News
  • โปรเจ็กต์นี้ น่าประทับใจมาก
    มันแก้ปัญหาการคำนวณความสูงของข้อความที่ตัดบรรทัดได้อย่างมีประสิทธิภาพโดยไม่ต้องเรนเดอร์ข้อความจริงบนเว็บเพจ
    มีการ แคชความกว้างและความสูง ของเซกเมนต์ที่แบ่งระดับคำ และลงมืออิมพลีเมนต์อัลกอริทึมการตัดบรรทัดของเบราว์เซอร์เอง
    เป็นงานที่ยากมากเพราะต้องรองรับอักขระหลากหลายแบบ เช่น เครื่องหมายยัติภังค์ อีโมจิ ภาษาจีน รวมถึงความต่างของการเรนเดอร์ระหว่างเบราว์เซอร์ (รวม Safari)
    ใช้ ชุดข้อมูล corpora และ หน้า accuracy test เพื่อทดสอบเทียบกับเบราว์เซอร์จริง

    • ฉันก็เคยทำอะไรคล้าย ๆ กันมาก่อน เป็นเวอร์ชันเรียบง่ายขนาด 2KB ชื่อ uWrap.js และทำโดยไม่ใช้ AI
      สำหรับข้อความ ASCII โค้ดของฉันใช้เวลา 80ms ส่วน pretext ใช้ 2200ms
      ยังไม่ได้ทดสอบความแม่นยำ แต่ตั้งใจจะลองคืนนี้
      ตอนนี้มี PR ปรับปรุงประสิทธิภาพเปิดไว้แล้วใน issue #18
    • เอนจินจัดเลย์เอาต์ข้อความนี่ ขึ้นชื่อว่ายากมาก
      ฉันเองก็เคยลำบากกับการเรนเดอร์ข้อความหลายบรรทัดบน canvas มาก่อน
      โปรเจ็กต์นี้มีการเชื่อมกับ DOM โดยตรง เลยมีประโยชน์กว่ามาก
      ตัวอย่าง: เดโม Scrawl
    • จากคำอธิบาย ดูเหมือนจริง ๆ แล้วมันใช้วิธี เรนเดอร์เซกเมนต์ลง canvas เพื่อวัดผล
      อาจช้ากว่า native API และก็รับประกันไม่ได้ว่าจะใช้ลอจิกเดียวกับการเรนเดอร์แบบ non-canvas ของเบราว์เซอร์
    • ในทางปฏิบัติแล้ว มันไม่ได้เป็นการ อิมพลีเมนต์อัลกอริทึมการเรนเดอร์ข้อความของเบราว์เซอร์ขึ้นมาเอง
      แต่มันเป็นการเรนเดอร์ลง canvas แล้ววัดผล และให้ API สำหรับวิเคราะห์เลย์เอาต์ข้อความมากกว่า
    • ตอนทำ ซับไตเติลแบบไดนามิก สำหรับวิดีโอ Remotion ฉันลำบากมากเพราะต้องคำนวณความสูงของข้อความ แต่ไลบรารีนี้น่าจะช่วยได้มาก
  • นี่คือฟีเจอร์ที่ รอกันมานานจริง ๆ
    ก่อนหน้านี้การทำอะไรอย่าง responsive accordion ให้ดีทำได้ยากเสมอ
    รูปแบบการพัฒนาเว็บมักเป็น ① มีความต้องการซับซ้อนเกิดขึ้น → ② แก้ด้วย JS/CSS hack → ③ กลายเป็นมาตรฐาน
    รอบนี้ผมคิดว่านี่คือขั้นที่ 2 แบบจริงจัง ไม่ใช่แค่แฮ็ก
    ดูจาก RESEARCH.md จะเห็นว่ามีการศึกษาอย่างละเอียดถึงขั้นความต่างของการวัดอีโมจิในแต่ละเบราว์เซอร์
    การดูแลรักษาอาจลำบาก แต่รู้สึกว่านี่อาจเป็น จุดเปลี่ยน สำคัญของเว็บ

    • ตอนนี้ responsive accordion ทำด้วย CSS ได้แล้ว แต่ API แบบนี้ก็ยังจำเป็นอยู่
      ที่น่าสนใจคือรอบนี้ มีการใช้ 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 | pretty

    • แต่ balance หรือ pretty ก็ยังแก้ปัญหาได้ไม่ครบ
      หลายครั้งเราไม่ได้ต้องการให้ความยาวแต่ละบรรทัดเท่ากัน
      CSSWG issue ที่เกี่ยวข้อง: #191
    • text-wrap ช่วยเรื่องจำนวนคำต่อบรรทัดได้บ้าง แต่ปัญหาพื้นที่ว่างด้านขวาก็ยังคงอยู่
  • pretext ไม่ได้ใช้ canvas.measureText โดยตรง แต่รับข้อความและแอตทริบิวต์ผ่าน JS API แล้วคำนวณเลย์เอาต์ให้อัตโนมัติ
    เมื่อก่อนต้องใช้ measureText เองหรือไม่ก็พอร์ต harfbuzz มาไว้บนเบราว์เซอร์
    มันอาจไม่ใช่ breakthrough ทางเทคนิคเท่าไร แต่เป็นผลลัพธ์จากการประกอบชิ้นส่วนที่มีอยู่ได้ดี
    แต่ก็ยังสงสัยว่ามันต่างจาก Skia-wasm / Canvaskit ยังไง

    • ความต่างคือความเรียบง่าย pretext ไม่ใช่ wasm
    • Skia คือเอนจินเรนเดอร์ขนาดใหญ่ ให้ API การเรนเดอร์ที่ไม่ขึ้นกับอุปกรณ์ แบบเดียวกับ Flutter
      สิ่งที่ต่างคือ pretext อิมพลีเมนต์การเรนเดอร์ glyph ด้วย Typescript ล้วนโดยใช้ AI
      ความรู้สึกคล้ายความต่างระหว่างอิมพลีเมนต์ ffmpeg ในภาษา C เองกับการเรียกใช้จาก Dart
      ความพยายามแบบนี้แสดงให้เห็นถึง ความเป็นไปได้ใหม่ ๆ ของ FOSS ฝั่งไคลเอนต์
    • ถ้าผู้เขียนพูดถูก นี่จะเปลี่ยนแปลง เว็บ GUI framework และ rich text editor อย่างมาก
  • ปีที่แล้วฉันทำระบบจัดหน้าบรอชัวร์สำหรับพิมพ์ด้วย HTML และต้องคำนวณขอบเขตกล่องซ้ำ ๆ ผ่าน Selection API เพื่อจัดการการตัดบรรทัดและป้องกัน widow
    ถึงตอนนี้มันยังทำงานได้ดี แต่มีแฮ็ก off-by-one แบบไม่รู้สาเหตุอยู่
    ความสามารถของ pretext ในการ สร้างบรรทัดแบบวนซ้ำ เป็นฟีเจอร์ที่น่ายินดีมาก

  • บน Fedora + Firefox เดโมทั้งหมดดูพังไปหมด
    ตัวอย่าง: ภาพหน้าจอ

    • มันแสดงให้เห็นว่าโปรเจ็กต์แบบนี้สุดท้ายแล้วคือการ ไล่ตาม edge case แบบไม่รู้จบ
  • ฟีเจอร์แบบนี้จริง ๆ แล้วควร มีให้ในรูปแบบ Browser Standard API
    สงสัยว่าถ้าจะยื่น feature request ไปที่ W3C ต้องทำอย่างไร หรือมีระบบโหวตจากชุมชนไหม

    • มี ข้อเสนอ Font Metrics API อยู่แล้ว
      แต่ฝั่งผู้ผลิตเบราว์เซอร์ยังไม่ได้ให้ความสำคัญมากนัก
      ตอนนี้ Chrome ดูจะโฟกัสกับ API ที่เกี่ยวกับ AI มากกว่า
  • ในเอนจิน Sciter มีฟีเจอร์ Graphics.Text อยู่แล้ว
    มันเป็น องค์ประกอบเรนเดอร์ข้อความบนแคนวาส ที่สามารถใช้สไตล์ CSS เดิมได้เลย

  • ฟีเจอร์ ค้นหาข้อความของเบราว์เซอร์ (Ctrl+F) ทำงานกับลิสต์แบบ virtual scroll ได้ไม่ดี
    ถ้าจะให้แก้ปัญหานี้จริง อาจต้องมี API “Search” แบบใหม่ที่ไม่ใช่ JS

    • เคยมี Virtual Scroller API แต่แทบไม่มีความคืบหน้า
      โปรเจ็กต์ที่เกี่ยวข้อง: Display Locking, เอกสาร MDN
    • การค้นหาแบบเนทีฟจะค้นหาเฉพาะโหนดที่มีอยู่ใน DOM
      รายการที่ถูก virtualize และอยู่นอกจอไม่มีอยู่ใน DOM จึงค้นหาไม่ได้
      การแก้เรื่องนี้ต้องมีสัญญาระดับเบราว์เซอร์แบบใหม่ที่รวมทั้ง การเลือก, โฟกัส, ตำแหน่งสกอลล์, และการไล่ดูผลลัพธ์ที่ตรงกัน เข้าด้วยกัน
      แต่ก็มีโอกาสต่ำที่เว็บไซต์ต่าง ๆ จะใช้งานมันอย่างสม่ำเสมอ