- Skia Canvas ช่วยให้ใช้ HTML Canvas Drawing API ใน Node.js สำหรับการเรนเดอร์ทั้งแบบบนหน้าจอและนอกหน้าจอ โดยอิงกับ Google Skia จึงให้ผลลัพธ์ที่ใกล้เคียงกับ
<canvas>ของ Chrome มาก - สามารถส่งออกผลลัพธ์เป็นเวกเตอร์อย่าง PDF·SVG และบิตแมปอย่าง JPEG·PNG·WEBP ได้ และเชื่อมต่อได้ทั้งเป็นไฟล์, dataURL, Buffer หรือออบเจ็กต์ Sharp
- รองรับขอบเขตที่อยู่นอกเหนือ Canvas ของเบราว์เซอร์ด้วย เช่น การเรนเดอร์ GUI Window, เฟรมเวิร์กอีเวนต์, Canvas หลายหน้า, การคำนวณเส้นทาง, การแปลงมุมมอง 3D, CSS filter และการควบคุม typography ขั้นสูง
- การเรนเดอร์แบบอะซิงโครนัสและไฟล์ I/O ถูกประมวลผลบน native thread ใน worker pool ที่ผู้ใช้กำหนดได้ จึงเหมาะกับการสร้างภาพฝั่งเซิร์ฟเวอร์และสภาพแวดล้อมแบบ serverless
- benchmark วัดโดยแยกโหมด serial และ async โดยใน async จะเริ่มงานซ้ำหลายรายการพร้อมกันเพื่อเปรียบเทียบผลของ multithreading
การใช้งาน Canvas บน Node.js
- Skia Canvas คือการใช้งาน HTML Canvas Drawing API สำหรับ Node.js ที่รองรับทั้งการเรนเดอร์บนหน้าจอและนอกหน้าจอ
- ใช้เอนจินกราฟิก Google Skia จึงให้ผลลัพธ์ที่คล้ายกับองค์ประกอบ
<canvas>ของ Chrome มาก - รวมถึงความสามารถที่ Canvas ของเบราว์เซอร์ยังไม่มีให้ด้วย
รูปแบบเอาต์พุตและวิธีส่งออก
- เอาต์พุตภาพรองรับทั้ง รูปแบบเวกเตอร์ และ รูปแบบบิตแมป
- เวกเตอร์: PDF, SVG
- บิตแมป: JPEG, PNG, WEBP
- สามารถส่งผลลัพธ์ของ Canvas ไปยังปลายทางได้หลายแบบ
- สามารถวาดโดยตรงลงใน GUI Window แบบอินเทอร์แอคทีฟ และมีเฟรมเวิร์กอีเวนต์ที่คล้ายกับเบราว์เซอร์
การเรนเดอร์แบบอะซิงโครนัสและหลายหน้า
- การเรนเดอร์และไฟล์ I/O ทำงานแบบอะซิงโครนัสใน worker pool ที่ใช้ native thread
- สามารถตั้งค่าโดยผู้ใช้สำหรับ worker pool ได้
- สามารถสร้างหลายหน้าภายใน Canvas เดียวได้
- สามารถส่งออกเป็น PDF หลายหน้าไฟล์เดียวได้
- หรือบันทึกเป็นลำดับภาพที่แบ่งออกเป็นหลายไฟล์ได้
ความสามารถด้านกราฟิกที่เหนือกว่า Canvas ของเบราว์เซอร์
- เส้นทาง Bézier รองรับการคำนวณบูลีนและการ interpolate แบบรายจุด
- ความสามารถในการแปลง นอกจาก scale·rotate·translate ของ Canvas พื้นฐานแล้ว ยังมีการแปลงมุมมอง 3D
- การเติมรูปทรงสามารถใช้ Texture แบบเวกเตอร์ได้ นอกเหนือจาก Pattern ที่อิงกับบิตแมป
- การวาดเส้นสามารถติด marker แบบกำหนดเองได้
- การประมวลผลภาพรองรับชุดตัวดำเนินการ CSS filter ครบถ้วน
การควบคุม Typography
- การเรนเดอร์ข้อความรองรับหลายบรรทัดและ การตัดบรรทัดอัตโนมัติ
- มีเมตริกข้อความระดับรายบรรทัด
- สามารถใช้ฟีเจอร์ OpenType เช่น small-caps และ ligature ด้วยไวยากรณ์มาตรฐาน font-variant
- ควบคุมระยะห่างระหว่างตัวอักษร ระยะห่างระหว่างคำ และ leading ได้แบบสัดส่วน
- รองรับ variable fonts และ map ค่าน้ำหนักได้อย่างโปร่งใส
- สามารถใช้ฟอนต์ที่ไม่ใช่ฟอนต์ระบบซึ่งโหลดจากไฟล์โลคัลได้
ตัวอย่างการใช้งาน
- ตัวอย่างการสร้างไฟล์ภาพจะสร้าง
Canvasแล้วใช้getContext("2d")เพื่อรับ 2D context จากนั้นใช้ conic gradient เป็น stroke style เพื่อวาดสี่เหลี่ยม- บันทึกภาพความหนาแน่นสูงด้วย
saveAs("rainbox.png", {density:2}) canvas.pngใช้เป็นรูปย่อของcanvas.toBuffer("png")- สามารถใส่ผลลัพธ์ของ
toDataURL("png")ลงในสตริง HTML ได้ - สามารถบันทึกแบบซิงโครนัสบน main thread ด้วย
saveAsSync("rainbox.pdf")ได้เช่นกัน
- บันทึกภาพความหนาแน่นสูงด้วย
- ตัวอย่างลำดับหลายหน้าใช้
canvas.newPage()เพื่อสร้างหน้าที่มีสีต่างกันแล้วบันทึก- บันทึก PDF หลายหน้าเป็น
all-pages.pdf - ใช้แพตเทิร์น
page-{2}.pngเพื่อสร้างไฟล์หลายไฟล์ เช่นpage-01.png,page-02.png
- บันทึก PDF หลายหน้าเป็น
- ตัวอย่างการเรนเดอร์ Window สร้าง
Window(300, 300)แล้ววาดวงกลมที่ความหนาของเส้นเปลี่ยนตามค่าเฟรมในอีเวนต์draw - ตัวอย่างการผสานกับ Sharp.js แสดงโฟลว์การแปลงผลลัพธ์ Canvas เป็น ออบเจ็กต์ Sharp เพื่อประมวลผลภายหลัง หรือวาดภาพที่สร้างด้วย Sharp ลงบน Canvas ด้วย
loadImage
วิธี benchmark และผลลัพธ์
- benchmark ทดสอบ Skia Canvas โดยแบ่งเป็นสองโหมด
- serial: รอให้งานเรนเดอร์แต่ละงานเสร็จก่อนจึงไปยังรอบถัดไป
- async: เริ่มการทดสอบทุกรอบพร้อมกันและรันแบบขนานด้วยการรองรับ multithreading
- ดูผลลัพธ์ทั้งหมดได้ที่หน้าผลลัพธ์ canvas-benchmarks
- การทดสอบ startup latency ที่ 100 รอบ skia-canvas ทำเวลาได้ต่ำกว่า 1ms ต่อรอบ รวม 33ms
- canvaskit-wasm: 25ms ต่อรอบ รวม 2.47 วินาที
- canvas: 88ms ต่อรอบ รวม 8.77 วินาที
- @napi-rs/canvas: 69ms ต่อรอบ รวม 6.87 วินาที
- การทดสอบ Bézier curves ที่ 20 รอบแสดงความแตกต่างจากการประมวลผลขนานในโหมด async
- skia-canvas serial: 137ms ต่อรอบ รวม 2.74 วินาที
- skia-canvas async: 28ms ต่อรอบ รวม 558ms
- ตัวเปรียบเทียบคือ canvaskit-wasm 790ms, canvas 486ms และ @napi-rs/canvas 230ms
- การทดสอบ SVG to PNG ที่ 100 รอบ canvaskit-wasm ไม่รองรับ ส่วน skia-canvas ทำได้ 58ms ต่อรอบใน serial และ 11ms ต่อรอบใน async
- skia-canvas serial: รวม 5.83 วินาที
- skia-canvas async: รวม 1.08 วินาที
- ผลการทดสอบ scale·rotate ภาพ 50 รอบ
- skia-canvas serial: 100ms ต่อรอบ รวม 5.00 วินาที
- skia-canvas async: 19ms ต่อรอบ รวม 935ms
- ผลการทดสอบข้อความพื้นฐาน 200 รอบ
- skia-canvas serial: 21ms ต่อรอบ รวม 4.26 วินาที
- skia-canvas async: 4ms ต่อรอบ รวม 819ms
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
https://windowjs.org ก็เป็นแนวคิดที่คล้ายกันมาก
มันห่อ Skia แล้วเปิดเผยเป็น Canvas API แต่แทนที่จะใช้ Node ก็ฝัง v8 ไว้ ทำให้ทำงานเป็นรันไทม์ขนาดเล็กมาก
เป็นโปรเจกต์โอเพนซอร์สแรกที่เปิดตัวเมื่อประมาณ 3 ปีก่อน และเคยมีแผนจะเปิดเผย WebGL, audio ฯลฯ เพื่อทำเป็นแพลตฟอร์มเกม JavaScript สำหรับเดสก์ท็อป แต่การพัฒนาหยุดไปเพราะงานและโปรเจกต์อื่น
ดีใจที่มีโปรเจกต์แบบนี้ออกมา ซึ่งทำให้ใช้ Canvas นอกเบราว์เซอร์ได้
บางครั้งก็แค่อยากส่งคำขอเครือข่ายธรรมดา ๆ
สงสัยว่าถ้าฝังแค่ V8 แทน Node.js จะลด ขนาดไบนารี ได้มากแค่ไหน
Node.js ก็ใช้ V8 อยู่แล้ว และก็สงสัยเหมือนกันว่าส่วนใหญ่ของขนาดไบนารี Node มาจากรันไทม์ ไม่ใช่ V8 หรือเปล่า
ลองดูทั้งเว็บไซต์และ GitHub repository แล้ว แต่หาไม่เจอว่าไบนารีของ Window.js ปกติใหญ่กี่ kB หรือ MB
แค่สงสัยว่าไลบรารีแบบนี้เอาไปใช้ทำอะไร
ถ้าเป็นเดสก์ท็อป ก็น่าจะมี ไลบรารีเนทีฟ ที่ดีกว่าสำหรับวาดรูปทรงอยู่ไม่ใช่หรือ
ฝั่งเซิร์ฟเวอร์ก็ต้องสร้าง vector PDF ด้วย เลยใช้ไลบรารี Node ที่ใช้ HTML Canvas API แล้วสร้าง PDF ได้
แบบนี้ดีตรงที่ผลลัพธ์จะตรงกับโน้ตดนตรีที่เรนเดอร์ในเว็บเบราว์เซอร์พอดี
สรุปคือไลบรารีแบบนี้ทำให้ใช้โค้ดซ้ำได้แม้ในสภาพแวดล้อมที่ไม่ใช่เบราว์เซอร์
จุดแข็งใหญ่ของ Skia คือการ ใช้ประโยชน์จากกราฟิกเนทีฟได้อย่างมีประสิทธิภาพ และนี่ก็เป็นเหตุผลที่มันถูกใช้ในเอนจินเบราว์เซอร์ตั้งแต่แรก
แถมการมี API เป้าหมายเพียงตัวเดียวก็เป็นข้อดีด้วย
อีกด้านหนึ่ง ตัวนี้ระดับต่ำกว่า แต่ในอีกด้านหนึ่ง ผมเคยใช้ Puppeteer เหมือนเป็นไลบรารีกราฟิก 2D ด้วย
ได้ทั้ง Canvas 2D, WebGL, WebGPU และ HTML/CSS ทั้งชุด รวมถึงใช้เอฟเฟกต์ข้อความ ภาพพื้นหลัง และ CSS transform ได้
โหลดรูปภาพและวิดีโอได้ด้วย และใช้ฟังก์ชัน screenshot เพื่อทำผลลัพธ์ที่ประกอบกันแล้วออกมาเป็นรูปภาพได้
มันเกินจำเป็นก็จริง แต่สำหรับงานของผมไม่เป็นปัญหา มันใช้งานได้ ข้ามแพลตฟอร์ม และแก้ปัญหาได้
Skia เร็วกว่า Direct2D เล็กน้อย และเร็วกว่า Cairo มาก
มันถูกปรับแต่งมาอย่างมาก และมีแบ็กเอนด์เร่งด้วย GPU ด้วย
Canvas ยังเป็น API วาด 2D ที่พบได้ทั่วไปมากด้วย
iOS ก็มีเวอร์ชันเนทีฟ Android ก็มีเวอร์ชันเนทีฟ และการทำงานด้านการวาดก็คล้ายกันโดยรวม
ใช้ประโยชน์จากสิ่งนี้แล้วได้ประโยชน์มาก
ใน Skia มี CanvasKit ซึ่งเป็น WASM build ที่รองรับ Node รวมอยู่ด้วย https://www.npmjs.com/package/canvaskit-wasm โมดูลนี้ดูเหมือนเป็น Rust binding
อยากรู้ข้อดีข้อเสียของทั้งสองแนวทาง
แค่ถูกสร้างจากโค้ดเบสผสม Rust/C++ ภายในเท่านั้น
ดังนั้นความต่างหลักคือ WASM ต้องมีรันไทม์แยกต่างหาก แต่ไบนารี Node ไม่ต้องมี
นี่เป็น wrapper ที่หุ้ม Rust crate อะไรทำนองนั้นหรือเปล่า?
https://rust-skia.github.io/doc/skia_safe/canvas/struct.Canvas.html
ไม่ค่อยเห็นว่ามีอะไรพิเศษเฉพาะกับ Node
รอของแบบนี้มานานแล้ว
ความพยายามก่อนหน้านี้ที่ทำอะไรคล้ายกันต้องติดตั้ง Node-Gyp ซึ่งเป็นฝันร้ายจริง ๆ
นี่เป็นมากกว่า API เรนเดอร์ธรรมดา: มีข้อความว่า “สามารถเรนเดอร์ไปยังหน้าต่างผ่าน graphics pipeline เนทีฟของ OS และมีเฟรมเวิร์ก UI event แบบเดียวกับเบราว์เซอร์”
อาจเพิ่ม wgpu เพื่อรองรับ WebGPU และเพิ่ม ANGLE เพื่อรองรับ WebGL ได้ด้วย
รองรับ rendering target จำนวนมาก รวมถึง WebGPU และ ANGLE
QPainter จัดการทั้งภาพดิบและหน้าต่าง และยังมีวิดเจ็ตที่ซับซ้อนกว่าพร้อมการรองรับ event ด้วย
อีกอย่างเป็นฟีเจอร์ที่ผมหามาสักพักแล้ว คือใน Qt มี graphicsview ที่สามารถ เรนเดอร์แบบไม่ขึ้นกับความละเอียด บนระนาบกราฟิกที่ใหญ่กว่าหน้าต่างและมีออบเจ็กต์นับล้าน พร้อมจัดการ event ได้ด้วย
ถ้าสนใจการทำ Canvas implementation ที่เข้ากันได้กับ Node ก็มีอยู่หลายตัว
canvaskit-wasm ของโปรเจกต์ Skia ดูเหมือนจะไม่ได้เร่งด้วย GPU: https://github.com/google/skia/tree/main/modules/canvaskit/npm_build
@napi-rs/canvas เป็น binding ที่เร็วที่สุด: https://github.com/Brooooooklyn/canvas?tab=readme-ov-file#performance
node-canvas ใช้ Cairo แทน Skia: https://github.com/Automattic/node-canvas
ตอนต้องการหน้าต่างกราฟิกง่าย ๆ ใน Advent of Code ตอนนี้ใช้อยู่กับ Deno
มี window-example ด้วย: https://skia-canvas.org/#rendering-to-a-window
เพิ่มด้วย
deno add npm:skia-canvasแล้วใส่"nodeModulesDir": "auto"ในdeno.jsonจากนั้นรันdeno install --allow-scriptsก็พอเห็นสิ่งนี้แล้วคาดหวังมากจริง ๆ
น่าจะทำให้เขียนกราฟิกที่ทำงานแบบ standalone ได้และเข้ากันได้กับเว็บสะดวกขึ้นมาก
ไม่ต้อง build จากซอร์สและ generate code ด้วยภาษาสคริปต์ตัวที่สองเพื่อคอมไพล์ไลบรารีสำหรับภาษาสคริปต์ตัวแรกอีกแล้ว
น่าสนใจด้วยว่าจะเหมาะแค่ไหนสำหรับการ เรนเดอร์ Canvas code แบบเรียลไทม์ ในที่อย่าง VS Code หรือสำหรับการรันโค้ดที่มีชั้นแปลงเพื่อให้ไปทำงานบนเว็บ Canvas