- canvas-confetti là thư viện phía client chạy animation confetti dựa trên canvas trên trang web, hỗ trợ cả cài đặt qua NPM lẫn nhúng trực tiếp bằng CDN
- API
confetti() cơ bản nhận một object tùy chọn để điều chỉnh số hạt, góc, độ lan, tốc độ, trọng lực, màu sắc, hình dạng, vị trí, z-index, v.v., và trong môi trường hỗ trợ Promise có thể nhận được thời điểm animation hoàn tất
- Với người dùng Reduced Motion, thư viện cung cấp tùy chọn
disableForReducedMotion; giá trị này hiện mặc định là false nhưng có thể thay đổi trong bản phát hành major tương lai
- Có thể tạo hình dạng tùy chỉnh dựa trên SVG Path và văn bản; ngoài các hình mặc định
square, circle, star còn có thể tạo hiệu ứng như emoji confetti
confetti.create() tạo một instance cho canvas cụ thể và hỗ trợ các tùy chọn toàn cục như resize, useWorker; tuy nhiên khi useWorker: true, quyền điều khiển canvas được chuyển sang web worker nên thao tác từ main thread sẽ gây lỗi
Cài đặt và cách chạy
- Có thể xem thư viện hoạt động trên trang demo
- Có thể cài đặt dưới dạng gói NPM
npm install --save canvas-confetti
- Trong build của dự án có thể dùng bằng
require('canvas-confetti')
- Thư viện này là client component và không chạy trên Node
- README hướng dẫn cần build dự án bằng công cụ như webpack
- Trong trang HTML có thể nhúng trực tiếp bằng script CDN
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.4/…;
- Khi dùng CDN, nên dùng phiên bản mới nhất tại thời điểm đưa vào dự án; có thể xem đầy đủ các phiên bản ở trang releases
Hỗ trợ Reduced Motion
- Một số người dùng không muốn hoặc muốn giảm chuyển động trên website, và trình duyệt có thể truyền tùy chọn này qua
prefers-reduced-motion
- Nếu dùng tùy chọn
disableForReducedMotion, có thể không hiển thị confetti cho những người dùng gặp khó khăn với animation gây rối mắt
- Tùy chọn này hiện có giá trị mặc định là
false
- Trong bản phát hành major tương lai, dự án đang cân nhắc đổi giá trị mặc định; nếu có ý kiến mạnh, có thể gửi qua issue
- Nếu
disableForReducedMotion được áp dụng và confetti bị vô hiệu hóa, Promise của confetti() sẽ resolve ngay lập tức
API cơ bản và cách Promise hoạt động
- Khi cài qua NPM, có thể
require nó như một client component trong build dự án; với bản CDN, nó được lộ ra dưới dạng hàm confetti trên window
confetti([options]) nhận một object tùy chọn duy nhất
- Nếu có
window.Promise, hàm sẽ trả về Promise báo khi animation kết thúc
- Trong môi trường không có Promise như IE, hàm trả về
null
- Có thể dùng Promise polyfill
- Cũng có thể tự cung cấp implementation Promise bằng dạng
confetti.Promise = MyPromise
- Nếu gọi
confetti nhiều lần trước khi hoàn tất, mỗi lần đều trả về cùng một Promise
- Bên trong, thư viện tái sử dụng cùng một phần tử canvas, tiếp tục animation hiện có và thêm confetti mới vào
- Promise trả về từ mỗi lần gọi sẽ resolve sau khi tất cả animation đã kết thúc
Các tùy chọn chính
particleCount: số confetti được bắn ra, mặc định 50
angle: góc bắn, mặc định 90, trong đó 90 nghĩa là hướng lên trên
spread: phạm vi lan từ tâm, mặc định 45
startVelocity: vận tốc ban đầu, mặc định 45
decay: mức giảm tốc, mặc định 0.9
- Cần nằm giữa 0 và 1; nếu ra ngoài phạm vi này, tốc độ có thể tăng lên
gravity: mức kéo hạt xuống dưới, mặc định 1
0.5 là nửa trọng lực, và vì không bị giới hạn nên cũng có thể làm cho hạt bay lên
drift: mức trôi sang trái/phải, mặc định 0
- Số âm là sang trái, số dương là sang phải
flat: có thể tắt hiệu ứng nghiêng và lắc như confetti 3D ngoài đời, mặc định false
ticks: số lần confetti di chuyển, mặc định 200
origin: vị trí bắt đầu bắn
origin.x: vị trí x trên trang, 0 là bên trái, 1 là bên phải, mặc định 0.5
origin.y: vị trí y trên trang, 0 là phía trên, 1 là phía dưới, mặc định 0.5
colors: mảng chuỗi màu ở định dạng HEX
shapes: mảng hình dạng confetti
- Giá trị dựng sẵn mặc định là
square, circle, star
- Mặc định trộn đều square và circle
- Có thể điều chỉnh tỷ lệ trộn bằng tần suất phần tử trong mảng như
['circle', 'circle', 'square']
scalar: tỷ lệ scale của từng hạt, mặc định 1
zIndex: lớp hiển thị của confetti, mặc định 100
disableForReducedMotion: vô hiệu hóa confetti cho người dùng ưu tiên Reduced Motion
Tạo hình dạng tùy chỉnh
confetti.shapeFromPath({ path, matrix? }) tạo hình confetti tùy chỉnh từ SVG Path string
- Hình dạng dựa trên Path có một số hạn chế
- Mọi path đều được xử lý như hình được tô kín; stroke path chưa được triển khai
- Path bị giới hạn ở một màu duy nhất
- Mọi path đều cần một transform matrix hợp lệ
- Việc tính matrix có chi phí, nên trong lúc phát triển tốt nhất là tính một lần cho mỗi path rồi cache lại
- Matrix luôn giống nhau với cùng một giá trị path
- Khi cập nhật thư viện, nên tạo và cache lại matrix để đảm bảo forward compatibility
- Confetti dựa trên path chỉ dùng được trên trình duyệt hỗ trợ
Path2D
- Giá trị trả về là object
Shape, có thể đưa trực tiếp vào mảng shapes
var triangle = confetti.shapeFromPath({ path: 'M0 10 L5 0 L10 10z' });
confetti({
shapes: [triangle]
});
confetti.shapeFromText({ text, scalar?, color?, fontFamily? }) tạo hình confetti dựa trên văn bản và có thể dùng emoji Unicode tiêu chuẩn
- Hình dạng dựa trên văn bản phù hợp với emoji confetti
- Với confetti rung lắc, ký tự đơn gần vuông thường hợp hơn, đặc biệt là emoji
- Vì văn bản được raster hóa thay vì vẽ lại mỗi lần, nếu thay đổi scale lớn sau khi tạo thì có thể bị mờ
- Nếu định dùng
scalar trong tùy chọn confetti, nên dùng cùng giá trị scalar khi tạo shape
- Các tùy chọn văn bản gồm
text, scalar, color, fontFamily
fontFamily mặc định tuân theo cách render emoji gốc của hệ điều hành và fallback sang sans-serif
- Nếu dùng web font, font phải được tải xong trước khi render confetti
var scalar = 2;
var pineapple = confetti.shapeFromText({ text: '🍍', scalar });
confetti({
shapes: [pineapple],
scalar
});
Canvas tùy chỉnh và render bằng worker
confetti.create(canvas, [globalOptions]) tạo một instance hàm confetti dùng canvas cụ thể
- Hữu ích khi muốn giới hạn confetti chỉ trong một vùng nhất định trên trang
- Mặc định, ngoài việc vẽ lên canvas thì phương thức này không chỉnh sửa canvas
- Kích thước hiển thị bằng CSS không làm thay đổi kích thước ảnh thực của canvas, nên ảnh có thể bị kéo giãn và mờ
- Nếu bật tùy chọn
resize, thư viện sẽ khớp kích thước ảnh canvas và cũng phản ứng với thay đổi kích thước cửa sổ hoặc xoay màn hình di động
- Không nên khởi tạo instance confetti nhiều lần trên cùng một phần tử canvas; hãy giữ lại custom instance đã tạo
-
Tùy chọn toàn cục
resize: quyết định có đặt kích thước ảnh canvas và duy trì theo thay đổi cửa sổ hay không, mặc định false
useWorker: render animation confetti trong web worker bất đồng bộ nếu có thể, mặc định false
- Với mặc định, animation luôn chạy trên main thread
- Nếu trình duyệt hỗ trợ, animation sẽ chạy ngoài main thread để tránh chặn main thread
- Trên trình duyệt không hỗ trợ, giá trị này sẽ bị bỏ qua
disableForReducedMotion: buộc instance confetti này luôn tôn trọng yêu cầu Reduced Motion của người dùng
-
Lưu ý với useWorker: true
- Khi dùng
useWorker: true, quyền điều khiển canvas sẽ được chuyển sang web worker
- Trong trường hợp này, ngoài việc gỡ canvas khỏi DOM thì mọi thao tác từ main thread đều sẽ gây lỗi
- Nếu cần thao tác trực tiếp với canvas, không nên dùng tùy chọn
useWorker
var myCanvas = document.createElement('canvas');
document.body.appendChild(myCanvas);
var myConfetti = confetti.create(myCanvas, {
resize: true,
useWorker: true
});
myConfetti({
particleCount: 100,
spread: 160
});
Dừng animation và các mẫu ví dụ
confetti.reset() dừng animation, xóa toàn bộ confetti và resolve ngay mọi Promise đang chờ
- Instance riêng được tạo bằng
confetti.create() cũng có phương thức reset riêng
confetti();
setTimeout(() => {
confetti.reset();
}, 100);
- Cách chạy cơ bản là gọi
confetti() không có tham số
- Có thể bắn nhiều confetti với
particleCount: 150
- Có thể tạo confetti lan rộng với
spread: 180
- Nếu dùng
Math.random() cho origin, có thể tạo hiệu ứng nổ nhỏ tại vị trí ngẫu nhiên trên trang
- Ví dụ trong README cho thấy mẫu dùng
requestAnimationFrame để liên tục bắn confetti từ mép trái và mép phải trong 30 giây
1 bình luận
Ý kiến trên Hacker News
Mẹo để tạo animation có hiệu năng tốt ở đây là vẽ lên canvas, rồi đặt canvas đó lên trên tất cả các phần tử khác nhưng tắt pointer events để vẫn có thể tương tác với trang
Nhớ lại những ngày đẹp hồi làm web ở trường trung học năm 2015. Tôi đã làm một website nhỏ có confetti để hỏi một bạn nữ có muốn đi homecoming cùng không; nhìn lại thì đúng là cực kỳ nerd
Khi đó, với một đứa trẻ, việc tạo website giống như siêu năng lực vậy. Xét về thời điểm thì chắc không phải package này, nhưng animation khá ổn
Tôi thích những dự án nhỏ thuần túy vui vẻ như thế. Đó cũng là lý do tôi bắt đầu lập trình, và đến giờ vẫn là động lực lớn
Tôi thích đoạn này trên trang demo:
Sự ám ảnh với chi tiết kiểu này khá hiếm, và mỗi khi bắt gặp — dù là trực quan hóa thống kê, đạo cụ phim hay confetti trên website — tôi đều thấy rất đáng quý
Về giải pháp, có lẽ tôi sẽ thử thay đổi chính phân phối ngẫu nhiên. Phải kiểm chứng thực tế, nhưng tôi đoán phân phối ngoài đời có vẻ gần với phân phối Gauss hơn
Tôi đã thêm confetti trên dashboard quản trị xuất hiện khi nhân viên kinh doanh chốt được đơn, và bất ngờ là nó khá vui, tạo động lực
Giá mà hàm reset được gọi là confetti.resetti() thì hay
"confetti.resetti = confetti.reset"Cách này có thể có chút chi phí về kỹ nghệ phần mềm, nhưng như bất kỳ người quan sát tinh ý nào cũng thấy rõ, lợi ích áp đảo hoàn toàn, nên tôi nghĩ cứ làm thôi
Ngoài việc là một thư viện hay và hữu ích, đây còn là ví dụ tốt về module sâu mà John Ousterhout nói đến trong Philosophy of Software Design
Phiên bản cơ bản nhất, tức chức năng gọi confetti, rất dễ dùng, nhưng nếu xem các tùy chọn thì có thể làm được khá nhiều thứ như tuyết, màu cụ thể, nhiều hiệu ứng confetti khác nhau, v.v.
Hay và ấn tượng
Đồng thời tôi không muốn thấy nó chạy trên bất kỳ website nào tôi dùng. Đặc biệt là không muốn confetti đi kèm popup newsletter hay khi thêm sản phẩm vào giỏ hàng
Lạ là hiệu ứng này có thể được dùng khá hiệu quả. Không chắc kiểu toàn màn hình như thế này, nhưng phần mềm quản lý dự án mà một khách hàng tôi ghé gần đây dùng có hiệu ứng này khi đóng một mục: nút chuyển sang màu xanh và kèm hiệu ứng kiểu này
Nó tinh tế nhưng đủ nổi bật, và sau cuộc họp, tôi với một dev khác đều nói “hiệu ứng đó khá ổn nhỉ”. Nó truyền tải cảm giác “tốt, có tiến triển rồi!”
Chỉ cần làm cho nó có thể tùy chọn là được
Một chỗ dùng hợp lý có lẽ là nút Like của YouTube. Có animation đẹp, và trên app di động thiết bị còn rung nữa. Đó là trải nghiệm người dùng rất dễ chịu
Có thể thiết lập trong trình duyệt để ưu tiên giảm chuyển động. Chủ site và maintainer thư viện nên tôn trọng điều này khi triển khai những thứ như confetti. Thư viện này đặc biệt có tùy chọn
disableForReducedMotionCó những nơi hiệu ứng như vậy phù hợp, ví dụ khi hoàn thành game
Chúng tôi dùng thư viện này khi ai đó đạt một điều kiện nhất định. Nó tạo hiệu ứng khá ổn trong luồng onboarding
Cũng có thư viện Party.js: https://party.js.org/
Vậy bên nào nhỏ hơn?
10.4 kB sau minify, 4.2kB sau minify + Gzip
https://bundlephobia.com/package/canvas-confetti@1.9.2
28.3kB sau minify, 7.4kB sau minify + Gzip
https://bundlephobia.com/package/party-js@2.2.0
Tuy nhiên, tôi không rõ bundlephobia hoạt động thế nào. Có thể nó không phản ánh tốt nhất kích thước cuối cùng của package. Có lẽ nó không tính đến code splitting hoặc chỉ import phần cần dùng. Tôi chỉ xem như một cái nhìn tổng quan nhanh và tương đối
Theo Gzip thì confetti có vẻ thắng vài KB, nên nếu không nhất thiết phải vắt kiệt từng KB đó, cả hai đều dùng được tùy theo bên nào có tính năng bạn cần
Script trong bài gốc có vẻ hiệu năng tốt hơn nhiều trên mobile
Thư viện trong bài gốc có vẻ hiệu năng tốt hơn hẳn. Trên chiếc máy tính làm việc cũ của tôi, Party.js chỉ cần click 3 lần là đã cảm thấy hơi trễ
canvas-confetti chỉ bắt đầu trễ khi tôi click liên tục không nghỉ trong vài giây, có lẽ tạo hơn 30 instance confetti và rất nhiều hạt
Tôi giải ô chữ trên downforacross.com, và khi giải xong puzzle thì có confetti
Có lẽ họ có thể dùng một phần code hiệu năng tốt hơn ở đây để cảm giác nhẹ hơn
Nhưng nếu không phải site “vui” hoặc dùng hiếm hoi, tôi không muốn thấy animation kiểu này xuất hiện khắp nơi
Tôi nghĩ không cần cố đưa chữ useful vào tiêu đề