Kayan kitabın, bu Chrometober'daki eğlenceli ve korkutucu ipuçları ile püf noktalarını paylaşarak nasıl ortaya çıktığını öğrenin.
Designcember'dan sonra, topluluğun ve Chrome ekibinin web içeriğini öne çıkarıp paylaşabileceğiniz bir yol olarak bu yıl sizin için Chrometober'ı geliştirmek istedik. Designcember, Kapsayıcı Sorguları'nın kullanımını gösterdi. Ancak bu yıl, CSS kaydırma bağlantılı animasyonlar API'sini sergileyeceğiz.
Kaydırma 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 sıra dışı bir deneyim sunmaktı. Deneyimin tuhaf olmasına rağmen duyarlı ve erişilebilir olması da gerekiyordu. Proje, aktif geliştirme aşamasında olan API çoklu dolgusunu test etmek ve farklı teknik ve araçları birlikte denemek için de harika bir yol oldu. Hepsi eğlenceli bir Cadılar Bayramı temasıyla!
Ekip yapısı şu şekilde görünüyordu:
- Tyler Reed: İllüstrasyon ve tasarım
- Jhey Tompkins: Mimari ve kreatif yönetici
- Una Kravets: Proje lideri
- Bramus Van Damme: Siteye katkıda bulunan kullanıcı
- Adam Argyle: Erişilebilirlik incelemesi
- Aaron Forinton: Metin Yazarlığı
Kaydırarak anlatma deneyimi tasarlama
Chrometober ile ilgili fikirler Mayıs 2022'de tesis dışındaki ilk ekibimizde akmaya başladı. Bir karalama koleksiyonu, bizi bir tür görsel senaryo taslağında ne şekilde kaydırabileceklerine dair bir yol düşünmemize sebep oldu. Video oyunlarından esinlenen bu oyunda, mezarlıklar ve perili köşk gibi sahnelerde kayma deneyimi olduğunu düşündük.
İlk Google projemi beklenmedik bir yöne doğru götürecek yaratıcı özgürlüğe sahip olmak heyecan vericiydi. Bu, bir kullanıcının içerikte nasıl gezinebileceğinin ilk prototiplerinden biriydi.
Kullanıcı yana kaydırdıkça bloklar döner ve ölçeklenir. Ancak bu deneyimi her boyuttan cihaz kullanıcısı için mükemmel bir hale nasıl getirebileceğimiz konusundaki endişem nedeniyle bu fikirden uzaklaşmaya karar verdim. Bunun yerine, geçmişte yaptığım bir şeyin tasarımına yöneldim. 2020'de sürüm demoları oluşturmak için GreenSock's ScrollTrigger'a erişimim olduğu için şanslıydım.
Oluşturduğum demolardan biri siz sayfayı kaydırırken sayfaların çevirildiği bir 3D-CSS kitabıydı ve bu, Chrometober için istediğimiz şey için çok daha uygun görünüyordu. Kaydırma bağlantılı animasyonlar API'si bu işlevin mükemmel bir alternatifidir. Bu, scroll-snap
ile de iyi sonuç verecektir.
Proje için çizerimiz Tyler Reed, fikirleri değiştirirken tasarımı değiştirmede harika bir iş çıkardı. Tyler, kendisine verilen yaratıcı fikirleri alıp hayata geçirmek konusunda harika bir iş çıkardı. Birlikte beyin fırtınası yapmak çok eğlenceliydi. Bunun çalışmasını sağlamamızın en büyük amacı, birbirinden farklı bloklara ayrılmış özelliklere sahip olmaktı. Bu sayede sahneler oluşturabiliyor ve ardından hayata geçirdiklerimizi seçebiliyoruz.
Ana fikir, kullanıcının kitapta ilerlerken içerik bloklarına erişebilmesiydi. Ayrıca, deneyime dahil ettiğimiz Paskalya yumurtaları gibi alışılmadık çizgilerle de (örneğin, perili bir evdeki, işaretçinizi izleyen gözleri olan bir portre veya medya sorgularının tetiklediği, ustaca animasyonlar) etkileşimde bulunabilirler. Bu fikirler ve özelliklere kaydırma sırasında animasyon eklenir. İlk fikirlerden biri, kullanıcı kaydırıldığında x ekseni boyunca yükselip çevrilen bir zombi tavşandı.
API'yi tanıma
Bağımsız özellikler ve Paskalya yumurtalarıyla oynamaya başlamadan önce bir kitaba ihtiyacımız vardı. Biz de bunu, yeni ortaya çıkan CSS kaydırma bağlantılı animasyonlar API'si için özellik kümesini test etmek amacıyla bir fırsata dönüştürmeye karar verdik. Kaydırma bağlantılı animasyonlar API'si şu anda hiçbir tarayıcıda desteklenmemektedir. Bununla birlikte, etkileşim ekibindeki mühendisler, API'yi geliştirirken bir polyfill üzerinde çalışıyor. Bu, gelişen API'nin şeklini test etmek için bir yol sağlar. Bu durum, bu API'yi bugün kullanabileceğimiz anlamına geliyor. Bunun gibi eğlenceli projeler genellikle deneysel özellikleri denemek ve geri bildirim sağlamak için harika bir yerdir. Öğrendiklerimizi ve ilettiğimiz geri bildirimleri makalenin ilerleyen bölümlerinde bulabilirsiniz.
Üst düzeyde, kaydırılacak animasyonları bağlamak için bu API'yi kullanabilirsiniz. Kaydırma sırasında animasyonu tetikleyemeyeceğinizi unutmayın. Bu daha sonra olabilecek bir şeydir. Kaydırma bağlantılı animasyonlar da iki ana kategoriye ayrılır:
- Kaydırma konumuna tepki verenler.
- Bir öğenin kaydırma kapsayıcısındaki konumuna tepki verenler.
İkinci ayarı oluşturmak için animation-timeline
özelliği 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şturur ve bunun eksenini tanımlarız. Bu örnekte block
, mantıksal block
anlamına gelir. Animasyon, animation-timeline
özelliğiyle kaydırmaya bağlanır. Bu aşama animation-delay
ve animation-end-delay
(bu yazının yazıldığında) aşamalarını tanımlıyoruz.
Bu aşamalar, animasyonun bir öğenin kaydırma kapsayıcısındaki konumuna göre bağlanması gereken noktaları tanımlar. Örneğimizde, öğe kaydırma kapsayıcısına girdiğinde (enter 0%
) animasyonu başlat diyoruz. Ve kaydırma kapsayıcısının %50'sini (cover 50%
) kapladığında işlemi bitirin.
Uygulama demomuzu aşağıda bulabilirsiniz:
Görüntü alanında hareket eden öğeye bir animasyon da bağlayabilirsiniz. Bunu, animation-timeline
öğesini öğenin view-timeline
değerine ayarlayarak yapabilirsiniz. Bu, liste animasyonları gibi senaryolar için uygundur. Bu davranış, IntersectionObserver
kullanarak girişte öğeleri canlandırmanı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;
}
}
Bununla birlikte,"Mover", görüntü alanına girerken yukarı ölçeklenerek "Spner"ın döndürülmesini tetikler.
Denemelerle elde ettiğim sonuç, API'nin scroll-snap ile çok iyi çalıştığıydu. Kaydırma özelliği ViewTimeline
ile birlikte kullanıldığında, kitapta sayfayı tutturmak için mükemmel bir çözümdür.
Mekaniklerin prototipini oluşturma
Biraz deneme yaptıktan sonra, bir kitap prototipini çalıştırmayı başardım. Kitabın sayfalarını çevirmek için sayfayı yatay olarak kaydırırsınız.
Demoda, farklı tetikleyicilerin kesik çizgili kenarlıklarla vurgulandığını görebilirsiniz.
İşaretleme biraz ş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ı döner ancak kendiliğinden açılır veya kapanır. Bu, tetikleyicilerin kaydırmaya 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, ancak JavaScript'te Web Animasyonları API'sini kullanıyoruz. Bu, öğelerin her birini manuel olarak oluşturmak yerine bir dizi öğe üzerinde döngüye alabilme ve ihtiyacımız olan ViewTimeline
'yi oluşturabilme avantajını da sunar.
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 tetikleyicinin ilişkili sayfasını canlandırırız. 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 çevirerek kitap gibi davranırız.
Özet
Kitabın mekanizmasını çözdükten sonra, Tyler'ın çizimlerini hayata geçirmeye odaklanabilirim.
Astro
Ekip 2021'de Astro'yu Designcember için kullandı ve ben de onu Chrometober'da tekrar kullanmak istiyordum. Öğeleri bileşenlere bölen geliştirici deneyimi bu proje için oldukça uygundur.
Kitabın kendisi bir bileşendir. Aynı zamanda sayfa bileşenlerinin bir koleksiyonudur. Her sayfanın iki yüzü ve arka planları vardır. Sayfa tarafının alt öğeleri kolayca eklenebilen, kaldırılabilen ve konumlandırılabilen bileşenlerdir.
Kitap oluşturma
Blokların yönetimini kolaylaştırmak benim için önemliydi. Ayrıca ekibin geri kalanının da katkıda bulunmasını kolaylaştırmak istedim.
Üst düzeydeki sayfalar bir yapılandırma dizisi ile 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ılmaktadır ancak küresel olarak oluşturulmuş birden çok ViewTimeline
örneğini paylaşıyoruz.
window.CHROMETOBER_TIMELINES.push(viewTimeline);
Bu sayede, yeniden oluşturmak yerine başka yerlerde kullanılacak zaman çizelgelerini paylaşabiliyoruz. Bu konuyla ilgili daha fazla bilgi vereceğiz.
Sayfa bileşimi
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, 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çindir. Katkıda bulunanlar, çoğunlukla bu koda dokunmadan kitabın içeriği üzerinde çalışabilir.
Backdrop
Yaratıcı bir kitaba geçiş, bölümlerin bölünmesini çok daha kolay hale getirdi ve kitabın her yayılması orijinal tasarımdan alınmış bir sahneydi.
Kitabın en boy oranına karar verdiğimizden, her sayfanın arka planında 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çta duran bir baykuş yer alıyor.
Bu alan, yapılandırmada tanımlandığı gibi bir PageThree
bileşeni ile doldurulur. Bu bir Astro bileşenidir (PageThree.astro
). Bu bileşenler HTML dosyalarına benzer ancak üst kısımda ön pakete benzer bir kod sınırı vardır. Bu sayede, diğer bileşenleri içe aktarmak gibi işlemler yapabiliyoruz. Üçüncü sayfanın bileşeni şuna benzer:
---
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ın doğası atomiktir. Bir dizi özellikten oluşturulurlar. Üçüncü sayfada bir içerik bloğu ve etkileşimli baykuş yer aldığı için her birinin bir bileşeni vardır.
İçerik blokları, kitap içinde görülen içeriklerin bağlantılarıdır. Bunlar aynı zamanda bir yapılandırma nesnesi tarafından da 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 engellemelerinin gerekli olduğu yerlerde içe aktarılır. Daha sonra, ilgili blok yapılandırması ContentBlock
bileşenine iletilir.
<ContentBlock {...contentBlocks[3]} id="four" />
Burada, içeriği konumlandırmak için bir yer olarak sayfa bileşenini nasıl kullandığımıza ilişkin bir örnek de bulunmaktadır. 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ğuna ilişkin genel stiller bileşen koduyla aynı konumda yer alır.
.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şumuza gelince, bu etkileşimli bir özelliktir ve bu projedeki pek çok özellikten biri. Bu, oluşturduğumuz paylaşılan ViewTimeline'ı nasıl kullandığımızı gösteren küçük ve güzel bir örnek.
Üst düzeyde, baykuş bileşenimiz bazı SVG'leri içe aktarır ve bunu Astro'nun Fragment işlevini kullanarak satır içine alır.
---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />
Baykuşumuzun konumlandırma stilleri, bileşen koduyla aynı yerdedir.
.owl {
width: 34%;
left: 10%;
bottom: 34%;
}
Baykuş için transform
davranışını tanımlayan fazladan bir stil öğesi vardır.
.owl__owl {
transform-origin: 50% 100%;
transform-box: fill-box;
}
transform-box
kullanımı transform-origin
etkiler. Nesnenin SVG içindeki sınırlayıcı kutusuyla alakalı hale getirir. Baykuşun ölçeği alt merkezden yukarıya doğru yükseldiği için transform-origin: 50% 100%
kullanılıyor.
İşin eğlenceli tarafı, baykuşu oluşturduğumuz ViewTimeline
ürünlerinden birine bağlamaktır:
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 yaparız:
- Kullanıcının hareket tercihlerini kontrol edin.
- Tercihleri yoksa kaydırmak için bir baykuş animasyonu bağlayın.
İkinci bölümde baykuş, Web Animasyonları API'sini kullanarak y ekseninde animasyon oluşturur. translate
bağımsız dönüşüm özelliği kullanılıyor ve bir ViewTimeline
'ye bağlı. CHROMETOBER_TIMELINES[1]
hizmetine timeline
mülkü aracılığıyla bağlıdır. Bu değer, sayfa çevirme işlemleri için oluşturulmuş bir ViewTimeline
'dir. Bu, enter
aşamasını kullanarak baykuşun animasyonunu sayfa çevirmene bağlar. Bu özellik, sayfa% 80 döndürüldüğünde baykuşu hareket ettirmeye başlar. %90 olduğunda baykuş çevirisini bitirmelidir.
Kitap özellikleri
Artık sayfa oluşturma yaklaşımını ve proje mimarisinin nasıl çalıştığını gördünüz. Bu özelliğin, katkıda bulunanların istedikleri bir sayfaya veya özelliğe hemen girip üzerinde çalışmasına nasıl olanak tanıdığını görebilirsiniz. Kitabın çeşitli özelliklerinin animasyonları kitabın sayfa çevirme işlemiyle bağlantılıdır; örneğin, sayfa çevirerek içeri ve dışarı çıkan yarasa.
Ayrıca, CSS animasyonları tarafından desteklenen öğelere de sahiptir.
İçerik blokları kitaba eklendiğinde, diğer özellikler konusunda yaratıcı olmak için zaman olmuştu. Bu da farklı etkileşimler oluşturma ve bunları uygulamak için farklı yöntemler deneme fırsatı sağladı.
Öğelerin duyarlı olmasını sağlama
Duyarlı görüntü alanı birimleri, kitabı ve özelliklerini boyutlandırır. Ancak yazı tiplerinin duyarlı olmasını sağlamak ilginç bir zorluktu. Kapsayıcı sorgu birimleri bu duruma uygundur. Ancak henüz her yerde desteklenmemektedir. Kitabın boyutu ayarlandığı için kapsayıcı sorgusu gerekmiyor. 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);
}
Geceleri parlayan bal kabakları
Gözü dikkatli olanlar, önceki sayfa arka planları ile ilgili olarak <source>
öğelerinin kullanıldığını fark etmiş olabilir. Una, renk şeması tercihine tepki veren bir etkileşim kurmak istiyordu. Sonuç olarak, arka planlar farklı varyantlara sahip hem açık hem de koyu modu destekler. Medya sorgularını <picture>
öğesiyle birlikte kullanabildiğiniz için bu yöntem, 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 bağlı olarak başka değişiklikler de uygulayabilirsiniz. İkinci sayfadaki bal kabakları, kullanıcının renk şeması tercihine tepki veriyor. Kullanılan SVG'de, ölçeği büyüten ve koyu modda canlanan 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 bakarsanız bir şey fark edebilirsiniz. İzleniyorsunuz. Portrenin gözleri, siz sayfada dolaşırken işaretçinizi izler. 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 için giriş değeri, her bir gözün merkez noktasıdır veya artı ya da eksi piksel mesafesidir. Çıkış aralığı, gözlerin piksel cinsinden ne kadar çeviri yapabildiğini belirler. Ardından x veya y eksenindeki işaretçi konumu değer olarak aktarılır. Hareket ederken gözlerin merkez noktasını elde etmek için iki göz çoğaltılır. Orijinal fotoğraflar hareket etmez, şeffaftır ve referans olarak kullanılır.
Ardından, bunu birbirine bağlayarak gözlerin hareket edebilmesi için CSS özel özellik değerlerini güncelleyebilirsiniz. Bir işlev, window
öğesine göre pointermove
etkinliğine bağlıdır. Bu ateşle birlikte, 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ğerlere 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 geçirildikten sonra, stiller bunlarla istediklerini yapabilir. Bunun en iyi tarafı, her bir göz için davranışı farklı hale getirmek üzere CSS clamp()
kullanmaktır. Böylece, JavaScript'e tekrar dokunmadan her bir gözün farklı şekilde 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 kendinizi büyülü hissediyor musunuz? Bu sayfada fantastik sihirli tilkimizin tasarımı kullanılmıştır. İşaretçinizi hareket ettirirseniz özel bir imleç izi efekti görebilirsiniz. Bu işlem tuval animasyonu kullanır. Bir <canvas>
öğesi, pointer-events: none
öğesiyle sayfa içeriğinin geri kalanının üzerinde yer alır. Bu, kullanıcıların altındaki içerik bloklarını tıklamaya devam edebileceği anlamına gelir.
.wand-canvas {
height: 100%;
width: 200%;
pointer-events: none;
right: 0;
position: fixed;
}
Portremizin window
tarihindeki bir pointermove
etkinliğini dinleme şekli gibi, <canvas>
öğemiz de öyle. Yine de her etkinlik tetiklendiğinde, <canvas>
öğesinde animasyonu yapılacak bir nesne oluşturuyoruz. Bu nesneler, imleç izinde kullanılan şekilleri temsil eder. Koordinatları ve rastgele bir tonu vardır.
Önceki mapRange
işlevimiz, işaretçi deltasını size
ve rate
ile eşlemek için kullanabildiğimiz için tekrar kullanılmıştır. Nesneler, <canvas>
öğesine çizildiğinde döngüye alınan bir dizide depolanır. Her nesnenin özellikleri, <canvas>
öğemize nesnelerin nerede çizilmesi gerektiğini bildirir.
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ümdeyken oluşturulmalıdır. Güncellenen ve hangi sayfaların görüntüleneceğini belirleyen bir IntersectionObserver
sunuyoruz. Sayfa görünümdeyse nesneler zemin üzerinde daire şeklinde oluşturulur.
Daha sonra, blocks
dizisini döngüye alır ve yolun her bir bölümünü çizeriz. Her kare boyutu küçültür ve nesnenin konumunu rate
oranında değiştirir. Bu da düşme ve ölçekleme etkisine neden olur. Nesne tamamen daralırsa 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ümün dışına çıkarsa etkinlik işleyiciler kaldırılır ve animasyon karesi döngüsü iptal edilir. blocks
dizisi de temizlendi.
İmleç izini işte burada!
Erişilebilirlik incelemesi
Keşfetmek için eğlenceli bir deneyim oluşturmak iyidir, ancak kullanıcılar için erişilebilir değilse iyi bir şey olmaz. Adam'ın bu alandaki uzmanlığı, Chrometober'ı yayınlanmadan önce erişilebilirlik incelemesine hazırlamada paha biçilmez katkı sağladı.
Ele alınan önemli alanlardan bazıları:
- Kullanılan HTML'nin anlamsal olduğundan emin olun. Buna, kitap için
<main>
gibi uygun önemli noktalar, her içerik bloğu için<article>
öğesi ve kısaltmaların eklendiği<abbr>
öğeleri dahildi. Kitabın oluşturulduğu şekilde ileriye dönük düşünmek her şeyi daha erişilebilir kıldı. Başlık ve bağlantıların kullanılması, kullanıcının gezinmesini kolaylaştırır. Sayfalar için bir liste kullanılması, sayfa sayısının da yardımcı teknoloji tarafından duyurulacağı anlamına gelir. - Tüm resimlerde uygun
alt
özelliklerinin kullanıldığından emin olun. Satır içi SVG'lerdetitle
öğesi gerektiğinde bulunur. - Deneyimi iyileştirmek için
aria
özelliklerini kullanma. Sayfalar ve yanlar içinaria-label
kullanımı, kullanıcıya hangi sayfada olduklarını bildirir. "Devamı" bağlantılarındaaria-describedBy
kullanımı, içerik bloğu metnini iletir. Bu, bağlantının kullanıcıyı nereye götüreceği konusundaki belirsizliği ortadan kaldırır. - İçerik blokları konusunda, sadece "Devamı" bağlantısını değil, kartın tamamını tıklayabilme seçeneği mevcuttur.
- Görüntülemede olan sayfaların daha önce gösterildiğini izlemek için
IntersectionObserver
kullanılması. Bunun yalnızca performansla ilgili olmayan birçok avantajı vardır. Görünmeyen sayfalarda animasyon veya etkileşim duraklatılır. Ancak bu sayfalarainert
özelliği de uygulandı. Bu, ekran okuyucu kullanan kullanıcıların, gören kullanıcılarla aynı içeriği keşfedebileceği anlamına gelir. Odak, görüntülenmekte olan sayfada kalır ve kullanıcılar sekme tuşuyla başka bir sayfaya geçemez. - Son olarak, kullanıcıların hareket tercihini gözetmek için medya sorgularından da yararlanıyoruz.
Yorumdan alınmış, bazı önlemlerin vurgulandığı bir ekran görüntüsünü burada bulabilirsiniz.
öğesi, kitabın tamamının çevresinde olarak tanımlanıyor. Böylece, bu öğe, yardımcı teknoloji kullanıcılarının aradıkları ana yer olması gerekiyor. Daha fazlası ekran görüntüsünde özetlenmiştir." width="800" height="465">
Öğrendiklerimiz
Chrometober'in arkasındaki motivasyon yalnızca topluluktan gelen web içeriklerini öne çıkarmak değil, aynı zamanda geliştirme aşamasındaki kaydırma bağlantılı animasyonlar API'si polyfill'ini test etmemiz için de bir yöntem oldu.
Projeyi test etmek ve ortaya çıkan sorunlarla başa çıkmak için New York'taki ekip zirvemizde bir oturum ayırdık. Ekibin katkısı paha biçilmezdi. Bu ayrıca, canlı yayına girmeden önce halledilmesi gereken her şeyi listelemek için de mükemmel bir fırsattı.
Örneğin, kitabı cihazlarda test etmek oluşturma sorunu ortaya çıkardı. Kitabımız iOS cihazlarda beklendiği gibi oluşturulmadı. Görüntü alanı birimleri sayfayı boyutlandırır ancak bir çentik olduğunda kitabı etkiler. Çözüm, meta
görüntü alanında viewport-fit=cover
kullanılmasıydı:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
Bu oturumda, API çoklu dolgusu ile ilgili bazı sorunlar da ortaya çıktı. Bramus bu sorunları çoklu doldurma deposunda ortaya çıkardı. Daha sonra bu sorunlara çözüm buldu ve bunları çoklu dolguda birleştirdi. Örneğin, bu pull isteği, çoklu dolgunun bir kısmına önbelleğe alma ekleyerek performans kazancı sağladı.
İşte bu kadar.
Üzerinde çalışılması gerçekten eğlenceli olan bu proje, topluluktaki harika içeriklerin öne çıkarıldığı tuhaf bir kaydırma deneyimiyle sonuçlandı. Bunun yanı sıra, çoklu dolguyu test etmek ve mühendislik ekibine geri bildirim göndererek çoklu dolguyu iyileştirmek için de son derece memnun oldular.
Chrometober 2022 sona erdi.
Keyif aldığınızı umuyoruz! En sevdiğiniz özellik ne? Beni tweet'leyerek bize haber verin!
Hatta bizi bir etkinlikte görürseniz ekiplerin birinden çıkartmalar bile alabilirsiniz.
David Menidrey'nin Unsplash'teki Hero fotoğrafı