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 vurgulayan sıra dışı bir deneyim sunmaktı. Ancak tuhaf olmakla birlikte, deneyimin duyarlı ve erişilebilir olması da 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 kreatif direktör
- 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ı. Çizimlerden oluşan bir koleksiyon, kullanıcının bir görsel senaryo taslağı boyunca ilerleyebileceği seçenekleri düşünmemizi sağladı. Video oyunlarından esinlenerek, mezarlık ve perili ev gibi sahnelerin arasında gezinme deneyimini yaşadı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ırdıkça bloklar döner ve ölçeklendirilir. 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 çok 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ştirip hangilerini hayata geçireceğimizi seçebildik.
Temel 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ıyla 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ırma bağlantılı animasyonlar API'si şu anda hiçbir tarayıcıda desteklenmemektedir. Bununla birlikte, API'yi geliştirirken etkileşim ekibindeki mühendisler bir çoklu dolgu üzerinde çalışıyor. Bu sayede, API'nin geliştirilme aşamasında API'nin şeklini test edebilirsiniz. Yani bu API'yi bugün kullanabiliriz ve bunun gibi eğlenceli projeler genellikle deneysel özellikleri deneyip geri bildirim vermek için iyi bir yerdir. Neler öğrendiğimizi ve verdiğimiz geri bildirimi makalenin ilerleyen bölümlerinde bulabilirsiniz.
Genel olarak, animasyonları kaydırmayla bağlamak için bu API'yi kullanabilirsiniz. Kaydırma sırasında bir animasyonu tetikleyemeyeceğinizi unutmamanız önemlidir. Bu tür bir şey daha sonra gelebilir. 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ığı sırada) aşamaları tanımlamak için kullandığımız terimlerdir.
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 girdiğinde (enter 0%
) 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, görüntü alanında 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 tutturma hareketinin ViewTimeline
ile birlikte kullanılması kitapta sayfa çevirmelerini tutturmak için mükemmel bir çözümdür.
Mekanizmaların prototipini oluşturma
Biraz deneme yaptıktan sonra bir kitap prototipini çalıştırmaya başladım. Kitabın sayfalarını çevirmek için yatay olarak kaydırırsınız.
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>
Siz kaydırdıkça kitabın sayfaları döner ancak kapanır. Bu, tetikleyicilerin kaydırma 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 sefer ViewTimeline
öğesini CSS'de bağlamıyoruz, JavaScript'te Web Animations API'yi kullanıyoruz. Bu, her bir öğeyi manuel olarak oluşturmak yerine bir dizi öğeyi döngüye alıp ihtiyacımız olan ViewTimeline
öğesini oluşturma avantajına da sahiptir.
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ı çevirmek için sayfanın bir öğesini y ekseninde döndürüyoruz. Ayrıca, sayfanın kendisini z ekseninde de çevirip kitap gibi davranırız.
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. Bir şeyleri bileşenlere ayırma becerisi, bu projeye oldukça uygun bir geliştirici deneyimi.
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.
Kitap oluşturma
Blokların kolayca yönetilmesini sağlamak benim için önemliydi. Ayrıca ekibin geri kalanının da katkıda bulunmasını kolaylaştırmak istiyordum.
Üst düzeydeki sayfalar bir yapılandırma dizisiyle tanımlanır. Dizideki her sayfa nesnesi, bir sayfanın içeriğini, arka planı ve diğer meta verileri 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 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.
Kitabın en boy oranını belirlerken her sayfanın arka planında bir resim öğesi bulunabilir. Bu öğeyi% 200 genişliğe ayarlamak ve sayfa tarafına göre object-position
kullanmak sorunu çözer.
.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.
Bu öğe, yapılandırmada tanımlanan 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>
Tekrarlamak gerekirse, sayfalar atomik bir yapıya sahiptir. 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, 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 merkezden yukarı doğru ölçeklendirildiğinden transform-origin: 50% 100%
kullanılmıştır.
Eğlenceli kısım, baykuşu oluşturduğumuz ViewTimeline
'lerden birine bağladığımızda ortaya çıkı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 ş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üşüm özelliği translate
kullanılıyor ve tek bir ViewTimeline
özelliğine bağlı. 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 istedikleri bir sayfaya veya özellik üzerinde çalışmaya nasıl olanak tanıdığını görebilirsiniz. Kitaptaki çeşitli özelliklerin animasyonları kitabın sayfa çevirme işlemine bağlıdır. Örneğin, sayfalar çevrildiğinde uçan yarasa.
Ayrıca, CSS animasyonları tarafından desteklenen öğeler içerir.
İç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ı tiplerini duyarlı tutmak zor bir işti. 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şim elde etmek 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 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 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şleyip 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ıp verilen değerleri eşler. Örneğin, bu kullanım 625 değerini verir.
mapRange(0, 100, 250, 1000, 50) // 625
Dikey resimlerde giriş değeri, her bir gözün merkez noktası ve artı veya eksi bir piksel mesafesidir. Çıkış aralığı, gözlerin piksel cinsinden ne kadar çevirebileceğini belirtir. Daha sonra, x veya y eksenindeki işaretçi konumu değer olarak aktarılır. Gözler hareket ettirilirken gözlerin orta 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
karşısında pointermove
etkinliğine bağlı. 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 attığınızda hevesli olduğunuzu düşünüyor musunuz? Bu sayfada, fantastik sihirli tilkimizin tasarımı sergileniyor. İş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. Bununla birlikte, etkinlik her etkinleştiğinde <canvas>
öğesinde canlandırılacak 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 dizide depolanı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ç yolu, yalnızca sayfa görüntülenirken oluşturulmalıdır. Güncellenen ve hangi sayfaların görüntüleneceğini belirleyen bir IntersectionObserver
var. Bir sayfa görüntülenirse nesneler tuvalde 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ırlamak için ç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. "Devamı" bağlantılarındaaria-describedBy
kullanımı, 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ış. Bu sayede ekran okuyucu kullanan kullanıcılar, görme engeli olmayan kullanıcılarla aynı içeriği 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, uygulanan önlemlerden bazılarının vurgulandığı incelemenin ekran görüntüsünü bulabilirsiniz.
öğesi, kitabın tamamının etrafında olduğu için yardımcı teknoloji kullanıcılarının bulabileceği ana belirgin işaret olmalıdır. Ekran görüntüsünde daha fazla açıklama verilmiştir." genişlik="800" yükseklik="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 çoklu dolgusu ile ilgili bazı sorunlar da ortaya çıkmıştır. Bramus, bu sorunları polyfill deposunda gündeme getirdi. Ardından bu sorunlara çözümler buldu ve bunları polyfill ile birleştirdi. Örneğin, bu pull isteği, polyfill'in bir kısmına önbelleğe alma ekleyerek bir performans artışı sağlamıştır.
İşte bu kadar.
Gerçekten eğlenceli bir proje olan bu proje, topluluğun muhteşem içeriklerini öne çıkaran ilginç bir kaydırma deneyimi sunmayı başardı. Bununla birlikte, polyfill'i test etmek ve polyfill'in iyileştirilmesine yardımcı olmak için mühendislik ekibine geri bildirim sağlamak için de mükemmel bir araç oldu.
Chrometober 2022 sona erdi.
Umarız beğenmişsinizdir. En sevdiğiniz özellik hangisi? Bize tweet atarak düşüncelerinizi bizimle paylaşın.
Bir etkinlikte bizi görürseniz ekipten birinden çıkartma bile alabilirsiniz.
Unsplash'taki David Menidrey tarafından çekilen hero fotoğrafı