Chrometober oluşturuluyor!

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:

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.

Masanın üzerinde, projeyle ilgili çeşitli karalamalar ve notlar bulunan bir defter.

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

Bir yılan, kolları dışarı çıkan bir tabut, kazan başında değnek tutan bir tilki, korkutucu yüzlü bir ağaç ve kabak fener tutan bir heykelin yer aldığı kompozisyon sahnelerinden biri.

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:

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

Mezarlıkta bir elma ağacı bulunan kitabın sayfa yayılımı resmi. Mezarlıkta birden fazla mezar taşı var ve gökyüzünde büyük bir ayın önünde bir yarasa var.

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:

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

Chrometober kitabının açıkken ekran görüntüsü. Kullanıcı arayüzünün çeşitli yönlerinde, amaçlanan erişilebilirlik işlevini ve sayfanın sunacağı kullanıcı deneyimi sonuçlarını açıklayan yeşil kenarlı kutular sağlanır. Örneğin, resimlerde alternatif metin bulunur. Görünmeyen sayfaların etkin olmadığını belirten erişilebilirlik etiketleri de bu kapsamdadır. Ekran görüntüsünde ayrıntılı bilgi verilmiştir.

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

CSS, kullanıcı arayüzü ve DevTools ekibi, bir konferans odasındaki masanın etrafında oturuyor. Una, yapışkan notlarla kaplı bir beyaz tahtanın önünde duruyor. Diğer ekip üyeleri, masanın etrafında atıştırmalıkları ve dizüstü bilgisayarlarıyla oturuyor.

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

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

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

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

Jhey, Chrometober&#39;daki karakterlerin yer aldığı çıkartma sayfasını tutuyor.

Bir etkinlikte bizi görürseniz ekipten birinden çıkartma bile alabilirsiniz.

Unsplash'taki David Menidrey tarafından çekilen hero fotoğrafı