วิธีที่หนังสือแบบเลื่อนได้มีชีวิตขึ้นมาเพื่อแชร์กลเม็ดเคล็ดลับที่สนุกสนานและน่ากลัวใน Chrometober นี้
ต่อจาก Designcember เราต้องการสร้าง Chrometober ให้คุณในปีนี้เพื่อไฮไลต์และแชร์เนื้อหาเว็บจากชุมชนและทีม Chrome Designcember ได้แสดงให้เห็นถึงการใช้ Container Query แต่ในปีนี้เราจะแสดงให้เห็นถึง API ภาพเคลื่อนไหวที่ลิงก์กับการเลื่อนของ CSS
ดูประสบการณ์การเลื่อนหนังสือได้ที่ web.dev/chrometober-2022
ภาพรวม
เป้าหมายของโปรเจ็กต์นี้คือการมอบประสบการณ์ที่แปลกใหม่ซึ่งไฮไลต์ API ภาพเคลื่อนไหวที่ลิงก์กับการเลื่อน แต่ถึงแม้จะดูแปลกตา ประสบการณ์นี้ก็ต้องตอบสนองได้ดีและเข้าถึงได้ง่ายด้วย นอกจากนี้ โปรเจ็กต์นี้ยังเป็นวิธีที่ยอดเยี่ยมในการทดสอบ API Polyfill ที่อยู่ระหว่างการพัฒนาอย่างต่อเนื่อง รวมถึงการลองใช้เทคนิคและเครื่องมือต่างๆ ร่วมกัน และทั้งหมดนี้มาในธีมฮาโลวีนสุดรื่นเริง
โครงสร้างทีมของเรามีลักษณะดังนี้
- Tyler Reed: ภาพประกอบและการออกแบบ
- Jhey Tompkins: หัวหน้าฝ่ายสถาปัตยกรรมและครีเอทีฟ
- Una Kravets: หัวหน้าโปรเจ็กต์
- Bramus Van Damme: ผู้ร่วมเขียนบทความในเว็บไซต์
- Adam Argyle: การตรวจสอบการช่วยเหลือพิเศษ
- Aaron Forinton: การเขียนคำโฆษณา
การร่างประสบการณ์การเล่าเรื่องแบบ Scrollytelling
ไอเดียสำหรับ Chrometober เริ่มหลั่งไหลเข้ามาในการประชุมนอกสถานที่ครั้งแรกของทีมเมื่อเดือนพฤษภาคม 2022 ภาพวาดลายเส้นจำนวนมากทำให้เราคิดหาวิธีที่ผู้ใช้จะเลื่อนดูสตอรีบอร์ดในรูปแบบต่างๆ ได้ เราได้รับแรงบันดาลใจจากวิดีโอเกม จึงพิจารณาประสบการณ์การเลื่อนดูฉากต่างๆ เช่น สุสานและบ้านผีสิง

ฉันตื่นเต้นมากที่ได้มีอิสระในการสร้างสรรค์เพื่อนำโปรเจ็กต์ Google แรกของฉันไปในทิศทางที่คาดไม่ถึง นี่เป็นต้นแบบแรกๆ ของวิธีที่ผู้ใช้อาจไปยังส่วนต่างๆ ของเนื้อหา
เมื่อผู้ใช้เลื่อนไปด้านข้าง บล็อกจะหมุนและปรับขนาดให้พอดี แต่ฉันตัดสินใจที่จะไม่ใช้แนวคิดนี้เนื่องจากกังวลว่าเราจะสร้างประสบการณ์ที่ยอดเยี่ยมสำหรับผู้ใช้บนอุปกรณ์ทุกขนาดได้อย่างไร แต่ฉันกลับเลือกใช้การออกแบบที่เคยทำในอดีต ในปี 2020 ผมโชคดีที่ได้เข้าถึง ScrollTrigger ของ GreenSock เพื่อสร้างเดโมเวอร์ชันต่างๆ
หนึ่งในการสาธิตที่ฉันสร้างขึ้นคือหนังสือ 3D-CSS ที่หน้าหนังสือจะพลิกเมื่อคุณเลื่อน และฉันรู้สึกว่าวิธีนี้เหมาะกับสิ่งที่เราต้องการสำหรับ Chrometober มากกว่า API ภาพเคลื่อนไหวที่ลิงก์กับการเลื่อนเป็นตัวเลือกที่สมบูรณ์แบบสำหรับฟังก์ชันการทำงานดังกล่าว นอกจากนี้ยังใช้ได้ดีกับ scroll-snap ด้วย ดังที่คุณจะได้เห็นต่อไป
Tyler Reed นักวาดภาพประกอบของโปรเจ็กต์นี้สามารถปรับเปลี่ยนการออกแบบตามไอเดียที่เปลี่ยนไปของเราได้อย่างยอดเยี่ยม Tyler ทำหน้าที่ได้อย่างยอดเยี่ยมในการนำไอเดียสร้างสรรค์ทั้งหมดที่ได้รับมาทำให้เป็นจริง การระดมความคิดร่วมกันเป็นเรื่องที่สนุกมาก ส่วนสำคัญของวิธีที่เราต้องการให้ฟีเจอร์นี้ทำงานคือการแยกฟีเจอร์ออกเป็นบล็อกที่แยกกัน วิธีนี้ช่วยให้เราสามารถจัดองค์ประกอบเป็นฉากๆ แล้วเลือกสิ่งที่เราต้องการทำให้มีชีวิตขึ้นมาได้

แนวคิดหลักคือเมื่อผู้ใช้เลื่อนดูหนังสือ ก็จะเข้าถึงบล็อกเนื้อหาได้ นอกจากนี้ ผู้ใช้ยังสามารถโต้ตอบกับลูกเล่นสนุกๆ ได้ด้วย เช่น อีสเตอร์เอ้กที่เราใส่ไว้ในประสบการณ์การใช้งาน เช่น ภาพบุคคลในบ้านผีสิงที่ดวงตาจะมองตามเคอร์เซอร์ หรือภาพเคลื่อนไหวเล็กๆ น้อยๆ ที่ทริกเกอร์โดย Media Query ไอเดียและฟีเจอร์เหล่านี้จะเคลื่อนไหวเมื่อเลื่อนดู ไอเดียแรกๆ คือกระต่ายซอมบี้ที่จะลุกขึ้นและเคลื่อนที่ไปตามแกน X เมื่อผู้ใช้เลื่อนหน้าจอ
ทำความคุ้นเคยกับ API
ก่อนที่จะเริ่มเล่นกับฟีเจอร์ต่างๆ และอีสเตอร์เอ้ก เราต้องมีหนังสือก่อน เราจึงตัดสินใจใช้โอกาสนี้ในการทดสอบชุดฟีเจอร์สำหรับ CSS scroll-linked animations API ที่กำลังจะเปิดตัว ปัจจุบันยังไม่มีเบราว์เซอร์ใดที่รองรับ API ภาพเคลื่อนไหวที่ลิงก์กับการเลื่อน อย่างไรก็ตาม ในระหว่างการพัฒนา API วิศวกรในทีมการโต้ตอบได้ทำงานเกี่ยวกับ Polyfill ซึ่งจะช่วยให้คุณทดสอบรูปแบบของ API ได้ในขณะที่พัฒนา ซึ่งหมายความว่าเราสามารถใช้ API นี้ได้ในปัจจุบัน และโปรเจ็กต์สนุกๆ เช่นนี้มักเป็นจุดเริ่มต้นที่ดีในการลองใช้ฟีเจอร์ทดลองและแสดงความคิดเห็น ดูสิ่งที่เราได้เรียนรู้และความคิดเห็นที่เราสามารถให้ได้ในบทความนี้
ในภาพรวม คุณสามารถใช้ API นี้เพื่อลิงก์ภาพเคลื่อนไหวกับการเลื่อนได้ โปรดทราบว่าคุณไม่สามารถเรียกให้ภาพเคลื่อนไหวทำงานเมื่อเลื่อนดู ซึ่งอาจเป็นฟีเจอร์ที่จะเพิ่มเข้ามาในภายหลัง ภาพเคลื่อนไหวที่ลิงก์กับการเลื่อนยังแบ่งออกเป็น 2 หมวดหมู่หลักๆ ดังนี้
- โฆษณาที่ตอบสนองต่อตำแหน่งการเลื่อน
- องค์ประกอบที่ตอบสนองต่อตำแหน่งขององค์ประกอบในคอนเทนเนอร์การเลื่อน
ในการสร้างอย่างหลัง เราใช้ ViewTimeline ที่ใช้ผ่านพร็อพเพอร์ตี้ animation-timeline
ตัวอย่างการใช้ ViewTimeline ใน CSS มีลักษณะดังนี้
.element-moving-in-viewport {
view-timeline-name: foo;
view-timeline-axis: block;
}
.element-scroll-linked {
animation: rotate both linear;
animation-timeline: foo;
animation-delay: enter 0%;
animation-end-delay: cover 50%;
}
@keyframes rotate {
to {
rotate: 360deg;
}
}
เราสร้าง ViewTimeline ด้วย view-timeline-name และกำหนดแกนสำหรับ ในตัวอย่างนี้ block หมายถึง block เชิงตรรกะ ภาพเคลื่อนไหวจะลิงก์กับการเลื่อนด้วยพร็อพเพอร์ตี้ animation-timeline animation-delay และ animation-end-delay (ณ เวลาที่เขียน) คือวิธีที่เรากำหนดระยะต่างๆ
ระยะเหล่านี้จะกำหนดจุดที่ควรลิงก์ภาพเคลื่อนไหวโดยสัมพันธ์กับตำแหน่งขององค์ประกอบในคอนเทนเนอร์การเลื่อน ในตัวอย่างนี้ เราจะกำหนดให้เริ่มภาพเคลื่อนไหวเมื่อองค์ประกอบเข้าสู่ (enter 0%) คอนเทนเนอร์การเลื่อน และสิ้นสุดเมื่อครอบคลุม 50% (cover 50%) ของคอนเทนเนอร์การเลื่อน
ตัวอย่างการทำงานมีดังนี้
นอกจากนี้ คุณยังลิงก์ภาพเคลื่อนไหวกับองค์ประกอบที่เคลื่อนไหวในวิวพอร์ตได้ด้วย ซึ่งทำได้โดยตั้งค่า animation-timeline ให้เป็น view-timeline ขององค์ประกอบ ซึ่งเหมาะสำหรับสถานการณ์ต่างๆ เช่น ภาพเคลื่อนไหวของรายการ ลักษณะการทำงานจะคล้ายกับวิธีที่คุณอาจทำให้องค์ประกอบเคลื่อนไหวเมื่อเข้าสู่หน้าเว็บโดยใช้ IntersectionObserver
element-moving-in-viewport {
view-timeline-name: foo;
view-timeline-axis: block;
animation: scale both linear;
animation-delay: enter 0%;
animation-end-delay: cover 50%;
animation-timeline: foo;
}
@keyframes scale {
0% {
scale: 0;
}
}
ด้วยวิธีนี้ "Mover" จะปรับขนาดขึ้นเมื่อเข้าสู่วิวพอร์ต ซึ่งจะทริกเกอร์การหมุนของ "Spinner"
สิ่งที่ฉันพบจากการทดลองคือ API ทำงานได้ดีมากกับ scroll-snap การสแนปการเลื่อนที่ใช้ร่วมกับ ViewTimeline จะเหมาะอย่างยิ่งสำหรับการสแนปการเปลี่ยนหน้าในหนังสือ
การสร้างต้นแบบกลไก
หลังจากทดลองอยู่พักหนึ่ง ฉันก็สร้างต้นแบบหนังสือที่ใช้งานได้สำเร็จ คุณเลื่อนในแนวนอนเพื่อพลิกหน้าหนังสือ
ในการสาธิต คุณจะเห็นทริกเกอร์ต่างๆ ที่ไฮไลต์ด้วยเส้นขอบประ
มาร์กอัปจะมีลักษณะดังนี้
<body>
<div class="book-placeholder">
<ul class="book" style="--count: 7;">
<li
class="page page--cover page--cover-front"
data-scroll-target="1"
style="--index: 0;"
>
<div class="page__paper">
<div class="page__side page__side--front"></div>
<div class="page__side page__side--back"></div>
</div>
</li>
<!-- Markup for other pages here -->
</ul>
</div>
<div>
<p>intro spacer</p>
</div>
<div data-scroll-intro>
<p>scale trigger</p>
</div>
<div data-scroll-trigger="1">
<p>page trigger</p>
</div>
<!-- Markup for other triggers here -->
</body>
เมื่อคุณเลื่อน หน้าหนังสือจะพลิก แต่จะเปิดหรือปิดอย่างรวดเร็ว ซึ่งขึ้นอยู่กับการจัดแนวสแนปการเลื่อนของทริกเกอร์
html {
scroll-snap-type: x mandatory;
}
body {
grid-template-columns: repeat(var(--trigger-count), auto);
overflow-y: hidden;
overflow-x: scroll;
display: grid;
}
body > [data-scroll-trigger] {
height: 100vh;
width: clamp(10rem, 10vw, 300px);
}
body > [data-scroll-trigger] {
scroll-snap-align: end;
}
ครั้งนี้เราจะไม่เชื่อมต่อ ViewTimeline ใน CSS แต่จะใช้ Web Animations API ใน JavaScript ซึ่งมีประโยชน์เพิ่มเติมคือสามารถวนซ้ำชุดองค์ประกอบและสร้าง ViewTimeline ที่เราต้องการได้ แทนที่จะสร้างทีละรายการด้วยตนเอง
const triggers = document.querySelectorAll("[data-scroll-trigger]")
const commonProps = {
delay: { phase: "enter", percent: CSS.percent(0) },
endDelay: { phase: "enter", percent: CSS.percent(100) },
fill: "both"
}
const setupPage = (trigger, index) => {
const target = document.querySelector(
`[data-scroll-target="${trigger.getAttribute("data-scroll-trigger")}"]`
);
const viewTimeline = new ViewTimeline({
subject: trigger,
axis: 'inline',
});
target.animate(
[
{
transform: `translateZ(${(triggers.length - index) * 2}px)`
},
{
transform: `translateZ(${(triggers.length - index) * 2}px)`,
offset: 0.75
},
{
transform: `translateZ(${(triggers.length - index) * -1}px)`
}
],
{
timeline: viewTimeline,
…commonProps,
}
);
target.querySelector(".page__paper").animate(
[
{
transform: "rotateY(0deg)"
},
{
transform: "rotateY(-180deg)"
}
],
{
timeline: viewTimeline,
…commonProps,
}
);
};
const triggers = document.querySelectorAll('[data-scroll-trigger]')
triggers.forEach(setupPage);
เราจะสร้าง ViewTimeline สำหรับทริกเกอร์แต่ละรายการ จากนั้นเราจะสร้างภาพเคลื่อนไหวของหน้าที่เชื่อมโยงกับทริกเกอร์โดยใช้ ViewTimeline นั้น ซึ่งจะลิงก์ภาพเคลื่อนไหวของหน้าเว็บกับการเลื่อน สำหรับภาพเคลื่อนไหว เราจะหมุนองค์ประกอบของหน้าบนแกน Y เพื่อพลิกหน้า นอกจากนี้ เรายังแปลหน้าเว็บในแกน Z เพื่อให้ทำงานเหมือนหนังสือด้วย
สรุปข้อมูลทั้งหมด
เมื่อฉันคิดกลไกสำหรับหนังสือได้แล้ว ฉันก็สามารถมุ่งเน้นไปที่การทำให้ภาพประกอบของ Tyler มีชีวิตขึ้นมาได้
โหมดถ่ายภาพดวงดาว
ทีมใช้ Astro สำหรับ Designcember ในปี 2021 และฉันก็อยากใช้เครื่องมือนี้อีกครั้งสำหรับ Chrometober ประสบการณ์ของนักพัฒนาซอฟต์แวร์ในการแยกสิ่งต่างๆ ออกเป็นคอมโพเนนต์เหมาะกับโปรเจ็กต์นี้เป็นอย่างดี
หนังสือเป็นองค์ประกอบหนึ่ง นอกจากนี้ยังเป็นคอลเล็กชันของคอมโพเนนต์หน้าเว็บด้วย แต่ละหน้ามี 2 ด้านและมีฉากหลัง องค์ประกอบย่อยของหน้าคือคอมโพเนนต์ที่สามารถเพิ่ม นำออก และจัดตำแหน่งได้อย่างง่ายดาย
การสร้างสมุดภาพ
ฉันจึงต้องทำให้บล็อกต่างๆ จัดการได้ง่าย นอกจากนี้ ฉันยังต้องการให้สมาชิกคนอื่นๆ ในทีมมีส่วนร่วมได้ง่ายๆ ด้วย
หน้าเว็บในระดับสูงจะกำหนดโดยอาร์เรย์การกำหนดค่า ออบเจ็กต์หน้าเว็บแต่ละรายการในอาร์เรย์จะกำหนดเนื้อหา ฉากหลัง และข้อมูลเมตาอื่นๆ ของหน้าเว็บ
const pages = [
{
front: {
marked: true,
content: PageTwo,
backdrop: spreadOne,
darkBackdrop: spreadOneDark
},
back: {
content: PageThree,
backdrop: spreadTwo,
darkBackdrop: spreadTwoDark
},
aria: `page 1`
},
/* Obfuscated page objects */
]
ระบบจะส่งข้อมูลเหล่านี้ไปยังคอมโพเนนต์ Book
<Book pages={pages} />
คอมโพเนนต์ Book คือตำแหน่งที่ใช้กลไกการเลื่อนและสร้างหน้าหนังสือ เราใช้กลไกเดียวกันจากต้นแบบ แต่เราแชร์ ViewTimeline หลายอินสแตนซ์ที่สร้างขึ้นทั่วโลก
window.CHROMETOBER_TIMELINES.push(viewTimeline);
วิธีนี้ช่วยให้เราแชร์ไทม์ไลน์เพื่อใช้ที่อื่นได้แทนที่จะต้องสร้างใหม่ เดี๋ยวเราจะไปลงรายละเอียดเพิ่มเติมกันในภายหลัง
องค์ประกอบของหน้าเว็บ
แต่ละหน้าเป็นรายการที่อยู่ภายในลิสต์ ดังนี้
<ul class="book">
{
pages.map((page, index) => {
const FrontSlot = page.front.content
const BackSlot = page.back.content
return (
<Page
index={index}
cover={page.cover}
aria={page.aria}
backdrop={
{
front: {
light: page.front.backdrop,
dark: page.front.darkBackdrop
},
back: {
light: page.back.backdrop,
dark: page.back.darkBackdrop
}
}
}>
{page.front.content && <FrontSlot slot="front" />}
{page.back.content && <BackSlot slot="back" />}
</Page>
)
})
}
</ul>
และการกำหนดค่าที่ระบุจะส่งต่อไปยังอินสแตนซ์ Page แต่ละรายการ หน้าเว็บใช้ฟีเจอร์ช่องของ Astro เพื่อแทรกเนื้อหาลงในแต่ละหน้า
<li
class={className}
data-scroll-target={target}
style={`--index:${index};`}
aria-label={aria}
>
<div class="page__paper">
<div
class="page__side page__side--front"
aria-label={`Right page of ${index}`}
>
<picture>
<source
srcset={darkFront}
media="(prefers-color-scheme: dark)"
height="214"
width="150"
>
<img
src={lightFront}
class="page__background page__background--right"
alt=""
aria-hidden="true"
height="214"
width="150"
>
</picture>
<div class="page__content">
<slot name="front" />
</div>
</div>
<!-- Markup for back page -->
</div>
</li>
โค้ดนี้มีไว้เพื่อตั้งค่าโครงสร้างเป็นหลัก ผู้มีส่วนร่วมสามารถทำงานกับเนื้อหาของหนังสือได้โดยไม่ต้องแตะโค้ดนี้
ฉากหลัง
การเปลี่ยนแนวคิดสร้างสรรค์มาเป็นหนังสือทำให้การแบ่งส่วนต่างๆ ง่ายขึ้นมาก และหนังสือแต่ละหน้าก็เป็นฉากที่นำมาจากการออกแบบดั้งเดิม

เนื่องจากเราได้กำหนดสัดส่วนภาพสำหรับหนังสือแล้ว ฉากหลังของแต่ละหน้าจึงมีองค์ประกอบรูปภาพได้ การตั้งค่าองค์ประกอบนั้นให้มีความกว้าง 200% และใช้ object-position ตามด้านของหน้าจะช่วยแก้ปัญหานี้ได้
.page__background {
height: 100%;
width: 200%;
object-fit: cover;
object-position: 0 0;
position: absolute;
top: 0;
left: 0;
}
.page__background--right {
object-position: 100% 0;
}
เนื้อหาของหน้า
มาดูการสร้างหน้าเว็บกัน หน้าที่ 3 มีนกฮูกที่โผล่ขึ้นมาในต้นไม้
ระบบจะป้อนข้อมูลคอมโพเนนต์ PageThree ตามที่กำหนดไว้ในการกำหนดค่า ซึ่งเป็นคอมโพเนนต์ Astro (PageThree.astro) คอมโพเนนต์เหล่านี้มีลักษณะเหมือนไฟล์ HTML แต่มีโค้ดเฟนซ์ที่ด้านบนคล้ายกับ Frontmatter ซึ่งจะช่วยให้เราทำสิ่งต่างๆ เช่น นำเข้าคอมโพเนนต์อื่นๆ ได้ คอมโพเนนต์สำหรับหน้าที่ 3 จะมีลักษณะดังนี้
---
import TreeOwl from '../TreeOwl/TreeOwl.astro'
import { contentBlocks } from '../../assets/content-blocks.json'
import ContentBlock from '../ContentBlock/ContentBlock.astro'
---
<TreeOwl/>
<ContentBlock {...contentBlocks[3]} id="four" />
<style is:global>
.content-block--four {
left: 30%;
bottom: 10%;
}
</style>
ขอย้ำอีกครั้งว่าหน้าเว็บเป็นหน่วยที่เล็กที่สุด โดยสร้างขึ้นจากคอลเล็กชันฟีเจอร์ หน้าที่ 3 มีบล็อกเนื้อหาและนกฮูกแบบอินเทอร์แอกทีฟ จึงมีคอมโพเนนต์สำหรับแต่ละรายการ
บล็อกเนื้อหาคือลิงก์ไปยังเนื้อหาที่เห็นภายในหนังสือ ซึ่งขับเคลื่อนโดยออบเจ็กต์การกำหนดค่าด้วย
{
"contentBlocks": [
{
"id": "one",
"title": "New in Chrome",
"blurb": "Lift your spirits with a round up of all the tools and features in Chrome.",
"link": "https://www.youtube.com/watch?v=qwdN1fJA_d8&list=PLNYkxOF6rcIDfz8XEA3loxY32tYh7CI3m"
},
…otherBlocks
]
}
ระบบจะนำเข้าการกำหนดค่านี้เมื่อจำเป็นต้องใช้บล็อกเนื้อหา จากนั้นระบบจะส่งการกำหนดค่าบล็อกที่เกี่ยวข้องไปยังคอมโพเนนต์ ContentBlock
<ContentBlock {...contentBlocks[3]} id="four" />
นอกจากนี้ยังมีตัวอย่างวิธีที่เราใช้องค์ประกอบของหน้าเป็นตำแหน่งในการวางเนื้อหาด้วย บล็อกเนื้อหาจะอยู่ในตำแหน่งนี้
<style is:global>
.content-block--four {
left: 30%;
bottom: 10%;
}
</style>
แต่สไตล์ทั่วไปสำหรับบล็อกเนื้อหาจะอยู่ร่วมกับโค้ดคอมโพเนนต์
.content-block {
background: hsl(0deg 0% 0% / 70%);
color: var(--gray-0);
border-radius: min(3vh, var(--size-4));
padding: clamp(0.75rem, 2vw, 1.25rem);
display: grid;
gap: var(--size-2);
position: absolute;
cursor: pointer;
width: 50%;
}
ส่วนนกฮูกของเราเป็นฟีเจอร์แบบอินเทอร์แอกทีฟ ซึ่งเป็นหนึ่งในหลายๆ ฟีเจอร์ในโปรเจ็กต์นี้ นี่เป็นตัวอย่างเล็กๆ ที่ดีในการแสดงให้เห็นว่าเราใช้ ViewTimeline ที่แชร์ซึ่งเราสร้างขึ้นอย่างไร
ในระดับสูง คอมโพเนนต์นกฮูกของเราจะนำเข้า SVG บางส่วนและแทรกในบรรทัดโดยใช้ Fragment ของ Astro
---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />
และสไตล์สำหรับการจัดตำแหน่งนกฮูกจะอยู่ร่วมกับโค้ดคอมโพเนนต์
.owl {
width: 34%;
left: 10%;
bottom: 34%;
}
มีการจัดรูปแบบเพิ่มเติมอีก 1 รายการที่กำหนดลักษณะการทำงานของ transform สำหรับนกฮูก
.owl__owl {
transform-origin: 50% 100%;
transform-box: fill-box;
}
การใช้ transform-box จะส่งผลต่อ transform-origin ซึ่งจะทำให้สัมพันธ์กับกรอบล้อมรอบของออบเจ็กต์ภายใน SVG นกฮูกจะขยายขนาดขึ้นจากตรงกลางด้านล่าง จึงใช้ transform-origin: 50% 100%
ส่วนที่สนุกคือตอนที่เราลิงก์นกฮูกกับ ViewTimeline ที่สร้างขึ้นมา
const setUpOwl = () => {
const owl = document.querySelector('.owl__owl');
owl.animate([
{
translate: '0% 110%',
},
{
translate: '0% 10%',
},
], {
timeline: CHROMETOBER_TIMELINES[1],
delay: { phase: "enter", percent: CSS.percent(80) },
endDelay: { phase: "enter", percent: CSS.percent(90) },
fill: 'both'
});
}
if (window.matchMedia('(prefers-reduced-motion: no-preference)').matches)
setUpOwl()
ในโค้ดบล็อกนี้ เราจะทำ 2 อย่างดังนี้
- ตรวจสอบค่ากำหนดการเคลื่อนไหวของผู้ใช้
- หากไม่มีความชอบ ให้ลิงก์ภาพเคลื่อนไหวของนกฮูกกับการเลื่อน
สำหรับส่วนที่ 2 นกฮูกจะเคลื่อนไหวบนแกน Y โดยใช้ Web Animations API มีการใช้พร็อพเพอร์ตี้การเปลี่ยนรูปแบบแต่ละรายการ translate และลิงก์กับ ViewTimeline 1 รายการ โดยลิงก์กับ CHROMETOBER_TIMELINES[1] ผ่านพร็อพเพอร์ตี้ timeline นี่คือ ViewTimeline ที่สร้างขึ้นสำหรับการเปลี่ยนหน้า ซึ่งจะลิงก์ภาพเคลื่อนไหวของนกฮูกกับการเปลี่ยนหน้าโดยใช้ระยะ enter ซึ่งกำหนดว่าเมื่อพลิกหน้าไป 80% แล้ว ให้เริ่มขยับนกฮูก เมื่อถึง 90% นกฮูกควรแปลเสร็จแล้ว
ฟีเจอร์สำหรับหนังสือ
ตอนนี้คุณได้เห็นแนวทางในการสร้างหน้าเว็บและวิธีการทำงานของสถาปัตยกรรมโปรเจ็กต์แล้ว คุณจะเห็นว่าเครื่องมือนี้ช่วยให้ผู้ร่วมเขียนสามารถเข้ามาทำงานในหน้าหรือฟีเจอร์ที่ตนเลือกได้อย่างไร ฟีเจอร์ต่างๆ ในหนังสือมีภาพเคลื่อนไหวที่ลิงก์กับการเปลี่ยนหน้าของหนังสือ เช่น ค้างคาวที่บินเข้าและออกเมื่อเปลี่ยนหน้า
นอกจากนี้ยังมีองค์ประกอบที่ทำงานด้วยภาพเคลื่อนไหว CSS ด้วย
เมื่อบล็อกเนื้อหาอยู่ในหนังสือแล้ว ก็ถึงเวลาสร้างสรรค์ด้วยฟีเจอร์อื่นๆ ซึ่งทำให้เรามีโอกาสสร้างการโต้ตอบที่แตกต่างออกไป และลองใช้วิธีต่างๆ ในการนำสิ่งต่างๆ ไปใช้
การทำให้สิ่งต่างๆ ตอบสนองได้ดี
หน่วยวิวพอร์ตที่ปรับเปลี่ยนตามอุปกรณ์จะปรับขนาดหนังสือและฟีเจอร์ต่างๆ อย่างไรก็ตาม การทำให้แบบอักษรปรับเปลี่ยนตามอุปกรณ์เป็นความท้าทายที่น่าสนใจ หน่วยการค้นหาคอนเทนเนอร์จึงเหมาะอย่างยิ่งในกรณีนี้ อย่างไรก็ตาม ฟีเจอร์นี้ยังไม่พร้อมให้บริการในบางพื้นที่ เนื่องจากมีการกำหนดขนาดของหนังสือไว้แล้ว เราจึงไม่จำเป็นต้องใช้การค้นหาคอนเทนเนอร์ คุณสร้างหน่วยการค้นหาคอนเทนเนอร์แบบอินไลน์ได้ด้วย CSS calc() และใช้สำหรับการปรับขนาดแบบอักษร
.book-placeholder {
--size: clamp(12rem, 72vw, 80vmin);
--aspect-ratio: 360 / 504;
--cqi: calc(0.01 * (var(--size) * (var(--aspect-ratio))));
}
.content-block h2 {
color: var(--gray-0);
font-size: clamp(0.6rem, var(--cqi) * 4, 1.5rem);
}
.content-block :is(p, a) {
font-size: clamp(0.6rem, var(--cqi) * 3, 1.5rem);
}
ฟักทองส่องแสงยามค่ำคืน
ผู้ที่สังเกตเห็นอาจสังเกตเห็นการใช้องค์ประกอบ <source> เมื่อพูดถึงฉากหลังของหน้าก่อนหน้านี้ Una ต้องการให้มีการโต้ตอบที่ตอบสนองต่อค่ากำหนดรูปแบบสี ด้วยเหตุนี้ ฉากหลังจึงรองรับทั้งโหมดสว่างและโหมดมืดที่มีตัวแปรแตกต่างกัน เนื่องจากคุณสามารถใช้การสืบค้นสื่อกับองค์ประกอบ <picture> ได้ จึงเป็นวิธีที่ยอดเยี่ยมในการจัดเตรียมรูปแบบฉากหลัง 2 รูปแบบ องค์ประกอบ <source> จะค้นหาค่ากำหนดรูปแบบสีและแสดงฉากหลังที่เหมาะสม
<picture>
<source srcset={darkFront} media="(prefers-color-scheme: dark)" height="214" width="150">
<img src={lightFront} class="page__background page__background--right" alt="" aria-hidden="true" height="214" width="150">
</picture>
คุณอาจทำการเปลี่ยนแปลงอื่นๆ โดยอิงตามค่ากำหนดรูปแบบสีนั้น ฟักทองในหน้าที่ 2 จะตอบสนองต่อค่ากำหนดรูปแบบสีของผู้ใช้ SVG ที่ใช้มีวงกลมที่แสดงถึงเปลวไฟ ซึ่งจะขยายขนาดและเคลื่อนไหวในโหมดมืด
.pumpkin__flame,
.pumpkin__flame circle {
transform-box: fill-box;
transform-origin: 50% 100%;
}
.pumpkin__flame {
scale: 0.8;
}
.pumpkin__flame circle {
transition: scale 0.2s;
scale: 0;
}
@media(prefers-color-scheme: dark) {
.pumpkin__flame {
animation: pumpkin-flicker 3s calc(var(--index, 0) * -1s) infinite linear;
}
.pumpkin__flame circle {
scale: 1;
}
@keyframes pumpkin-flicker {
50% {
scale: 1;
}
}
}
ภาพเหมือนนี้กำลังมองคุณอยู่ใช่ไหม
หากดูหน้า 10 คุณอาจสังเกตเห็นบางอย่าง มีคนกำลังดูคุณอยู่ ดวงตาของภาพบุคคลจะมองตามเคอร์เซอร์ขณะที่คุณเลื่อนไปรอบๆ หน้า เคล็ดลับคือการแมปตำแหน่งเคอร์เซอร์กับค่าการแปล และส่งไปยัง CSS
const mapRange = (inputLower, inputUpper, outputLower, outputUpper, value) => {
const INPUT_RANGE = inputUpper - inputLower
const OUTPUT_RANGE = outputUpper - outputLower
return outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0)
}
โค้ดนี้จะรับช่วงอินพุตและเอาต์พุต และจับคู่ค่าที่ระบุ ตัวอย่างเช่น การใช้งานนี้จะให้ค่า 625
mapRange(0, 100, 250, 1000, 50) // 625
สำหรับภาพบุคคล ค่าที่ป้อนคือจุดกึ่งกลางของดวงตาแต่ละข้าง บวกหรือลบระยะห่างพิกเซลบางส่วน ช่วงเอาต์พุตคือระยะที่ดวงตาสามารถแปลเป็นพิกเซลได้ จากนั้นระบบจะส่งตำแหน่งตัวชี้บนแกน x หรือ y เป็นค่า ระบบจะทำซ้ำดวงตาเพื่อให้ได้จุดศูนย์กลางของดวงตาขณะขยับ โดยที่ไฟล์ต้นฉบับจะไม่ย้ายไปไหน มีความโปร่งใส และใช้เพื่อการอ้างอิง
จากนั้นก็เป็นเรื่องของการผูกเข้าด้วยกันและอัปเดตค่าคุณสมบัติที่กำหนดเองของ CSS บนดวงตาเพื่อให้ดวงตาเคลื่อนไหวได้ ฟังก์ชันเชื่อมโยงกับเหตุการณ์ pointermove เทียบกับ window เมื่อเหตุการณ์นี้เริ่มทำงาน ระบบจะใช้ขอบเขตของดวงตาแต่ละข้างเพื่อคำนวณจุดศูนย์กลาง จากนั้นระบบจะแมปตำแหน่งตัวชี้กับค่าที่ตั้งไว้เป็นค่าคุณสมบัติที่กำหนดเองในดวงตา
const RANGE = 15
const LIMIT = 80
const interact = ({ x, y }) => {
// map a range against the eyes and pass in via custom properties
const LEFT_EYE_BOUNDS = LEFT_EYE.getBoundingClientRect()
const RIGHT_EYE_BOUNDS = RIGHT_EYE.getBoundingClientRect()
const CENTERS = {
lx: LEFT_EYE_BOUNDS.left + LEFT_EYE_BOUNDS.width * 0.5,
rx: RIGHT_EYE_BOUNDS.left + RIGHT_EYE_BOUNDS.width * 0.5,
ly: LEFT_EYE_BOUNDS.top + LEFT_EYE_BOUNDS.height * 0.5,
ry: RIGHT_EYE_BOUNDS.top + RIGHT_EYE_BOUNDS.height * 0.5,
}
Object.entries(CENTERS)
.forEach(([key, value]) => {
const result = mapRange(value - LIMIT, value + LIMIT, -RANGE, RANGE)(key.indexOf('x') !== -1 ? x : y)
EYES.style.setProperty(`--${key}`, result)
})
}
เมื่อส่งค่าไปยัง CSS แล้ว สไตล์จะทำสิ่งต่างๆ กับค่าเหล่านั้นได้ตามต้องการ ส่วนที่ยอดเยี่ยมคือการใช้ CSS clamp() เพื่อทำให้พฤติกรรมของดวงตาแต่ละข้างแตกต่างกัน คุณจึงทำให้ดวงตาแต่ละข้างมีพฤติกรรมแตกต่างกันได้โดยไม่ต้องแตะ JavaScript อีก
.portrait__eye--mover {
transition: translate 0.2s;
}
.portrait__eye--mover.portrait__eye--left {
translate:
clamp(-10px, var(--lx, 0) * 1px, 4px)
clamp(-4px, var(--ly, 0) * 0.5px, 10px);
}
.portrait__eye--mover.portrait__eye--right {
translate:
clamp(-4px, var(--rx, 0) * 1px, 10px)
clamp(-4px, var(--ry, 0) * 0.5px, 10px);
}
การร่ายคาถา
ถ้าคุณดูหน้าที่ 6 คุณจะรู้สึกเหมือนต้องมนต์สะกดไหม หน้านี้ใช้การออกแบบของสุนัขจิ้งจอกวิเศษที่น่าอัศจรรย์ หากเลื่อนเคอร์เซอร์ไปรอบๆ คุณอาจเห็นเอฟเฟกต์เส้นทางเคอร์เซอร์ที่กำหนดเอง ซึ่งใช้ภาพเคลื่อนไหวของ Canvas องค์ประกอบ <canvas> อยู่เหนือเนื้อหาอื่นๆ ในหน้าเว็บที่มี pointer-events: none ซึ่งหมายความว่าผู้ใช้จะยังคงคลิกบล็อกเนื้อหาที่อยู่ด้านล่างได้
.wand-canvas {
height: 100%;
width: 200%;
pointer-events: none;
right: 0;
position: fixed;
}
องค์ประกอบ <canvas> ของเราก็ฟังเหตุการณ์ pointermove ใน window เช่นเดียวกับแนวตั้ง แต่ทุกครั้งที่เหตุการณ์เริ่มทำงาน เราจะสร้างออบเจ็กต์เพื่อแสดงภาพเคลื่อนไหวในองค์ประกอบ <canvas> ออบเจ็กต์เหล่านี้แสดงถึงรูปร่างที่ใช้ในเส้นทางเคอร์เซอร์ โดยมีพิกัดและสีแบบสุ่ม
เราใช้ฟังก์ชัน mapRange จากก่อนหน้านี้อีกครั้ง เนื่องจากเราสามารถใช้ฟังก์ชันนี้เพื่อแมปเดลต้าของเคอร์เซอร์กับ size และ rate ได้ ออบเจ็กต์จะจัดเก็บไว้ในอาร์เรย์ที่วนซ้ำเมื่อมีการวาดออบเจ็กต์ไปยังองค์ประกอบ <canvas> พร็อพเพอร์ตี้ของออบเจ็กต์แต่ละรายการจะบอกองค์ประกอบ <canvas> ของเราว่าควรวาดสิ่งต่างๆ ไว้ที่ใด
const blocks = []
const createBlock = ({ x, y, movementX, movementY }) => {
const LOWER_SIZE = CANVAS.height * 0.05
const UPPER_SIZE = CANVAS.height * 0.25
const size = mapRange(0, 100, LOWER_SIZE, UPPER_SIZE, Math.max(Math.abs(movementX), Math.abs(movementY)))
const rate = mapRange(LOWER_SIZE, UPPER_SIZE, 1, 5, size)
const { left, top, width, height } = CANVAS.getBoundingClientRect()
const block = {
hue: Math.random() * 359,
x: x - left,
y: y - top,
size,
rate,
}
blocks.push(block)
}
window.addEventListener('pointermove', createBlock)
สำหรับการวาดภาพบน Canvas ระบบจะสร้างลูปด้วย requestAnimationFrame เส้นทางเคอร์เซอร์ควรแสดงผลเมื่อหน้าเว็บอยู่ในมุมมองเท่านั้น เรามี IntersectionObserver ที่อัปเดตและกำหนดว่าหน้าใดอยู่ในมุมมอง หากหน้าเว็บอยู่ในมุมมอง ระบบจะแสดงผลออบเจ็กต์เป็นวงกลมบน Canvas
จากนั้นเราจะวนซ้ำอาร์เรย์ blocks และวาดแต่ละส่วนของเส้นทาง แต่ละเฟรมจะลดขนาดและเปลี่ยนตำแหน่งของวัตถุตามrate ซึ่งจะทำให้เกิดเอฟเฟกต์การตกและการปรับขนาด หากวัตถุหดตัวจนหมด วัตถุนั้นจะถูกนำออกจากอาร์เรย์ blocks
let wandFrame
const drawBlocks = () => {
ctx.clearRect(0, 0, CANVAS.width, CANVAS.height)
if (PAGE_SIX.className.indexOf('in-view') === -1 && wandFrame) {
blocks.length = 0
cancelAnimationFrame(wandFrame)
document.body.removeEventListener('pointermove', createBlock)
document.removeEventListener('resize', init)
}
for (let b = 0; b < blocks.length; b++) {
const block = blocks[b]
ctx.strokeStyle = ctx.fillStyle = `hsla(${block.hue}, 80%, 80%, 0.5)`
ctx.beginPath()
ctx.arc(block.x, block.y, block.size * 0.5, 0, 2 * Math.PI)
ctx.stroke()
ctx.fill()
block.size -= block.rate
block.y += block.rate
if (block.size <= 0) {
blocks.splice(b, 1)
}
}
wandFrame = requestAnimationFrame(drawBlocks)
}
หากหน้าเว็บอยู่นอกมุมมอง ระบบจะนำ Listener เหตุการณ์ออกและยกเลิกการวนซ้ำเฟรมภาพเคลื่อนไหว นอกจากนี้ ระบบจะล้างอาร์เรย์ blocks ด้วย
นี่คือตัวอย่างการทำงานของรอยทางเคอร์เซอร์
การตรวจสอบการช่วยเหลือพิเศษ
การสร้างประสบการณ์ที่สนุกสนานในการสำรวจเป็นสิ่งที่ดี แต่จะไม่มีประโยชน์เลยหากผู้ใช้เข้าถึงไม่ได้ ความเชี่ยวชาญของ Adam ในด้านนี้มีคุณค่าอย่างยิ่งในการเตรียม Chrometober ให้พร้อมสำหรับการตรวจสอบการช่วยเหลือพิเศษก่อนเผยแพร่
หัวข้อสำคัญบางส่วนที่ครอบคลุมมีดังนี้
- ตรวจสอบว่า HTML ที่ใช้เป็นแบบเชิงความหมาย ซึ่งรวมถึงสิ่งต่างๆ เช่น องค์ประกอบจุดสังเกตที่เหมาะสม เช่น
<main>สำหรับหนังสือ รวมถึงการใช้องค์ประกอบ<article>สำหรับบล็อกเนื้อหาแต่ละบล็อก และองค์ประกอบ<abbr>ที่มีการแนะนำตัวย่อ การคิดล่วงหน้าขณะสร้างหนังสือทำให้เข้าถึงสิ่งต่างๆ ได้ง่ายขึ้น การใช้ส่วนหัวและลิงก์ช่วยให้ผู้ใช้ไปยังส่วนต่างๆ ได้ง่ายขึ้น การใช้รายการสำหรับหน้าเว็บยังหมายความว่าเทคโนโลยีความช่วยเหลือพิเศษจะประกาศจำนวนหน้าด้วย - ตรวจสอบว่ารูปภาพทั้งหมดใช้แอตทริบิวต์
altที่เหมาะสม สำหรับ SVG ในบรรทัด องค์ประกอบtitleจะแสดงเมื่อจำเป็น - ใช้แอตทริบิวต์
ariaในกรณีที่ช่วยปรับปรุงประสบการณ์การใช้งาน การใช้aria-labelสำหรับหน้าและด้านข้างของหน้าจะสื่อสารให้ผู้ใช้ทราบว่าตนเองอยู่ในหน้าใด การใช้aria-describedByในลิงก์ "อ่านเพิ่มเติม" จะสื่อสารข้อความของบล็อกเนื้อหา ซึ่งจะช่วยขจัดความคลุมเครือเกี่ยวกับปลายทางที่ลิงก์จะนำผู้ใช้ไป - ในเรื่องของบล็อกเนื้อหา คุณจะคลิกทั้งการ์ดได้ ไม่ใช่แค่ลิงก์ "อ่านเพิ่มเติม" เท่านั้น
- เราได้พูดถึงการใช้
IntersectionObserverเพื่อติดตามว่าหน้าใดอยู่ในมุมมองไปแล้วก่อนหน้านี้ ซึ่งมีประโยชน์หลายประการที่ไม่ใช่แค่เรื่องประสิทธิภาพเท่านั้น หน้าเว็บที่ไม่ได้อยู่ในมุมมองจะหยุดภาพเคลื่อนไหวหรือการโต้ตอบชั่วคราว แต่หน้าเหล่านี้ยังมีแอตทริบิวต์inertที่ใช้ด้วย ซึ่งหมายความว่าผู้ใช้ที่ใช้โปรแกรมอ่านหน้าจอจะสำรวจเนื้อหาเดียวกันกับผู้ใช้ที่มองเห็นได้ โฟกัสจะยังคงอยู่ภายในหน้าเว็บที่แสดงอยู่ และผู้ใช้จะกด Tab เพื่อไปยังหน้าอื่นไม่ได้ - สุดท้ายแต่ไม่ท้ายสุด เราใช้การค้นหาสื่อเพื่อเคารพความต้องการของผู้ใช้เกี่ยวกับการเคลื่อนไหว
นี่คือภาพหน้าจอจากการตรวจสอบที่ไฮไลต์มาตรการบางส่วนที่มีอยู่
องค์ประกอบจะได้รับการระบุว่าอยู่รอบๆ หนังสือทั้งเล่ม ซึ่งบ่งชี้ว่าควรเป็นจุดสังเกตหลักที่ผู้ใช้เทคโนโลยีความช่วยเหลือพิเศษจะค้นหา More is outlined in the screenshot." width="800" height="465">

สิ่งที่เราได้เรียนรู้
แรงจูงใจเบื้องหลัง Chrometober ไม่เพียงแต่เป็นการไฮไลต์เนื้อหาเว็บจากชุมชนเท่านั้น แต่ยังเป็นวิธีที่เราทดสอบการขับเคลื่อน Polyfill ของ API ภาพเคลื่อนไหวที่ลิงก์กับการเลื่อนซึ่งอยู่ระหว่างการพัฒนาด้วย
เราได้จัดเซสชันหนึ่งขึ้นระหว่างการประชุมสุดยอดของทีมในนิวยอร์กเพื่อทดสอบโปรเจ็กต์และแก้ไขปัญหาที่เกิดขึ้น การมีส่วนร่วมของทีมมีคุณค่าอย่างยิ่ง นอกจากนี้ยังเป็นโอกาสดีในการระบุสิ่งต่างๆ ทั้งหมดที่ต้องจัดการก่อนที่เราจะเผยแพร่เว็บไซต์

เช่น การทดสอบหนังสือบนอุปกรณ์ทำให้เกิดปัญหาการแสดงผล หนังสือของเราไม่แสดงผลตามที่คาดไว้ในอุปกรณ์ iOS หน่วยวิวพอร์ตจะกำหนดขนาดหน้าเว็บ แต่เมื่อมีรอยบาก ก็จะส่งผลต่อหนังสือ วิธีแก้ปัญหาคือใช้ viewport-fit=cover ในวิวพอร์ต meta ดังนี้
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
เซสชันนี้ยังทำให้เกิดปัญหาบางอย่างเกี่ยวกับ Polyfill ของ API ด้วย Bramus ได้แจ้งปัญหาเหล่านี้ในที่เก็บ Polyfill ต่อมาเขาพบวิธีแก้ปัญหาเหล่านั้นและรวมเข้ากับ Polyfill ตัวอย่างเช่น Pull Request นี้ช่วยเพิ่มประสิทธิภาพด้วยการเพิ่มการแคชลงในส่วนของ Polyfill


เท่านี้ก็เรียบร้อย
โปรเจ็กต์นี้เป็นโปรเจ็กต์ที่สนุกมาก ซึ่งส่งผลให้เกิดประสบการณ์การเลื่อนดูที่แปลกใหม่ซึ่งไฮไลต์เนื้อหาที่น่าทึ่งจากชุมชน นอกจากนี้ ยังเหมาะสำหรับการทดสอบ Polyfill รวมถึงการแสดงความคิดเห็นต่อทีมวิศวกรเพื่อช่วยปรับปรุง Polyfill ด้วย
Chrometober 2022 สิ้นสุดลงแล้ว
เราหวังว่าคุณจะชอบ ฟีเจอร์ที่คุณชอบคืออะไร ทวีตหาเราเพื่อแจ้งให้เราทราบ

คุณอาจได้รับสติกเกอร์จากทีมของเราหากพบเราที่งานกิจกรรม