5 คะแนน โดย GN⁺ 2024-04-26 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • canvas-confetti เป็นไลบรารีฝั่งไคลเอนต์สำหรับรันแอนิเมชัน confetti แบบใช้ canvas บนหน้าเว็บ รองรับทั้งการติดตั้งผ่าน NPM และการใส่ผ่าน CDN โดยตรง
  • API พื้นฐาน confetti() ใช้ออบเจ็กต์ตัวเลือกเพียงตัวเดียวเพื่อปรับจำนวนอนุภาค มุม การกระจาย ความเร็ว แรงโน้มถ่วง สี รูปร่าง ตำแหน่ง z-index ฯลฯ และในสภาพแวดล้อมที่รองรับ Promise จะสามารถรับจังหวะที่แอนิเมชันเสร็จสิ้นได้
  • มีตัวเลือก disableForReducedMotion สำหรับผู้ใช้ Reduced Motion โดยค่าเริ่มต้นของค่านี้คือ false แต่มีความเป็นไปได้ว่าจะเปลี่ยนในเมเจอร์รีลีสถัดไป
  • สามารถสร้างรูปร่างแบบกำหนดเองจาก SVG Path และข้อความได้ และนอกจากรูปร่างพื้นฐาน square, circle, star แล้ว ยังทำเอฟเฟกต์อย่าง emoji confetti ได้ด้วย
  • confetti.create() สร้างอินสแตนซ์บน canvas ที่ระบุ และรองรับตัวเลือกระดับ global อย่าง resize, useWorker แต่เมื่อใช้ useWorker: true การควบคุม canvas จะถูกย้ายไปยังเว็บเวิร์กเกอร์ หากจัดการจากเธรดหลักจะเกิดข้อผิดพลาด

วิธีติดตั้งและใช้งาน

  • สามารถดูการทำงานของไลบรารีได้ที่ หน้าเดโม
  • ติดตั้งเป็นแพ็กเกจ NPM ได้
npm install --save canvas-confetti
  • ในการ build โปรเจกต์ สามารถใช้ผ่าน require('canvas-confetti') ได้
  • ไลบรารีนี้เป็น คอมโพเนนต์ฝั่งไคลเอนต์ และไม่ทำงานบน Node
    • README ระบุว่าต้อง build โปรเจกต์ด้วยเครื่องมืออย่าง webpack
  • ในหน้า HTML สามารถใส่ผ่านสคริปต์ CDN ได้โดยตรง
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.4/…;
  • เมื่อใช้ CDN แนะนำให้ใช้เวอร์ชันล่าสุด ณ เวลาที่นำเข้าโปรเจกต์ และดูรายการเวอร์ชันทั้งหมดได้ที่ releases page

การรองรับ Reduced Motion

  • ผู้ใช้บางคนอาจไม่ต้องการหรืออยากลดการเคลื่อนไหวบนเว็บไซต์ และเบราว์เซอร์สามารถส่งค่านี้ผ่าน prefers-reduced-motion ได้
  • เมื่อใช้ตัวเลือก disableForReducedMotion จะสามารถไม่แสดง confetti ให้กับผู้ใช้ที่ใช้งาน แอนิเมชันที่รบกวนสมาธิ ได้ยาก
  • ค่าเริ่มต้นของตัวเลือกนี้ในปัจจุบันคือ false
  • กำลังพิจารณาเปลี่ยนค่าเริ่มต้นในเมเจอร์รีลีสถัดไป และหากมีความเห็นชัดเจนสามารถแจ้งผ่าน issue ได้
  • หาก disableForReducedMotion ถูกใช้งานและทำให้ confetti ถูกปิดใช้งาน Promise ของ confetti() จะ resolve ทันที

API พื้นฐานและพฤติกรรมของ Promise

  • เมื่อติดตั้งผ่าน NPM สามารถ require เป็นคอมโพเนนต์ฝั่งไคลเอนต์ในการ build โปรเจกต์ได้ และในเวอร์ชัน CDN จะถูกเปิดเผยเป็นฟังก์ชัน confetti บน window
  • confetti([options]) รับออบเจ็กต์ตัวเลือกแบบไม่บังคับหนึ่งตัว
  • หากมี window.Promise จะคืนค่า Promise ที่แจ้งว่าแอนิเมชันเสร็จสิ้น
    • ในสภาพแวดล้อมอย่าง IE ที่ไม่มี Promise จะคืนค่า null
    • สามารถใช้ Promise polyfill ได้
    • สามารถระบุ implementation ของ Promise เองได้ในรูปแบบ confetti.Promise = MyPromise
  • หากเรียก confetti หลายครั้งก่อนเสร็จสิ้น จะคืนค่า Promise เดิมทุกครั้ง
  • ภายในจะนำ องค์ประกอบ canvas เดิมกลับมาใช้ซ้ำ และเพิ่ม confetti ใหม่ต่อจากแอนิเมชันที่มีอยู่
  • Promise ที่คืนจากแต่ละการเรียกจะ resolve หลังจากแอนิเมชันทั้งหมดจบลง

ตัวเลือกหลัก

  • particleCount: จำนวน confetti ที่จะยิงออกไป ค่าเริ่มต้น 50
  • angle: มุมในการยิง ค่าเริ่มต้น 90 โดย 90 หมายถึงด้านบน
  • spread: ช่วงการกระจายจากจุดศูนย์กลาง ค่าเริ่มต้น 45
  • startVelocity: ความเร็วเริ่มต้น ค่าเริ่มต้น 45
  • decay: ระดับที่ความเร็วลดลง ค่าเริ่มต้น 0.9
    • ควรคงไว้ระหว่าง 0 ถึง 1 หากอยู่นอกช่วงนี้ความเร็วอาจเพิ่มขึ้นได้
  • gravity: ระดับที่อนุภาคถูกดึงลงด้านล่าง ค่าเริ่มต้น 1
    • 0.5 คือแรงโน้มถ่วงครึ่งหนึ่ง และไม่มีข้อจำกัด จึงสามารถทำให้ลอยขึ้นด้านบนได้ด้วย
  • drift: ระดับการไหลไปทางซ้ายหรือขวา ค่าเริ่มต้น 0
    • ค่าลบหมายถึงซ้าย ค่าบวกหมายถึงขวา
  • flat: ปิดเอฟเฟกต์การเอียงและสั่นไหวเหมือน confetti 3D ในโลกจริงได้ ค่าเริ่มต้น false
  • ticks: จำนวนครั้งที่ confetti เคลื่อนที่ ค่าเริ่มต้น 200
  • origin: ตำแหน่งเริ่มต้นในการยิง
    • origin.x: ตำแหน่ง x ของหน้า 0 คือซ้าย 1 คือขวา ค่าเริ่มต้น 0.5
    • origin.y: ตำแหน่ง y ของหน้า 0 คือด้านบน 1 คือด้านล่าง ค่าเริ่มต้น 0.5
  • colors: อาร์เรย์ของสตริงสีรูปแบบ HEX
  • shapes: อาร์เรย์ของรูปร่าง confetti
    • ค่าพื้นฐานที่มีมาให้คือ square, circle, star
    • ค่าเริ่มต้นผสม square และ circle ในสัดส่วนเท่ากัน
    • สามารถปรับสัดส่วนการผสมด้วยอัตราส่วนในอาร์เรย์ เช่น ['circle', 'circle', 'square']
  • scalar: สเกลของแต่ละอนุภาค ค่าเริ่มต้น 1
  • zIndex: เลเยอร์การแสดงผล confetti ค่าเริ่มต้น 100
  • disableForReducedMotion: ปิดใช้งาน confetti สำหรับผู้ใช้ที่ตั้งค่า Reduced Motion

การสร้างรูปร่างแบบกำหนดเอง

  • confetti.shapeFromPath({ path, matrix? }) สร้างรูปร่าง confetti แบบกำหนดเองจาก SVG Path string
  • รูปร่างที่อิง Path มีข้อจำกัดบางอย่าง
    • path ทั้งหมดจะถูกประมวลผลเป็นรูปทรงที่มีการเติมสี และยังไม่ได้รองรับ stroke path
    • path ถูกจำกัดให้เป็นสีเดียว
    • path ทั้งหมดต้องมี transform matrix ที่ถูกต้อง
    • การคำนวณ matrix มีต้นทุน จึงควรคำนวณครั้งเดียวต่อ path ระหว่างการพัฒนาแล้วแคชไว้
    • matrix จะเหมือนเดิมเสมอสำหรับค่า path เดียวกัน
    • เมื่ออัปเดตไลบรารี แนะนำให้สร้างและแคช matrix ใหม่เพื่อ forward compatibility
    • confetti แบบอิง path จำกัดเฉพาะเบราว์เซอร์ที่รองรับ Path2D
  • ค่าที่คืนมาคือออบเจ็กต์ Shape และสามารถใส่ลงในอาร์เรย์ shapes เพื่อใช้ได้โดยตรง
var triangle = confetti.shapeFromPath({ path: 'M0 10 L5 0 L10 10z' });

confetti({
  shapes: [triangle]
});
  • confetti.shapeFromText({ text, scalar?, color?, fontFamily? }) สร้างรูปร่าง confetti จากข้อความ และสามารถใช้ emoji มาตรฐาน Unicode ได้
  • รูปร่างที่อิงข้อความเหมาะกับ emoji confetti
    • สำหรับ confetti ที่สั่นไหว ตัวอักษรเดี่ยวที่ใกล้เคียงสี่เหลี่ยมจัตุรัส โดยเฉพาะ emoji มักเหมาะสม
    • เนื่องจาก rasterize ไว้แทนการวาดข้อความทุกครั้ง หากเปลี่ยนสเกลให้ใหญ่ขึ้นมากหลังสร้างแล้ว อาจดูเบลอได้
    • หากวางแผนใช้ scalar ในตัวเลือก confetti แนะนำให้ใช้ค่า scalar เดียวกันตอนสร้าง shape
  • ตัวเลือกข้อความรับ text, scalar, color, fontFamily
    • ค่าเริ่มต้นของ fontFamily ทำตามแนวปฏิบัติการเรนเดอร์ emoji ของ OS แบบเนทีฟ และ fallback เป็น sans-serif
    • เมื่อใช้เว็บฟอนต์ ต้องโหลด font ให้เสร็จก่อนการเรนเดอร์ confetti
var scalar = 2;
var pineapple = confetti.shapeFromText({ text: '🍍', scalar });

confetti({
  shapes: [pineapple],
  scalar
});

canvas แบบกำหนดเองและการเรนเดอร์ด้วยเวิร์กเกอร์

  • confetti.create(canvas, [globalOptions]) สร้างอินสแตนซ์ฟังก์ชัน confetti ที่ใช้ canvas ที่ระบุ
  • มีประโยชน์เมื่อต้องการจำกัด confetti ให้อยู่เฉพาะพื้นที่หนึ่งบนหน้า
  • โดยค่าเริ่มต้น เมธอดนี้จะไม่แก้ไข canvas นอกจากการวาดลงบน canvas
  • แม้เปลี่ยนขนาดการแสดงผลของ canvas ด้วย CSS ขนาดภาพจริงของ canvas จะไม่เปลี่ยน จึงอาจถูกยืดและดูเบลอได้
    • หากเปิดตัวเลือก resize ไลบรารีจะปรับขนาดภาพ canvas ให้ตรงกัน และรองรับการเปลี่ยนขนาดหน้าต่างหรือการหมุนหน้าจอบนมือถือด้วย
  • อย่า initialize อินสแตนซ์ confetti ด้วยองค์ประกอบ canvas เดียวกันหลายครั้ง และควรเก็บอินสแตนซ์แบบกำหนดเองที่สร้างไว้
  • ตัวเลือกระดับ global

    • resize: กำหนดว่าจะตั้งขนาดภาพ canvas และรักษาให้สอดคล้องกับการเปลี่ยนแปลงของหน้าต่างหรือไม่ ค่าเริ่มต้น false
    • useWorker: หากเป็นไปได้ จะเรนเดอร์แอนิเมชัน confetti ในเว็บเวิร์กเกอร์แบบอะซิงโครนัส ค่าเริ่มต้น false
    • ในค่าเริ่มต้น แอนิเมชันจะทำงานบนเธรดหลักเสมอ
    • หากเบราว์เซอร์รองรับ แอนิเมชันจะทำงานนอกเธรดหลักเพื่อไม่ให้บล็อกเธรดหลัก
    • ในเบราว์เซอร์ที่ไม่รองรับ ค่านี้จะถูกละเว้น
    • disableForReducedMotion: ทำให้อินสแตนซ์ confetti นั้นเคารพคำขอ Reduced Motion ของผู้ใช้เสมอ
  • ข้อควรระวังของ useWorker: true

    • เมื่อใช้ useWorker: true การควบคุม canvas จะถูกย้ายไปยัง เว็บเวิร์กเกอร์
    • ในกรณีนี้ หากจัดการ canvas จากเธรดหลักนอกเหนือจากการลบออกจาก DOM จะเกิดข้อผิดพลาด
    • หากต้องจัดการ canvas โดยตรง ไม่ควรใช้ตัวเลือก useWorker
    var myCanvas = document.createElement('canvas');
    document.body.appendChild(myCanvas);
    
    var myConfetti = confetti.create(myCanvas, {
      resize: true,
      useWorker: true
    });
    myConfetti({
      particleCount: 100,
      spread: 160
    });
    

การหยุดแอนิเมชันและแพตเทิร์นตัวอย่าง

  • confetti.reset() หยุดแอนิเมชันและลบ confetti ทั้งหมด พร้อม resolve Promise ที่ค้างอยู่ทันที
  • อินสแตนซ์แยกที่สร้างด้วย confetti.create() จะมีเมธอด reset ของตัวเอง
confetti();

setTimeout(() => {
  confetti.reset();
}, 100);
  • การรันพื้นฐานคือเรียก confetti() โดยไม่ส่งอาร์กิวเมนต์
  • สามารถยิง confetti จำนวนมากด้วย particleCount: 150 ได้
  • สามารถสร้าง confetti ที่กระจายกว้างด้วย spread: 180 ได้
  • หากใช้ Math.random() ใน origin จะสร้างเอฟเฟกต์ระเบิดเล็ก ๆ จากตำแหน่งสุ่มบนหน้าได้
  • ตัวอย่างใน README แสดงแพตเทิร์นที่ใช้ requestAnimationFrame เพื่อยิง confetti ต่อเนื่องจากขอบซ้ายและขวาเป็นเวลา 30 วินาที

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

 
GN⁺ 2024-04-26
ความคิดเห็นจาก Hacker News
  • เคล็ดลับในการทำ แอนิเมชันที่ประสิทธิภาพดี ตรงนี้คือวาดลงบน canvas แล้วนำ canvas นั้นไปวางไว้ด้านหน้าขององค์ประกอบทั้งหมด แต่ปิด pointer events เพื่อให้ยังโต้ตอบกับหน้าเว็บต่อไปได้

    • ใช่เลย การ ปิดใช้งาน pointer events มีประโยชน์กว่าที่คิด
    • บอกว่านี่เป็นเคล็ดลับสำหรับแอนิเมชันที่ประสิทธิภาพดี แต่ก็นึกวิธีอื่นในการทำสิ่งนี้ไม่ค่อยออก การทำแบบตรงไปตรงมาจะหน้าตาเป็นยังไงนะ?
  • นึกถึงช่วงเวลาดี ๆ ตอนทำเว็บในโรงเรียนมัธยมเมื่อปี 2015 ผมทำ เว็บไซต์เล็ก ๆ ที่มีคอนเฟตตีโปรยลงมา เพื่อจะชวนผู้หญิงคนหนึ่งไปงานโฮมคัมมิงด้วยกัน ย้อนกลับไปดูแล้วเนิร์ดสุด ๆ
    ตอนนั้นการสร้างเว็บไซต์ให้เด็กคนหนึ่งรู้สึกเหมือนเป็นพลังวิเศษเลย จากช่วงเวลาน่าจะไม่ใช่แพ็กเกจนี้ แต่แอนิเมชันก็ค่อนข้างดี
    ชอบโปรเจกต์เล็ก ๆ ที่สนุกแบบบริสุทธิ์แบบนี้ นั่นคือเหตุผลที่เริ่มเขียนโปรแกรม และจนถึงตอนนี้ก็ยังเป็นแรงผลักดันสำคัญอยู่

    • สำเร็จไหม? เธอตอบตกลงหรือเปล่า?
  • ชอบส่วนนี้ในหน้าเดโม:

    If you happened to get curious and changed the particle count to 400 or so, you saw something disappointing. An even "flattened cone" look to the confetti, making it look way too perfect and ruining the illusion.

    ความใส่ใจในรายละเอียด แบบนี้พบได้ไม่บ่อย และทุกครั้งที่เจอ ไม่ว่าจะเป็น visualization ทางสถิติ อุปกรณ์ประกอบฉากในหนัง หรือคอนเฟตตีบนเว็บไซต์ ก็รู้สึกว่าน่าทะนุถนอม
    ถ้าเป็นทางแก้ ผมน่าจะลองเปลี่ยนตัวการแจกแจงแบบสุ่มเองดู จริง ๆ คงต้องตรวจสอบ แต่เดาว่าการแจกแจงในโลกจริงน่าจะใกล้เคียงกับ Gaussian distribution

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

  • น่าจะเรียกฟังก์ชัน reset ว่า confetti.resetti() นะ

    • เพราะเป็น JavaScript อย่างน้อยในเครื่องก็แก้ง่าย ๆ ด้วย "confetti.resetti = confetti.reset" ได้
      แนวทางนี้อาจมีต้นทุนเชิงวิศวกรรมซอฟต์แวร์อยู่บ้าง แต่ผู้สังเกตการณ์ที่รอบคอบทุกคนย่อมเห็นชัดว่าประโยชน์นั้นมากกว่ามหาศาล ดังนั้นผมว่าแค่ทำไปเลยก็ได้
    • ควรจ้างคนนี้ ถ้ามีงานอยู่แล้วก็ควรให้คุกกี้อย่างน้อยสักชิ้น
    • อาจลองทำ PR ดูก็ได้
  • นอกเหนือจากการเป็นไลบรารีที่เจ๋งและมีประโยชน์แล้ว นี่ยังเป็นตัวอย่างที่ดีของ deep module ที่ John Ousterhout พูดถึงใน Philosophy of Software Design
    เวอร์ชันพื้นฐานที่สุดคือฟีเจอร์เรียกคอนเฟตตี ซึ่งใช้ง่ายมาก แต่ถ้าดูตัวเลือกต่าง ๆ ก็จะได้อะไรอีกค่อนข้างมาก เช่น หิมะ สีเฉพาะ เอฟเฟกต์คอนเฟตตีหลายแบบ เป็นต้น

  • เจ๋งและน่าประทับใจ
    ขณะเดียวกันก็ไม่อยากเห็นมันทำงานบนเว็บไซต์ใด ๆ ที่ผมใช้ โดยเฉพาะไม่อยากให้มีคอนเฟตตีตามมาพร้อมป๊อปอัปจดหมายข่าว หรือเวลาหยิบสินค้าใส่ตะกร้า

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

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

    • https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pref...

      สามารถตั้งค่าในเบราว์เซอร์ให้ชอบ ลดการเคลื่อนไหว ได้ ผู้ดูแลเว็บไซต์และผู้ดูแลไลบรารีควรเคารพค่านี้เมื่อต้องทำอะไรอย่างคอนเฟตตี ไลบรารีนี้มีตัวเลือก disableForReducedMotion โดยเฉพาะ

    • มีบางที่ที่เอฟเฟกต์แบบนี้เหมาะอยู่ เช่น ตอนเล่นเกมจบ

    • เราใช้ไลบรารีนี้เมื่อมีคนผ่านคุณสมบัติบางอย่าง มันให้เอฟเฟกต์กับ โฟลว์ onboarding ได้ค่อนข้างดี

  • ยังมีไลบรารี Party.js ด้วย: https://party.js.org/

    • ถ้าอย่างนั้นอันไหนเล็กกว่ากัน?
      10.4 kB เมื่อย่อ, 4.2kB เมื่อย่อ + Gzip
      https://bundlephobia.com/package/canvas-confetti@1.9.2

      28.3kB เมื่อย่อ, 7.4kB เมื่อย่อ + Gzip
      https://bundlephobia.com/package/party-js@2.2.0

      แต่ก็ไม่ค่อยรู้ว่า bundlephobia ทำงานอย่างไร อาจไม่ได้แสดงขนาดสุดท้ายของแพ็กเกจได้ดีที่สุด บางทีอาจไม่สะท้อน code splitting หรือการนำเข้าเฉพาะสิ่งที่ต้องใช้ ผมมองว่าเป็นภาพรวมแบบเร็ว ๆ คร่าว ๆ เท่านั้น

      ถ้าดูตาม Gzip ฝั่ง confetti น่าจะชนะอยู่ไม่กี่ KB ดังนั้นถ้าไม่จำเป็นต้องรีดเค้น KB พวกนั้นจริง ๆ ทั้งสองตัวก็น่าจะใช้ได้ ขึ้นอยู่กับว่าฟีเจอร์ที่ต้องการอยู่ฝั่งไหน

    • สคริปต์ในต้นฉบับดู ประสิทธิภาพดีกว่ามาก บนมือถือ

    • ไลบรารีในต้นฉบับดูประสิทธิภาพดีกว่ามาก บนคอมพิวเตอร์ทำงานเก่าของผม Party.js แค่คลิก 3 ครั้งก็เริ่มรู้สึกหน่วงเล็กน้อยแล้ว
      ส่วน canvas-confetti จะเริ่มหน่วงก็ตอนคลิกรัว ๆ ไม่หยุดอยู่หลายวินาที จนน่าจะสร้างอินสแตนซ์คอนเฟตตีเกิน 30 ตัวพร้อมอนุภาคจำนวนมากแล้ว

  • ผมเล่นครอสเวิร์ดบน downforacross.com แล้วเมื่อแก้ปริศนาเสร็จก็จะมีคอนเฟตตีออกมา
    อาจใช้บางส่วนของ โค้ดที่ประสิทธิภาพดีกว่า ที่นี่เพื่อให้รู้สึกเบาขึ้นได้
    แต่ถ้าไม่ใช่ไซต์ “สนุก ๆ” หรือการใช้งานที่เกิดไม่บ่อย ผมก็ไม่อยากเห็นแอนิเมชันแบบนี้โผล่ไปทุกที่

  • ผมว่าไม่จำเป็นต้องใส่คำว่า useful ในชื่อเรื่องก็ได้

    • ถ้าเป็นเครื่องมือสร้างแรงจูงใจและวิธีตรวจว่าคอมไพล์โค้ดแล้วเป็นไง: https://squint-cljs.github.io/squint/
    • ใช่ แต่เพราะคำนั้นนี่แหละที่ทำให้สนใจจริง ๆ และพอพบว่าจริง ๆ แล้วไม่ได้มีประโยชน์เท่าไหร่ก็เลยขำ แนะนำเลย
    • มีประโยชน์พอ ๆ กับคอนเฟตตีในโลกจริง นั่นคือมีประโยชน์ 100%