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 öne çıkaran ilginç bir deneyim sunmaktı. Ancak bu deneyimin hem eğlenceli hem de duyarlı ve erişilebilir olması gerekiyordu. Proje, aktif olarak geliştirilmekte olan API polyfill'ini test etmek ve farklı teknikleri ve araçları birlikte denemek için de mükemmel bir yöntem oldu. Tüm bunlara Cadılar Bayramı teması da eklendi.

Ekip yapımız şu şekildeydi:

Kaydırmayla okuma deneyimi taslağı oluşturma

Chrometober ile ilgili fikirler, Mayıs 2022'de düzenlediğimiz ilk ekip toplantımızda ortaya çıkmaya başladı. Bir karalama defteri koleksiyonu, kullanıcıların bir tür hikaye tahtası boyunca nasıl kaydırabileceğini düşünmemizi sağladı. Video oyunlarından esinlenerek, mezarlık ve hayaletli ev gibi sahnelerde kaydırma deneyimi sunmayı düşündük.

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ırırken bloklar döner ve yakınlaştırılır. Ancak bu deneyimi her boyutta cihaz kullanan kullanıcılar için nasıl mükemmel hale getirebileceğimiz konusunda endişelendiğim için bu fikirden vazgeçmeye karar verdim. Bunun yerine, geçmişte yaptığım bir tasarıma yöneldim. 2020'de, sürüm denemeleri oluşturmak için GreenSock'un ScrollTrigger aracına erişebildim.

Hazırladığım demolardan biri, siz kaydırdıkça sayfaların döndüğü bir 3D CSS kitabıydı. Bu, Chrometober için istediğimiz şeye çok daha uygundu. Kaydırmayla bağlantılı animasyonlar API'si, bu işlev için mükemmel bir değişimdir. Göreceğiniz gibi scroll-snap ile de iyi çalışır.

Projenin illüstratörlüğünü yapan Tyler Reed, fikirlerimiz değiştikçe tasarımda gerekli değişiklikleri yapma konusunda oldukça başarılıydı. Tyler, kendisine iletilen tüm yaratıcı fikirleri hayata geçirmek için harika bir iş çıkardı. Birlikte beyin fırtınası yapmak çok eğlenceliydi. Bu özelliğin çalışmasını istediğimiz şekilde sağlamanın önemli bir kısmı, özelliklerin ayrı bloklara bölünmesini sağlamaktı. Böylece, bunları sahnelere yerleştirebilir ve hangilerini hayata geçireceğimizi seçebiliriz.

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.

Ana fikir, kullanıcının kitapta ilerlerken içerik bloklarına erişebilmesiydi. Ayrıca, deneyime eklediğimiz sürprizler de dahil olmak üzere eğlenceli öğelerle etkileşime geçebilirlerdi. Örneğin, gözleri işaretçinizi takip eden, hayaletli bir evdeki bir portre veya medya sorguları tarafından tetiklenen ince animasyonlar. Bu fikirler ve özellikler, ekranı kaydırırken animasyonlu olarak gösterilir. İlk fikir, kullanıcı ekranı kaydırdığında yükselip x ekseni boyunca hareket edecek bir zombi tavşandı.

API'yi tanıma

Ayrı ayrı özelliklerle ve sürprizlerle oynamaya başlamadan önce bir kitaba ihtiyacımız vardı. Bu nedenle, bu fırsatı yeni CSS kaydırma bağlantılı animasyon API'sinin özellik grubunu test etme fırsatına dönüştürmeye karar verdik. Kaydırmayla bağlantılı animasyonlar API'si şu anda hiçbir tarayıcıda desteklenmemektedir. Ancak API geliştirilirken etkileşimler ekibindeki mühendisler bir polyfill üzerinde çalışıyordu. Bu sayede, API'nin geliştirilme aşamasında şeklini test edebilirsiniz. Bu, bu API'yi hemen kullanabileceğimiz anlamına geliyor. Bu tür eğlenceli projeler, deneysel özellikleri denemek ve geri bildirimde bulunmak için genellikle mükemmel bir yerdir. Neler öğrendiğimizi ve verdiğimiz geri bildirimi makalenin ilerleyen bölümlerinde bulabilirsiniz.

Genel olarak, bu API'yi animasyonlarla kaydırma işlemini bağlamak için kullanabilirsiniz. Ekranı kaydırırken animasyon tetikleyemeyeceğinizi unutmayın. Bu özellik daha sonra kullanıma sunulabilir. Kaydırmayla bağlantılı animasyonlar da iki ana kategoriye ayrılır:

  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ığı tarihte) aşamaları tanımlama şeklimizdir.

Bu aşamalar, animasyonla öğenin kaydırılabilir kapsayıcısındaki konumu arasındaki bağlantının oluşturulacağı noktaları tanımlar. Örneğimizde, öğe kaydırılabilir kapsayıcıya (enter 0%) girdiğinde animasyonun başlatılmasını söylüyoruz. Kaydırma kapsayıcısının %50'sini (cover 50%) kapladığında bitirin.

Demomuzu aşağıda görebilirsiniz:

Ayrıca, bir animasyonu, ekran görünümünde hareket eden öğeye bağlayabilirsiniz. Bunu yapmak için animation-timeline değerini öğenin view-timeline değerine ayarlayın. Bu, liste animasyonlar gibi senaryolar için iyidir. Bu davranış, IntersectionObserver kullanarak girişte öğeleri nasıl canlandıracağınıza benzer.

element-moving-in-viewport {
  view-timeline-name: foo;
  view-timeline-axis: block;
  animation: scale both linear;
  animation-delay: enter 0%;
  animation-end-delay: cover 50%;
  animation-timeline: foo;
}

@keyframes scale {
  0% {
    scale: 0;
  }
}

Bu sayede "Hareket Eden", görüntü alanına girerken ölçeğini büyütür ve "Dönen"in dönmesini tetikler.

Denemelerden, API'nin scroll-snap ile çok iyi çalıştığını öğrendim. Kaydırma yakalama özelliği, ViewTimeline ile birlikte kullanıldığında bir kitapta sayfa çevirme işlemi için mükemmel bir seçimdir.

Mekanizmaların prototipini oluşturma

Biraz denedikten sonra bir kitap prototipini çalıştırmayı başardım. Kitabın sayfalarını çevirmek için yatay olarak kaydırın.

Demoda, farklı tetikleyicilerin noktalı kenarlıklarla vurgulandığını görebilirsiniz.

İşaretleme şu şekilde görünür:

<body>
  <div class="book-placeholder">
    <ul class="book" style="--count: 7;">
      <li
        class="page page--cover page--cover-front"
        data-scroll-target="1"
        style="--index: 0;"
      >
        <div class="page__paper">
          <div class="page__side page__side--front"></div>
          <div class="page__side page__side--back"></div>
        </div>
      </li>
      <!-- Markup for other pages here -->
    </ul>
  </div>
  <div>
    <p>intro spacer</p>
  </div>
  <div data-scroll-intro>
    <p>scale trigger</p>
  </div>
  <div data-scroll-trigger="1">
    <p>page trigger</p>
  </div>
  <!-- Markup for other triggers here -->
</body>

Kaydırırken kitabın sayfaları döner ancak açılmaz veya kapanmaz. Bu, tetikleyicilerin kaydırma anında hizalanmasına bağlıdır.

html {
  scroll-snap-type: x mandatory;
}

body {
  grid-template-columns: repeat(var(--trigger-count), auto);
  overflow-y: hidden;
  overflow-x: scroll;
  display: grid;
}

body > [data-scroll-trigger] {
  height: 100vh;
  width: clamp(10rem, 10vw, 300px);
}

body > [data-scroll-trigger] {
  scroll-snap-align: end;
}

Bu sefer ViewTimeline öğesini CSS'de bağlamıyoruz, JavaScript'te Web Animations API'yi kullanıyoruz. Bu yöntemin bir diğer avantajı, her bir öğeyi manuel olarak oluşturmak yerine bir dizi öğeyi döngüye alıp ihtiyacımız olan ViewTimeline öğesini oluşturabilmemizdir.

const triggers = document.querySelectorAll("[data-scroll-trigger]")

const commonProps = {
  delay: { phase: "enter", percent: CSS.percent(0) },
  endDelay: { phase: "enter", percent: CSS.percent(100) },
  fill: "both"
}

const setupPage = (trigger, index) => {
  const target = document.querySelector(
    `[data-scroll-target="${trigger.getAttribute("data-scroll-trigger")}"]`
  );

  const viewTimeline = new ViewTimeline({
    subject: trigger,
    axis: 'inline',
  });

  target.animate(
    [
      {
        transform: `translateZ(${(triggers.length - index) * 2}px)`
      },
      {
        transform: `translateZ(${(triggers.length - index) * 2}px)`,
        offset: 0.75
      },
      {
        transform: `translateZ(${(triggers.length - index) * -1}px)`
      }
    ],
    {
      timeline: viewTimeline,
      commonProps,
    }
  );
  target.querySelector(".page__paper").animate(
    [
      {
        transform: "rotateY(0deg)"
      },
      {
        transform: "rotateY(-180deg)"
      }
    ],
    {
      timeline: viewTimeline,
      commonProps,
    }
  );
};

const triggers = document.querySelectorAll('[data-scroll-trigger]')
triggers.forEach(setupPage);

Her tetikleyici için bir ViewTimeline oluştururuz. Ardından, tetikleyicinin ilişkili sayfasını bu ViewTimeline değerini kullanarak hareketlendiririz. Bu, sayfanın animasyonunu kaydırmaya bağlar. Animasyonumuzda, sayfayı döndürmek için sayfadaki bir öğeyi y ekseninde döndürüyoruz. Ayrıca sayfanın kendisini z ekseninde çeviririz. Böylece sayfa bir kitap gibi davranır.

Tüm unsurların birleşimi

Kitabın mekanizmasını belirledikten sonra Tyler'ın çizimlerini hayata geçirmeye odaklandım.

Astro

Ekip 2021'de Designcember için Astro'yu kullandı ve ben de Chrometober için tekrar kullanmak istedim. Geliştiricilerin öğeleri bileşenlere ayırabilmesi, bu projeye çok uygun bir deneyimdir.

Kitabın kendisi bir bileşendir. Ayrıca bir sayfa bileşenleri koleksiyonudur. Her sayfanın iki yüzü ve arka planı vardır. Sayfa tarafının alt öğeleri, kolayca eklenebilen, kaldırılabilen ve yerleştirilebilen bileşenlerdir.

Albüm oluşturma

Blokların kolayca yönetilebilmesi benim için önemliydi. Ayrıca, ekibin geri kalanının katkıda bulunmasını kolaylaştırmak istedim.

Üst düzey sayfalar bir yapılandırma dizisiyle tanımlanır. Dizideki her sayfa nesnesi, bir sayfanın içeriğini, arka planını ve diğer meta verilerini tanımlar.

const pages = [
  {
    front: {
      marked: true,
      content: PageTwo,
      backdrop: spreadOne,
      darkBackdrop: spreadOneDark
    },
    back: {
      content: PageThree,
      backdrop: spreadTwo,
      darkBackdrop: spreadTwoDark
    },
    aria: `page 1`
  },
  /* Obfuscated page objects */
]

Bunlar Book bileşenine iletilir.

<Book pages={pages} />

Kaydırma mekanizmasının uygulandığı ve albümün sayfalarının oluşturulduğu yer Book bileşenidir. Prototipteki mekanizmanın aynısı kullanılır ancak dünya genelinde oluşturulan birden fazla ViewTimeline örneği paylaşırız.

window.CHROMETOBER_TIMELINES.push(viewTimeline);

Bu sayede, zaman çizelgelerini yeniden oluşturmak yerine başka yerlerde kullanılmak üzere paylaşabiliriz. Bu konuyla ilgili daha fazla bilgiyi aşağıda bulabilirsiniz.

Sayfa kompozisyonu

Her sayfa, bir liste içindeki bir liste öğesidir:

<ul class="book">
  {
    pages.map((page, index) => {
      const FrontSlot = page.front.content
      const BackSlot = page.back.content
      return (
        <Page
          index={index}
          cover={page.cover}
          aria={page.aria}
          backdrop={
            {
              front: {
                light: page.front.backdrop,
                dark: page.front.darkBackdrop
              },
              back: {
                light: page.back.backdrop,
                dark: page.back.darkBackdrop
              }
            }
          }>
          {page.front.content && <FrontSlot slot="front" />}    
          {page.back.content && <BackSlot slot="back" />}    
        </Page>
      )
    })
  }
</ul>

Tanımlanan yapılandırma her Page örneğine iletilir. Sayfalar, her sayfaya içerik eklemek için Astro'nun slot özelliğini kullanır.

<li
  class={className}
  data-scroll-target={target}
  style={`--index:${index};`}
  aria-label={aria}
>
  <div class="page__paper">
    <div
      class="page__side page__side--front"
      aria-label={`Right page of ${index}`}
    >
      <picture>
        <source
          srcset={darkFront}
          media="(prefers-color-scheme: dark)"
          height="214"
          width="150"
        >
        <img
          src={lightFront}
          class="page__background page__background--right"
          alt=""
          aria-hidden="true"
          height="214"
          width="150"
        >
      </picture>
      <div class="page__content">
        <slot name="front" />
      </div>
    </div>
    <!-- Markup for back page -->
  </div>
</li>

Bu kod çoğunlukla yapı oluşturmak için kullanılır. Katkıda bulunanlar, kitabın içeriğinin büyük bir kısmında bu koda dokunmak zorunda kalmadan çalışabilir.

Backdrop

Reklam öğesinin kitap şeklinde tasarlanması, bölümlerin ayrılmasını çok daha kolay hale getirdi. Kitabın her sayfası, orijinal tasarımdan alınmış bir sahnedir.

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.

Kitap için bir en boy oranına karar verdiğimizden her sayfanın arka planında bir resim öğesi olabilir. Bu öğeyi% 200 genişliğe ayarlamak ve sayfa tarafına göre object-position kullanmak işe yarar.

.page__background {
  height: 100%;
  width: 200%;
  object-fit: cover;
  object-position: 0 0;
  position: absolute;
  top: 0;
  left: 0;
}

.page__background--right {
  object-position: 100% 0;
}

Sayfa içeriği

Sayfalardan birini oluşturmaya bakalım. Üçüncü sayfada, ağaçtan çıkan bir baykuş gösterilmektedir.

Yapılandırmada tanımlandığı şekilde bir PageThree bileşeniyle doldurulur. Bu bir Astro bileşenidir (PageThree.astro). Bu bileşenler HTML dosyalarına benzer ancak üst kısımda ön metne benzer bir kod çiti bulunur. Bu sayede diğer bileşenleri içe aktarma gibi işlemler yapabiliriz. Üçüncü sayfanın bileşeni şu şekilde görünür:

---
import TreeOwl from '../TreeOwl/TreeOwl.astro'
import { contentBlocks } from '../../assets/content-blocks.json'
import ContentBlock from '../ContentBlock/ContentBlock.astro'
---
<TreeOwl/>
<ContentBlock {...contentBlocks[3]} id="four" />

<style is:global>
  .content-block--four {
    left: 30%;
    bottom: 10%;
  }
</style>

Yine de sayfalar atomik yapıdadır. Bu gruplar, bir dizi özellikten oluşur. Üçüncü sayfada bir içerik bloğu ve etkileşimli baykuş bulunduğundan her biri için bir bileşen vardır.

İçerik blokları, kitapta görülen içeriğin bağlantılarıdır. Bunlar da bir yapılandırma nesnesi tarafından yönlendirilir.

{
 "contentBlocks": [
    {
      "id": "one",
      "title": "New in Chrome",
      "blurb": "Lift your spirits with a round up of all the tools and features in Chrome.",
      "link": "https://www.youtube.com/watch?v=qwdN1fJA_d8&list=PLNYkxOF6rcIDfz8XEA3loxY32tYh7CI3m"
    },
    …otherBlocks
  ]
}

Bu yapılandırma, içerik bloklarının gerekli olduğu durumlarda içe aktarılır. Ardından ilgili blok yapılandırması ContentBlock bileşenine iletilir.

<ContentBlock {...contentBlocks[3]} id="four" />

Ayrıca, sayfanın bileşenini içeriği konumlandırmak için nasıl kullandığımıza dair bir örnek de verilmiştir. Burada bir içerik bloğu yerleştirilir.

<style is:global>
  .content-block--four {
    left: 30%;
    bottom: 10%;
  }
</style>

Ancak bir içerik bloğunun genel stilleri, bileşen koduyla birlikte yerleştirilir.

.content-block {
  background: hsl(0deg 0% 0% / 70%);
  color: var(--gray-0);
  border-radius:  min(3vh, var(--size-4));
  padding: clamp(0.75rem, 2vw, 1.25rem);
  display: grid;
  gap: var(--size-2);
  position: absolute;
  cursor: pointer;
  width: 50%;
}

Baykuşumuz ise bu projedeki birçok etkileşimli özellikten biridir. Bu, oluşturduğumuz paylaşılan zaman çizelgesi görünümünü nasıl kullandığımızı gösteren güzel bir örnektir.

Baykuş bileşenimiz, üst düzeyde bazı SVG'leri içe aktarır ve Astro'nun Fragment'ini kullanarak satır içi olarak yerleştirir.

---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />

Baykuşumuzu konumlandırma stillerinin, bileşen koduyla birlikte yerleştirildiğini görüyoruz.

.owl {
  width: 34%;
  left: 10%;
  bottom: 34%;
}

Baykuş için transform davranışını tanımlayan ek bir stil öğesi vardır.

.owl__owl {
  transform-origin: 50% 100%;
  transform-box: fill-box;
}

transform-box kullanımı transform-origin'u etkiler. Bu, nesnenin SVG içindeki sınırlayıcı kutusuna göre yapılır. Baykuş, alt ortadan yukarı doğru ölçeklendirilir. Bu nedenle transform-origin: 50% 100% kullanılır.

Eğlenceli kısım, baykuşu oluşturduğumuz ViewTimeline'lerden birine bağladığımızda ortaya çıkar:

const setUpOwl = () => {
   const owl = document.querySelector('.owl__owl');

   owl.animate([
     {
       translate: '0% 110%',
     },
     {
       translate: '0% 10%',
     },
   ], {
     timeline: CHROMETOBER_TIMELINES[1],
     delay: { phase: "enter", percent: CSS.percent(80) },
     endDelay: { phase: "enter", percent: CSS.percent(90) },
     fill: 'both' 
   });
 }

 if (window.matchMedia('(prefers-reduced-motion: no-preference)').matches)
   setUpOwl()

Bu kod bloğunda iki şey yapıyoruz:

  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üştürme mülkü translate kullanılır ve bir ViewTimeline'a bağlanır. timeline mülkü aracılığıyla CHROMETOBER_TIMELINES[1]'e bağlıdır. Bu, sayfa çevirme işlemleri için oluşturulan bir ViewTimeline öğesidir. Bu, enter aşamasını kullanarak baykuşun animasyonunu sayfa çevirme işlemine bağlar. Sayfa% 80 döndürüldüğünde baykuşun hareket etmeye başlayacağını belirtir. %90'a ulaşıldığında baykuş çeviriyi tamamlar.

Kitap özellikleri

Artık bir sayfa oluşturma yaklaşımını ve proje mimarisinin işleyişini gördünüz. Bu özelliğin, katkıda bulunanların diledikleri sayfa veya özellik üzerinde çalışmaya nasıl olanak tanıdığını görebilirsiniz. Kitaptaki çeşitli özelliklerin animasyonları kitabın sayfalarının çevrilmesine bağlıdır. Örneğin, sayfa çevrildiğinde uçan yarasa.

Ayrıca CSS animasyonlarından güç alan öğeler de vardır.

İçerik blokları kitaba eklendikten sonra diğer özelliklerle yaratıcılığımızı konuşturmanın zamanı geldi. Bu sayede farklı etkileşimler oluşturma ve farklı uygulama yöntemleri deneme fırsatı bulduk.

Duyarlılığı koruma

Duyarlı görüntü alanı birimleri, kitabı ve özelliklerini boyutlandırır. Ancak yazı tiplerinin duyarlı kalmasını sağlamak ilginç bir sorundu. Kapsayıcı sorgu birimleri bu durumda iyi bir seçimdir. Ancak henüz her yerde desteklenmiyor. Kitabın boyutu belirlendiğinden kapsayıcı sorgusuna gerek yoktur. Satır içi kapsayıcı sorgu birimi, CSS calc() ile oluşturulabilir ve yazı tipi boyutlandırması için kullanılabilir.


.book-placeholder {
  --size: clamp(12rem, 72vw, 80vmin);
  --aspect-ratio: 360 / 504;
  --cqi: calc(0.01 * (var(--size) * (var(--aspect-ratio))));
}

.content-block h2 {
  color: var(--gray-0);
  font-size: clamp(0.6rem, var(--cqi) * 4, 1.5rem);
}

.content-block :is(p, a) {
  font-size: clamp(0.6rem, var(--cqi) * 3, 1.5rem);
}

Gece ışıldayan kabaklar

Dikkatli bir göz, daha önce sayfa arka planları hakkında konuşurken <source> öğelerinin kullanıldığını fark etmiş olabilir. Una, renk şeması tercihine tepki veren bir etkileşime sahip olmak istiyordu. Bu nedenle arka planlar, farklı varyantlarla hem açık hem de koyu modları destekler. <picture> öğesiyle medya sorgularını kullanabileceğiniz için bu öğe, iki arka plan stili sağlamanın mükemmel bir yoludur. <source> öğesi, renk şeması tercihini sorgulayarak uygun arka planı gösterir.

<picture>
  <source srcset={darkFront} media="(prefers-color-scheme: dark)" height="214" width="150">
  <img src={lightFront} class="page__background page__background--right" alt="" aria-hidden="true" height="214" width="150">
</picture>

Bu renk şeması tercihine göre başka değişiklikler de yapabilirsiniz. İkinci sayfadaki balkabakları, kullanıcının renk şeması tercihine göre değişir. Kullanılan SVG'de, koyu modda ölçeklendirilip animasyonlu hale gelen alevleri temsil eden daireler bulunur.

.pumpkin__flame,
 .pumpkin__flame circle {
   transform-box: fill-box;
   transform-origin: 50% 100%;
 }

 .pumpkin__flame {
   scale: 0.8;
 }

 .pumpkin__flame circle {
   transition: scale 0.2s;
   scale: 0;
 }

@media(prefers-color-scheme: dark) {
   .pumpkin__flame {
     animation: pumpkin-flicker 3s calc(var(--index, 0) * -1s) infinite linear;
   }

   .pumpkin__flame circle {
     scale: 1;
   }

   @keyframes pumpkin-flicker {
     50% {
       scale: 1;
     }
   }
 }

Bu portre sizi izliyor mu?

10. sayfaya göz atarsanız bir şey fark edebilirsiniz. Sizi izliyorlar! Sayfada gezinirken portrenin gözleri işaretçinizi takip eder. Buradaki püf noktası, işaretçi konumunu bir çeviri değeriyle eşlemek ve CSS'ye iletmektir.

const mapRange = (inputLower, inputUpper, outputLower, outputUpper, value) => {
   const INPUT_RANGE = inputUpper - inputLower
   const OUTPUT_RANGE = outputUpper - outputLower
   return outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0)
 }

Bu kod, giriş ve çıkış aralıklarını alır ve verilen değerleri eşler. Örneğin, bu kullanım 625 değerini verir.

mapRange(0, 100, 250, 1000, 50) // 625

Portre için giriş değeri, her bir gözün merkez noktası artı veya eksi bir miktar piksel mesafesidir. Çıkış aralığı, gözlerin piksel cinsinden ne kadar çevirebileceğini belirtir. Ardından, x veya y eksenindeki işaretçi konumu değer olarak iletilir. Gözler hareket ettirilirken göz merkez noktasını bulmak için gözler kopyalanır. Orijinaller hareket etmez, şeffaftır ve referans olarak kullanılır.

Ardından, bunları birbirine bağlamanız ve gözlerin hareket edebilmesi için gözlerdeki CSS özel mülk değerlerini güncellemeniz gerekir. Bir işlev, window etkinliğine karşı pointermove etkinliğine bağlanır. Bu işlem tetiklendiğinde, merkez noktaları hesaplamak için her bir gözün sınırları kullanılır. Ardından işaretçi konumu, gözlerde özel mülk değerleri olarak ayarlanan değerlerle eşlenir.

const RANGE = 15
const LIMIT = 80
const interact = ({ x, y }) => {
   // map a range against the eyes and pass in via custom properties
   const LEFT_EYE_BOUNDS = LEFT_EYE.getBoundingClientRect()
   const RIGHT_EYE_BOUNDS = RIGHT_EYE.getBoundingClientRect()

   const CENTERS = {
     lx: LEFT_EYE_BOUNDS.left + LEFT_EYE_BOUNDS.width * 0.5,
     rx: RIGHT_EYE_BOUNDS.left + RIGHT_EYE_BOUNDS.width * 0.5,
     ly: LEFT_EYE_BOUNDS.top + LEFT_EYE_BOUNDS.height * 0.5,
     ry: RIGHT_EYE_BOUNDS.top + RIGHT_EYE_BOUNDS.height * 0.5,
   }

   Object.entries(CENTERS)
     .forEach(([key, value]) => {
       const result = mapRange(value - LIMIT, value + LIMIT, -RANGE, RANGE)(key.indexOf('x') !== -1 ? x : y)
       EYES.style.setProperty(`--${key}`, result)
     })
 }

Değerler CSS'ye iletildikten sonra stiller bunlarla istediklerini yapabilir. Buradaki en iyi nokta, her göz için davranışı farklı hale getirmek üzere CSS clamp() kullanmaktır. Böylece, JavaScript'e tekrar dokunmadan her gözün farklı davranmasını sağlayabilirsiniz.

.portrait__eye--mover {
   transition: translate 0.2s;
 }

 .portrait__eye--mover.portrait__eye--left {
   translate:
     clamp(-10px, var(--lx, 0) * 1px, 4px)
     clamp(-4px, var(--ly, 0) * 0.5px, 10px);
 }

 .portrait__eye--mover.portrait__eye--right {
   translate:
     clamp(-4px, var(--rx, 0) * 1px, 10px)
     clamp(-4px, var(--ry, 0) * 0.5px, 10px);
 }

Büyü yapma

Altıncı sayfaya göz atarsanız büyülenir misiniz? Bu sayfa, fantastik sihirli tilkimizin tasarımını yansıtır. İşaretçinizi hareket ettirdiğinizde özel bir imleç izi efekti görebilirsiniz. Bu yöntemde kanvas animasyonu kullanılır. <canvas> öğesi, pointer-events: none ile birlikte sayfa içeriğinin geri kalanının üzerinde yer alır. Bu sayede kullanıcılar, alttaki içerik bloklarını tıklamaya devam edebilir.

.wand-canvas {
  height: 100%;
  width: 200%;
  pointer-events: none;
  right: 0;
  position: fixed;
}

Portre öğemizin window üzerinde pointermove etkinliğini dinlemesi gibi <canvas> öğemiz de window üzerinde pointermove etkinliğini dinler. Ancak etkinlik her tetiklendiğinde <canvas> öğesinde animasyon oluşturacak bir nesne oluşturuyoruz. Bu nesneler, işaretçi izinde kullanılan şekilleri temsil eder. Bu noktalar koordinatlara ve rastgele bir tona sahiptir.

İşaretçi deltasını size ve rate ile eşlemek için kullanabileceğimiz için daha önceki mapRange işlevimiz tekrar kullanılır. Nesneler, <canvas> öğesine çizildiğinde döngüye alınan bir dizede saklanır. Her nesnenin özellikleri, <canvas> öğemize nesnelerin nereye çizileceğini söyler.

const blocks = []
  const createBlock = ({ x, y, movementX, movementY }) => {
    const LOWER_SIZE = CANVAS.height * 0.05
    const UPPER_SIZE = CANVAS.height * 0.25
    const size = mapRange(0, 100, LOWER_SIZE, UPPER_SIZE, Math.max(Math.abs(movementX), Math.abs(movementY)))
    const rate = mapRange(LOWER_SIZE, UPPER_SIZE, 1, 5, size)
    const { left, top, width, height } = CANVAS.getBoundingClientRect()
    
    const block = {
      hue: Math.random() * 359,
      x: x - left,
      y: y - top,
      size,
      rate,
    }
    
    blocks.push(block)
  }
window.addEventListener('pointermove', createBlock)

Kanvas üzerinde çizim yapmak için requestAnimationFrame ile bir döngü oluşturulur. İmleç izi yalnızca sayfa görünümdeyken oluşturulmalıdır. Hangi sayfaların görüntülendiğini güncelleyen ve belirleyen bir IntersectionObserver'imiz var. Bir sayfa görünümdeyse nesneler kanvasta daireler olarak oluşturulur.

Ardından blocks dizisini döngüye alır ve parkurun her bir bölümünü çizeriz. Her kare, nesnenin boyutunu rate oranında azaltır ve konumunu değiştirir. Bu sayede, düşme ve ölçeklendirme efekti elde edilir. Nesne tamamen küçülürse blocks dizisinden kaldırılır.

let wandFrame
const drawBlocks = () => {
   ctx.clearRect(0, 0, CANVAS.width, CANVAS.height)
  
   if (PAGE_SIX.className.indexOf('in-view') === -1 && wandFrame) {
     blocks.length = 0
     cancelAnimationFrame(wandFrame)
     document.body.removeEventListener('pointermove', createBlock)
     document.removeEventListener('resize', init)
   }
  
   for (let b = 0; b < blocks.length; b++) {
     const block = blocks[b]
     ctx.strokeStyle = ctx.fillStyle = `hsla(${block.hue}, 80%, 80%, 0.5)`
     ctx.beginPath()
     ctx.arc(block.x, block.y, block.size * 0.5, 0, 2 * Math.PI)
     ctx.stroke()
     ctx.fill()

     block.size -= block.rate
     block.y += block.rate

     if (block.size <= 0) {
       blocks.splice(b, 1)
     }

   }
   wandFrame = requestAnimationFrame(drawBlocks)
 }

Sayfa görünümden çıkarsa etkinlik işleyicileri kaldırılır ve animasyon kare döngüsü iptal edilir. blocks dizisi de temizlenir.

İşaretçi izinin nasıl kullanıldığını burada görebilirsiniz.

Erişilebilirlik incelemesi

Keşfedilecek eğlenceli bir deneyim oluşturmak iyidir ancak kullanıcılar bu deneyime erişemiyorsa bu deneyimin bir anlamı yoktur. Adam'ın bu alandaki uzmanlığı, Chrometober'u yayınlanmadan önce erişilebilirlik incelemesine hazırlarken çok faydalı oldu.

Ele alınan önemli alanlardan bazıları:

  • Kullanılan HTML'nin semantik olduğundan emin olun. Kitap için <main> gibi uygun yer işareti öğeleri, her içerik bloğu için <article> öğesinin kullanılması ve kısaltmaların tanıtıldığı <abbr> öğeleri buna dahildir. Kitabı oluştururken ileriyi düşünmek, her şeyi daha erişilebilir hale getirdi. Başlıklar ve bağlantılar kullanmak, kullanıcıların gezinmesini kolaylaştırır. Sayfalar için liste kullanılması, sayfa sayısının yardımcı teknolojiler tarafından da açıklandığı anlamına gelir.
  • Tüm resimlerde uygun alt özelliklerinin kullanılmasını sağlayın. Satır içi SVG'lerde, gerektiğ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. "Daha fazla bilgi edinin" bağlantılarında aria-describedBy kullanılması, içerik bloğunun metnini iletir. Bu sayede, bağlantının kullanıcıyı nereye yönlendireceği konusunda belirsizlik ortadan kalkar.
  • İçerik blokları söz konusu olduğunda, yalnızca "Devamı" bağlantısını değil, kartın tamamını tıklayabilirsiniz.
  • Hangi sayfaların görüntülendiğini izlemek için IntersectionObserver kullanılması daha önce de bahsedildi. Bu, yalnızca performansla ilgili olmayan birçok avantaj sağlar. Görüntüleme alanında olmayan sayfalardaki animasyonlar veya etkileşimler duraklatılır. Ancak bu sayfalara inert özelliği de uygulanmıştır. Bu sayede ekran okuyucu kullanan kullanıcılar, görme engeli olmayan kullanıcılarla aynı içerikleri keşfedebilir. Odak, görüntülenen sayfa içinde kalır ve kullanıcılar sekme tuşuyla başka bir sayfaya geçemez.
  • Son olarak, kullanıcının hareket tercihine saygı göstermek için medya sorgularından yararlanırız.

Aşağıda, incelemede yer alan ve uygulanan önlemlerden bazılarının vurgulandığı bir ekran görüntüsü verilmiştir.

öğesi, kitabın tamamının etrafında olduğu şekilde tanımlanır. Bu, yardımcı teknoloji kullanıcılarının bu öğeyi bulması için ana belirgin işaret olması gerektiğini gösterir. Daha fazla bilgiyi ekran görüntüsünde bulabilirsiniz." width="800" height="465">

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. Daha fazla bilgiyi ekran görüntüsünde bulabilirsiniz.

Öğ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 polyfill ile ilgili bazı sorunlar da ortaya çıktı. Bramus, bu sorunları polyfill deposunda gündeme getirdi. Ardından bu sorunlara çözümler buldu ve bunları polyfill ile birleştirdi. Örneğin, bu alma isteği, polyfill'in bir kısmına önbelleğe alma özelliği ekleyerek performans artışı sağladı.

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

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.

Üzerinde çalıştığımız bu proje gerçekten eğlenceliydi. Topluluğun muhteşem içeriklerini öne çıkaran eğlenceli bir kaydırma deneyimi ortaya çıktı. Bununla birlikte, polyfill'i test etmek ve mühendislik ekibine polyfill'i iyileştirmeye yardımcı olacak geri bildirimler sağlamak için de mükemmel bir araç oldu.

Chrometober 2022 sona erdi.

Keyifle izlediğinizi umuyoruz. En sevdiğiniz özellik hangisi? Bize tweet atarak düşüncelerinizi bizimle paylaşın.

Jhey, Chrometober&#39;daki karakterlerin çıkartma tabakasını tutuyor.

Bir etkinlikte bizi görürseniz ekip üyelerinden çıkartma alabilirsiniz.

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