Chrometober oluşturuluyor!

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:

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.

Masanın üzerinde projeyle ilgili çeşitli doodle'ların ve karalamaların bulunduğu bir defter.

İ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.

Bir yılan, kolları dışarı çıkan bir tabut, kazanda asalı bir tilki, ürkütücü suratlı bir ağaç ve bal kabağı feneri tutan bir çörtenin bulunduğu beste sahnelerinden biri.

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:

  1. Kaydırma konumuna tepki verenler.
  2. 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.

Mezarlıkta bir elma ağacının göründüğü kitaptan sayfa yayılmış çizimi. Mezarlıkta birden fazla mezar taşı ve büyük bir ayın önünde gökyüzünde bir yarasa görülüyor.

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:

  1. Kullanıcının hareket tercihlerini kontrol edin.
  2. 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'lerde title öğesi gerektiğinde bulunur.
  • Deneyimi iyileştirmek için aria özelliklerini kullanma. Sayfalar ve yanlar için aria-label kullanımı, kullanıcıya hangi sayfada olduklarını bildirir. "Devamı" bağlantılarında aria-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 sayfalara inert ö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">

Açık durumdaki Chrometober kitabının ekran görüntüsü. Kullanıcı arayüzünün çeşitli bölümleri için, amaçlanan erişilebilirlik işlevselliğini ve sayfanın sağlayacağı kullanıcı deneyimi sonuçlarını açıklayan yeşil anahatlar içeren kutular vardır. Örneğin, resimlerde alternatif metin bulunur. Başka bir örnek de, görünüm dışındaki sayfaların hareketsiz olduğunu belirten bir erişilebilirlik etiketidir. Daha fazlası ekran görüntüsünde özetlenmiştir.

Öğ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ı.

CSS, kullanıcı arayüzü ve DevTools ekibi bir konferans odasında masa başında. Una, yapışkan notlarla kaplı bir beyaz tahtanın yanında duruyor. Diğer ekip üyeleri masa etrafında oturuyorlar, ikramlar ve dizüstü bilgisayarlarla karşılaşıyorlar.

Ö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ı.

Chrome&#39;da açık bir demonun ekran görüntüsü. Geliştirici Araçları açıktır ve bir temel performans ölçümü gösterilir.

Chrome&#39;da açık bir demonun ekran görüntüsü. Geliştirici Araçları açıktır ve iyileştirilmiş bir performans ölçümü gösterir.

İş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!

Elinde Chrometober karakterlerinin olduğu bir çıkartma sayfası tutan Jhey.

Hatta bizi bir etkinlikte görürseniz ekiplerin birinden çıkartmalar bile alabilirsiniz.

David Menidrey'nin Unsplash'teki Hero fotoğrafı