- 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 ความคิดเห็น
ความคิดเห็นจาก Hacker News
เคล็ดลับในการทำ แอนิเมชันที่ประสิทธิภาพดี ตรงนี้คือวาดลงบน canvas แล้วนำ canvas นั้นไปวางไว้ด้านหน้าขององค์ประกอบทั้งหมด แต่ปิด pointer events เพื่อให้ยังโต้ตอบกับหน้าเว็บต่อไปได้
นึกถึงช่วงเวลาดี ๆ ตอนทำเว็บในโรงเรียนมัธยมเมื่อปี 2015 ผมทำ เว็บไซต์เล็ก ๆ ที่มีคอนเฟตตีโปรยลงมา เพื่อจะชวนผู้หญิงคนหนึ่งไปงานโฮมคัมมิงด้วยกัน ย้อนกลับไปดูแล้วเนิร์ดสุด ๆ
ตอนนั้นการสร้างเว็บไซต์ให้เด็กคนหนึ่งรู้สึกเหมือนเป็นพลังวิเศษเลย จากช่วงเวลาน่าจะไม่ใช่แพ็กเกจนี้ แต่แอนิเมชันก็ค่อนข้างดี
ชอบโปรเจกต์เล็ก ๆ ที่สนุกแบบบริสุทธิ์แบบนี้ นั่นคือเหตุผลที่เริ่มเขียนโปรแกรม และจนถึงตอนนี้ก็ยังเป็นแรงผลักดันสำคัญอยู่
ชอบส่วนนี้ในหน้าเดโม:
ความใส่ใจในรายละเอียด แบบนี้พบได้ไม่บ่อย และทุกครั้งที่เจอ ไม่ว่าจะเป็น visualization ทางสถิติ อุปกรณ์ประกอบฉากในหนัง หรือคอนเฟตตีบนเว็บไซต์ ก็รู้สึกว่าน่าทะนุถนอม
ถ้าเป็นทางแก้ ผมน่าจะลองเปลี่ยนตัวการแจกแจงแบบสุ่มเองดู จริง ๆ คงต้องตรวจสอบ แต่เดาว่าการแจกแจงในโลกจริงน่าจะใกล้เคียงกับ Gaussian distribution
ผมเพิ่ม คอนเฟตตีในแดชบอร์ดผู้ดูแลระบบ ที่จะโผล่ขึ้นมาตอนฝ่ายขายปิดการขายได้สำเร็จ ซึ่งสนุกและช่วยสร้างแรงจูงใจได้อย่างคาดไม่ถึง
น่าจะเรียกฟังก์ชัน reset ว่า confetti.resetti() นะ
"confetti.resetti = confetti.reset"ได้แนวทางนี้อาจมีต้นทุนเชิงวิศวกรรมซอฟต์แวร์อยู่บ้าง แต่ผู้สังเกตการณ์ที่รอบคอบทุกคนย่อมเห็นชัดว่าประโยชน์นั้นมากกว่ามหาศาล ดังนั้นผมว่าแค่ทำไปเลยก็ได้
นอกเหนือจากการเป็นไลบรารีที่เจ๋งและมีประโยชน์แล้ว นี่ยังเป็นตัวอย่างที่ดีของ deep module ที่ John Ousterhout พูดถึงใน Philosophy of Software Design
เวอร์ชันพื้นฐานที่สุดคือฟีเจอร์เรียกคอนเฟตตี ซึ่งใช้ง่ายมาก แต่ถ้าดูตัวเลือกต่าง ๆ ก็จะได้อะไรอีกค่อนข้างมาก เช่น หิมะ สีเฉพาะ เอฟเฟกต์คอนเฟตตีหลายแบบ เป็นต้น
เจ๋งและน่าประทับใจ
ขณะเดียวกันก็ไม่อยากเห็นมันทำงานบนเว็บไซต์ใด ๆ ที่ผมใช้ โดยเฉพาะไม่อยากให้มีคอนเฟตตีตามมาพร้อมป๊อปอัปจดหมายข่าว หรือเวลาหยิบสินค้าใส่ตะกร้า
แปลกดีที่เอฟเฟกต์นี้สามารถใช้ได้ผลทีเดียว ไม่แน่ใจเรื่องแบบเต็มจอแบบนี้ แต่ซอฟต์แวร์จัดการโปรเจกต์ที่บริษัทลูกค้ารายหนึ่งที่ไปเยี่ยมเมื่อไม่นานนี้ใช้ จะเปลี่ยนปุ่มเป็นสีเขียวพร้อมเอฟเฟกต์แบบนี้เมื่อปิดรายการ
มันละเอียดอ่อนแต่ก็พอสังเกตได้ และหลังประชุม ผมกับนักพัฒนาอีกคนต่างก็พูดว่า “เอฟเฟกต์นั้นค่อนข้างดีนะ” มันสื่อความรู้สึกว่า “ดีเลย มีความคืบหน้าแล้ว!”
แค่ทำให้ เลือกเปิดปิดได้ ก็พอ
กรณีใช้งานที่สมเหตุสมผลน่าจะเป็นอย่างปุ่มไลก์ของ YouTube มีแอนิเมชันที่ดี และในแอปมือถือเครื่องก็สั่นด้วย เป็น ประสบการณ์ผู้ใช้ ที่รู้สึกดีมาก
สามารถตั้งค่าในเบราว์เซอร์ให้ชอบ ลดการเคลื่อนไหว ได้ ผู้ดูแลเว็บไซต์และผู้ดูแลไลบรารีควรเคารพค่านี้เมื่อต้องทำอะไรอย่างคอนเฟตตี ไลบรารีนี้มีตัวเลือก
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 ในชื่อเรื่องก็ได้