หนังสือแบบเลื่อนได้กลายเป็นจริงได้อย่างไรจากการแชร์กลเม็ดเคล็ดลับที่สนุกและน่ากลัว Chrometober นี้
เราต้องการสร้าง Chrometober สำหรับคุณในปีนี้ ซึ่งต่อยอดมาจาก Designcember สำหรับคุณในปีนี้ เพื่อเป็นการไฮไลต์และแชร์เนื้อหาเว็บจากชุมชนและทีม Chrome Designcember ได้แสดงให้เห็นถึงการใช้งานการค้นหาคอนเทนเนอร์ แต่ปีนี้เรากําลังแสดง API ภาพเคลื่อนไหวแบบเลื่อนลิงก์ของ CSS
ดูประสบการณ์อ่านหนังสือแบบเลื่อนได้ที่ web.dev/chrometober-2022
ภาพรวม
เป้าหมายของโครงการนี้คือการมอบประสบการณ์สุดแปลกตาในการไฮไลต์ API ภาพเคลื่อนไหวที่ลิงก์ด้วยการเลื่อน แต่ถึงแม้อาจจะดูแปลกๆ แต่ประสบการณ์การใช้งานก็ต้องมีการตอบสนองและเข้าถึงได้ด้วย โปรเจ็กต์นี้ยังเป็นวิธีที่ยอดเยี่ยมในการทดสอบ Polyfill ของ API ที่อยู่ระหว่างการพัฒนา รวมถึงลองใช้เทคนิคและเครื่องมือต่างๆ ร่วมกัน ทั้งหมดนี้มาพร้อมธีมเทศกาลฮาโลวีน
โครงสร้างทีมของเรามีดังนี้
- Tyler Reed: ภาพประกอบและการออกแบบ
- Jhey Tompkins: หัวหน้าฝ่ายสถาปัตยกรรมและการสร้างสรรค์
- Una Kravets: หัวหน้าโครงการ
- Bramus Van Damme: ผู้จัดทำเว็บไซต์
- Adam Argyle: การตรวจสอบการช่วยเหลือพิเศษ
- Aaron Forinton: การเขียนบทกวี
การร่างประสบการณ์การเล่าเรื่องด้วยการเลื่อน
ไอเดียสำหรับ Chrometober เริ่มหลั่งไหลเข้ามาจากทีมนอกสถานที่ของเราเมื่อเดือนพฤษภาคม 2022 คอลเล็กชันลายเส้นทำให้เราคิดถึงวิธีที่ผู้ใช้จะเลื่อนไปตามสตอรีบอร์ดบางรูปแบบได้ ด้วยแรงบันดาลใจจากวิดีโอเกม เราถือว่าเป็นประสบการณ์แบบเลื่อนผ่านฉากต่างๆ เช่น สุสานและบ้านผีสิง
การมีอิสระในการสร้างสรรค์ในการนำโปรเจ็กต์แรกของ Google ของฉันไปใช้ในทิศทางที่คาดไม่ถึงนั้นเป็นเรื่องน่าตื่นเต้น นี่เป็นต้นแบบระยะแรกของการพาผู้ใช้ไปยังเนื้อหาต่างๆ
เมื่อผู้ใช้เลื่อนหน้าจอไป บล็อกจะหมุนและปรับสเกลเข้า แต่ฉันตัดสินใจเลิกแนวคิดนี้เพราะกังวลว่าเราจะมอบประสบการณ์ที่ยอดเยี่ยมให้กับผู้ใช้อุปกรณ์ทุกขนาด แต่กลับหันไปเน้นการออกแบบสิ่งที่ฉันทำในอดีตแทน ในปี 2020 เราดีใจที่ได้ใช้งาน GreenSock's ScrollTrigger เพื่อสร้างการสาธิตรุ่น
ตัวอย่างหนึ่งที่ผมสร้างขึ้นคือหนังสือ CSS แบบ 3 มิติโดยพลิกหน้าต่างๆ ตามที่คุณเลื่อนดู ซึ่งวิธีนี้เหมาะกับสิ่งที่เราต้องการสำหรับ Chrometober มากกว่า API ภาพเคลื่อนไหวที่ลิงก์ด้วยการเลื่อนถือเป็นการสลับฟังก์ชันการทำงานที่ลงตัวมาก นอกจากนี้ยังทำงานได้ดีกับ scroll-snap
อย่างที่เห็น!
นักวาดภาพประกอบของเราที่ชื่อ Tyler Reed เป็นคนเก่งในการปรับเปลี่ยนการออกแบบเมื่อเราเปลี่ยนแนวคิด Tyler ทำงานได้อย่างดีเยี่ยมในการนำไอเดียสร้างสรรค์ทั้งหมดที่เขาเสนอมาทำให้เป็นจริง การระดมความคิดร่วมกันสนุกมากเลย ส่วนสำคัญของวิธีที่เราต้องการให้กระบวนการนี้ใช้ได้ผลคือการมีฟีเจอร์ต่างๆ ที่แบ่งออกเป็นส่วนต่างๆ แยกกัน วิธีนี้ช่วยให้เราสามารถรวมภาพเหล่านั้นเป็นฉากต่างๆ แล้วเลือกหัวข้อที่จะสร้างสรรค์ขึ้นมา
แนวคิดหลักคือเมื่อดูข้อมูลในหนังสือ ผู้ใช้จะเข้าถึงบล็อกเนื้อหาได้ นอกจากนี้ยังสามารถโต้ตอบกับขีดกลางที่ดูแปลกๆ ได้ ซึ่งรวมถึงไข่อีสเตอร์ที่เราสร้างไว้ในประสบการณ์นี้ เช่น ภาพบุคคลในบ้านผีสิง ที่มีสายตาตามเคอร์เซอร์ของคุณ หรือภาพภาพเคลื่อนไหวเล็กๆ น้อยๆ ที่เกิดจากคำค้นหาสื่อ ไอเดียและฟีเจอร์เหล่านี้จะเป็นภาพเคลื่อนไหวเมื่อเลื่อนดู แนวคิดในช่วงแรกๆ คือกระต่ายซอมบี้ที่จะโผล่ขึ้นมาแล้วเลื่อนไปตามแกน X ขณะที่ผู้ใช้เลื่อนดู
ทำความคุ้นเคยกับ API
ก่อนที่เราจะสามารถเริ่มเล่นเกมโดยใช้แต่ละฟีเจอร์ และไข่อีสเตอร์ เราต้องการหนังสือสักเล่ม เราจึงตัดสินใจใช้ชุดฟีเจอร์เป็นโอกาสทดสอบชุดฟีเจอร์สำหรับ API ภาพเคลื่อนไหวแบบเลื่อนลิงก์ของ CSS ที่เพิ่งเกิดใหม่ ปัจจุบันทุกเบราว์เซอร์ไม่รองรับ 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" จะขยายขึ้นเมื่อเข้าสู่วิวพอร์ต ซึ่งทำให้มีการหมุน "ตัวหมุน"
สิ่งที่ฉันพบจากการทดลองคือ 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 เพื่อให้หน้ามีลักษณะเหมือนหนังสือ
สรุปข้อมูลทั้งหมด
เมื่อผมคิดหากลยุทธ์สำหรับหนังสือเล่มนี้แล้ว ผมก็จะสามารถโฟกัสกับงานเขียนของไทเลอร์ให้มีชีวิต
อัสโตร
ทีมนี้ใช้ 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 แต่มีรั้วโค้ดที่ด้านบนคล้ายกับส่วนหน้า ซึ่งทำให้เราสามารถทำสิ่งต่างๆ ได้ เช่น นำเข้าคอมโพเนนต์อื่นๆ องค์ประกอบของหน้าที่ 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>
ขอย้ำอีกครั้งว่าหน้าเว็บต่างๆ มีลักษณะเป็นปรมาจารย์ ซึ่งสร้างขึ้นจากชุดฟีเจอร์ หน้าสามประกอบด้วยบล็อกเนื้อหาและนกฮูกที่โต้ตอบได้ จึงมีองค์ประกอบสำหรับทั้งสองหน้า
บล็อกเนื้อหาคือลิงก์ไปยังเนื้อหาที่เห็นภายในหนังสือ ซึ่งทั้งหมดนี้เกิดจากออบเจ็กต์การกำหนดค่า
{
"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 สิ่งต่อไปนี้
- ตรวจสอบค่ากำหนดการเคลื่อนไหวของผู้ใช้
- หากเด็กๆ ไม่มีตัวเลือกที่ชอบ ให้ลิงก์ภาพเคลื่อนไหวของนกฮูกเพื่อเลื่อน
ส่วนที่สอง นกฮูกจะเคลื่อนไหวบนแกน y โดยใช้ Web Animations API มีการใช้พร็อพเพอร์ตี้การเปลี่ยนรูปแบบ translate
แต่ละรายการ และลิงก์กับ ViewTimeline
รายการหนึ่ง ซึ่งลิงก์กับ 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;
}
ภาพบุคคลของเราฟังเหตุการณ์ pointermove
ในวันที่ window
เช่นเดียวกับวิธีที่ภาพบุคคลของเราฟัง องค์ประกอบ <canvas>
ของเราก็คล้ายกัน อย่างไรก็ตาม ทุกครั้งที่เหตุการณ์เริ่มทำงาน เราจะสร้างออบเจ็กต์เพื่อให้เคลื่อนไหวในองค์ประกอบ <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)
สำหรับการวาดในผืนผ้าใบ ระบบจะสร้างลูปด้วย requestAnimationFrame
เส้นทางเคอร์เซอร์ควรแสดงผลเมื่อหน้าอยู่ในมุมมองเท่านั้น เรามี IntersectionObserver
ที่อัปเดตและกําหนดหน้าที่ดูอยู่ ถ้าหน้าเว็บอยู่ในมุมมอง วัตถุจะแสดงเป็นวงกลมบนผืนผ้าใบ
จากนั้นเราจะวนซ้ำอาร์เรย์ 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 ไปยังหน้าอื่นได้ - สุดท้ายแต่ไม่ท้ายสุด เราใช้คำค้นหาตามสื่อเพื่อเคารพการตัดสินใจของผู้ใช้เกี่ยวกับการเคลื่อนไหว
ต่อไปนี้เป็นภาพหน้าจอจากการตรวจสอบที่ไฮไลต์มาตรการบางส่วนที่มีผลบังคับใช้
จะได้รับการระบุรอบๆ หนังสือทั้งเล่ม ซึ่งแสดงให้เห็นว่าหนังสือดังกล่าวควรเป็นจุดสังเกตหลักเพื่อให้ผู้ใช้เทคโนโลยีความช่วยเหลือพิเศษค้นพบ โปรดดูข้อมูลเพิ่มเติมในภาพหน้าจอ" 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 ตัวอย่างเช่น คำขอพุลนี้ได้รับประสิทธิภาพที่ดีขึ้นโดยการเพิ่มการแคชลงในส่วนของ Polyfill
เท่านี้ก็เรียบร้อย
และเป็นโปรเจ็กต์ที่สนุกจริงๆ สำหรับทำ ส่งผลให้เกิดประสบการณ์เลื่อนดูคอนเทนต์ที่สนุกสุดเหวี่ยง เพื่อไฮไลต์เนื้อหาที่ยอดเยี่ยมจากชุมชน ไม่เพียงเท่านั้น คุณยังทดสอบ Polyfill ได้ด้วย และยังเป็นการให้ความคิดเห็นแก่ทีมวิศวกรเพื่อช่วยปรับปรุง Polyfill อีกด้วย
Chrometober 2022 สิ้นสุดลงแล้ว
หวังว่าคุณจะชอบนะ ฟีเจอร์ใดที่คุณชื่นชอบ ทวีตฉันและแจ้งให้เราทราบ
คุณอาจไปรับสติกเกอร์จากทีมงานได้ ถ้าเห็นเราที่กิจกรรม
รูปภาพหลักโดย David Menidrey ใน Unsplash