Bu Chrometober'da eğlenceli ve korkutucu ipuçları ve püf noktaları paylaşmak için nasıl bir kaydırmalı kitap oluşturduk?
Tasarım Ayı'ndan sonra, bu yıl topluluğun ve Chrome Ekibi'nin web içeriklerini öne çıkararak paylaşmak için Chrome Ekim'i sizin için hazırladık. Designcember'de Container Sorguları'nın kullanımı gösterilmişti. Bu yıl ise CSS kaydırma bağlantılı animasyonlar API'sini tanıtıyoruz.
Kaydırma özelliğine sahip kitap deneyimine web.dev/chrometober-2022 adresinden göz atın.
Genel Bakış
Projenin amacı, kaydırma bağlantılı animasyonlar API'sini öne çıkaran ilginç bir deneyim sunmaktı. Ancak bu deneyimin hem eğlenceli hem de duyarlı ve erişilebilir olması gerekiyordu. Proje, aktif olarak geliştirilmekte olan API polyfill'ini test etmek ve farklı teknikleri ve araçları birlikte denemek için de mükemmel bir yöntem oldu. Tüm bunlara Cadılar Bayramı teması da eklendi.
Ekip yapımız şu şekildeydi:
- Tyler Reed: İllüstrasyon ve tasarım
- Jhey Tompkins: Mimari ve reklam öğesi yöneticisi
- Una Kravets: Proje yöneticisi
- Bramus Van Damme: Siteye katkıda bulunan
- Adam Argyle: Erişilebilirlik incelemesi
- Aaron Forinton: Metin Yazarlığı
Kaydırmayla okuma deneyimi taslağı oluşturma
Chrometober ile ilgili fikirler, Mayıs 2022'de düzenlediğimiz ilk ekip toplantımızda ortaya çıkmaya başladı. Bir karalama defteri koleksiyonu, kullanıcıların bir tür hikaye tahtası boyunca nasıl kaydırabileceğini düşünmemizi sağladı. Video oyunlarından esinlenerek, mezarlık ve hayaletli ev gibi sahnelerde kaydırma deneyimi sunmayı düşündük.
İlk Google projemi beklenmedik bir yöne götürme konusunda yaratıcı özgürlüğüm olması heyecan vericiydi. Bu, kullanıcının içerikte nasıl gezinebileceğine dair erken bir prototipti.
Kullanıcı yana doğru kaydırırken bloklar döner ve yakınlaştırılır. Ancak bu deneyimi her boyutta cihaz kullanan kullanıcılar için nasıl mükemmel hale getirebileceğimiz konusunda endişelendiğim için bu fikirden vazgeçmeye karar verdim. Bunun yerine, geçmişte yaptığım bir tasarıma yöneldim. 2020'de, sürüm denemeleri oluşturmak için GreenSock'un ScrollTrigger aracına erişebildim.
Hazırladığım demolardan biri, siz kaydırdıkça sayfaların döndüğü bir 3D CSS kitabıydı. Bu, Chrometober için istediğimiz şeye çok daha uygundu. Kaydırmayla bağlantılı animasyonlar API'si, bu işlev için mükemmel bir değişimdir. Göreceğiniz gibi scroll-snap
ile de iyi çalışır.
Projenin illüstratörlüğünü yapan Tyler Reed, fikirlerimiz değiştikçe tasarımda gerekli değişiklikleri yapma konusunda oldukça başarılıydı. Tyler, kendisine iletilen tüm yaratıcı fikirleri hayata geçirmek için harika bir iş çıkardı. Birlikte beyin fırtınası yapmak çok eğlenceliydi. Bu özelliğin çalışmasını istediğimiz şekilde sağlamanın önemli bir kısmı, özelliklerin ayrı bloklara bölünmesini sağlamaktı. Böylece, bunları sahnelere yerleştirebilir ve hangilerini hayata geçireceğimizi seçebiliriz.
Ana fikir, kullanıcının kitapta ilerlerken içerik bloklarına erişebilmesiydi. Ayrıca, deneyime eklediğimiz sürprizler de dahil olmak üzere eğlenceli öğelerle etkileşime geçebilirlerdi. Örneğin, gözleri işaretçinizi takip eden, hayaletli bir evdeki bir portre veya medya sorguları tarafından tetiklenen ince animasyonlar. Bu fikirler ve özellikler, ekranı kaydırırken animasyonlu olarak gösterilir. İlk fikir, kullanıcı ekranı kaydırdığında yükselip x ekseni boyunca hareket edecek bir zombi tavşandı.
API'yi tanıma
Ayrı ayrı özelliklerle ve sürprizlerle oynamaya başlamadan önce bir kitaba ihtiyacımız vardı. Bu nedenle, bu fırsatı yeni CSS kaydırma bağlantılı animasyon API'sinin özellik grubunu test etme fırsatına dönüştürmeye karar verdik. Kaydırmayla bağlantılı animasyonlar API'si şu anda hiçbir tarayıcıda desteklenmemektedir. Ancak API geliştirilirken etkileşimler ekibindeki mühendisler bir polyfill üzerinde çalışıyordu. Bu sayede, API'nin geliştirilme aşamasında şeklini test edebilirsiniz. Bu, bu API'yi hemen kullanabileceğimiz anlamına geliyor. Bu tür eğlenceli projeler, deneysel özellikleri denemek ve geri bildirimde bulunmak için genellikle mükemmel bir yerdir. Neler öğrendiğimizi ve verdiğimiz geri bildirimi makalenin ilerleyen bölümlerinde bulabilirsiniz.
Genel olarak, bu API'yi animasyonlarla kaydırma işlemini bağlamak için kullanabilirsiniz. Ekranı kaydırırken animasyon tetikleyemeyeceğinizi unutmayın. Bu özellik daha sonra kullanıma sunulabilir. Kaydırmayla bağlantılı animasyonlar da iki ana kategoriye ayrılır:
- Kaydırma konumuna tepki verenler.
- Bir öğenin kaydırılabilir kapsayıcısındaki konumuna tepki verenler.
İkincisini oluşturmak için bir animation-timeline
mülkü aracılığıyla uygulanan bir ViewTimeline
kullanırız.
ViewTimeline
kullanımının CSS'de nasıl göründüğüne 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ımlarız. Bu örnekte block
, mantıksal block
anlamına gelir. Animasyon, animation-timeline
mülkü aracılığıyla kaydırmaya bağlanır. animation-delay
ve animation-end-delay
(bu makalenin yazıldığı tarihte) aşamaları tanımlama şeklimizdir.
Bu aşamalar, animasyonla öğenin kaydırılabilir kapsayıcısındaki konumu arasındaki bağlantının oluşturulacağı noktaları tanımlar. Örneğimizde, öğe kaydırılabilir kapsayıcıya (enter 0%
) girdiğinde animasyonun başlatılmasını söylüyoruz. Kaydırma kapsayıcısının %50'sini (cover 50%
) kapladığında bitirin.
Demomuzu aşağıda görebilirsiniz:
Ayrıca, bir animasyonu, ekran görünümünde hareket eden öğeye bağlayabilirsiniz. Bunu yapmak için animation-timeline
değerini öğenin view-timeline
değerine ayarlayın. Bu, liste animasyonlar gibi senaryolar için iyidir. Bu davranış, IntersectionObserver
kullanarak girişte öğeleri nasıl canlandıracağınıza 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 "Hareket Eden", görüntü alanına girerken ölçeğini büyütür ve "Dönen"in dönmesini tetikler.
Denemelerden, API'nin scroll-snap ile çok iyi çalıştığını öğrendim. Kaydırma yakalama özelliği, ViewTimeline
ile birlikte kullanıldığında bir kitapta sayfa çevirme işlemi için mükemmel bir seçimdir.
Mekanizmaların prototipini oluşturma
Biraz denedikten sonra bir kitap prototipini çalıştırmayı başardım. Kitabın sayfalarını çevirmek için yatay olarak kaydırın.
Demoda, farklı tetikleyicilerin noktalı kenarlıklarla vurgulandığını görebilirsiniz.
İşaretleme şu şekilde görünür:
<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>
Kaydırırken kitabın sayfaları döner ancak açılmaz veya kapanmaz. Bu, tetikleyicilerin kaydırma anında hizalanması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 sefer ViewTimeline
öğesini CSS'de bağlamıyoruz, JavaScript'te Web Animations API'yi kullanıyoruz. Bu yöntemin bir diğer avantajı, her bir öğeyi manuel olarak oluşturmak yerine bir dizi öğeyi döngüye alıp ihtiyacımız olan ViewTimeline
öğesini oluşturabilmemizdir.
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, tetikleyicinin ilişkili sayfasını bu ViewTimeline
değerini kullanarak hareketlendiririz. Bu, sayfanın animasyonunu kaydırmaya bağlar. Animasyonumuzda, sayfayı döndürmek için sayfadaki bir öğeyi y ekseninde döndürüyoruz. Ayrıca sayfanın kendisini z ekseninde çeviririz. Böylece sayfa bir kitap gibi davranır.
Tüm unsurların birleşimi
Kitabın mekanizmasını belirledikten sonra Tyler'ın çizimlerini hayata geçirmeye odaklandım.
Astro
Ekip 2021'de Designcember için Astro'yu kullandı ve ben de Chrometober için tekrar kullanmak istedim. Geliştiricilerin öğeleri bileşenlere ayırabilmesi, bu projeye çok uygun bir deneyimdir.
Kitabın kendisi bir bileşendir. Ayrıca bir sayfa bileşenleri koleksiyonudur. Her sayfanın iki yüzü ve arka planı vardır. Sayfa tarafının alt öğeleri, kolayca eklenebilen, kaldırılabilen ve yerleştirilebilen bileşenlerdir.
Albüm oluşturma
Blokların kolayca yönetilebilmesi benim için önemliydi. Ayrıca, ekibin geri kalanının katkıda bulunmasını 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} />
Kaydırma mekanizmasının uygulandığı ve albümün sayfalarının oluşturulduğu yer Book
bileşenidir. Prototipteki mekanizmanın aynısı kullanılır ancak dünya genelinde oluşturulan birden fazla ViewTimeline
örneği paylaşırız.
window.CHROMETOBER_TIMELINES.push(viewTimeline);
Bu sayede, zaman çizelgelerini yeniden oluşturmak yerine başka yerlerde kullanılmak üzere paylaşabiliriz. Bu konuyla ilgili daha fazla bilgiyi aşağıda bulabilirsiniz.
Sayfa kompozisyonu
Her sayfa, bir liste içindeki bir 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 Page
örneğine iletilir. 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ı oluşturmak için kullanılır. Katkıda bulunanlar, kitabın içeriğinin büyük bir kısmında bu koda dokunmak zorunda kalmadan çalışabilir.
Backdrop
Reklam öğesinin kitap şeklinde tasarlanması, bölümlerin ayrılmasını çok daha kolay hale getirdi. Kitabın her sayfası, orijinal tasarımdan alınmış bir sahnedir.
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 ayarlamak ve 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
Sayfalardan birini oluşturmaya bakalım. Üçüncü sayfada, ağaçtan çıkan bir baykuş gösterilmektedir.
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ımda ön metne benzer bir kod çiti 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>
Yine de sayfalar atomik yapıdadır. Bu gruplar, bir dizi özellikten oluşur. Üçüncü sayfada bir içerik bloğu ve etkileşimli baykuş bulunduğundan her biri için bir bileşen vardır.
İçerik blokları, kitapta görülen içeriğin 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 durumlarda içe aktarılır. Ardından ilgili blok yapılandırması ContentBlock
bileşenine iletilir.
<ContentBlock {...contentBlocks[3]} id="four" />
Ayrıca, 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 yerleştirilir.
<style is:global>
.content-block--four {
left: 30%;
bottom: 10%;
}
</style>
Ancak bir içerik bloğunun genel stilleri, bileşen koduyla birlikte yerleştirilir.
.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 biridir. Bu, oluşturduğumuz paylaşılan zaman çizelgesi görünümünü nasıl kullandığımızı gösteren güzel bir örnektir.
Baykuş bileşenimiz, üst düzeyde bazı SVG'leri içe aktarır ve Astro'nun Fragment'ini kullanarak satır içi olarak yerleştirir.
---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />
Baykuşumuzu konumlandırma stillerinin, bileşen koduyla birlikte yerleştirildiğini görüyoruz.
.owl {
width: 34%;
left: 10%;
bottom: 34%;
}
Baykuş için transform
davranışını tanımlayan ek bir stil öğesi vardır.
.owl__owl {
transform-origin: 50% 100%;
transform-box: fill-box;
}
transform-box
kullanımı transform-origin
'u etkiler. Bu, nesnenin SVG içindeki sınırlayıcı kutusuna göre yapılır. Baykuş, alt ortadan yukarı doğru ölçeklendirilir. Bu nedenle transform-origin: 50% 100%
kullanılır.
Eğlenceli kısım, baykuşu oluşturduğumuz ViewTimeline
'lerden birine bağladığımızda ortaya çıkar:
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 şey yapıyoruz:
- Kullanıcının hareket tercihlerini kontrol edin.
- Tercihleri yoksa kaydırılacak baykuşun animasyonunu bağlayın.
İkinci bölümde, baykuş Web Animations API'yi kullanarak y ekseninde animasyonlu olarak hareket eder. Bağımsız dönüştürme mülkü translate
kullanılır ve bir ViewTimeline
'a bağlanır. timeline
mülkü aracılığıyla CHROMETOBER_TIMELINES[1]
'e bağlıdır. Bu, sayfa çevirme işlemleri için oluşturulan bir ViewTimeline
öğesidir. Bu, enter
aşamasını kullanarak baykuşun animasyonunu sayfa çevirme işlemine bağlar. Sayfa% 80 döndürüldüğünde baykuşun hareket etmeye başlayacağını belirtir. %90'a ulaşıldığında baykuş çeviriyi tamamlar.
Kitap özellikleri
Artık bir sayfa oluşturma yaklaşımını ve proje mimarisinin işleyişini gördünüz. Bu özelliğin, katkıda bulunanların diledikleri sayfa veya özellik üzerinde çalışmaya nasıl olanak tanıdığını görebilirsiniz. Kitaptaki çeşitli özelliklerin animasyonları kitabın sayfalarının çevrilmesine bağlıdır. Örneğin, sayfa çevrildiğinde uçan yarasa.
Ayrıca CSS animasyonlarından güç alan öğeler de vardır.
İçerik blokları kitaba eklendikten sonra diğer özelliklerle yaratıcılığımızı konuşturmanın zamanı geldi. Bu sayede farklı etkileşimler oluşturma ve farklı uygulama yöntemleri deneme fırsatı bulduk.
Duyarlılığı koruma
Duyarlı görüntü alanı birimleri, kitabı ve özelliklerini boyutlandırır. Ancak yazı tiplerinin duyarlı kalmasını sağlamak ilginç bir sorundu. Kapsayıcı sorgu birimleri bu durumda iyi bir seçimdir. Ancak henüz her yerde desteklenmiyor. Kitabın boyutu belirlendiğinden kapsayıcı sorgusuna gerek yoktur. Satır içi kapsayıcı sorgu birimi, CSS calc()
ile oluşturulabilir ve yazı tipi boyutlandırması 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 ışıldayan kabaklar
Dikkatli bir göz, daha önce sayfa arka planları hakkında konuşurken <source>
öğelerinin kullanıldığını fark etmiş olabilir. Una, renk şeması tercihine tepki veren bir etkileşime sahip olmak istiyordu. Bu nedenle arka planlar, farklı varyantlarla hem açık hem de koyu modları destekler. <picture>
öğesiyle medya sorgularını kullanabileceğiniz için bu öğe, iki arka plan stili sağlamanın mükemmel bir yoludur. <source>
öğesi, renk şeması tercihini sorgulayarak 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 de yapabilirsiniz. İkinci sayfadaki balkabakları, kullanıcının renk şeması tercihine göre değişir. Kullanılan SVG'de, koyu modda ölçeklendirilip animasyonlu hale gelen alevleri temsil eden daireler bulunur.
.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 sizi izliyor mu?
10. sayfaya göz atarsanız bir şey fark edebilirsiniz. Sizi izliyorlar! Sayfada gezinirken portrenin gözleri işaretçinizi takip eder. Buradaki püf noktası, işaretçi konumunu bir çeviri değeriyle eşlemek ve CSS'ye iletmektir.
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 çevirebileceğini belirtir. Ardından, x veya y eksenindeki işaretçi konumu değer olarak iletilir. Gözler hareket ettirilirken göz merkez noktasını bulmak için gözler kopyalanır. Orijinaller hareket etmez, şeffaftır ve referans olarak kullanılır.
Ardından, bunları birbirine bağlamanız ve gözlerin hareket edebilmesi için gözlerdeki CSS özel mülk değerlerini güncellemeniz gerekir. Bir işlev, window
etkinliğine karşı pointermove
etkinliğine bağlanır. Bu işlem tetiklendiğinde, merkez noktaları hesaplamak için her bir gözün sınırları kullanılır. Ardından işaretçi konumu, gözlerde özel mülk 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 bunlarla istediklerini yapabilir. Buradaki en iyi nokta, her göz için davranışı farklı hale getirmek üzere CSS clamp()
kullanmaktır. Böylece, JavaScript'e tekrar dokunmadan her 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 göz atarsanız büyülenir misiniz? Bu sayfa, fantastik sihirli tilkimizin tasarımını yansıtır. İşaretçinizi hareket ettirdiğinizde özel bir imleç izi efekti görebilirsiniz. Bu yöntemde kanvas animasyonu kullanılır. <canvas>
öğesi, pointer-events: none
ile birlikte sayfa içeriğinin geri kalanının üzerinde yer alır. Bu sayede kullanıcılar, alttaki içerik bloklarını tıklamaya devam edebilir.
.wand-canvas {
height: 100%;
width: 200%;
pointer-events: none;
right: 0;
position: fixed;
}
Portre öğemizin window
üzerinde pointermove
etkinliğini dinlemesi gibi <canvas>
öğemiz de window
üzerinde pointermove
etkinliğini dinler. Ancak etkinlik her tetiklendiğinde <canvas>
öğesinde animasyon oluşturacak bir nesne oluşturuyoruz. Bu nesneler, işaretçi izinde kullanılan şekilleri temsil eder. Bu noktalar koordinatlara ve rastgele bir tona sahiptir.
İşaretçi deltasını size
ve rate
ile eşlemek için kullanabileceğimiz için daha önceki mapRange
işlevimiz tekrar kullanılır. Nesneler, <canvas>
öğesine çizildiğinde döngüye alınan bir dizede saklanır. Her nesnenin özellikleri, <canvas>
öğemize nesnelerin nereye çizileceğ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)
Kanvas üzerinde çizim yapmak için requestAnimationFrame
ile bir döngü oluşturulur. İmleç izi yalnızca sayfa görünümdeyken oluşturulmalıdır. Hangi sayfaların görüntülendiğini güncelleyen ve belirleyen bir IntersectionObserver
'imiz var. Bir sayfa görünümdeyse nesneler kanvasta daireler olarak oluşturulur.
Ardından blocks
dizisini döngüye alır ve parkurun her bir bölümünü çizeriz. Her kare, nesnenin boyutunu rate
oranında azaltır ve konumunu değiştirir. Bu sayede, düşme ve ölçeklendirme efekti elde edilir. 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şleyicileri kaldırılır ve animasyon kare döngüsü iptal edilir. blocks
dizisi de temizlenir.
İşaretçi izinin nasıl kullanıldığını burada görebilirsiniz.
Erişilebilirlik incelemesi
Keşfedilecek eğlenceli bir deneyim oluşturmak iyidir ancak kullanıcılar bu deneyime erişemiyorsa bu deneyimin bir anlamı yoktur. Adam'ın bu alandaki uzmanlığı, Chrometober'u yayınlanmadan önce erişilebilirlik incelemesine hazırlarken çok faydalı oldu.
Ele alınan önemli alanlardan bazıları:
- Kullanılan HTML'nin semantik olduğundan emin olun. Kitap için
<main>
gibi uygun yer işareti öğeleri, her içerik bloğu için<article>
öğesinin kullanılması ve kısaltmaların tanıtıldığı<abbr>
öğeleri buna dahildir. Kitabı oluştururken ileriyi düşünmek, her şeyi daha erişilebilir hale getirdi. Başlıklar ve bağlantılar kullanmak, kullanıcıların gezinmesini kolaylaştırır. Sayfalar için liste kullanılması, sayfa sayısının yardımcı teknolojiler tarafından da açıklandığı anlamına gelir. - Tüm resimlerde uygun
alt
özelliklerinin kullanılmasını sağlayın. Satır içi SVG'lerde, gerektiğindetitle
öğesi bulunur. - Deneyimi iyileştirdiği yerlerde
aria
özelliklerini kullanın. Sayfalar ve sayfaların tarafları içinaria-label
kullanılması, kullanıcıya hangi sayfada olduklarını bildirir. "Daha fazla bilgi edinin" bağlantılarındaaria-describedBy
kullanı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 "Devamı" bağlantısını değil, kartın tamamını tıklayabilirsiniz.
- Hangi sayfaların görüntülendiğini izlemek için
IntersectionObserver
kullanılması daha önce de bahsedildi. Bu, yalnızca performansla ilgili olmayan birçok avantaj sağlar. Görüntüleme alanında olmayan sayfalardaki animasyonlar veya etkileşimler duraklatılır. Ancak bu sayfalarainert
özelliği de uygulanmıştır. Bu sayede ekran okuyucu kullanan kullanıcılar, görme engeli olmayan kullanıcılarla aynı içerikleri keşfedebilir. Odak, görüntülenen sayfa içinde kalır ve kullanıcılar sekme tuşuyla başka bir sayfaya geçemez. - Son olarak, kullanıcının hareket tercihine saygı göstermek için medya sorgularından yararlanırız.
Aşağıda, incelemede yer alan ve uygulanan önlemlerden bazılarının vurgulandığı bir ekran görüntüsü verilmiştir.
öğesi, kitabın tamamının etrafında olduğu şekilde tanımlanır. Bu, yardımcı teknoloji kullanıcılarının bu öğeyi bulması için ana belirgin işaret olması gerektiğini gösterir. Daha fazla bilgiyi ekran görüntüsünde bulabilirsiniz." width="800" height="465">
Öğrendiklerimiz
Chrometober'un amacı yalnızca topluluktan web içeriklerini öne çıkarmak değil, geliştirme aşamasındaki kaydırma bağlantılı animasyonlar API polyfill'ini deneme sürüşümüzü gerçekleştirmekti.
New York'taki ekip zirvemizde projeyi test etmek ve ortaya çıkan sorunları çözmek için bir oturum ayırdık. Ekibin katkısı çok değerliydi. Ayrıca, yayına geçmeden önce ele alınması gereken tüm konuları listelemek için mükemmel bir fırsattı.
Örneğin, kitabı cihazlarda test ederken oluşturma sorunuyla karşılaştınız. Kitabımız iOS cihazlarda beklendiği gibi oluşturulmuyordu. Görüntü alanı birimleri sayfanın boyutunu belirler ancak çentik varsa kitap etkilenir. Çö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. Ardından bu sorunlara çözümler buldu ve bunları polyfill ile birleştirdi. Örneğin, bu alma isteği, polyfill'in bir kısmına önbelleğe alma özelliği ekleyerek performans artışı sağladı.
İşte bu kadar.
Üzerinde çalıştığımız bu proje gerçekten eğlenceliydi. Topluluğun muhteşem içeriklerini öne çıkaran eğlenceli bir kaydırma deneyimi ortaya çıktı. Bununla birlikte, polyfill'i test etmek ve mühendislik ekibine polyfill'i iyileştirmeye yardımcı olacak geri bildirimler sağlamak için de mükemmel bir araç oldu.
Chrometober 2022 sona erdi.
Keyifle izlediğinizi umuyoruz. En sevdiğiniz özellik hangisi? Bize tweet atarak düşüncelerinizi bizimle paylaşın.
Bir etkinlikte bizi görürseniz ekip üyelerinden çıkartma alabilirsiniz.
Unsplash'taki David Menidrey tarafından çekilen hero fotoğrafı