CSS Deep-Dive – Ma trận3d() cho thanh cuộn tuỳ chỉnh hoàn hảo về khung hình

Thanh cuộn tuỳ chỉnh cực kỳ hiếm gặp, chủ yếu là do thanh cuộn là một trong số các bit còn lại trên web chiếm phần lớn không thể cách điệu (tôi nhìn bạn, bộ chọn ngày). Bạn có thể sử dụng JavaScript để tạo quảng cáo của riêng mình, nhưng cách làm đó tốn kém, ít sự chân thực và có thể gây ra hiệu ứng chậm. Trong bài viết này, chúng tôi sẽ tận dụng một số ma trận CSS độc đáo để tạo một trình cuộn tuỳ chỉnh mà không yêu cầu JavaScript trong khi cuộn, chỉ cần một số mã thiết lập.

TL;DR

Bạn không quan tâm đến những chuyện nhỏ? Bạn chỉ muốn xem Bản minh hoạ về chú mèo Nyan và tải thư viện? Bạn có thể tìm thấy mã của bản minh hoạ trong Kho lưu trữ GitHub.

LAM;WRA (Dài và mang tính toán học; vẫn sẽ đọc)

Cách đây không lâu, chúng tôi đã tạo công cụ cuộn thị sai (Bạn đã đọc bài viết đó không? Nội dung này thực sự rất hay, rất đáng để bạn dành thời gian tham gia!). Bằng cách đẩy lại các phần tử bằng cách sử dụng CSS 3D biến đổi, các phần tử di chuyển chậm hơn tốc độ cuộn thực tế.

Tóm tắt

Hãy bắt đầu bằng việc tóm tắt cách hoạt động của công cụ cuộn thị sai.

Như minh hoạ trong ảnh động, chúng tôi đã tạo ra hiệu ứng thị sai bằng cách đẩy các phần tử “ngược” trong không gian 3D, dọc theo trục Z. Cuộn tài liệu là một cách theo trục Y. Vì vậy, nếu chúng ta cuộn xuống, giả sử 100px, mỗi thành phần sẽ được dịch lên trên 100px. Điều này áp dụng cho tất cả phần tử, ngay cả những nhà quảng cáo “lúc trước”. Nhưng họ ở xa hơn camera thì chuyển động trên màn hình quan sát được sẽ nhỏ hơn 100 px, tạo ra chuyển động hiệu ứng thị sai mong muốn.

Tất nhiên, di chuyển một phần tử về lại không gian cũng sẽ làm cho phần tử đó trông nhỏ hơn, mà chúng tôi khắc phục bằng cách mở rộng quy mô sao lưu phần tử. Chúng tôi đã tìm ra phép toán chính xác khi xây dựng công cụ cuộn thị sai, nên tôi sẽ không nhắc lại toàn bộ chi tiết.

Bước 0: Chúng ta muốn làm gì?

Thanh cuộn. Đó là những gì chúng ta sẽ xây dựng. Nhưng bạn có thực sự nghĩ rằng về những gì họ làm? Tất nhiên là tôi không làm như vậy. Thanh cuộn là chỉ báo lượng nội dung có sẵn hiển thị và tiến độ bạn như độc giả đã đưa ra. Nếu bạn cuộn xuống, thanh cuộn cũng sẽ cho biết rằng bạn đang tiến triển đến cuối. Nếu tất cả nội dung đều phù hợp vào khung nhìn, thanh cuộn thường bị ẩn. Nếu nội dung có chiều cao gấp 2 lần khung nhìn, thanh cuộn lấp đầy 1⁄2 chiều cao của khung nhìn. Nội dung có giá trị gấp 3 lần chiều cao của khung nhìn chia tỷ lệ thanh cuộn bằng 1⁄3 khung nhìn, v.v. Bạn sẽ thấy mẫu. Thay vì cuộn, bạn cũng có thể nhấp và kéo thanh cuộn để di chuyển qua trang web nhanh hơn. Đó là một số lượng lớn hành vi đáng ngạc nhiên đối với một ứng dụng không dễ thấy như thế. Hãy chiến đấu từng trận một.

Bước 1: Đảo ngược

Chúng ta có thể làm cho các phần tử di chuyển chậm hơn tốc độ cuộn bằng CSS 3D biến đổi như được nêu trong bài viết về cuộn thị sai. Chúng ta cũng có thể đảo ngược không chỉ đường? Hoá ra chúng ta có thể và đó là cách chúng ta tạo ra thanh cuộn tuỳ chỉnh, hoàn hảo. Để hiểu cách hoạt động của quy trình này, chúng ta cần xem xét một số khái niệm cơ bản về CSS 3D.

Để có được bất kỳ loại phép chiếu phối cảnh nào theo ý nghĩa toán học, bạn sẽ có thể cuối cùng sử dụng tọa độ đồng nhất. Tôi sẽ không đi sâu vào chi tiết là gì cũng như tại sao chúng hoạt động, nhưng bạn có thể nghĩ đến chúng như toạ độ 3D với toạ độ bổ sung, thứ tư có tên là w. Chiến dịch này toạ độ phải là 1 trừ trường hợp bạn muốn có độ méo phối cảnh. T4 không cần lo lắng về thông tin chi tiết về w vì chúng tôi sẽ không sử dụng bất kỳ giá trị khác 1. Do đó tất cả các điểm đều nằm trên vectơ 4 chiều [x, y, z, w=1] và do đó cần các ma trận 4x4.

Một trường hợp mà bạn có thể thấy rằng CSS sử dụng toạ độ đồng nhất trong hood là khi bạn xác định ma trận 4x4 của riêng mình trong thuộc tính biến đổi bằng cách sử dụng Hàm matrix3d(). matrix3d nhận 16 đối số (vì ma trận có giá trị là 4x4), chỉ định lần lượt từng cột. Vì vậy, chúng ta có thể sử dụng hàm này để chỉ định thủ công cách xoay, bản dịch, v.v. Nhưng những gì công cụ này cũng cho phép chúng tôi thực hiện khá lộn xộn với toạ độ w đó!

Trước khi có thể sử dụng matrix3d(), chúng ta cần có ngữ cảnh 3D – vì nếu không có Bối cảnh 3D sẽ không có bất kỳ sự biến dạng phối cảnh nào và không cần toạ độ đồng nhất. Để tạo bối cảnh 3D, chúng ta cần một vùng chứa có perspective và một số phần tử bên trong mà chúng ta có thể biến đổi trong đã tạo không gian 3D. Cho ví dụ:

Một đoạn mã CSS làm biến dạng một div bằng cách sử dụng đoạn mã CSS
    phối cảnh.

Các phần tử bên trong vùng chứa phối cảnh do công cụ CSS xử lý như sau:

  • Chuyển từng góc (đỉnh) của một phần tử thành toạ độ đồng nhất [x,y,z,w], liên quan đến vùng chứa phối cảnh.
  • Áp dụng tất cả các phép biến đổi của phần tử dưới dạng ma trận từ phải sang trái.
  • Nếu phần tử phối cảnh có thể cuộn được, hãy áp dụng ma trận cuộn.
  • Áp dụng ma trận phối cảnh.

Ma trận cuộn là bản dịch dọc theo trục y. Nếu chúng ta cuộn xuống theo 400px, tất cả các phần tử cần phải được di chuyển lên 400px. Ma trận phối cảnh là một ma trận "kéo" chỉ vào gần điểm biến mất càng về phía sau trong mô hình 3D không gian của chúng. Điều này đạt được cả hai hiệu ứng làm cho mọi thứ trông nhỏ hơn khi chúng ở xa hơn và cũng làm cho chúng “di chuyển chậm hơn” khi được dịch. Vì vậy, nếu một phần tử bị đẩy lùi, bản dịch 400px sẽ khiến phần tử đó để chỉ di chuyển 300px trên màn hình.

Nếu muốn biết tất cả thông tin chi tiết, bạn nên đọc quy cách của CSS chuyển đổi mô hình kết xuất, nhưng để thực hiện bài viết này, tôi đã đơn giản hoá thuật toán ở trên.

Hộp của chúng ta nằm trong một vùng chứa phối cảnh có giá trị p cho perspective và giả sử vùng chứa có thể cuộn và được cuộn xuống n pixel.

Ma trận phối cảnh nhân thời gian cuộn ma trận lần phần tử biến đổi ma trận
  bằng 4 x 4 ma trận đơn vị có dấu trừ một trên p ở hàng thứ tư
  cột thứ ba nhân với 4 x 4 ma trận đơn vị, trong đó dấu trừ n ở phần thứ hai
  hàng thứ tư cột nhân với ma trận biến đổi phần tử.

Ma trận đầu tiên là ma trận phối cảnh, ma trận thứ hai là cuộn ma trận. Tóm lại: Nhiệm vụ của ma trận cuộn là làm cho một phần tử di chuyển lên khi chúng ta đang cuộn xuống, do đó mang dấu âm.

Tuy nhiên, đối với thanh cuộn, chúng ta muốn phần tử ngược lại – chúng ta muốn phần tử di chuyển xuống khi chúng ta cuộn xuống. Sau đây là một thủ thuật chúng ta có thể dùng: Đảo ngược toạ độ w của các góc hộp. Nếu toạ độ w là -1, tất cả các bản dịch sẽ có hiệu lực theo chiều ngược lại. Vậy làm cách nào để vậy? Công cụ CSS đảm nhiệm việc chuyển đổi các góc của hộp thành toạ độ đồng nhất và đặt w thành 1. Đã đến lúc matrix3d() toả sáng!

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    );
}

Ma trận này không làm gì khác ngoài phủ định w. Vì vậy, khi công cụ CSS chuyển từng góc thành một vectơ có dạng [x,y,z,1], thì ma trận sẽ chuyển đổi giá trị đó thành [x,y,z,-1].

Ma trận đơn vị 4 x 4 có dấu trừ một trên p ở hàng thứ tư
  cột thứ ba nhân với 4 x 4 ma trận đơn vị, trong đó dấu trừ n ở phần thứ hai
  hàng thứ tư nhân với 4 x 4 ma trận đơn vị với dấu trừ một trong
  hàng thứ tư cột thứ tư nhân vectơ bốn chiều x, y, z, 1 bằng bốn
  theo bốn ma trận đơn vị có dấu trừ một trên p ở cột thứ ba của hàng thứ tư,
  trừ n ở hàng thứ hai cột thứ tư và trừ một ở hàng thứ tư
  cột thứ tư bằng vectơ bốn chiều x, y cộng với n, z, trừ z trên
  p trừ 1.

Tôi đã liệt kê một bước trung gian để trình bày hiệu quả của việc biến đổi phần tử ma trận. Nếu bạn không quen với toán ma trận thì cũng không sao. Eureka ở dòng cuối cùng, chúng ta cộng độ lệch cuộn n vào y thay vì trừ đi. Thành phần này sẽ được dịch xuống dưới nếu chúng ta cuộn xuống.

Tuy nhiên, nếu chỉ đưa ma trận này vào ví dụ, thì phần tử đó sẽ không được hiển thị. Điều này là do quy cách CSS yêu cầu bất kỳ đỉnh có w < 0 sẽ chặn không cho hiển thị phần tử. Vì z của chúng ta toạ độ hiện tại là 0 và p là 1, w sẽ là -1.

Thật may là chúng ta có thể chọn được giá trị của z! Để đảm bảo kết quả cuối cùng là w=1, chúng ta cần để đặt z = -2.

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    )
    translateZ(-2px);
}

Hãy xem nào, hộp đã trở lại!

Bước 2: Di chuyển

Giờ thì hộp của chúng ta đã ở đó và trông tương tự như trước đây nếu không có biến đổi. Hiện tại, vùng chứa phối cảnh không cuộn được, vì vậy chúng ta không thể nhìn thấy nó, nhưng chúng ta biết rằng phần tử sẽ đi theo hướng khác khi đã cuộn. Vậy hãy làm cho vùng chứa cuộn, chúng ta phải không? Chúng ta có thể chỉ cần thêm một phần tử dấu cách chiếm dung lượng:

<div class="container">
    <div class="box"></div>
    <span class="spacer"></span>
</div>

<style>
/* … all the styles from the previous example … */
.container {
    overflow: scroll;
}
.spacer {
    display: block;
    height: 500px;
}
</style>

Và bây giờ cuộn hộp! Hộp màu đỏ sẽ di chuyển xuống.

Bước 3: Đặt kích thước

Chúng ta có một phần tử di chuyển xuống khi trang cuộn xuống. Đó là điều khó khăn hơi khác một chút. Bây giờ, chúng ta cần tạo kiểu cho thẻ trông giống như một thanh cuộn và trở nên tương tác hơn một chút.

Thanh cuộn thường bao gồm một "ngón tay cái" và một "bản nhạc", còn bản nhạc thì không luôn hiển thị. Chiều cao của ngón cái tỷ lệ thuận trực tiếp với nội dung hiển thị.

<script>
    const scroller = document.querySelector('.container');
    const thumb = document.querySelector('.box');
    const scrollerHeight = scroller.getBoundingClientRect().height;
    thumb.style.height = /* ??? */;
</script>

scrollerHeight là chiều cao của phần tử có thể cuộn, trong khi scroller.scrollHeight là tổng chiều cao của nội dung có thể cuộn. scrollerHeight/scroller.scrollHeight là một phần của nội dung hiển thị. Tỷ lệ không gian dọc mà các ngón cái phải bằng với tỷ lệ nội dung hiển thị:

ngón tay cái chấm kiểu dấu chấm chiều cao trên scrollerHeight bằng chiều cao cuộn của cuộn
  trên chiều cao cuộn (scroller chấm) khi và chỉ khi chiều cao chấm kiểu ngón cái chấm
  bằng chiều cao cuộn nhân với chiều cao cuộn so với cuộn chấm cuộn
  chiều cao.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

Kích thước của ngón cái là đẹp mắt, nhưng tốc độ di chuyển quá nhanh. Đây là nơi chúng tôi có thể áp dụng kỹ thuật từ công cụ cuộn thị sai. Nếu chúng ta di chuyển phần tử trở lại xa hơn, phần tử sẽ di chuyển chậm hơn trong khi cuộn. Chúng tôi có thể điều chỉnh kích thước bằng cách mở rộng quy mô. Tuy nhiên, chúng ta nên đẩy mạnh bao nhiêu chính xác không? Hãy cùng thử một số phép toán nhé! Đây là lần cuối cùng, tôi hứa hẹn.

Thông tin quan trọng là chúng tôi muốn phần đáy của ngón cái khớp với cạnh dưới của phần tử có thể cuộn khi cuộn hết cỡ xuống. Nói cách khác: Nếu chúng ta cuộn scroller.scrollHeight - scroller.height pixel, chúng ta muốn bản dịch của scroller.height - thumb.height. Đối với mỗi pixel cuộn, chúng tôi muốn ngón cái của chúng ta di chuyển một phần nhỏ pixel:

Hệ số bằng chiều cao của điểm cuộn trừ chiều cao dấu chấm ngón tay cái trên trình cuộn
  chiều cao cuộn chấm trừ chiều cao dấu chấm của cuộn cuộn.

Đó là hệ số tỷ lệ của chúng tôi. Bây giờ, chúng ta cần chuyển đổi hệ số tỷ lệ thành bản dịch dọc theo trục z mà chúng ta đã thực hiện trong phép cuộn thị sai bài viết. Theo phần liên quan trong quy cách: Hệ số tỷ lệ bằng p/(p − z). Chúng ta có thể giải phương trình này cho z thành tính toán lượng giá trị mà chúng ta cần dịch ngón cái dọc theo trục z. Nhưng hãy giữ hãy lưu ý rằng do mưu mẹo phối hợp, chúng ta cần dịch -2px bổ sung dọc theo z. Ngoài ra, xin lưu ý rằng các phép biến đổi của một phần tử được áp dụng phải sang trái, nghĩa là tất cả các bản dịch trước ma trận đặc biệt của chúng tôi sẽ không nhưng tất cả bản dịch sau ma trận đặc biệt của chúng ta đều sẽ bị đảo ngược! Hãy mã hoá nội dung này!

<script>
    // ... code from above...
    const factor =
    (scrollerHeight - thumbHeight)/(scroller.scrollHeight - scrollerHeight);
    thumb.style.transform = `
    matrix3d(
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, -1
    )
    scale(${1/factor})
    translateZ(${1 - 1/factor}px)
    translateZ(-2px)
    `;
</script>

Chúng tôi có một thanh cuộn! Và đó chỉ là một phần tử DOM mà chúng tôi có thể tạo kiểu theo ý muốn. Một điều cần lưu ý là điều quan trọng cần làm về khả năng tiếp cận là làm cho ngón tay cái phản hồi nhấp và kéo, vì nhiều người dùng đã quen tương tác với thanh cuộn theo cách đó. Vì không đăng bài trên blog lâu hơn nữa, tôi sẽ không giải thích thông tin chi tiết về phần đó. Hãy xem mã thư viện để biết thông tin chi tiết nếu bạn muốn xem cách thực hiện.

Còn iOS thì sao?

À, người bạn cũ của tôi trên iOS Safari. Giống như khi cuộn thị sai, chúng ta gặp phải vấn đề tại đây. Do chúng ta đang cuộn trên một phần tử, nên chúng ta cần chỉ định -webkit-overflow-scrolling: touch, nhưng điều đó sẽ gây ra hiện tượng làm phẳng 3D và toàn bộ thì hiệu ứng cuộn sẽ ngừng hoạt động. Chúng ta đã giải quyết vấn đề này bằng công cụ cuộn thị sai bằng cách phát hiện Safari trên iOS và dựa vào position: sticky làm giải pháp, và chúng tôi sẽ làm y như vậy ở đây. Hãy xem bài viết thị sai để làm mới kỷ niệm.

Còn thanh cuộn của trình duyệt thì sao?

Trên một số hệ thống, chúng ta sẽ phải xử lý thanh cuộn cố định gốc. Trước đây, thanh cuộn không thể bị ẩn (ngoại trừ trường hợp có bộ chọn giả không chuẩn). Vì vậy, để che giấu nó, chúng ta phải dùng đến vài vụ tin tặc (không có toán học). Chúng tôi gói phần tử cuộn trong vùng chứa có overflow-x: hidden và làm cho rộng hơn vùng chứa. Thanh cuộn gốc của trình duyệt là hiện đã bị lộ.

Fin

Kết hợp tất cả lại với nhau, giờ đây chúng ta có thể xây dựng một thanh cuộn – giống như thanh cuộn trong Bản minh hoạ về mèo Nyan.

Nếu không nhìn thấy mèo Nyan, tức là bạn đang trải nghiệm một lỗi mà chúng tôi phát hiện và gửi trong khi tạo bản minh hoạ này (nhấp vào ngón cái để làm mèo Nyan xuất hiện). Chrome thực sự giỏi tránh các công việc không cần thiết như vẽ hoặc tạo ảnh động cho những thứ xuất hiện ngoài màn hình. Thật không may là ma trận lừa đảo khiến Chrome nghĩ rằng ảnh GIF mèo Nyan thực sự nằm ngoài màn hình. Hy vọng vấn đề này sẽ sớm được khắc phục.

Vậy là xong. tốn rất nhiều công sức. Cảm ơn bạn đã đọc toàn bộ điều gì đó. Đây là một số thực sự là một thủ đoạn phức tạp để làm được điều này và hiếm khi tốn công sức, trừ phi thanh cuộn tuỳ chỉnh là một phần thiết yếu trong trải nghiệm. Nhưng thật vui khi biết rằng điều đó là có thể, không? Trên thực tế, việc thực hiện một thanh cuộn tuỳ chỉnh cho biết rằng CSS vẫn còn việc cần phải làm. Nhưng đừng lo lắng! Trong tương lai, của Houdini AnimationWorklet sẽ giúp hiệu ứng cuộn liên kết hoàn hảo với khung hình như thế này dễ dàng hơn rất nhiều.