7 điểm bởi GN⁺ 2024-05-06 | 1 bình luận | Chia sẻ qua WhatsApp
  • Khi CSS ngày càng hỗ trợ nhiều hàm toán học, cách tiếp cận tính trạng thái animation bằng biến tick thời gian --t thay cho keyframes lại trở nên thực dụng
  • Kết hợp @property --t với animation gốc dài 86400000ms có thể tạo ra thời gian tăng theo mili giây, rồi liên kết nó với các phép tính thuộc tính như rotate, translate
  • Dùng step() có thể giới hạn frame rate như 8fps, 24fps, 60fps; nhờ tối ưu hóa của trình duyệt, tần suất cập nhật cao hơn 60fps thường cũng không gây vấn đề
  • Dùng min(), mod(), sin(), pow(), round() để giới hạn, lặp, dao động và easing giá trị thời gian, qua đó biểu diễn custom easing và dịch chuyển vị trí mà không cần keyframes
  • css-doodle cung cấp các biểu thức rút gọn như @t, @T, @TS, giúp viết các demo nhiều công thức toán ngắn hơn và thử nghiệm nhanh hơn

Tạo tick thời gian làm biến CSS

  • Cách tiếp cận trước đây bị giới hạn phạm vi áp dụng vì khó thực hiện tính toán toán học phức tạp trong CSS
  • CSS hiện nay hỗ trợ các hàm toán học cần thiết như mod(), round(), hàm lượng giác, nên có thể thử lại animation dựa trên thời gian
  • Có thể cần bật Experimental feature flags để xem demo

Cách hoạt động cơ bản

  • CSS không thể trực tiếp khởi chạy timer như JavaScript, nhưng có thể dùng CSS Houdini API để định nghĩa biến tùy chỉnh --t nhằm theo dõi thời gian theo đơn vị mili giây
  • Ví dụ cơ bản khai báo @property --t<integer>, cho nó thay đổi từ 0 đến 86400000 trong @keyframes tick, rồi áp dụng animation 86400000ms linear infinite cho :root
  • --t tăng thêm 1 mỗi 1 mili giây và đạt 1000 sau 1 giây
  • Dùng counter() có thể hiển thị giá trị --t hiện tại trên màn hình
  • Khi liên kết phép tính dựa trên --t với thuộc tính, chẳng hạn rotate: calc(var(--t) * .001turn), giá trị sẽ thay đổi theo thời gian và trở thành animation

Điều khiển frame rate

  • Với animation mượt, tần suất cập nhật 60fps là đủ
  • Nhờ tối ưu hóa render của trình duyệt, tần suất cập nhật cao hơn 60fps thường không gây vấn đề
  • Khi cần, có thể dùng step() trong animation-timing-function để trực tiếp giới hạn frame rate
    • 8fps: step(calc(86400000/(1000/8)))
    • 24fps: step(calc(86400000/(1000/24)))
    • 60fps: step(calc(86400000/(1000/60)))

Biến giá trị thời gian tăng dần thành giá trị animation

  • Giá trị --t tiếp tục tăng theo một chiều
  • Giá trị angle vượt quá 360deg cũng không sao, nhưng không phải thuộc tính CSS nào cũng xử lý giá trị theo kiểu tuần hoàn
  • translate: calc(var(--t) * .001px) sẽ khiến hộp tiếp tục di chuyển sang phải
  • Dừng bằng min()

    • Có thể dùng min() để dừng ngay khi đạt tới một offset nhất định
    • translate: min(270px, calc(var(--t) * .5px)) giới hạn giá trị dịch chuyển ở 270px
    • Muốn kiểm soát chính xác thời lượng, có thể giới hạn chính --t
    • translate: calc(min(3000, var(--t)) * (270px / 3000))
    • Phép tính này khiến phần tử di chuyển 270px trong 3 giây
  • Lặp bằng mod()

    • Dùng mod() nếu muốn hộp di chuyển sang phải rồi bắt đầu lại từ đầu
    • Có thể lặp offset bằng translate: calc(mod(var(--t)/4, 270) * 1px)
  • Tạo chuyển động qua lại bằng sin()

    • Chuyển động qua lại có thể biểu diễn bằng sin()
    • Liên kết giá trị thời gian với hàm lượng giác như translate: calc(sin(mod(var(--t)/135, 270)) * 135px)

Hàm custom easing

  • Với các hàm toán học CSS và biến --t, có thể tạo custom easingcubic-bezier() có thể khó đạt được
  • Trước hết giới hạn --t thành giá trị trong khoảng 0 đến 1
    • --t01: calc(min(1000, var(--t)) / 1000) thay đổi từ 0 đến 1 trong 1 giây
  • ease-out-cubic

    • ease-out-cubic được cấu thành bởi công thức 1 - pow(1 - t, 3)
    • Sau khi tạo giá trị --ease-out-cubic, liên kết nó với translate: calc(var(--ease-out-cubic) * 270px)
  • ease-out-elastic

    • ease-out-elastic được cấu thành bởi công thức pow(2, -10t) * sin((10t - .75) * 2/3 * PI) + 1
    • Tạo giá trị --ease-out-elastic theo cùng cách rồi nhân với translate để áp dụng cho chuyển động 270px

Rút gọn công thức bằng css-doodle

  • Công thức càng phức tạp, var()calc() càng khiến code khó đọc
  • Phiên bản mới nhất của css-doodle đã thêm hàm @t biểu diễn --t, có thể nhận trực tiếp các công thức toán học đơn giản trong đối số
  • Biểu thức rotate được rút gọn như sau
    • Cũ: rotate: calc(mod(var(--t) / 1000, 10) * 5deg)
    • css-doodle: rotate: @t(/1000, %10, *5deg)
  • Có thể tạo animation dựa trên toán học bằng code ngắn mà không cần viết keyframes
  • Thay đổi tham số là có thể nhanh chóng thử nghiệm kết quả mới
  • @T và @TS

    • Ngoài @t, chữ hoa @T biểu thị time ticks kể từ đầu ngày
    • @TS là shorthand của @t(/1000) và theo dõi thời gian theo đơn vị giây
    • Ví dụ đồng hồ có thể xem trên CodePen
    • Kim giây: rotate: @TS(*6, %360deg)
    • Kim phút: rotate: @TS(/60, *6, %360deg)
    • Kim giờ: rotate: @TS(/60, /12, *6, %360deg)
  • Tạo kim giây nhảy bằng round()

    • Có thể dùng round() để khiến kim giây chuyển động theo kiểu nhảy từng nấc
    • Tham số thứ ba của round() chỉ định khoảng làm tròn
    • Trong đồng hồ, mỗi bước là 360 / 60 = 6deg, nên dùng rotate: round(down, @TS(*6, %360deg), 6deg)

Demo và điểm áp dụng

  • Ví dụ animation đồng thời màu sắc và vị trí có thể xem trên CodePen
  • Ví dụ animation màu chữ có trên CodePen
  • Keyframes có thể trông trực quan hơn, nhưng trong các cảnh demo có nhiều phép tính toán học và biến đầu vào, cách dùng thời gian làm biến dễ tạo ra nhiều kết quả đa dạng hơn
  • Bản cập nhật ngày 17/05/2024 đã thêm demo animation màu chữ

1 bình luận

 
GN⁺ 2024-05-06
Ý kiến trên Hacker News
  • Nếu muốn điều khiển tiến độ bằng JS thì cũng nên dùng giá trị animation-delay âm
    Ví dụ animation-delay: -1500ms sẽ khiến animation bắt đầu ngay lập tức nhưng nhảy tới mốc 1,5 giây
    Nếu điều khiển giá trị này bằng JS thì về cơ bản có thể scrub animation CSS từ JS, đồng bộ toàn bộ animation với vòng lặp tick compute-update-render kiểu game engine
    https://developer.mozilla.org/en-US/docs/Web/CSS/animation-d...

    • Vậy thì chẳng phải cũng có thể làm animation dựa trên offset cuộn kiểu iOS sao?
  • Cách tiếp cận này sẽ nhanh chóng chạm trần khi chỉ cần phức tạp hơn một chút so với hàm easing đơn giản hay keyframe cơ bản cho một hai kênh
    Tôi đã dùng Theatre.js trong vài năm qua và khá hài lòng
    Đây là thư viện tách thành UI studio để chỉnh sửa keyframe và đường cong Bézier trên timeline, cùng một runtime nội suy các keyframe đó theo timeline
    Nếu cần đồng bộ nhiều animation với nhau thì đáng để thử
    https://www.theatrejs.com/

  • Rất ấn tượng, nhưng cũng có cảm giác là cuối cùng chúng ta mới tiệm cận được những gì Flash đã làm được từ vài chục năm trước
    Không phải tôi muốn Flash quay lại, nhưng giá mà có những công cụ thân thiện hơn với người dùng để tạo animation CSS

  • CSS giờ đã hỗ trợ khá đầy đủ các hàm toán học như mod(), round(), và các hàm lượng giác
    CSS không thể khởi động timer như JavaScript, nhưng giờ có thể dùng CSS Houdini API để định nghĩa biến tùy chỉnh theo dõi thời gian ở mức mili giây
    Nhưng nếu đã có JavaScript thì tại sao lại cần tất cả những thứ này?
    Chẳng phải tầng vẽ nên tập trung vào các nguyên thủy vẽ sao? Tại sao lại nhét thêm nhiều tính năng tầng cao hơn vào đó?

    • Cần nhớ rằng animation CSS được tăng tốc bằng phần cứng
      Nếu chạy các hàm lượng giác trong JS để làm animation thì có lẽ bạn cũng đang thao tác DOM để cập nhật UI mỗi mili giây, và chi phí đó rất lớn
    • CSS không phải là tầng vẽ
      Việc vẽ do DOM dẫn dắt, DOM này được cấu thành từ HTML hoặc được JS cập nhật
      CSS chỉ là một lớp phủ lên trên, truyền tham số cho engine vẽ về việc cần làm gì với DOM và ghi đè các giá trị mặc định
      Nó có thể thêm các khối DOM, thay đổi hành vi bố trí bằng các thuật toán phức tạp, và ngay cả margin thực ra cũng không hề đơn giản
      Tôi không cho đó là lời giải tuyệt vời, nhưng có vẻ nó bắt đầu từ nhu cầu không phải cập nhật mã HTML chỉ để đổi màu trong lúc render DOM
      Có lẽ vì việc cập nhật mã HTML khi đó hoàn toàn không thú vị, nhưng tôi nghĩ ngay từ đầu người ta đã giải sai bài toán
      Cũng phải nhớ đó là thời của “webmaster”
    • Một lợi ích hiện ra ngay là tách biệt logic ứng dụng và phần trình bày
      Sẽ dễ đọc hơn nếu trong logic fetch không trộn lẫn logic diễn tiến hình học của phần tử DOM loader, chẳng hạn tính góc quay và áp inline style
      Tôi biết bây giờ cách để riêng socola và bơ đậu phộng không còn được ưa chuộng, nhưng những quy ước như vậy giúp vạch ra ranh giới để dễ suy luận code nên nằm ở đâu và cần đặt ở đâu
    • JS về cơ bản là đơn luồng, dĩ nhiên có worker, nhưng bất cứ thứ gì có thể đẩy công việc sang nơi khác để giữ UI ở tốc độ khung hình ổn định đều là điều tốt
    • Hãy đợi tới khi CSS có WebSocket!
      Nói đùa vậy thôi, CSS mang tính khai báo, và chuyện có những bài toán giải được bằng cả JS lẫn CSS là điều tự nhiên
      Tôi thực sự háo hức với các tính năng mới được thêm vào CSS gần đây, và việc có ít mã phải thông dịch hơn là điều tốt
  • CSS @property tùy chỉnh dùng trong bài rất tuyệt, và mức hỗ trợ trình duyệt đã là 88% [1]
    Tuy nhiên cần cẩn thận với khác biệt hành vi giữa các trình duyệt khi đặt initial-value dự phòng
    Chrome dùng initial-value nếu biến CSS chưa được định nghĩa hoặc được gán giá trị không hợp lệ
    Firefox chỉ dùng initial-value khi biến chưa được định nghĩa
    Với hầu hết dự án thì không thành vấn đề, nhưng trong một dự án gần đây tôi đã phải đặt giá trị mặc định bằng JavaScript trên Firefox để khớp khác biệt triển khai giữa các trình duyệt
    [1] https://caniuse.com/?search=%40property

    • Nếu muốn thử demo trên Firefox thì vào about:config rồi bật layout.css.properties-and-values.enabled
    • Khi cần, có thể đặt giá trị thuộc tính từ JavaScript như sau
      Lấy giá trị thuộc tính được định nghĩa trong :root{}:
      getComputedStyle(document.documentElement).getPropertyValue("--some-custom-css-var");
      Đặt giá trị thuộc tính:
      document.documentElement.style.setProperty("--some-custom-css-var", "some value");
      Trên các trình duyệt chưa bật hỗ trợ CSS @property, có thể dùng như một polyfill
  • Trên Android Chrome mới nhất, khá thú vị là các demo trước mod() vẫn chạy ngay cả khi không bật Experimental feature flags, nhưng mod()sin() thì không có tác dụng gì

  • Tôi đã muốn có thứ này một thời gian vì muốn làm kiểu box-breathing
    Tôi muốn có thể điều chỉnh toàn bộ các giai đoạn hít vào - giữ - thở ra - giữ cho phù hợp với cơ thể và hiệu ứng mong muốn
    Nếu không có cách như trên trang này thì tôi không tìm ra cách nào để làm điều đó mà không cần JS, hoặc không rơi vào mức độ phức tạp vô lý
    Thật là stylesheet! Các demo trên trang này thực sự rất sáng tạo

    • Chẳng phải đó chỉ là đường cong animation của CSS sao?
  • Bài viết rất hay
    Chrome vẫn chưa phát hành chính thức hỗ trợ CSS mod(), nên nếu không dùng bản preview của Chrome thì phần lớn ví dụ trên trang sẽ không có animation
    Nếu không cần điều khiển theo tick mà chỉ muốn animation CSS mượt trên mọi trình duyệt thì kỹ thuật FLIP rất hữu ích
    https://medium.com/outsystems-experts/flip-your-60-fps-anima...

  • Animation ở cuối trang rất ấn tượng
    Nó trông giống một video thống nhất hơn là các phần tử riêng lẻ

  • https://RTCode.io là một web playground thời gian thực chạy mà không cần refresh hay reset trạng thái, nên có thể rất hữu ích cho những ai làm việc với animation hay transition CSS
    Demo có tại https://RTCode.io
    Để nhập một Pen, chỉ cần đổi hostname trong URL thành xcodepen.io

    • Có ai từng dùng RTCode chưa? Trông khá thú vị, nhưng nhìn trang chủ thì hơi khó hiểu là nó chỉ là bản thay thế CodePen có hỗ trợ thời gian thực, hay còn có thể dùng trong production nữa