Bu Chrometober'da eğlenceli ve korkutucu ipuçları ve püf noktaları paylaşmak için kayan kitap nasıl hayata geçirildi?
Designcember'ın ardından, bu yıl topluluktan ve Chrome ekibinden web içeriklerini öne çıkarmak ve paylaşmak için Chrometober'ı oluşturmak istedik. Designcember'da kapsayıcı sorgularının kullanımı gösterilmişti. Bu yıl ise CSS kaydırmaya bağlı animasyonlar API'sini tanıtıyoruz.
web.dev/chrometober-2022 adresinde kaydırarak kitap okuma deneyimini inceleyin.
Genel Bakış
Projenin amacı, kaydırmaya bağlı animasyonlar API'sini öne çıkaran eğlenceli bir deneyim sunmaktı. Ancak bu deneyimin eğlenceli olmasının yanı sıra duyarlı ve erişilebilir olması da gerekiyordu. Proje, aktif olarak geliştirilmekte olan API polyfill'i test etmek ve farklı teknik ve araçları bir arada denemek için de harika bir fırsat oldu. Üstelik tüm bunlar Cadılar Bayramı temasıyla sunuluyor.
Ekip yapımız şu şekildeydi:
- Tyler Reed: İllüstrasyon ve tasarım
- Jhey Tompkins: Mimari ve yaratıcı lider
- Una Kravets: Proje yöneticisi
- Bramus Van Damme: Site contributor
- Adam Argyle: Erişilebilirlik incelemesi
- Aaron Forinton: Metin Yazarlığı
Kaydırarak hikaye anlatımı deneyimi tasarlama
Chrometober ile ilgili fikirler, Mayıs 2022'de ekip olarak yaptığımız ilk toplantıda ortaya çıktı. Bir dizi karalama, kullanıcının bir tür storyboard'da nasıl gezinebileceği konusunda düşünmemizi sağladı. Video oyunlarından ilham alarak mezarlıklar ve perili ev gibi sahneler arasında gezinme deneyimi sunmayı düşündük.

İlk Google projemde yaratıcı özgürlüğe sahip olmak ve projeyi beklenmedik bir yöne götürmek heyecan vericiydi. Bu, kullanıcının içerikte nasıl gezinebileceğini gösteren erken bir prototipti.
Kullanıcı yana doğru kaydırdıkça bloklar dönüyor ve büyüyor. Ancak bu deneyimi her boyuttaki cihazda kullanıcılar için nasıl mükemmel hale getirebileceğimizle ilgili endişelerim nedeniyle bu fikirden vazgeçtim. Bunun yerine, geçmişte yaptığım bir tasarımın benzerini yapmaya yöneldim. 2020'de, sürüm demoları oluşturmak için GreenSock'un ScrollTrigger aracına erişebildiğim için şanslıydım.
Oluşturduğum demolardan biri, sayfaları kaydırdıkça dönen bir 3D-CSS kitabıydı ve bu, Chrometober için istediğimiz şeye çok daha uygun görünüyordu. Kaydırmaya bağlı animasyonlar API'si, bu işlev için mükemmel bir alternatiftir. Ayrıca scroll-snap ile de iyi çalışır.
Projenin illüstratörü Tyler Reed, fikirlerimiz değiştikçe tasarımı değiştirmede çok başarılıydı. Tyler, kendisine sunulan tüm yaratıcı fikirleri hayata geçirme konusunda harika bir iş çıkardı. Birlikte fikir alışverişi yapmak çok eğlenceliydi. Bu sistemin işleyişinde en çok istediğimiz şeylerden biri, özelliklerin izole bloklara bölünmesiydi. Böylece bunları sahnelere yerleştirebiliyor ve ardından hayata geçirdiğimiz şeyleri seçebiliyorduk.

Ana fikir, kullanıcının kitapta ilerledikçe içerik bloklarına erişebilmesiydi. Ayrıca, deneyime eklediğimiz sürpriz yumurtalar gibi eğlenceli öğelerle de etkileşime geçebiliyorlardı. Örneğin, perili evdeki bir portrenin gözleri imlecinizi takip ediyordu veya medya sorgularıyla tetiklenen küçük animasyonlar vardı. Bu fikirler ve özellikler, kaydırma sırasında animasyonlu olarak gösterilir. İlk fikirlerden biri, kullanıcı sayfayı kaydırdıkça x ekseni boyunca yükselip hareket eden bir zombi tavşandı.
API'yi tanıma
Özellikleri ve sürprizleri tek tek denemeye başlamadan önce bir kitaba ihtiyacımız vardı. Bu nedenle, yeni CSS kaydırmaya bağlı animasyonlar API'sinin özellik grubunu test etmek için bunu bir fırsat olarak değerlendirmeye karar verdik. Kaydırmaya bağlı animasyonlar API'si şu anda hiçbir tarayıcıda desteklenmemektedir. Ancak etkileşimler ekibindeki mühendisler, API'yi geliştirirken bir polyfill üzerinde çalışıyor. Bu sayede, API'nin şeklini geliştirme aşamasında test edebilirsiniz. Bu API'yi bugün kullanabiliriz. Bu tür eğlenceli projeler, deneysel özellikleri denemek ve geri bildirim sağlamak için genellikle harika bir yerdir. Neler öğrendiğimizi ve hangi geri bildirimleri sağlayabildiğimizi makalenin ilerleyen bölümlerinde bulabilirsiniz.
Genel olarak, animasyonları kaydırmaya bağlamak için bu API'yi kullanabilirsiniz. Kaydırma sırasında animasyon tetikleyemeyeceğinizi belirtmek isteriz. Bu özellik daha sonra eklenebilir. Kaydırmaya bağlı animasyonlar da iki ana kategoriye ayrılır:
- Kaydırma konumuna tepki verenler.
- Kaydırma kapsayıcısındaki bir öğenin konumuna tepki verenler.
İkinciyi oluşturmak için animation-timeline özelliği aracılığıyla uygulanan bir ViewTimeline kullanırız.
CSS'de ViewTimeline kullanımına dair bir örneği aşağıda bulabilirsiniz:
.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;
}
}
view-timeline-name ile bir ViewTimeline oluşturup eksenini tanımlıyoruz. Bu örnekte block, mantıksal block anlamına gelir. Animasyon, animation-timeline özelliğiyle kaydırmaya bağlanır. Aşamaları animation-delay ve animation-end-delay (bu yazının yazıldığı tarih itibarıyla) olarak tanımlıyoruz.
Bu aşamalar, animasyonun kaydırma kapsayıcısındaki bir öğenin konumuna göre bağlanması gereken noktaları tanımlar. Örneğimizde, öğe kaydırma kapsayıcısına girdiğinde (enter 0%) animasyonun başlatılmasını istiyoruz. Kaydırılabilir kapsayıcının %50'sini (cover 50%) kapladığında sona erer.
Demomuzu burada görebilirsiniz:
Görüntü alanında hareket eden öğeye bir animasyon da bağlayabilirsiniz. Bunu yapmak için animation-timeline özelliğini öğenin view-timeline değeri olarak ayarlayabilirsiniz. Bu, liste animasyonları gibi senaryolar için kullanışlıdır. Bu davranış, IntersectionObserver kullanarak giriş sırasında öğelere animasyon ekleme şeklinize benzer.
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;
}
}
Bu sayede "Mover", görüntü alanına girdiğinde ölçeklenir ve "Spinner"ın dönüşünü tetikler.
Denemelerim sonucunda API'nin scroll-snap ile çok iyi çalıştığını gördüm. Kaydırarak tutturma, ViewTimeline ile birlikte kullanıldığında bir kitaptaki sayfa çevirme işlemlerini tutturmak için idealdir.
Mekaniklerin prototipini oluşturma
Biraz deneme yaptıktan sonra çalışan bir kitap prototipi elde ettim. Kitabın sayfalarını çevirmek için yatay olarak kaydırırsınız.
Demoda, farklı tetikleyicilerin kesikli kenarlıklarla vurgulandığını görebilirsiniz.
İşaretleme şuna benzer:
<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>
Siz kaydırdıkça kitabın sayfaları çevrilir ancak açık veya kapalı olarak sabitlenir. Bu, tetikleyicilerin kaydırarak tutturma hizalamasına bağlıdır.
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;
}
Bu kez CSS'de ViewTimeline öğesini bağlamıyoruz, JavaScript'te Web Animations API'yi kullanıyoruz. Bu sayede, her birini manuel olarak oluşturmak yerine bir dizi öğe üzerinde döngü oluşturabilir ve ihtiyacımız olan ViewTimeline öğesini oluşturabiliriz.
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);
Her tetikleyici için bir ViewTimeline oluştururuz. Ardından, bu ViewTimeline öğesini kullanarak tetikleyiciyle ilişkili sayfaya animasyon ekleriz. Bu, sayfanın animasyonunu kaydırmaya bağlar. Animasyonumuzda, sayfanın bir öğesini Y ekseninde döndürerek sayfayı çeviriyoruz. Ayrıca sayfanın kendisini de z ekseninde çevirerek kitap gibi davranmasını sağlıyoruz.
Özet
Kitabın mekanizmasını çözdükten sonra Tyler'ın çizimlerini hayata geçirmeye odaklanabildim.
Astro
Ekip, 2021'de Designcember için Astro'yu kullanmıştı. Ben de Chrometober için tekrar kullanmak istedim. Geliştiricilerin her şeyi bileşenlere ayırabilmesi bu proje için çok uygun.
Kitabın kendisi bir bileşendir. Aynı zamanda sayfa bileşenlerinin bir koleksiyonudur. Her sayfanın iki tarafı vardır ve bu taraflarda arka planlar bulunur. Sayfa tarafının alt öğeleri, kolayca eklenebilen, kaldırılabilen ve konumlandırılabilen bileşenlerdir.
Kitap oluşturma
Blokların kolayca yönetilebilmesi benim için önemliydi. Ayrıca ekibin geri kalanının katkıda bulunmasını da kolaylaştırmak istedim.
Üst düzey sayfalar bir yapılandırma dizisiyle tanımlanır. Dizideki her sayfa nesnesi, bir sayfanın içeriğini, arka planını ve diğer meta verilerini tanımlar.
const pages = [
{
front: {
marked: true,
content: PageTwo,
backdrop: spreadOne,
darkBackdrop: spreadOneDark
},
back: {
content: PageThree,
backdrop: spreadTwo,
darkBackdrop: spreadTwoDark
},
aria: `page 1`
},
/* Obfuscated page objects */
]
Bunlar Book bileşenine iletilir.
<Book pages={pages} />
Book bileşeni, kaydırma mekanizmasının uygulandığı ve kitabın sayfalarının oluşturulduğu yerdir. Prototipteki mekanizmanın aynısı kullanılır ancak global olarak oluşturulan birden fazla ViewTimeline örneği paylaşılır.
window.CHROMETOBER_TIMELINES.push(viewTimeline);
Bu sayede, zaman çizelgelerini yeniden oluşturmak yerine başka yerlerde kullanılmak üzere paylaşabiliriz. Bu konuya daha sonra değineceğiz.
Sayfa kompozisyonu
Her sayfa, bir liste içindeki liste öğesidir:
<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>
Tanımlanan yapılandırma her bir Page örneğine aktarılır. Sayfalar, her sayfaya içerik eklemek için Astro'nun slot özelliğini kullanır.
<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>
Bu kod, çoğunlukla yapıyı oluşturmak için kullanılır. Katkıda bulunanlar, bu koda dokunmak zorunda kalmadan kitabın içeriği üzerinde çalışabilir.
Backdrop
Kitap formatına geçiş, bölümleri ayırmayı çok daha kolay hale getirdi. Kitabın her bir sayfası, orijinal tasarımdan alınmış bir sahneden oluşuyor.

Kitap için bir en-boy oranına karar verdiğimizden her sayfanın arka planında bir resim öğesi olabilir. Bu öğeyi% 200 genişliğe ayarlayıp sayfa tarafına göre object-position kullanmak işe yarar.
.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;
}
Sayfa içeriği
Şimdi sayfalardan birini oluşturmaya bakalım. Üçüncü sayfada, ağaçta beliren bir baykuş var.
Yapılandırmada tanımlandığı şekilde bir PageThree bileşeniyle doldurulur. Bu bir Astro bileşenidir (PageThree.astro). Bu bileşenler HTML dosyalarına benzer ancak üst kısımlarında ön kısıma benzer bir kod bloğu bulunur. Bu sayede diğer bileşenleri içe aktarma gibi işlemler yapabiliriz. Üçüncü sayfanın bileşeni şu şekilde görünür:
---
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>
Sayfalar da atomik yapıdadır. Bu modeller, bir dizi özellikten oluşturulur. Üçüncü sayfada bir içerik bloğu ve etkileşimli baykuş yer alır. Bu nedenle her biri için bir bileşen vardır.
İçerik blokları, kitapta görülen içeriklerin bağlantılarıdır. Bunlar da bir yapılandırma nesnesi tarafından yönlendirilir.
{
"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
]
}
Bu yapılandırma, içerik bloklarının gerekli olduğu yerlere aktarılır. Ardından, ilgili blok yapılandırması ContentBlock bileşenine iletilir.
<ContentBlock {...contentBlocks[3]} id="four" />
Burada, sayfanın bileşenini içeriği konumlandırmak için nasıl kullandığımıza dair bir örnek de verilmiştir. Burada bir içerik bloğu konumlandırılır.
<style is:global>
.content-block--four {
left: 30%;
bottom: 10%;
}
</style>
Ancak bir içerik bloğunun genel stilleri, bileşen koduyla aynı yerde bulunur.
.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%;
}
Baykuşumuz ise bu projedeki birçok etkileşimli özellikten biri. Bu, oluşturduğumuz paylaşılan ViewTimeline'ı nasıl kullandığımızı gösteren güzel ve küçük bir örnektir.
Genel olarak, baykuş bileşenimiz bazı SVG'leri içe aktarır ve Astro'nun Fragment'ini kullanarak satır içine alır.
---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />
Baykuşumuzu konumlandırmak için kullanılan stiller, bileşen koduyla aynı yerde bulunur.
.owl {
width: 34%;
left: 10%;
bottom: 34%;
}
Baykuşun transform davranışını tanımlayan ek bir stil daha vardır.
.owl__owl {
transform-origin: 50% 100%;
transform-box: fill-box;
}
transform-box kullanımı transform-origin etkiler. Bu, gradyanı SVG içindeki nesnenin sınırlayıcı kutusuna göre ayarlar. Baykuş, alt orta kısımdan yukarı doğru büyür. Bu nedenle transform-origin: 50% 100% kullanılır.
İşin eğlenceli kısmı, baykuşu oluşturduğumuz ViewTimelinelardan birine bağladığımızda başlıyor:
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()
Bu kod bloğunda iki işlem yapıyoruz:
- Kullanıcının hareket tercihlerini kontrol edin.
- Tercihleri yoksa baykuşun kaydırma animasyonunu bağlayın.
İkinci bölümde baykuş, Web Animations API kullanılarak Y ekseninde animasyonlu olarak gösterilir. Bağımsız dönüşüm özelliği translate kullanılıyor ve bir ViewTimeline ile bağlantılı. timeline mülkü aracılığıyla CHROMETOBER_TIMELINES[1] ile bağlantılı. Bu, sayfa çevirme işlemleri için oluşturulan bir ViewTimeline. Bu, enter aşamasını kullanarak baykuşun animasyonunu sayfa çevirmeye bağlar. Sayfa% 80 oranında çevrildiğinde baykuşun hareket etmeye başlamasını tanımlar. Baykuş, %90'da çevirisini tamamlamalıdır.
Kitap özellikleri
Sayfa oluşturma yaklaşımını ve proje mimarisinin nasıl çalıştığını gördünüz. Katkıda bulunanların nasıl dahil olup istedikleri sayfa veya özellik üzerinde çalışabildiklerini görebilirsiniz. Kitaptaki çeşitli özelliklerin animasyonları, kitabın sayfa çevirme işlemine bağlıdır. Örneğin, sayfa çevrildiğinde uçarak girip çıkan yarasa.
Ayrıca CSS animasyonları tarafından desteklenen öğeler de içerir.
İçerik blokları kitaba eklendikten sonra diğer özelliklerle yaratıcılığınızı konuşturabilirsiniz. Bu sayede farklı etkileşimler oluşturma ve farklı uygulama yöntemleri deneme fırsatı bulduk.
Duyarlı kalma
Duyarlı görüntü alanı birimleri, kitabı ve özelliklerini boyutlandırır. Ancak yazı tiplerini duyarlı tutmak ilginç bir zorluktu. Kapsayıcı sorgu birimleri bu durumda iyi bir seçimdir. Ancak bu özellik henüz her yerde desteklenmiyor. Kitabın boyutu ayarlandığından kapsayıcı sorgusuna ihtiyacımız yoktur. Satır içi kapsayıcı sorgu birimi, CSS calc() ile oluşturulabilir ve yazı tipi boyutlandırma için kullanılabilir.
.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);
}
Gece parlayan bal kabakları
Dikkatli okuyucular, sayfa arka planlarından bahsederken <source> öğelerinin kullanıldığını fark etmiş olabilir. Una, renk şeması tercihine göre tepki veren bir etkileşim istiyordu. Bu nedenle arka planlar, farklı varyantlarla hem açık hem de koyu modu destekler. Medya sorgularını <picture> öğesiyle kullanabildiğiniz için bu, iki arka plan stili sağlamanın harika bir yoludur. <source> öğesi, renk şeması tercihini sorgular ve uygun arka planı gösterir.
<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>
Bu renk şeması tercihine göre başka değişiklikler yapabilirsiniz. İkinci sayfadaki balkabakları, kullanıcının renk şeması tercihine göre değişir. Kullanılan SVG'de alevleri temsil eden daireler vardır. Bu daireler koyu modda büyür ve canlanır.
.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;
}
}
}
Bu portre size mi bakıyor?
10. sayfaya bakarsanız bir şey fark edebilirsiniz. İzleniyorsunuz. Sayfada gezinirken portrenin gözleri işaretçinizi takip eder. Buradaki püf noktası, işaretçi konumunu bir çeviri değerine eşlemek ve CSS'ye aktarmaktır.
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)
}
Bu kod, giriş ve çıkış aralıklarını alır ve verilen değerleri eşler. Örneğin, bu kullanım 625 değerini verir.
mapRange(0, 100, 250, 1000, 50) // 625
Portre için giriş değeri, her bir gözün merkez noktası artı veya eksi bir miktar piksel mesafesidir. Çıkış aralığı, gözlerin piksel cinsinden ne kadar çevrilebileceğini gösterir. Ardından, işaretçinin x veya y eksenindeki konumu değer olarak iletilir. Gözleri hareket ettirirken merkez noktasını belirlemek için gözler kopyalanır. Orijinal öğeler hareket etmez, şeffaftır ve referans olarak kullanılır.
Ardından, bunları bir araya getirip gözlerdeki CSS özel özellik değerlerini güncelleyerek gözlerin hareket etmesini sağlayabilirsiniz. window için pointermove etkinliğine bir işlev bağlı. Bu işlem gerçekleştiğinde, merkez noktalarını hesaplamak için her bir gözün sınırları kullanılır. Ardından, işaretçi konumu, gözlerde özel özellik değerleri olarak ayarlanan değerlerle eşlenir.
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)
})
}
Değerler CSS'ye iletildikten sonra stiller bu değerlerle istediklerini yapabilir. Buradaki en iyi nokta, her bir gözün davranışını farklılaştırmak için CSSclamp() kullanılmasıdır. Böylece, JavaScript'e tekrar dokunmadan her bir gözün farklı davranmasını sağlayabilirsiniz.
.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);
}
Büyü yapma
Altıncı sayfaya baktığınızda büyülenmiş gibi hissediyor musunuz? Bu sayfada, muhteşem sihirli tilkimizin tasarımı yer alıyor. İşaretçinizi hareket ettirdiğinizde özel bir imleç izi efekti görebilirsiniz. Bu, tuval animasyonunu kullanır. <canvas> öğesi, pointer-events: none ile sayfa içeriğinin geri kalanının üzerinde yer alır. Bu, kullanıcıların alttaki içerik bloklarını tıklayabileceği anlamına gelir.
.wand-canvas {
height: 100%;
width: 200%;
pointer-events: none;
right: 0;
position: fixed;
}
Portremiz window üzerinde bir pointermove etkinliğini dinlediği gibi <canvas> öğemiz de aynı şeyi yapar. Ancak etkinlik her tetiklendiğinde <canvas> öğesinde animasyon oluşturmak için bir nesne oluşturuyoruz. Bu nesneler, imleç izinde kullanılan şekilleri temsil eder. Koordinatları ve rastgele bir tonu vardır.
Daha önce kullandığımız mapRange işlevini, işaretçi deltasını size ve rate ile eşlemek için tekrar kullanabiliriz. Nesneler, <canvas> öğesine çizilirken döngüye alınan bir dizide depolanır. Her nesnenin özellikleri, <canvas> öğemize nesnelerin nereye çizilmesi gerektiğini söyler.
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)
Tuvale çizim yapmak için requestAnimationFrame ile bir döngü oluşturulur. İmleç izi yalnızca sayfa görünür durumdayken oluşturulmalıdır. Hangi sayfaların görüntülendiğini belirleyen ve güncelleyen bir IntersectionObserver var. Bir sayfa görünümdeyse nesneler tuvalde daire olarak oluşturulur.
Ardından blocks dizisi üzerinde döngü oluşturup izin her bir parçasını çizeriz. Her kare, nesnenin boyutunu küçültür ve konumunu rate oranında değiştirir. Bu, düşme ve ölçeklendirme efektini oluşturur. Nesne tamamen küçülürse blocks dizisinden kaldırılır.
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)
}
Sayfa görünümden çıkarsa etkinlik işleyiciler kaldırılır ve animasyon karesi döngüsü iptal edilir. blocks dizisi de temizlenir.
İmleç izi bu şekilde çalışır.
Erişilebilirlik incelemesi
Keşfedilecek eğlenceli bir deneyim oluşturmak iyi bir şeydir ancak kullanıcılar bu deneyime erişemiyorsa bir işe yaramaz. Adam'ın bu alandaki uzmanlığı, Chrometober'ın yayınlanmadan önce erişilebilirlik incelemesine hazırlanmasında çok değerliydi.
Ele alınan önemli konulardan bazıları:
- Kullanılan HTML'nin semantik olduğundan emin olmak. Bu, kitap için
<main>gibi uygun referans noktası öğelerini, her içerik bloğu için<article>öğesinin kullanımını ve kısaltmaların tanıtıldığı<abbr>öğelerini içeriyordu. Kitap oluşturulurken ileriyi düşünmek, her şeyi daha erişilebilir hale getirdi. Başlıkların ve bağlantıların kullanılması, kullanıcının gezinmesini kolaylaştırır. Sayfalar için liste kullanılması, sayfa sayısının yardımcı teknoloji tarafından duyurulması anlamına da gelir. - Tüm resimlerde uygun
altözelliklerinin kullanıldığından emin olun. Satır içi SVG'ler içintitleöğesi gerektiğinde mevcuttur. - Deneyimi iyileştiren
ariaözelliklerini kullanma Sayfalar ve sayfaların kenarları içinaria-labelkullanılması, kullanıcıya hangi sayfada olduğunu bildirir. "Daha fazla bilgi" bağlantılarındaaria-describedBykullanılması, içerik bloğunun metnini iletir. Bu sayede, bağlantının kullanıcıyı nereye yönlendireceği konusunda belirsizlik ortadan kalkar. - İçerik blokları söz konusu olduğunda, yalnızca "Daha fazla bilgi" bağlantısını değil, kartın tamamını tıklayabilirsiniz.
- Hangi sayfaların görüntülendiğini izlemek için
IntersectionObserverkullanılması konusuna daha önce değinmiştik. Bu durumun yalnızca performansla ilgili olmayan birçok avantajı vardır. Görünümde olmayan sayfalardaki animasyonlar veya etkileşimler duraklatılır. Ancak bu sayfalardainertözelliği de uygulanır. Bu sayede, ekran okuyucu kullanan kullanıcılar, gören kullanıcılarla aynı içeriği keşfedebilir. Odak, görüntülenen sayfada kalır ve kullanıcılar sekme tuşuyla başka bir sayfaya geçemez. - Son olarak, kullanıcının hareket tercihine uymak için medya sorgularından yararlanırız.
İncelemeden alınan bu ekran görüntüsünde, uygulanan önlemlerden bazıları vurgulanmaktadır.
öğesi, tüm kitabın etrafında olarak tanımlanır. Bu, yardımcı teknoloji kullanıcılarının bulması gereken ana belirgin işaret olması gerektiğini gösterir. More is outlined in the screenshot." width="800" height="465">

Öğrendiklerimiz
Chrometober'ın amacı, yalnızca topluluğun web içeriklerini öne çıkarmak değil, aynı zamanda geliştirme aşamasında olan kaydırmaya bağlı animasyonlar API polyfill'ini test etmekti.
New York'taki ekip zirvemizde projeyi test etmek ve ortaya çıkan sorunları çözmek için bir oturum düzenledik. Ekibin katkısı paha biçilemezdi. Ayrıca, canlı yayına geçmeden önce ele alınması gereken tüm konuları listelemek için de harika bir fırsattı.

Örneğin, kitabı cihazlarda test ederken bir oluşturma sorunu ortaya çıktı. Kitabımız iOS cihazlarda beklendiği gibi oluşturulmuyor. Görüntü alanı birimleri sayfayı boyutlandırır ancak çentik olduğunda kitabı etkiler. Çözüm, meta görüntü alanında viewport-fit=cover kullanmaktı:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
Bu oturumda API polyfill ile ilgili bazı sorunlar da ortaya çıktı. Bramus, bu sorunları polyfill deposunda gündeme getirdi. Daha sonra bu sorunlara çözüm buldu ve bunları polyfill'e birleştirdi. Örneğin, bu pull isteği, polyfill'in bir kısmına önbelleğe alma özelliği ekleyerek performans artışı sağladı.


İşte bu kadar.
Üzerinde çalışması gerçekten eğlenceli olan bu proje, topluluğun muhteşem içeriklerini öne çıkaran eğlenceli bir kaydırma deneyimi sunuyor. Ayrıca, polyfill'i test etmek ve mühendislik ekibine polyfill'i iyileştirmeye yardımcı olacak geri bildirimler sağlamak için de harika bir araç oldu.
Chrometober 2022 sona erdi.
Keyifle izlediğinizi umuyoruz. En sevdiğiniz özellik hangisi? Bana tweet atarak düşüncelerinizi paylaşın.

Bizi bir etkinlikte görürseniz ekibimizden çıkartma da alabilirsiniz.