İçeriğe geç
academia.sh

Ders 11 / 23

Duyarlı Görseller

Çözünürlük değişimi ile sanat yönetiminin ayrılması, kaynak boyutu bildiriminin çözülmesi, aday seçim kuralı, kaynak sırasının etkisi ve ayrılan yerin içerik kaymasını önlemesi.

İçindekiler

Yazı ölçekle birlikte değişiyor, düzen kabına göre uyarlanıyor. Geriye sayfanın en ağır ögesi kalıyor: konum bölümündeki görsel.

Görselde iki ayrı sorun aynı anda bulunur ve karıştırıldıklarında ikisi de çözülmez. Birincisi ölçü sorunudur: aynı kare, dar bir ekranda gereğinden büyük bir dosyayla indirilir ya da yoğun bir ekranda bulanık görünür. İkincisi içerik sorunudur: geniş bir manzara karesi dar bir ekranda küçüldüğünde ayrıntısı okunamaz hâle gelir; gereken küçültme değil kırpmadır.

Bu ders iki sorunu ayırır ve her birinin aracını gösterir.

Çözünürlük Değişimi

Birinci sorunun adı çözünürlük değişimidir (resolution switching): aynı görselin farklı piksel ölçülerindeki kopyaları hazırlanır ve tarayıcı hangisini indireceğine karar verir.

Karar iki bilgiye dayanır. srcset özniteliği bir aday kümesi (candidate set) verir; her aday, dosyanın gerçek piksel genişliğiyle birlikte yazılır. sizes özniteliği ise görselin sayfada kaplayacağı yeri bildirir — buna kaynak boyutu (source size) denir.

İkincisi neden gereklidir? Tarayıcı, biçem dosyasını çözmeden önce görsel indirmeye başlar; görselin sayfada ne kadar yer kaplayacağını o anda bilemez. sizes, bu bilgiyi biçem dosyasından bağımsız olarak belgeye yazar.

// srcset.mjs — kaynak boyutunun cozulmesi ve aday secimi
// sizes: sirayla denenir, ilk eslesen kosulun uzunlugu kaynak boyutudur.
const SIZES = [
  ["(min-width: 812px)", "33vw"],
  ["(min-width: 500px)", "50vw"],
  [null, "calc(100vw - 48px)"],
];

// srcset: her aday, gercek piksel genisligiyle (w tanimlayicisi) bildirilir
const ADAYLAR = [320, 480, 640, 960, 1280, 1920];

function kosulDogru(kosul, gorusAlani) {
  if (kosul === null) return true;
  const m = kosul.match(/^\((min|max)-width:\s*(\d+)px\)$/);
  return m[1] === "min" ? gorusAlani >= Number(m[2]) : gorusAlani <= Number(m[2]);
}

function uzunluk(ifade, gorusAlani) {
  let m = ifade.match(/^(\d+(?:\.\d+)?)vw$/);
  if (m) return (Number(m[1]) * gorusAlani) / 100;
  m = ifade.match(/^calc\((\d+(?:\.\d+)?)vw\s*-\s*(\d+(?:\.\d+)?)px\)$/);
  if (m) return (Number(m[1]) * gorusAlani) / 100 - Number(m[2]);
  m = ifade.match(/^(\d+(?:\.\d+)?)px$/);
  if (m) return Number(m[1]);
  throw new Error(`cozulemeyen uzunluk: ${ifade}`);
}

function kaynakBoyutu(gorusAlani) {
  for (const [kosul, ifade] of SIZES) {
    if (kosulDogru(kosul, gorusAlani)) return { ifade, px: uzunluk(ifade, gorusAlani) };
  }
  throw new Error("sizes listesi bos");
}

// Secim kurali: gereken piksel genisligini karsilayan en kucuk aday.
// Hicbiri karsilamiyorsa en buyuk aday secilir.
function secim(gereken) {
  return ADAYLAR.find((a) => a >= gereken) ?? ADAYLAR[ADAYLAR.length - 1];
}

console.log("sizes: " + SIZES.map(([k, i]) => (k ? `${k} ${i}` : i)).join(", "));
console.log("srcset adaylari: " + ADAYLAR.map((a) => `${a}w`).join(", "));
console.log("\ngorus  kaynak boyutu ifadesi   kaynak   piksel  gereken  secilen  fazla");
console.log("alani                          boyutu   orani            aday     piksel");
for (const V of [360, 500, 700, 812, 1280, 1600]) {
  for (const dpr of [1, 2, 3]) {
    const { ifade, px } = kaynakBoyutu(V);
    const gereken = px * dpr;
    const secilen = secim(gereken);
    const fazla = secilen - gereken;
    console.log(
      `${String(V).padStart(5)}  ${ifade.padEnd(21)}  ${px.toFixed(0).padStart(6)}  ` +
        `${String(dpr + "x").padStart(6)}  ${gereken.toFixed(0).padStart(7)}  ` +
        `${String(secilen + "w").padStart(7)}  ${fazla.toFixed(0).padStart(6)}`,
    );
  }
}

// aday genisliklerinin piksel sayisi (3:2 en boy orani varsayilarak)
console.log("\n--- aday dosyalarin piksel sayisi (3:2 en boy orani) ---");
console.log("aday   genislik x yukseklik   piksel      320w'in kaci kadar");
for (const a of ADAYLAR) {
  const y = Math.round((a * 2) / 3);
  const piksel = a * y;
  const taban = 320 * Math.round((320 * 2) / 3);
  console.log(
    `${String(a + "w").padStart(5)}  ${String(a + " x " + y).padStart(18)}  ` +
      `${String(piksel).padStart(9)}  ${(piksel / taban).toFixed(1).padStart(18)}x`,
  );
}

// sizes yanlis yazilirsa: gorsel 33vw yer kapliyorken 100vw bildirilmis olsun
console.log("\n--- sizes yanlis yazilirsa (gercekte 33vw, bildirilen 100vw) ---");
console.log("gorus  piksel  dogru sizes  yanlis sizes  secilen piksel orani");
console.log("alani  orani   ile secilen  ile secilen");
for (const V of [812, 1280, 1600]) {
  for (const dpr of [1, 2]) {
    const dogru = secim(kaynakBoyutu(V).px * dpr);
    const yanlis = secim(V * dpr);
    const y1 = Math.round((dogru * 2) / 3), y2 = Math.round((yanlis * 2) / 3);
    const oran = (yanlis * y2) / (dogru * y1);
    console.log(
      `${String(V).padStart(5)}  ${String(dpr + "x").padStart(6)}  ` +
        `${String(dogru + "w").padStart(11)}  ${String(yanlis + "w").padStart(12)}  ${oran.toFixed(1).padStart(19)}x`,
    );
  }
}
sizes: (min-width: 812px) 33vw, (min-width: 500px) 50vw, calc(100vw - 48px)
srcset adaylari: 320w, 480w, 640w, 960w, 1280w, 1920w

gorus  kaynak boyutu ifadesi   kaynak   piksel  gereken  secilen  fazla
alani                          boyutu   orani            aday     piksel
  360  calc(100vw - 48px)        312      1x      312     320w       8
  360  calc(100vw - 48px)        312      2x      624     640w      16
  360  calc(100vw - 48px)        312      3x      936     960w      24
  500  50vw                      250      1x      250     320w      70
  500  50vw                      250      2x      500     640w     140
  500  50vw                      250      3x      750     960w     210
  700  50vw                      350      1x      350     480w     130
  700  50vw                      350      2x      700     960w     260
  700  50vw                      350      3x     1050    1280w     230
  812  33vw                      268      1x      268     320w      52
  812  33vw                      268      2x      536     640w     104
  812  33vw                      268      3x      804     960w     156
 1280  33vw                      422      1x      422     480w      58
 1280  33vw                      422      2x      845     960w     115
 1280  33vw                      422      3x     1267    1280w      13
 1600  33vw                      528      1x      528     640w     112
 1600  33vw                      528      2x     1056    1280w     224
 1600  33vw                      528      3x     1584    1920w     336

--- aday dosyalarin piksel sayisi (3:2 en boy orani) ---
aday   genislik x yukseklik   piksel      320w'in kaci kadar
 320w           320 x 213      68160                 1.0x
 480w           480 x 320     153600                 2.3x
 640w           640 x 427     273280                 4.0x
 960w           960 x 640     614400                 9.0x
1280w          1280 x 853    1091840                16.0x
1920w         1920 x 1280    2457600                36.1x

--- sizes yanlis yazilirsa (gercekte 33vw, bildirilen 100vw) ---
gorus  piksel  dogru sizes  yanlis sizes  secilen piksel orani
alani  orani   ile secilen  ile secilen
  812      1x         320w          960w                  9.0x
  812      2x         640w         1920w                  9.0x
 1280      1x         480w         1280w                  7.1x
 1280      2x         960w         1920w                  4.0x
 1600      1x         640w         1920w                  9.0x
 1600      2x        1280w         1920w                  2.3x

Birinci tablo iki adımı ayrı ayrı gösteriyor. Önce sizes listesi soldan sağa denenir ve koşulu doğru olan ilk uzunluk kaynak boyutu olur — 812 birimlik bir görüntü alanında 33 görüntü alanı birimi, yani 268 piksel. Sonra bu değer aygıtın piksel oranıyla (device pixel ratio) çarpılır: iki katlık bir ekranda 536 gerçek piksel gerekir. Aday kümesinden bu sayıyı karşılayan en küçük dosya seçilir.

Sayılar bir noktayı hemen görünür kılıyor: aynı sayfa, aynı görüntü alanı genişliğinde, piksel oranına göre 320, 640 ya da 960 piksellik dosya indirir. Görüntü alanı geniş olduğu için büyük dosya seçilmez; kaynak boyutu ile piksel oranının çarpımı seçer.

İkinci tablo adaylar arasındaki farkın büyüklüğünü veriyor. Piksel sayısı genişliğin karesiyle büyür: 1920 piksellik dosya, 320 piksellik dosyanın 36 katı piksel taşır. Bu yüzden aday kümesinde bir basamak yanlış seçim, katlanarak büyüyen bir maliyettir.

Kaynak Boyutu Yanlış Bildirilirse

Üçüncü tablo, en sık yapılan hatanın ölçüsünü veriyor. Görsel gerçekte görüntü alanının üçte birini kaplarken sizes bildirimi tamamını kapladığını söylerse, tarayıcı gereğinden çok büyük adayı seçer. Fark 2,3 ile 9 kat arasında değişiyor.

Hatanın gizlenmesi kolaydır, çünkü görsel doğru görünür — yalnız gereğinden büyük bir dosyayla. Denetim yolu şudur: kaynak boyutu bildirimi, biçem dosyasındaki genişlik kurallarının bir kopyasıdır ve düzen değiştiğinde birlikte güncellenmelidir. İkisinin ayrı yerlerde durması bu yazımın yapısal zayıflığıdır.

İki not daha. Aday kümesi genişlik yerine piksel oranı tanımlayıcısıyla da yazılabilir (gorsel.jpg 1x, [email protected] 2x); bu yazımda kaynak boyutu hesaba katılmaz ve yalnız aygıtın piksel oranı seçer. Sabit ölçülü küçük görsellerde — bir simge, bir rozet — yeterlidir.

İkincisi, buradaki seçim kuralı bir modeldir. Aday seçimi tarayıcının takdirine bırakılmıştır; ağ koşullarına ya da kullanıcı ayarına göre daha küçük bir aday seçilebilir. Yazılan bildirimin görevi doğru bilgiyi vermektir, seçimi zorlamak değil.

Sanat Yönetimi

İkinci sorun ölçüyle çözülmez. Geniş bir panorama karesi dar bir ekranda ne kadar küçültülürse küçültülsün, ölçüm istasyonunun bulunduğu nokta ayırt edilemez hâle gelir. Gereken, aynı karenin farklı bir kırpımıdır.

Bu ayrıma sanat yönetimi (art direction) denir ve aracı picture ögesidir. Öge, koşullu source ögeleri ve bir img ögesi taşır.

// sanat.mjs — picture ogesinde kaynak secimi ve ayrilan yerin hesabi
// Kural: kaynaklar belge sirasinda denenir, kosulu dogru olan ILK kaynak secilir.

const KAYNAKLAR = [
  { ad: "konum-genis", kosul: "(min-width: 812px)", en: 1440, boy: 600 },   // 12:5 panorama
  { ad: "konum-orta", kosul: "(min-width: 500px)", en: 960, boy: 640 },     // 3:2
  { ad: "konum-dar", kosul: null, en: 480, boy: 600 },                      // 4:5 kirpik
];

const SIZES = [
  ["(min-width: 812px)", 0.33],
  ["(min-width: 500px)", 0.5],
  [null, null],   // calc(100vw - 48px)
];

function kosulDogru(kosul, V) {
  if (kosul === null) return true;
  const m = kosul.match(/^\(min-width:\s*(\d+)px\)$/);
  return V >= Number(m[1]);
}

function gosterilenGenislik(V) {
  for (const [kosul, oran] of SIZES) {
    if (kosulDogru(kosul, V)) return oran === null ? V - 48 : V * oran;
  }
}

function sec(liste, V) {
  return liste.find((k) => kosulDogru(k.kosul, V));
}

console.log("kaynaklar belge sirasinda:");
for (const k of KAYNAKLAR) {
  console.log(`  ${k.ad.padEnd(12)} ${String(k.kosul ?? "(kosulsuz img)").padEnd(20)} ${k.en}x${k.boy}  en boy orani ${(k.en / k.boy).toFixed(2)}`);
}

console.log("\ngorus  gosterilen  secilen       en boy  ayrilan");
console.log("alani  genislik    kaynak        orani   yukseklik");
for (const V of [360, 500, 700, 812, 1280, 1600]) {
  const g = gosterilenGenislik(V);
  const k = sec(KAYNAKLAR, V);
  const oran = k.en / k.boy;
  console.log(
    `${String(V).padStart(5)}  ${g.toFixed(0).padStart(10)}  ${k.ad.padEnd(12)}  ` +
      `${oran.toFixed(2).padStart(6)}  ${(g / oran).toFixed(0).padStart(9)}`,
  );
}

// sira bozulursa: kosullu kaynaklar dardan genise siralanirsa
const TERS = [KAYNAKLAR[1], KAYNAKLAR[0], KAYNAKLAR[2]];
console.log("\n--- kosullu kaynaklar dardan genise siralanirsa ---");
console.log("gorus  secilen kaynak  dogru secim  ayni mi");
for (const V of [360, 500, 812, 1280]) {
  const t = sec(TERS, V), d = sec(KAYNAKLAR, V);
  console.log(
    `${String(V).padStart(5)}  ${t.ad.padEnd(14)}  ${d.ad.padEnd(11)}  ${(t.ad === d.ad ? "evet" : "HAYIR").padStart(7)}`,
  );
}

// ayrilan yer kaynaktan kaynaga degisir: her kaynak kendi olculerini bildirmezse
console.log("\n--- ayni gosterilen genislikte (500px) kaynaklarin yukseklikleri ---");
for (const k of KAYNAKLAR) {
  console.log(`  ${k.ad.padEnd(12)} ${(500 / (k.en / k.boy)).toFixed(0).padStart(4)}px`);
}
kaynaklar belge sirasinda:
  konum-genis  (min-width: 812px)   1440x600  en boy orani 2.40
  konum-orta   (min-width: 500px)   960x640  en boy orani 1.50
  konum-dar    (kosulsuz img)       480x600  en boy orani 0.80

gorus  gosterilen  secilen       en boy  ayrilan
alani  genislik    kaynak        orani   yukseklik
  360         312  konum-dar       0.80        390
  500         250  konum-orta      1.50        167
  700         350  konum-orta      1.50        233
  812         268  konum-genis     2.40        112
 1280         422  konum-genis     2.40        176
 1600         528  konum-genis     2.40        220

--- kosullu kaynaklar dardan genise siralanirsa ---
gorus  secilen kaynak  dogru secim  ayni mi
  360  konum-dar       konum-dar       evet
  500  konum-orta      konum-orta      evet
  812  konum-orta      konum-genis    HAYIR
 1280  konum-orta      konum-genis    HAYIR

--- ayni gosterilen genislikte (500px) kaynaklarin yukseklikleri ---
  konum-genis   208px
  konum-orta    333px
  konum-dar     625px

Üç kaynak, üç ayrı en boy oranı. Dar düzende dikey bir kırpım kullanılıyor: aynı genişlikte daha çok dikey yer kaplıyor ve istasyonun bulunduğu nokta ölçekte kalıyor. Geniş düzende panorama kırpımı, çevredeki arazi biçimini de gösteriyor.

İkinci tablo, ortam sorgularından ayrılan bir kuralı ölçüyor. Biçem dosyasında eşleşen kuralların sonuncusu kazanıyordu; picture içinde koşulu doğru olan ilki seçilir. Kaynaklar dardan genişe sıralandığında geniş koşullar hiç sıraya gelmez: 812 ve 1280 birimde orta kırpım seçilir ve panorama kullanılmaz. Kaynaklar geniş koşuldan dar koşula doğru sıralanır.

Üçüncü blok, kırpımların birbirinden ne kadar farklı yer kapladığını gösteriyor. Aynı 500 birimlik genişlikte panorama 208, dikey kırpım 625 birim yükseklik ister. Bu fark, ayrılan yer sorununu doğrudan doğurur.

Ayrılan Yer

Görsel indirilmeden önce tarayıcı ona ne kadar yer ayıracağını bilemez. Bilmezse yer ayırmaz; görsel geldiğinde altındaki içerik aşağı itilir. Web’in Temelleri ve HTML kursunda tanıtılan içerik kayması budur.

Çözüm, en boy oranını belgeye yazmaktır. img ögesine width ve height öznitelikleri verildiğinde tarayıcı oranı ikisinin bölümünden hesaplar ve görselin kaplayacağı kutuyu önceden ayırır. Öznitelikler piksel değeridir ama sabit ölçü anlamına gelmez; biçem dosyasındaki inline-size bildirimi görselin gerçek genişliğini belirler, öznitelikler yalnız oranı taşır.

Sanat yönetiminde bir ek gerekir. Her kırpımın oranı farklı olduğu için ayrılacak yer de farklıdır; bu yüzden her source ögesi kendi width ve height değerlerini bildirir. Tek bir oran yazıldığında kaynak değiştiği anda yer yeniden hesaplanır ve kayma oluşur.

Aynı sonuç biçem tarafında aspect-ratio bildirimiyle de kurulabilir; iki yol birlikte kullanıldığında biçem kazanır.

<figure class="konum-gorseli">
  <picture>
    <source media="(min-width: 812px)"
            srcset="konum-genis-960.jpg 960w, konum-genis-1440.jpg 1440w"
            sizes="33vw" width="1440" height="600">
    <source media="(min-width: 500px)"
            srcset="konum-orta-640.jpg 640w, konum-orta-960.jpg 960w"
            sizes="50vw" width="960" height="640">
    <img src="konum-dar-480.jpg"
         srcset="konum-dar-320.jpg 320w, konum-dar-480.jpg 480w"
         sizes="calc(100vw - 48px)"
         width="480" height="600"
         loading="lazy" decoding="async"
         alt="Kuzey Yamaç ölçüm istasyonunun bulunduğu sırtın güneybatıdan görünümü">
  </picture>
  <figcaption>Ölçüm istasyonu, sırtın güneybatı yamacında 1840 metrede.</figcaption>
</figure>
/* duzen.css — 11. adım: görselin kutusu */
.konum-gorseli img {
  inline-size: 100%;
  block-size: auto;
  object-fit: cover;
  object-position: 50% 35%;
  background-color: var(--zemin);
}

.konum-gorseli figcaption {
  margin-block-start: 0.5rem;
  color: var(--metin-soluk);
  font-size: 0.875em;
}

Belge tarafında dört bildirim var ve her biri ayrı bir soruya yanıt veriyor: media hangi kırpımın kullanılacağını, srcset o kırpımın hangi ölçülerde bulunduğunu, sizes sayfada ne kadar yer kaplayacağını, width ile height hangi oranda yer ayrılacağını söylüyor. Alternatif metin kırpımdan bağımsızdır ve img ögesinde tek bir kez yazılır — üç kaynak da aynı sahneyi gösterir.

Biçem tarafında inline-size: 100% görseli kabına oturtur, block-size: auto oranı korur. object-fit: cover bildirimi, kutunun oranı görselin oranından farklı kaldığında görseli gerdirmek yerine kırpar; object-position kırpmanın hangi noktayı merkez alacağını bildirir. Yüzde 35 değeri, ufuk çizgisinin üst üçte birde kalmasını sağlar.

Arka plan görselleri için karşılık image-set() fonksiyonudur; aynı aday mantığını biçem dosyasında kurar. Ancak arka plan görselleri belgede karşılığı olmayan süsleyici görsellerdir; içerik taşıyan bir kare arka plana konmaz.

Özet

  • Görselde iki ayrı sorun vardır: aynı karenin ölçüsünü seçmek (çözünürlük değişimi) ve dar düzende farklı bir kırpım göstermek (sanat yönetimi).
  • Aday seçimi iki çarpanla belirlenir: sizes bildiriminden çözülen kaynak boyutu ve aygıtın piksel oranı; görüntü alanının genişliği tek başına seçmez.
  • Aday dosyaların piksel sayısı genişliğin karesiyle büyür; kaynak boyutu yanlış bildirildiğinde seçilen dosya 2 ile 9 kat arası fazla piksel taşır.
  • picture ögesinde koşulu doğru olan ilk kaynak seçilir; bu, biçem dosyasındaki “sonuncusu kazanır” kuralının tersidir ve kaynaklar geniş koşuldan dar koşula sıralanır.
  • Kırpımların en boy oranları farklı olduğu için her kaynak kendi width ve height değerlerini bildirir; tek bir oran yazmak kaynak değiştiğinde içerik kayması üretir.
  • object-fit ve object-position, kutunun oranı görselinkinden farklı olduğunda gerdirme yerine kırpma ve kırpma merkezi seçimi sağlar.

Sonraki Adım

Buraya kadar biçem hep ortamın ölçülebilir niteliklerine uyarlandı: genişlik, kabın ölçüsü, piksel oranı. Kullanıcının kendi tercihleri de aynı yolla okunabilir — koyu ya da açık renk şeması isteyip istemediği, kontrastı artırmak isteyip istemediği, hareketin azaltılmasını isteyip istemediği. Sonraki ders bu tercihleri ele alır ve sonuncusu bir soru bırakacaktır: hareket azaltma tercihi tam olarak neyi azaltır?

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat