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:
sizesbildiriminden çö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
widthveheightdeğerlerini bildirir; tek bir oran yazmak kaynak değiştiğinde içerik kayması üretir. object-fitveobject-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.