CSS 심층 분석 - 완벽한 프레임의 맞춤 스크롤바를 위한 trix3d()

맞춤 스크롤바는 극히 드물며, 가장 큰 이유는 스크롤바가 웹에서 거의 스타일 지정이 불가능한 나머지 비트 중 하나이기 때문입니다 (날짜 선택기를 보고 있습니다). JavaScript를 사용하여 직접 빌드할 수 있지만, 비용이 많이 들고 정확도가 낮고 속도가 느려질 수 있습니다. 이 문서에서는 몇 가지 색다른 CSS 매트릭스를 활용하여 스크롤하는 동안 JavaScript가 필요 없고 일부 설정 코드만 필요한 맞춤 스크롤러를 빌드합니다.

요약

사소한 것에 신경 안 써? Nyan cat 데모를 보고 라이브러리를 가져오려면 데모 코드는 GitHub 저장소에서 찾을 수 있습니다.

LAM;WRA (긴 수학적이며 계속 읽음)

Google에서는 예전에 패럴랙스 스크롤러를 빌드했습니다 (도움말을 읽어보셨나요? 정말 훌륭하고 시간을 할애할 가치가 있습니다.) CSS 3D 변환을 사용하여 요소를 다시 푸시하면 요소가 실제 스크롤 속도보다 느리게 움직였습니다.

요약

먼저 시차 스크롤러의 작동 방식을 요약해 보겠습니다.

애니메이션에서 볼 수 있듯이 3D 공간에서 Z축을 따라 요소를 '뒤로' 밀어 시차 효과를 얻었습니다. 문서 스크롤은 실질적으로 Y축을 따라 이동하는 것입니다. 따라서 아래로 스크롤하면(예: 100px) 모든 요소가 위쪽으로 100px로 번역됩니다. 이는 '더 멀리' 있는 요소를 포함하여 모든 요소에 적용됩니다. 그러나 카메라에서 더 멀리 떨어져 있기 때문에 화면에서 관찰된 화면 움직임은 100px 미만이 되어 원하는 시차 효과를 얻습니다.

물론 요소를 다시 공간으로 이동하면 요소가 더 작게 표시됩니다. 요소의 크기를 다시 조정하여 수정합니다. 정확한 계산은 패럴랙스 스크롤러를 빌드할 때 확인했으므로 모든 세부정보를 반복하지는 않겠습니다.

0단계: 수행할 작업

스크롤바 그것이 바로 우리가 빌드할 것입니다. 하지만 어른들이 무슨 일을 하는지 생각해 본 적 있으세요? 확실히 하지 않았어요. 스크롤바는 사용 가능한 콘텐츠 중 현재 표시되는 과 독자의 진행률을 나타냅니다. 아래로 스크롤하면 스크롤바도 끝을 향해 진행 중임을 나타냅니다. 모든 콘텐츠가 표시 영역에 맞으면 스크롤바는 일반적으로 숨겨집니다. 콘텐츠 높이가 표시 영역 높이의 2배라면 스크롤바는 표시 영역 높이의 1⁄2을 채웁니다. 표시 영역 높이의 3배에 해당하는 콘텐츠의 경우 스크롤바가 표시 영역의 1⁄3 등으로 조정됩니다. 패턴이 표시됩니다. 스크롤하는 대신 스크롤바를 클릭하고 드래그하여 사이트 내에서 더 빠르게 이동할 수도 있습니다. 이와 같이 눈에 띄지 않는 요소에는 놀라울 정도로 많은 동작이 적용됩니다. 한 번에 하나씩 싸우죠.

1단계: 거꾸로 만들기

시차 스크롤 도움말에 설명된 대로 CSS 3D 변환을 사용하면 요소가 스크롤 속도보다 느리게 움직이도록 할 수 있습니다. 방향을 반대로 할 수도 있을까요? 프레임에 맞는 완벽한 맞춤 스크롤바를 빌드할 수 있는 것으로 확인되었습니다. 작동 방식을 이해하려면 먼저 CSS 3D 기본 사항을 몇 가지 살펴보겠습니다.

수학적 의미에서 모든 원근 투영을 얻기 위해서는 동일 좌표를 사용하게 될 가능성이 가장 높습니다. 그것이 무엇인지, 왜 작동하는 지 자세히 설명하지는 않겠지만, 이 함수를 추가로 네 번째 좌표인 w가 추가된 3D 좌표라고 생각하면 됩니다. 원근 왜곡이 필요한 경우를 제외하고 이 좌표는 1이어야 합니다. w의 세부정보에는 신경 쓰지 않아도 됩니다. 1이 아닌 다른 값은 사용하지 않을 것이기 때문입니다. 따라서 이제 모든 점은 4차원 벡터 [x, y, z, w=1] 에 해당하므로 행렬도 4x4여야 합니다.

CSS가 내부적으로 동종 좌표를 사용하는 한 가지 경우는 matrix3d() 함수를 사용하여 변환 속성에 고유한 4x4 행렬을 정의하는 경우입니다. matrix3d은 행렬이 4x4이므로 인수 16개를 취하며 한 열을 차례로 지정합니다. 따라서 이 함수를 사용하여 회전, 변환 등을 수동으로 지정할 수 있습니다. 하지만 이 함수를 사용하면 w 좌표를 조작할 수도 있습니다.

matrix3d()를 사용하려면 3D 컨텍스트가 필요합니다. 3D 컨텍스트가 없으면 원근 왜곡이 없고 동종 좌표가 필요하지 않기 때문입니다. 3D 컨텍스트를 만들려면 perspective와 새로 생성된 3D 공간에서 변환할 수 있는 내부 요소가 있는 컨테이너가 필요합니다. :

CSS의 Perspective 속성을 사용하여 div를 왜곡하는 CSS 코드입니다.

Perspective 컨테이너 내부의 요소는 CSS 엔진에서 다음과 같이 처리합니다.

  • 요소의 각 모서리 (꼭짓점)를 원근법 컨테이너를 기준으로 동종 좌표 [x,y,z,w]로 바꿉니다.
  • 요소의 모든 변환을 오른쪽에서 왼쪽으로 행렬로 적용합니다.
  • 원근 요소를 스크롤할 수 있는 경우 스크롤 매트릭스를 적용합니다.
  • 원근 행렬을 적용합니다.

스크롤 행렬은 y축에 따른 변환입니다. 400px 아래로 스크롤하면 모든 요소를 400px 위로 이동해야 합니다. 원근법 행렬은 3D 공간에서 멀리 떨어진 소실점에 가까운 점을 '가져오는' 행렬입니다. 이렇게 하면 콘텐츠가 더 멀리 떨어져 있을 때 더 작게 보이고 번역 시 '느리게 이동'하는 효과를 모두 얻을 수 있습니다. 따라서 요소가 푸시백되는 경우 변환이 400px이면 요소가 화면에서 300px만 이동합니다.

모든 세부정보를 확인하려면 CSS의 변환 렌더링 모델의 spec을 읽어야 하지만, 이 문서에서는 위 알고리즘을 단순화했습니다.

이 상자는 perspective 속성의 값이 p인 Perspective 컨테이너 안에 있으며 컨테이너가 스크롤 가능하고 n픽셀 아래로 스크롤된다고 가정해 보겠습니다.

원근 행렬 곱하기 스크롤 행렬 곱하기 요소 변환 행렬은 네 번째 행의 세 번째 열에 p보다 1을 뺀 값을 곱하고 두 번째 행의 네 번째 열에 빼기 n이 있는 - n을 곱하고 요소 변환 행렬을 곱합니다.

첫 번째 행렬은 원근 행렬이고 두 번째 행렬은 스크롤 행렬입니다. 요약: 스크롤 매트릭스의 역할은 아래로 스크롤할 때 요소가 위로 이동하도록 하는 것이므로 음의 부호가 됩니다.

그러나 스크롤바의 경우 반대를 원합니다. 즉, 아래로 스크롤할 때 요소가 아래로 이동하도록 하려고 합니다. 여기서 한 가지 방법을 사용해 보겠습니다. 상자 모서리의 w 좌표를 반전시킵니다. w 좌표가 -1이면 모든 변환이 반대 방향으로 적용됩니다. 그렇다면 어떻게 해야 할까요? CSS 엔진은 상자의 모서리를 동종 좌표로 변환하고 w를 1로 설정합니다. matrix3d()이(가) 빛날 시간입니다.

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

이 행렬은 w를 부정하는 것 외에는 어떤 작업도 하지 않습니다. 따라서 CSS 엔진이 각 모서리를 [x,y,z,1] 형태의 벡터로 변환하면 행렬이 [x,y,z,-1]로 변환합니다.

Four by four identity matrix with minus one over p in the fourth row
  third column times four by four identity matrix with minus n in the second
  row fourth column times four by four identity matrix with minus one in the
  fourth row fourth column times four dimensional vector x, y, z, 1 equals four
  by four identity matrix with minus one over p in the fourth row third column,
  minus n in the second row fourth column and minus one in the fourth row
  fourth column equals four dimensional vector x, y plus n, z, minus z over
  p minus 1.

요소 변환 행렬의 효과를 보여주는 중간 단계를 나열했습니다. 행렬 계산에 익숙하지 않더라도 괜찮습니다. 유레카 순간은 마지막 줄에서 스크롤 오프셋 n을 y 좌표에 빼는 대신 더하게 된다는 것입니다. 아래로 스크롤하면 요소가 아래쪽으로 변환됩니다.

하지만 이 행렬을 에 넣기만 하면 요소가 표시되지 않습니다. 이는 CSS 사양에 따라 꼭짓점 w가 0보다 작으면 요소가 렌더링되지 않도록 차단하기 때문입니다. 그리고 z 좌표가 현재 0이고 p는 1이므로 w는 -1이 됩니다.

다행히 z 값을 선택할 수 있습니다. w=1이 되도록 하려면 z = -2로 설정해야 합니다.

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

상자가 돌아왔네요, 보세요!

2단계: 움직이기

이제 상자가 생겼고 변환이 없는 것과 같은 모습입니다. 현재 원근 컨테이너는 스크롤할 수 없으므로 볼 수는 없지만 스크롤하면 요소가 다른 방향으로 이동한다는 것을 알고 있습니다. 그럼 컨테이너를 스크롤해 보겠습니다. 공간을 차지하는 스페이서 요소를 추가하기만 하면 됩니다.

<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>

이제 상자를 스크롤합니다. 빨간색 상자가 아래로 이동합니다.

3단계: 크기 지정

페이지를 아래로 스크롤하면 아래로 이동하는 요소가 있습니다. 그것은 정말로 어려운 부분입니다. 이제 스크롤바처럼 보이도록 스타일을 지정하고 상호작용성을 높여야 합니다.

스크롤바는 일반적으로 'thumb'과 '트랙'으로 구성되지만 트랙이 항상 표시되는 것은 아닙니다. 엄지손가락의 높이는 표시되는 콘텐츠의 양에 정비례합니다.

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

scrollerHeight는 스크롤 가능한 요소의 높이이고 scroller.scrollHeight는 스크롤 가능한 콘텐츠의 총 높이입니다. scrollerHeight/scroller.scrollHeight는 표시되는 콘텐츠의 비율입니다. 엄지손가락이 가리는 세로 공간의 비율은 표시되는 콘텐츠의 비율과 같아야 합니다.

엄지 점 스타일 점 높이가 스크롤러 높이 x 스크롤러 높이와 스크롤러 도 스크롤 높이 이상인 경우에만 스크롤러 높이에 따른 엄지 점 스타일 점 높이와 스크롤러 점 스크롤 높이가 스크롤러 높이와 같습니다.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

엄지손가락의 크기가 양호해 보이지만 너무 빨리 움직입니다. 여기에서 시차 스크롤러에서 기법을 가져올 수 있습니다. 요소를 뒤로 이동하면 스크롤하는 동안 더 느리게 이동합니다. 크기를 확장하여 수정할 수 있습니다. 하지만 정확히 얼마나 푸시해야 할까요? 짐작하셨겠지만 수학 문제를 풀어볼까요? 장담합니다. 이번이 마지막이에요.

여기서 중요한 정보는 아래로 계속 스크롤할 때 엄지손가락의 하단 가장자리가 스크롤 가능한 요소의 하단 가장자리와 정렬되도록 해야 한다는 것입니다. 즉, scroller.scrollHeight - scroller.height픽셀을 스크롤했다면 엄지손가락이 scroller.height - thumb.height로 번역되어야 합니다. 스크롤러의 모든 픽셀에 관해 엄지손가락으로 픽셀의 일부를 이동하려고 합니다.

계수는 스크롤러 도트 높이에서 스크롤러 도트 스크롤 높이에서 엄지손가락 점 높이 - 스크롤러 도트 높이를 뺀 값과 같습니다.

이것이 바로 Google의 배율입니다. 이제 배율을 z축을 따라 좌표이동으로 변환해야 합니다. 이 작업은 시차 스크롤 도움말에서 이미 수행했습니다. 사양의 관련 섹션에 따르면 배율은 p/(p − z)와 같습니다. 이 z 방정식을 풀면 z축을 따라 엄지손가락을 얼마나 이동시켜야 하는지를 알 수 있습니다. 하지만 w 좌표 의미로 인해 z를 따라 추가 -2px를 변환해야 합니다. 또한 요소의 변환은 오른쪽에서 왼쪽으로 적용됩니다. 즉, 특수 행렬 이후의 모든 변환이 반전되지 않지만 특수 행렬 이후의 모든 변환은 반전됩니다. 이를 코드화해 봅시다.

<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>

스크롤바가 있습니다. 이 요소는 원하는 대로 스타일을 지정할 수 있는 DOM 요소일 뿐입니다. 접근성 측면에서 중요한 한 가지는 thumb이 클릭 및 드래그에 반응하도록 하는 것입니다. 많은 사용자가 이런 방식으로 스크롤바와 상호작용하는 데 익숙하기 때문입니다. 이 블로그 게시물을 더 길게 만들기 위해 해당 부분에 대해서는 자세히 설명하지 않겠습니다. 자세한 내용은 라이브러리 코드를 참고하세요.

iOS의 경우

아, 내 오랜 친구 iOS Safari. 시차 스크롤과 마찬가지로 여기서 문제가 발생합니다. 요소를 스크롤 중이므로 -webkit-overflow-scrolling: touch를 지정해야 합니다. 하지만 이렇게 하면 3D 평면화가 발생하고 전체 스크롤 효과가 작동하지 않습니다. iOS Safari를 감지하고 position: sticky를 해결 방법으로 사용하여 패럴랙스 스크롤러에서 이 문제를 해결했으며 이번에도 동일한 작업을 수행합니다. 시차 도움말을 살펴보고 기억을 되살리세요.

브라우저 스크롤바는 어떨까요?

일부 시스템에서는 영구적인 네이티브 스크롤바를 처리해야 합니다. 지금까지는 스크롤바를 숨길 수 없었습니다 (비표준 의사 선택기를 사용하는 경우 제외). 그래서 그것을 숨기려면 (수학이 필요 없는) 몇 가지 해커에 의지해야 합니다. 스크롤 요소를 overflow-x: hidden로 컨테이너의 래핑하고 스크롤 요소를 컨테이너보다 넓게 만듭니다. 브라우저의 네이티브 스크롤바가 이제 보이지 않습니다.

종합해보면 이제 Nyan cat 데모에서처럼 프레임에 잘 맞는 맞춤 스크롤바를 빌드할 수 있습니다.

냥 고양이가 보이지 않는다면 이 데모를 빌드하는 동안 Google에서 찾아서 신고한 버그가 발생한 것입니다. 엄지손가락을 클릭하면 냥 고양이가 나타납니다. Chrome은 화면에 보이지 않는 항목을 칠하거나 애니메이션을 적용하는 등의 불필요한 작업을 나쁜 소식은 행렬한 조작으로 인해 Chrome에서 냥 고양이 GIF가 실제로 화면 밖에 있다고 생각하게 된다는 것입니다. 이 문제가 곧 해결되기를 바랍니다.

자, 다 됐습니다. 수고 많았어요. 전체 책을 읽어주셔서 박수를 보냅니다 이는 이 기능을 작동하게 하는 진짜 속임수이며 맞춤설정된 스크롤바가 환경의 필수적인 부분인 경우를 제외하고는 아마도 노력을 아끼지 않을 것입니다. 하지만 가능하다는 것을 알고 있으면 다행입니다. 맞춤 스크롤바를 만들기가 이렇게 어렵다는 사실은 CSS 측에서 해야 할 작업이 있음을 보여줍니다. 하지만 걱정하지 마세요. 앞으로 HoudiniAnimationWorklet은 이러한 프레임에서 완벽한 스크롤 연결 효과를 훨씬 쉽게 만들 수 있을 것입니다.