แถบเลื่อนที่กำหนดเองนั้นหายากมาก และสาเหตุส่วนใหญ่มาจากการที่แถบเลื่อนเป็นหนึ่งในส่วนที่เหลือบนเว็บที่แทบจะปรับแต่งไม่ได้ (เราหมายถึงเครื่องมือเลือกวันที่) คุณใช้ JavaScript เพื่อสร้างเองได้ แต่วิธีนี้ใช้เงินมาก คุณภาพต่ำ และอาจทำให้รู้สึกหน่วง ในบทความนี้ เราจะใช้ประโยชน์จากเมทริกซ์ CSS ที่แปลกใหม่บางอย่างเพื่อสร้างตัวเลื่อนที่กําหนดเองซึ่งไม่จําเป็นต้องใช้ JavaScript ขณะเลื่อน เพียงแค่โค้ดการตั้งค่าบางส่วน
TL;DR
คุณไม่สนใจเรื่องเล็กๆ น้อยๆ ใช่ไหม คุณแค่ต้องการดูตัวอย่างเพลงของ Nyan Cat และรับคลังเพลงใช่ไหม คุณดูโค้ดของเดโมได้ในที่เก็บ GitHub
LAM;WRA (ยาวและเกี่ยวกับคณิตศาสตร์ แต่จะอ่านอยู่ดี)
เมื่อไม่นานมานี้ เราได้สร้างภาพสไลด์แบบพารัลแลกซ์ (คุณอ่านบทความนั้นไหม เนื้อหาดีมาก คุ้มค่ากับเวลาของคุณจริงๆ) การผลักองค์ประกอบกลับโดยใช้การเปลี่ยนรูปแบบ 3 มิติของ CSS ทำให้องค์ประกอบเคลื่อนไหวช้าลงกว่าความเร็วในการเลื่อนจริง
สรุป
มาเริ่มกันด้วยการทบทวนวิธีการทำงานของภาพสไลด์แบบพารัลแลกซ์
ดังที่แสดงในภาพเคลื่อนไหว เราสร้างเอฟเฟกต์ภาพพารัลแลกซ์โดยการผลักองค์ประกอบ "ถอยหลัง" ในอวกาศ 3 มิติตามแนวแกน Z การเลื่อนเอกสารเป็นการเลื่อนตามแนวแกน Y ดังนั้นหากเราเลื่อนลง 100 พิกเซล องค์ประกอบทั้งหมดจะเลื่อนขึ้น 100 พิกเซล ซึ่งมีผลกับองค์ประกอบทั้งหมด แม้แต่องค์ประกอบที่อยู่ "ด้านหลัง" ก็ตาม แต่เนื่องจากองค์ประกอบเหล่านี้อยู่ห่างจากกล้องมากกว่า การเคลื่อนไหวที่สังเกตได้บนหน้าจอจะน้อยกว่า 100 พิกเซล จึงทำให้เกิดเอฟเฟกต์ภาพพารัลแลกซ์ตามที่ต้องการ
แน่นอนว่าการย้ายองค์ประกอบกลับเข้าไปในพื้นที่จะทำให้องค์ประกอบนั้นดูเล็กลงด้วย ซึ่งเราจะแก้ไขโดยการปรับขนาดองค์ประกอบให้กลับมาเหมือนเดิม เราได้หาค่าทางคณิตศาสตร์ที่แน่นอนแล้วเมื่อสร้างแถบเลื่อนแบบพารัลแลกซ์ ดังนั้นเราจะไม่อธิบายรายละเอียดทั้งหมดซ้ำ
ขั้นตอนที่ 0: เราต้องการทําอะไร
แถบเลื่อน นั่นคือสิ่งที่เราจะสร้าง แต่คุณเคยคิดไหมว่าพวกเขาทำอะไร เราไม่ได้ทำเช่นนั้น แถบเลื่อนเป็นตัวบ่งชี้ปริมาณเนื้อหาที่มีอยู่ซึ่งแสดงอยู่ในขณะนี้ และความคืบหน้าที่คุณในฐานะผู้อ่านอ่านไปแล้ว เมื่อคุณเลื่อนลง แถบเลื่อนก็จะเลื่อนลงด้วยเพื่อบ่งบอกว่าคุณกำลังดำเนินการใกล้จะเสร็จสิ้น หากเนื้อหาทั้งหมดพอดีกับวิวพอร์ต ระบบมักจะซ่อนแถบเลื่อนไว้ หากเนื้อหามีความสูง 2 เท่าของวิวพอร์ต แถบเลื่อนจะกินพื้นที่ ½ ของความสูงของวิวพอร์ต เนื้อหาที่สูง 3 เท่าของวิวพอร์ตจะปรับแถบเลื่อนให้มีขนาด 1/3 ของวิวพอร์ต เป็นต้น คุณจะเห็นรูปแบบนี้ คุณยังคลิกและลากแถบเลื่อนเพื่อไปยังส่วนต่างๆ ของเว็บไซต์ได้เร็วขึ้นแทนการเลื่อน นี่เป็นลักษณะการทำงานที่น่าประหลาดใจสำหรับองค์ประกอบที่มองไม่เห็นเช่นนี้ มาสู้กันทีละปัญหา
ขั้นตอนที่ 1: เปลี่ยนเป็นโหมดถอยหลัง
โอเค เราทําให้องค์ประกอบเคลื่อนไหวช้ากว่าความเร็วในการเลื่อนได้ด้วยการเปลี่ยนรูปแบบ 3 มิติของ CSS ตามที่ระบุไว้ในบทความการเลื่อนแบบพารัลแลกซ์ เราเปลี่ยนทิศทางได้ไหม แต่ปรากฏว่าทำได้ และนี่จึงเป็นวิธีที่เราใช้สร้างแถบเลื่อนที่กำหนดเองซึ่งพอดีกับเฟรม ในการทำความเข้าใจวิธีการทํางานนี้ เราจําเป็นต้องอธิบายข้อมูลเบื้องต้นเกี่ยวกับ CSS 3 มิติก่อน
หากต้องการใช้การฉายภาพเชิงมุมในเชิงคณิตศาสตร์ คุณอาจต้องใช้พิกัดแบบสม่ำเสมอ เราจะไม่ลงรายละเอียดว่า 3D Coord นั้นคืออะไรและทํางานอย่างไร แต่คุณอาจจํากัดความ 3D Coord ว่าเป็นพิกัด 3 มิติที่มีพิกัดที่ 4 เพิ่มเติมที่เรียกว่า w พิกัดนี้ควรเป็น 1 เว้นแต่ว่าคุณต้องการให้ภาพบิดเบี้ยวตามมุมมอง เราไม่จำเป็นต้องกังวลเกี่ยวกับรายละเอียดของ w เนื่องจากเราจะไม่ใช้ค่าอื่นนอกเหนือจาก 1 ดังนั้นนับจากนี้ไปจุดทั้งหมดจะเป็นเวกเตอร์ 4 มิติ [x, y, z, w=1] และเมทริกซ์จึงต้องเป็น 4x4 ด้วย
กรณีที่คุณจะเห็นว่า CSS ใช้พิกัดแบบสม่ำเสมออยู่เบื้องหลังคือเมื่อคุณกำหนดเมทริกซ์ 4x4 ของคุณเองในพร็อพเพอร์ตี้ transform โดยใช้ฟังก์ชัน matrix3d()
matrix3d
ใช้อาร์กิวเมนต์ 16 รายการ (เนื่องจากเมทริกซ์มีขนาด 4x4) โดยระบุคอลัมน์ทีละคอลัมน์ เราจึงใช้ฟังก์ชันนี้เพื่อระบุการหมุน การแปล ฯลฯ ด้วยตนเองได้ แต่นอกจากนี้ เรายังใช้ฟังก์ชันนี้เพื่อเล่นกับพิกัด w ได้ด้วย
เราต้องสร้างบริบท 3 มิติก่อนจึงจะใช้ matrix3d()
ได้ เนื่องจากหากไม่มีบริบท 3 มิติ จะไม่มีภาพบิดเบี้ยวตามมุมมองและไม่จำเป็นต้องใช้พิกัดแบบสม่ำเสมอ หากต้องการสร้างบริบท 3 มิติ เราต้องใช้คอนเทนเนอร์ที่มี perspective
และองค์ประกอบบางอย่างภายในที่เราเปลี่ยนรูปแบบได้ในอวกาศ 3 มิติที่สร้างขึ้นใหม่ ตัวอย่างเช่น
องค์ประกอบภายในคอนเทนเนอร์ภาพ 3 มิติจะได้รับการประมวลผลโดยเครื่องมือ CSS ดังนี้
- เปลี่ยนแต่ละมุม (จุดยอด) ขององค์ประกอบเป็นพิกัดแบบสม่ำเสมอ
[x,y,z,w]
โดยสัมพันธ์กับคอนเทนเนอร์ภาพมุมมอง - ใช้การเปลี่ยนรูปแบบทั้งหมดขององค์ประกอบเป็นเมทริกซ์จากขวาไปซ้าย
- หากเอลิเมนต์ภาพ 3 มิติเลื่อนได้ ให้ใช้เมทริกซ์การเลื่อน
- ใช้เมทริกซ์มุมมอง
เมทริกซ์การเลื่อนคือการเปลี่ยนตำแหน่งตามแกน Y หากเราเลื่อนลง 400 พิกเซล องค์ประกอบทั้งหมดจะต้องเลื่อนขึ้น 400 พิกเซล เมทริกซ์มุมมองคือเมทริกซ์ที่ "ดึง" จุดให้ใกล้กับจุดที่เลือนหายไปมากขึ้นเมื่อจุดนั้นอยู่ด้านหลังในพื้นที่ 3 มิติ วิธีนี้ทำให้เกิดทั้ง 2 ผลลัพธ์ คือทำให้วัตถุดูเล็กลงเมื่ออยู่ไกลออกไป และทำให้วัตถุ "เคลื่อนไหวช้าลง" เมื่อมีการแปล ดังนั้นหากมีการผลักองค์ประกอบกลับ การเปลี่ยนตำแหน่ง 400 พิกเซลจะทำให้องค์ประกอบขยับเพียง 300 พิกเซลบนหน้าจอ
หากต้องการทราบรายละเอียดทั้งหมด คุณควรอ่านข้อกำหนดเกี่ยวกับรูปแบบการแสดงผลการเปลี่ยนรูปแบบของ CSS แต่เราอธิบายอัลกอริทึมข้างต้นให้เข้าใจง่ายขึ้นเพื่อประโยชน์ของบทความนี้
กล่องของเราอยู่ภายในคอนเทนเนอร์ภาพมุมมองที่มีค่า p สําหรับแอตทริบิวต์ perspective
และสมมติว่าคอนเทนเนอร์เลื่อนได้และเลื่อนลง 200 พิกเซล
เมทริกซ์แรกคือเมทริกซ์มุมมอง ส่วนเมทริกซ์ที่ 2 คือเมทริกซ์การเลื่อน โดยสรุปแล้ว หน้าที่ของเมทริกซ์การเลื่อนคือทําให้องค์ประกอบเลื่อนขึ้นเมื่อเราเลื่อนลง ด้วยเหตุนี้จึงมีเครื่องหมายลบ
แต่สำหรับแถบเลื่อน เราต้องการผลลัพธ์ที่ตรงกันข้าม คือต้องการให้องค์ประกอบเลื่อนลงเมื่อเราเลื่อนลง ตรงนี้เราสามารถใช้เทคนิคในการพลิกพิกัด 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]
เราได้แสดงขั้นตอนกลางเพื่อแสดงผลขององค์ประกอบ transform matrix หากไม่ถนัดเรื่องคณิตศาสตร์เชิงเมทริกซ์ ก็ไม่เป็นไร แนวคิดสำคัญคือในบรรทัดสุดท้าย เราเพิ่มการเลื่อน n ลงในพิกัด y แทนที่จะลบ องค์ประกอบจะเลื่อนลงหากเราเลื่อนลง
อย่างไรก็ตาม หากเราใส่เมทริกซ์นี้ในตัวอย่าง องค์ประกอบจะไม่แสดง เนื่องจากข้อกำหนด CSS กําหนดว่าจุดยอดใดก็ตามที่มี w < 0 จะบล็อกไม่ให้องค์ประกอบแสดงผล และเนื่องจากตอนนี้พิกัด z ของเราคือ 0 และ p คือ 1 w จึงเท่ากับ -1
แต่โชคดีที่เราสามารถเลือกค่าของ z ได้ เราต้องตั้งค่า z = -2 เพื่อให้ w = 1
.box {
transform:
matrix3d(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, -1
)
translateZ(-2px);
}
ในที่สุดแล้ว กล่องของเราก็กลับมาแล้ว
ขั้นตอนที่ 2: ทําให้ชิ้นงานเคลื่อนไหว
ตอนนี้กล่องของเราก็ปรากฏขึ้นแล้วและมีลักษณะเหมือนเดิมหากไม่มีการเปลี่ยนรูปแบบ ขณะนี้คอนเทนเนอร์ภาพ 3 มิติจะเลื่อนไม่ได้ เราจึงไม่เห็น แต่เราทราบว่าองค์ประกอบจะไปในอีกทิศทางหนึ่งเมื่อเลื่อน มาทำให้คอนเทนเนอร์เลื่อนกัน เราเพียงเพิ่มองค์ประกอบตัวเว้นวรรคที่กินพื้นที่
<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
แต่การระบุดังกล่าวจะทำให้ภาพ 3 มิติแบนราบและเอฟเฟกต์การเลื่อนทั้งหมดหยุดทำงาน เราได้แก้ปัญหานี้ในแถบเลื่อนภาพพารัลแลกซ์โดยการตรวจหา iOS Safari และใช้ position: sticky
เป็นวิธีแก้ปัญหาชั่วคราว และเราจะทำแบบเดียวกันกับที่นี่ โปรดอ่านบทความเกี่ยวกับภาพพารัลแลกซ์เพื่อทบทวนความจำ
แล้วแถบเลื่อนของเบราว์เซอร์ล่ะ
ในบางระบบ เราจะต้องจัดการกับแถบเลื่อนแบบเนทีฟถาวร
ที่ผ่านมา แถบเลื่อนจะซ่อนไม่ได้ (ยกเว้นตัวเลือกเสมือนที่ไม่เป็นมาตรฐาน)
ดังนั้นในการซ่อนข้อมูลดังกล่าว เราจึงต้องใช้วิธีแฮ็ก (ไม่ต้องใช้คณิตศาสตร์) เราตัดสิ้นสุดองค์ประกอบที่เลื่อนในคอนเทนเนอร์ด้วย overflow-x: hidden
และทำให้องค์ประกอบที่เลื่อนกว้างกว่าคอนเทนเนอร์ ตอนนี้แถบเลื่อนของเบราว์เซอร์จะมองไม่เห็นแล้ว
Fin
เมื่อนำทุกอย่างมารวมกัน ตอนนี้เราสามารถสร้างแถบเลื่อนที่ปรับแต่งเองซึ่งเฟรมจะพอดีกันทุกเฟรมได้ เช่น แถบเลื่อนในเดโมของ Nyan Cat
หากไม่เห็น Nyan Cat แสดงว่าคุณพบข้อบกพร่องที่เราพบและรายงานแล้วขณะสร้างเดโมนี้ (คลิกนิ้วโป้งเพื่อทำให้ Nyan Cat ปรากฏขึ้น) Chrome เก่งมากในการหลีกเลี่ยงการทำงานที่ไม่จำเป็น เช่น การวาดภาพหรือการสร้างภาพเคลื่อนไหวของสิ่งที่อยู่นอกหน้าจอ ข่าวร้ายคือการแสดงผลแบบ Matrix ของเราทําให้ Chrome คิดว่า GIF ของ Nyan Cat อยู่นอกหน้าจอ หวังว่าปัญหานี้จะได้รับการแก้ไขเร็วๆ นี้
เท่านี้ก็เรียบร้อย งานนี้ยากมาก เราชื่นชมที่คุณอ่านทุกอย่าง นี่เป็นวิธีที่ค่อนข้างซับซ้อนในการทำให้ใช้งานได้ และอาจไม่ค่อยคุ้มค่ากับเวลาและความพยายาม ยกเว้นในกรณีที่แถบเลื่อนที่ปรับแต่งเองเป็นส่วนสำคัญของประสบการณ์การใช้งาน แต่คุณก็ควรทราบว่าการดำเนินการดังกล่าวเป็นไปได้ การที่การทําแถบเลื่อนที่กําหนดเองนั้นทําได้ยากมากแสดงให้เห็นว่ายังมีงานที่ต้องทําในฝั่ง CSS แต่ไม่ต้องกังวล ในอนาคต AnimationWorklet ของ Houdini จะช่วยให้คุณสร้างเอฟเฟกต์ที่เชื่อมโยงกับการเลื่อนแบบเฟรมต่อเฟรมได้อย่างง่ายดาย