CSS Deep-Dive - matrix3d() برای یک اسکرول سفارشی با فریم کامل

نوارهای پیمایش سفارشی بسیار نادر هستند و این بیشتر به این دلیل است که نوارهای پیمایش یکی از بیت های باقی مانده در وب هستند که تقریباً بی سبک هستند (من به شما نگاه می کنم، انتخابگر تاریخ). شما می‌توانید از جاوا اسکریپت برای ساختن خود استفاده کنید، اما این گران است، وفاداری پایینی دارد و می‌تواند احساس کندی دارد. در این مقاله، ما از برخی ماتریس‌های غیر متعارف CSS برای ساختن یک اسکرول سفارشی استفاده می‌کنیم که در حین پیمایش به جاوا اسکریپت نیاز ندارد، فقط به مقداری کد راه‌اندازی نیاز دارد.

TL; DR

شما به چیزهای کوچک اهمیت نمی دهید؟ شما فقط می خواهید به نسخه ی نمایشی گربه Nyan نگاه کنید و کتابخانه را دریافت کنید؟ می‌توانید کد نسخه آزمایشی را در مخزن GitHub ما پیدا کنید.

LAM;WRA (طولانی و ریاضی؛ به هر حال خوانده خواهد شد)

چندی پیش، ما یک پیشینۀ اختلاف منظر ساختیم (آیا آن مقاله را خواندید؟ واقعاً خوب است، ارزش وقت گذاشتن را دارد!). با عقب راندن عناصر با استفاده از تبدیل های سه بعدی CSS، عناصر کندتر از سرعت پیمایش واقعی ما حرکت کردند.

خلاصه

بیایید با خلاصه ای از نحوه عملکرد پیمایش اختلاف منظر شروع کنیم.

همانطور که در انیمیشن نشان داده شده است، با فشار دادن عناصر "به عقب" در فضای سه بعدی، در امتداد محور Z، به جلوه اختلاف منظر دست یافتیم. پیمایش یک سند در واقع یک ترجمه در امتداد محور Y است. بنابراین اگر به پایین اسکرول کنیم، مثلاً 100 پیکسل، هر عنصر با 100 پیکسل به سمت بالا ترجمه می شود. این در مورد همه عناصر صدق می کند، حتی آنهایی که "به عقب تر" هستند. اما از آنجایی که آنها از دوربین دورتر هستند، حرکت مشاهده شده روی صفحه نمایش آنها کمتر از 100 پیکسل خواهد بود و اثر اختلاف منظر مطلوب را ایجاد می کند.

البته، جابجایی یک عنصر در فضا، آن را نیز کوچکتر نشان می‌دهد، که با بزرگ‌نمایی عنصر، آن را اصلاح می‌کنیم. زمانی که پیمایش اختلاف منظر را ساختیم، ریاضی دقیق را فهمیدیم، بنابراین تمام جزئیات را تکرار نمی‌کنم.

مرحله 0: می خواهیم چه کار کنیم؟

نوارهای پیمایش این چیزی است که ما قرار است بسازیم. اما آیا واقعاً تا به حال به این فکر کرده اید که آنها چه می کنند؟ من قطعا این کار را نکردم. نوارهای اسکرول نشانگر میزان قابل مشاهده بودن محتوای موجود در حال حاضر و میزان پیشرفت شما به عنوان خواننده است. اگر به پایین اسکرول کنید، نوار پیمایش نیز برای نشان دادن پیشرفت شما به سمت پایان انجام می شود. اگر تمام محتوا در پنجره نمایش قرار گیرد، نوار اسکرول معمولا پنهان می شود. اگر محتوا 2 برابر ارتفاع درگاه دید داشته باشد، نوار اسکرول ½ ارتفاع درگاه دید را پر می کند. محتوایی به ارزش 3 برابر ارتفاع درگاه دید، نوار پیمایش را به ⅓ درگاه دید و غیره تغییر می‌دهد. الگو را مشاهده می‌کنید. به جای اسکرول کردن، می توانید برای حرکت سریعتر در سایت، روی نوار اسکرول کلیک کرده و بکشید. این مقدار رفتار شگفت انگیزی برای عنصر نامحسوسی مانند آن است. بیایید یک نبرد در یک زمان بجنگیم.

مرحله 1: قرار دادن آن برعکس

بسیار خوب، می‌توانیم با تبدیل‌های سه بعدی CSS همانطور که در مقاله پیمایش اختلاف منظر مشخص شده است، عناصر را کندتر از سرعت پیمایش حرکت دهیم. آیا می توانیم جهت را نیز برعکس کنیم؟ به نظر می رسد که ما می توانیم و این راه ما برای ساختن یک نوار پیمایش سفارشی با فریم کامل است. برای درک اینکه چگونه این کار می‌کند، ابتدا باید چند مبانی سه‌بعدی CSS را پوشش دهیم.

برای به دست آوردن هر نوع طرح ریزی پرسپکتیو به معنای ریاضی، به احتمال زیاد در نهایت از مختصات همگن استفاده خواهید کرد. من به جزئیات نمی پردازم که آنها چیست و چرا کار می کنند، اما می توانید آنها را مانند مختصات سه بعدی با مختصات چهارم اضافی به نام w در نظر بگیرید. این مختصات باید 1 باشد مگر اینکه بخواهید اعوجاج پرسپکتیو داشته باشید. ما نیازی به نگرانی در مورد جزئیات w نداریم زیرا قرار نیست از مقدار دیگری غیر از 1 استفاده کنیم. بنابراین همه نقاط از این به بعد بردارهای 4 بعدی [x, y, z, w=1] و در نتیجه ماتریس ها هستند. باید 4*4 نیز باشد.

یکی از مواردی که می توانید ببینید CSS از مختصات همگن در زیر هود استفاده می کند، زمانی است که ماتریس های 4x4 خود را در یک ویژگی تبدیل با استفاده از تابع matrix3d() تعریف می کنید. matrix3d ​​16 آرگومان می گیرد (چون ماتریس 4x4 است) و ستونی را پس از دیگری مشخص می کند. بنابراین می‌توانیم از این تابع برای تعیین دستی چرخش‌ها، ترجمه‌ها و غیره استفاده کنیم.

قبل از اینکه بتوانیم از matrix3d() استفاده کنیم، به یک زمینه سه بعدی نیاز داریم – زیرا بدون یک زمینه سه بعدی هیچ گونه اعوجاج پرسپکتیو و نیازی به مختصات همگن وجود نخواهد داشت. برای ایجاد یک زمینه سه بعدی، به یک محفظه با یک perspective و برخی عناصر داخل آن نیاز داریم که بتوانیم در فضای سه بعدی جدید ایجاد کنیم. مثلا :

قطعه ای از کد CSS که با استفاده از ویژگی پرسپکتیو CSS یک div را تحریف می کند.

عناصر داخل یک کانتینر پرسپکتیو توسط موتور CSS به صورت زیر پردازش می شوند:

  • هر گوشه (راس) یک عنصر را به مختصات همگن [x,y,z,w] نسبت به ظرف پرسپکتیو تبدیل کنید.
  • همه تبدیل های عنصر را به عنوان ماتریس از راست به چپ اعمال کنید.
  • اگر عنصر پرسپکتیو قابل پیمایش است، یک ماتریس اسکرول اعمال کنید.
  • ماتریس پرسپکتیو را اعمال کنید.

ماتریس اسکرول یک ترجمه در امتداد محور y است. اگر 400 پیکسل به پایین اسکرول کنیم، همه عناصر باید 400 پیکسل به بالا منتقل شوند. ماتریس پرسپکتیو ماتریسی است که نقاط را هر چه بیشتر در فضای سه بعدی به عقب برمی گرداند به نقطه ناپدید شدن نزدیکتر می کند. این کار باعث می‌شود تا وقتی چیزها دورتر هستند کوچک‌تر به نظر برسند و همچنین باعث می‌شود هنگام ترجمه «آهسته‌تر حرکت کنند». بنابراین اگر یک عنصر به عقب رانده شود، ترجمه 400 پیکسل باعث می شود عنصر فقط 300 پیکسل روی صفحه حرکت کند.

اگر می خواهید تمام جزئیات را بدانید، باید مشخصات مدل رندر تبدیل CSS را بخوانید، اما به خاطر این مقاله، الگوریتم بالا را ساده کردم.

جعبه ما در داخل یک محفظه پرسپکتیو با مقدار p برای ویژگی perspective قرار دارد، و فرض کنید ظرف قابل پیمایش است و n پیکسل به پایین اسکرول می شود.

ماتریس پرسپکتیو بار ماتریس اسکرول بار ماتریس تبدیل عنصر برابر است با ماتریس چهار در چهار هویت با منهای یک بر p در ردیف چهارم ستون سوم ضرب در ماتریس چهار در چهار هویت با منهای 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] تبدیل می‌کند.

ماتریس هویت چهار در چهار با منهای یک روی p در ردیف چهارم ستون سوم ضربدر چهار در چهار ماتریس هویت با منهای n در ردیف دوم ستون چهارم ضربدر چهار در چهار ماتریس هویت با منهای یک در ردیف چهارم ستون چهارم ضربدر بردار چهار بعدی x، y، z، 1 برابر است با ماتریس چهار در چهار با منهای یک بر p در ستون سوم ردیف چهارم، منهای n در ردیف دوم ستون چهارم و منهای یک در ردیف چهارم ستون چهارم برابر است با بردار چهار بعدی x، y به اضافه n، z، منهای z بیش از p منهای 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: به آن اندازه بدهید

ما یک عنصر داریم که با پایین آمدن صفحه به سمت پایین حرکت می کند. واقعاً این کار سختی است. اکنون باید آن را طوری سبک کنیم که شبیه یک اسکرول بشود و کمی تعاملی تر شود.

یک نوار پیمایش معمولاً از یک "شست" و یک "تراک" تشکیل شده است، در حالی که مسیر همیشه قابل مشاهده نیست. ارتفاع انگشت شست مستقیماً با میزان قابل مشاهده بودن محتوا متناسب است.

<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 اسکرول کرده‌ایم، می‌خواهیم انگشت شست ما توسط scroller.height - thumb.height ترجمه شود. برای هر پیکسل اسکرول، می خواهیم انگشت شست ما کسری از پیکسل را حرکت دهد:

ضریب برابر است با ارتفاع نقطه پیمایش منهای ارتفاع نقطه انگشت شست بیش از ارتفاع پیمایش نقطه پیمایش منهای ارتفاع نقطه پیمایش.

این ضریب مقیاس ماست. اکنون باید ضریب مقیاس را به ترجمه در امتداد محور z تبدیل کنیم که قبلاً در مقاله پیمایش اختلاف منظر انجام دادیم. با توجه به بخش مربوطه در مشخصات : ضریب مقیاس برابر با p/(p − z) است. می‌توانیم این معادله را برای z حل کنیم تا بفهمیم چقدر باید انگشت شست خود را در امتداد محور z ترجمه کنیم. اما به خاطر داشته باشید که با توجه به مختصات w، باید یک -2px اضافی را در امتداد z ترجمه کنیم. همچنین توجه داشته باشید که تبدیل‌های یک عنصر از راست به چپ اعمال می‌شوند، به این معنی که همه ترجمه‌های قبل از ماتریس ویژه ما معکوس نمی‌شوند، اما همه ترجمه‌های بعد از ماتریس ویژه ما معکوس خواهند شد! بیایید این را مدون کنیم!

<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 مشخص کنیم، اما این باعث صاف شدن سه بعدی می شود و کل افکت اسکرول ما از کار می افتد. ما این مشکل را در پیمایش اختلاف منظر با شناسایی iOS Safari و تکیه بر position: sticky به عنوان یک راه حل حل کردیم، و دقیقاً همین کار را در اینجا انجام خواهیم داد. برای تازه کردن حافظه خود به مقاله پارالکسینگ نگاهی بیندازید.

در مورد نوار اسکرول مرورگر چطور؟

در برخی از سیستم‌ها، ما باید با یک نوار پیمایش دائمی و بومی سروکار داشته باشیم. از لحاظ تاریخی، نوار پیمایش را نمی توان پنهان کرد (به جز با یک شبه انتخابگر غیر استاندارد ). بنابراین برای پنهان کردن آن، باید به هکری (بدون ریاضی) متوسل شویم. عنصر اسکرول خود را در یک ظرف با overflow-x: hidden می پیچیم و عنصر اسکرول را از ظرف بازتر می کنیم. نوار اسکرول بومی مرورگر اکنون در معرض دید نیست.

فین

با کنار هم قرار دادن همه اینها، اکنون می‌توانیم یک نوار پیمایش سفارشی با فریم کامل بسازیم - مانند آنچه در نسخه ی نمایشی گربه Nyan ما وجود دارد.

اگر نمی‌توانید گربه Nyan را ببینید، با مشکلی مواجه شده‌اید که ما در حین ساخت این نسخه نمایشی پیدا کردیم و آن را ثبت کردیم (برای نشان دادن گربه Nyan روی انگشت شست کلیک کنید). کروم در اجتناب از کارهای غیرضروری مانند نقاشی یا متحرک سازی چیزهایی که خارج از صفحه هستند واقعاً خوب است. خبر بد این است که شیطنت‌های ماتریسی ما باعث می‌شود Chrome فکر کند که گیف Nyan cat واقعاً خارج از صفحه است. انشالله که این مشکل به زودی برطرف شود.

شما آن را دارید. این خیلی کار بود. من از شما برای خواندن کل مطلب تحسین می کنم. این یک ترفند واقعی برای به کار انداختن آن است و احتمالاً به ندرت ارزش تلاش را دارد، به جز زمانی که یک نوار پیمایش سفارشی بخشی ضروری از تجربه است. اما خوب است بدانید که ممکن است، نه؟ این واقعیت که انجام یک نوار اسکرول سفارشی بسیار سخت است، نشان می دهد که در سمت CSS باید کار انجام شود. اما نترس! در آینده، Houdini ’s AnimationWorklet می‌خواهد افکت‌های مرتبط با اسکرول کامل فریم مانند این را بسیار آسان‌تر کند.