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

맞춤 스크롤바는 매우 드물며, 이는 스크롤바가 웹에서 사용할 수 없는 나머지 부분 중 하나이기 때문입니다 (날짜 선택 도구를 살펴보고 있습니다). JavaScript를 사용하여 직접 빌드할 수도 있지만, 비용이 많이 들고 충실도가 낮으며 지체될 수 있습니다. 이 도움말에서는 몇 가지 색다른 CSS 매트릭스를 활용하여 스크롤하는 동안 자바스크립트가 필요 없고 설정 코드만 사용하는 맞춤 스크롤러를 빌드합니다.

요약

사소한 것에 신경 쓰지 않으시나요? Nyan cat 데모를 보고 라이브러리를 다운로드하시겠어요? 데모 코드는 GitHub 저장소에서 확인할 수 있습니다.

LAM;WRA (긴 형식 및 수학, 계속 읽음)

Google에서는 얼마 전에 시차 스크롤러를 만들었습니다 (도움말을 읽어보셨나요? 정말로 좋은 시간이니 시간을 할애해 주시기 바랍니다.) CSS 3D 변환을 사용하여 요소를 뒤로 푸시함으로써 요소가 실제 스크롤 속도보다 느리게 이동했습니다.

요약

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

애니메이션에서 볼 수 있듯이 Z축을 따라 3D 공간에서 요소를 '뒤로' 밀어 시차 효과를 얻었습니다. 문서를 스크롤하면 사실상 Y축을 따라 이동하는 것과 같습니다. 따라서 아래로 스크롤하면(예: 100px) 모든 요소가 위쪽 100픽셀로 변환됩니다. 이는 '더 뒤로' 있는 요소를 포함하여 모든 요소에 적용됩니다. 그러나 관측된 화면 이동이 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가 되어야 합니다.

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

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

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

원근법 컨테이너 내의 요소는 CSS 엔진에서 다음과 같이 처리됩니다.

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

스크롤 매트릭스는 y축을 따라 이동한 변환입니다. 400픽셀 아래로 스크롤하면 모든 요소를 400픽셀 위로 이동해야 합니다. 원근행렬은 3D 공간에서 소실점에 더 가까운 점을 '가져오는' 행렬입니다. 이렇게 하면 멀리 떨어질 때 더 작게 표시되는 효과가 있고 번역될 때는 '더 천천히 움직이게' 합니다. 따라서 요소가 푸시백되는 경우 변환이 400px이면 요소가 화면에서 300px만 이동합니다.

모든 세부정보를 알아보려면 CSS의 변환 렌더링 모델에 관한 spec을 읽어야 하지만 이 도움말에서는 위 알고리즘을 간단히 설명했습니다.

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

원근 행렬 곱하기 스크롤 행렬 곱하기 요소 변환 행렬은
 네 번째 행의 p 세 번째 열 곱하기 4x4 단위행렬 곱하기 네 번째 행 4열 곱하기 요소 변환 행렬의
 4x4 단위 변환 행렬입니다.

첫 번째 행렬은 원근 행렬이고 두 번째 행렬은 스크롤 행렬입니다. 요약: 스크롤 매트릭스는 아래로 스크롤할 때 요소가 위로 이동하게 하여 음수 부호를 만듭니다.

그러나 스크롤바의 경우 반대쪽이 필요합니다. 즉, 아래로 스크롤할 때 요소가 아래로 이동해야 합니다. 여기서 요령을 사용할 수 있습니다. 상자 모서리의 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]로 변환합니다.

네 번째 행의 p에 -1, 세 번째 열 곱하기 4 x 4 정체성행렬 곱하기 두 번째 행의 4번째 열 곱하기 4x4정식행렬 x 4번째 행 4열 x 4차원 벡터 x, y, z, 1은 4차원 벡터의 4차원 벡터 x, y, z, 1은 4차원행 x열의 4차원 z열, 4차원열의 4차원열에 -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단계: 움직이기

이제 상자가 있고 변환이 없을 때와 동일한 모양입니다. 현재 Perspective 컨테이너는 스크롤할 수 없으므로 볼 수 없지만 스크롤하면 요소가 다른 방향으로 가다는 것을 알고 있습니다. 그렇다면 컨테이너가 스크롤되도록 해봅시다. 공간을 차지하는 스페이서 요소를 추가하기만 하면 됩니다.

<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'과 '트랙'으로 구성되지만 트랙이 항상 표시되는 것은 아닙니다. 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는 표시되는 콘텐츠의 비율입니다. 미리보기 이미지가 가리는 세로 공간의 비율은 표시되는 콘텐츠의 비율과 동일해야 합니다.

엄지 점 스타일 점 높이가 스크롤러 점 스크롤 높이를 기준으로 한 스크롤러 높이와 스크롤러 높이의 스크롤러 높이를 곱하는 경우에만 엄지 점 스타일 도트 높이가 스크롤러 점 스크롤 높이보다 스크롤러 점 스크롤 높이와 같음
<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 픽셀을 스크롤했다면 thumb을 scroller.height - thumb.height로 번역하려고 합니다. thumb을 사용하여 스크롤러의 픽셀 중 1개 픽셀만 이동하도록 하려고 합니다.

스크롤러 점 높이에서 엄지 점 높이를 뺀 값 위 스크롤러 점 스크롤 높이 - 스크롤러 점 높이를 뺀 인수로 구합니다.

이것이 바로 배율입니다. 이제 배율을 z축을 따라 평행이동으로 변환해야 합니다. 시차 스크롤 도움말에서 이미 했습니다. 사양의 관련 섹션에 따르면 배율은 p/(p − z)와 같습니다. 이 z 방정식을 풀어 thumb을 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 요소일 뿐이며 원하는 스타일을 지정할 수 있습니다. 접근성 측면에서 중요한 한 가지는 엄지손가락이 클릭 앤 드래그에 반응하도록 하는 것입니다. 많은 사용자가 스크롤바와 상호작용하는 데 익숙하기 때문입니다. 이 블로그 게시물을 더 길게 작성하지 않기 위해 이 부분의 세부정보는 설명하지 않겠습니다. 실행 방법을 알아보려면 라이브러리 코드를 살펴보세요.

iOS는 어떤가요?

아, 내 오랜 친구 iOS Safari. 시차 스크롤과 마찬가지로 여기서 문제가 발생합니다. 요소에서 스크롤하므로 -webkit-overflow-scrolling: touch를 지정해야 하지만, 이렇게 하면 3D 평면화가 발생하고 전체 스크롤 효과가 더 이상 작동하지 않습니다. 시차 스크롤러에서 iOS Safari를 감지하고 해결 방법으로 position: sticky를 사용하여 이 문제를 해결했으며 여기에서도 똑같은 작업을 수행합니다. 시차 자료를 참고하여 메모리를 새로 고치세요.

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

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

종합하면 Nyan 고양이 데모에서와 같이 프레임에 완벽한 맞춤 스크롤바를 빌드할 수 있습니다.

Nyan cat이 표시되지 않으면 이 데모를 빌드하는 동안 Google에서 발견하여 제출한 버그가 발생한 것입니다 (Nyan cat이 표시되도록 thumb을 클릭). Chrome은 화면 밖에 있는 대상에 페인트를 칠하거나 애니메이션을 적용하는 등 불필요한 작업을 방지하는 데 능숙합니다. 좋지 않은 소식은 Google의 행렬 문제로 인해 Chrome에서 Nyan cat gif가 실제로 화면에서 벗어났다고 생각하게 만든다는 것입니다. 이 문제가 곧 해결되기를 바랍니다.

끝났습니다. 정말 엄청난 노력이었죠. 전체 내용을 읽어주셔서 박수를 보냅니다. 이는 이 동작을 작동하게 하는 실질적인 트릭이며, 맞춤설정된 스크롤바가 환경에서 필수적인 경우 외에는 노력할 가치가 거의 없습니다. 하지만 가능하다는 사실은 다행입니다. 맞춤 스크롤바를 만드는 것이 이렇게 어렵다는 사실은 CSS 측에서 해야 할 작업이 있음을 보여줍니다. 하지만 걱정하지 마세요. 향후 HoudiniAnimationWorklet은 이와 같은 프레임 완벽한 스크롤 연결 효과를 훨씬 쉽게 만들 예정입니다.