5 điểm bởi GN⁺ 2024-04-26 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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

 
GN⁺ 2024-04-26
Ý 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

    • Đúng vậy. Vô hiệu hóa pointer events hữu ích một cách bất ngờ trong nhiều trường hợp
    • Bạn gọi đây là mẹo cho animation hiệu năng tốt, nhưng tôi không nghĩ ra được cách nào khác để triển khai kiểu này. Một cách triển khai ngây thơ sẽ trông như thế nào?
  • 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

    • Có thành công không? Bạn ấy có nói yes không?
  • Tôi thích đoạn này trên trang demo:

    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.

    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

    • Vì là JavaScript nên ít nhất ở local có thể sửa đơn giản bằng "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
    • Phải tuyển người này. Nếu họ đã có việc rồi thì ít nhất cũng nên cho họ cái bánh quy
    • Cũng có thể tạo PR thử
  • 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

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

      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 disableForReducedMotion

    • Có 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 đề

    • Dùng làm công cụ tạo động lực và cách kiểm tra code đã compile chưa thì sao: https://squint-cljs.github.io/squint/
    • Đúng. Dù vậy chính từ đó khiến tôi thật sự thấy tò mò, rồi buồn cười vì thực ra nó không hữu ích lắm. Khuyến nghị
    • Hữu ích ngang confetti ngoài đời, tức là hữu ích 100%