Ders 17 / 22
Koyu Tema
Basamak aynalamasının hiyerarşi oranını neden bozduğu, koyu temanın kontrast korunumu koşuluyla türetilmesi, doygunluğun serbest açıklığın bağımlı olması ve yükseltinin kontrast payını tüketmesi.
İçindekiler
Önceki ders katalog arayüzünün bütün renk eşleşmelerini denetledi. Denetimin tamamı tek bir varsayım üzerine kuruluydu: yüzeyler açık, metin koyu. Aynı arayüz koyu bir temada çalıştığında bu ilişki tersine döner ve denetimin tamamı yeniden yapılmak zorundadır.
Koyu tema, renk sisteminin ikinci bir listesi değildir; birincisinden türetilen bir dönüşümdür. Bu dersin sorusu dönüşümün kuralıdır: hangi büyüklük korunur, hangisi serbest bırakılır ve dönüşümün doğru yapıldığı nasıl denetlenir?
Aynalama Neyi Korur, Neyi Bozar
En doğrudan dönüşüm, rampanın basamaklarını başa baş takas etmektir: 000 ile 900, 050 ile 800, 100 ile 700 yer değiştirir. Rol adları değişmez, yalnızca hangi basamağa baktıkları değişir. Bu dönüşümün sonucunu ölçmek gerekir.
// aynalama.mjs — basamaklarin basa bas takasiyla kurulan koyu temanin denetimi function hslRgb(h, s, l) { s /= 100; l /= 100; const k = (n) => (n + h / 30) % 12; const a = s * Math.min(l, 1 - l); const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))); return [f(0), f(8), f(4)].map((v) => Math.round(v * 255)); } const onaltilik = (rgb) => "#" + rgb.map((v) => v.toString(16).padStart(2, "0")).join(""); function kanal(v) { const s = v / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); } const parlaklik = ([r, g, b]) => 0.2126 * kanal(r) + 0.7152 * kanal(g) + 0.0722 * kanal(b); function kontrast(a, b) { const [x, y] = [parlaklik(a), parlaklik(b)].sort((p, q) => q - p); return (x + 0.05) / (y + 0.05); } const ACIKLIK = { "000": 100, "050": 97, 100: 92, 200: 84, 300: 74, 400: 62, 500: 50, 600: 40, 700: 31, 800: 22, 900: 14 }; const TON = { notr: [214, 8], ana: [214, 62], hata: [8, 68] }; const renk = (aile, b) => hslRgb(TON[aile][0], TON[aile][1], ACIKLIK[b]); const AYNA = { "000": "900", "050": "800", 100: "700", 200: "600", 300: "500", 400: "400", 500: "300", 600: "200", 700: "100", 800: "050", 900: "000" }; const ACIK_ROL = { "yuzey": ["notr", "000"], "yuzey-ikincil": ["notr", "050"], "kenarlik": ["notr", "500"], "metin-birincil": ["notr", "900"], "metin-ikincil": ["notr", "600"], "eylem-ana": ["ana", "600"], "eylem-ana-uzeri": ["notr", "000"], "secili-zemin": ["ana", "100"], "hata-metni": ["hata", "600"], }; const KOYU_ROL = Object.fromEntries( Object.entries(ACIK_ROL).map(([ad, [aile, b]]) => [ad, [aile, AYNA[b]]]) ); console.log("rol acik tema koyu tema (aynalanmis)"); for (const ad of Object.keys(ACIK_ROL)) { const [aa, ab] = ACIK_ROL[ad]; const [ka, kb] = KOYU_ROL[ad]; console.log( `${ad.padEnd(17)} ${(aa + "-" + ab).padEnd(10)} ${onaltilik(renk(aa, ab))} ${(ka + "-" + kb).padEnd(10)} ${onaltilik(renk(ka, kb))}` ); } const ESLESME = [ ["metin-birincil", "yuzey", 4.5], ["metin-ikincil", "yuzey", 4.5], ["metin-ikincil", "yuzey-ikincil", 4.5], ["kenarlik", "yuzey", 3.0], ["eylem-ana-uzeri", "eylem-ana", 4.5], ["eylem-ana", "yuzey", 3.0], ["hata-metni", "yuzey", 4.5], ["metin-ikincil", "secili-zemin", 4.5], ]; console.log("\non plan / zemin esik acik tema koyu tema koyu sonuc"); for (const [on, zemin, esik] of ESLESME) { const ka = kontrast(renk(...ACIK_ROL[on]), renk(...ACIK_ROL[zemin])); const kk = kontrast(renk(...KOYU_ROL[on]), renk(...KOYU_ROL[zemin])); console.log( `${(on + " / " + zemin).padEnd(32)} ${esik.toFixed(1).padStart(4)} ${ka.toFixed(2).padStart(10)} ${kk.toFixed(2).padStart(10)} ${kk >= esik ? "gecti" : "KALDI"}` ); }
rol acik tema koyu tema (aynalanmis) yuzey notr-000 #ffffff notr-900 #212327 yuzey-ikincil notr-050 #f7f7f8 notr-800 #34383d kenarlik notr-500 #757e8a notr-300 #b7bcc2 metin-birincil notr-900 #212327 notr-000 #ffffff metin-ikincil notr-600 #5e656e notr-200 #d3d6d9 eylem-ana ana-600 #275ea5 ana-200 #bdd3ef eylem-ana-uzeri notr-000 #ffffff notr-900 #212327 secili-zemin ana-100 #dee9f7 ana-700 #1e4980 hata-metni hata-600 #ab3321 hata-200 #f2c2ba on plan / zemin esik acik tema koyu tema koyu sonuc metin-birincil / yuzey 4.5 15.74 15.74 gecti metin-ikincil / yuzey 4.5 5.89 10.78 gecti metin-ikincil / yuzey-ikincil 4.5 5.51 8.09 gecti kenarlik / yuzey 3.0 4.11 8.23 gecti eylem-ana-uzeri / eylem-ana 4.5 6.50 10.29 gecti eylem-ana / yuzey 3.0 6.50 10.29 gecti hata-metni / yuzey 4.5 6.51 9.89 gecti metin-ikincil / secili-zemin 4.5 4.80 6.20 gecti
Sekiz eşleşmenin hepsi eşiği geçiyor. Aynalama bir uygunluk sorunu üretmedi; üstelik çoğu eşleşmede kontrast oranını yükseltti. Buradan aynalamanın doğru dönüşüm olduğu sonucu çıkarılamaz, çünkü denetim yanlış soruyu soruyor.
Eşik denetimi her eşleşmeye tek tek bakar ve alt sınırı sorar. Hiyerarşi ise eşleşmeler arasındaki ilişkiye bağlıdır. Açık temada birincil metin 15.74, ikincil metin 5.89 kontrast taşıyor; aralarındaki oran 2.67. Aynalanmış koyu temada birincil 15.74, ikincil 10.78; oran 1.46. İkinci düzey birinciye çok daha yakın duruyor.
Bu, Görsel Hiyerarşi dersinde ölçülen kontrast kanalının koyu temada zayıfladığı anlamına gelir. Kanal ölçütü geçiyor ama ayrım gücünü kaybediyor. Aynalamanın koruduğu şey eşik uygunluğu, bozduğu şey ise oran yapısıdır.
Nedeni bağıl parlaklık işlevinin doğrusal olmamasıdır. HSL açıklığındaki eşit adımlar parlaklıkta eşit adımlar üretmez; ölçeğin koyu ucu parlaklık ekseninde sıkışıktır, açık ucu ise seyrektir. Basamakları takas etmek, sıkışık ucu seyrek uca taşır ve aradaki bütün mesafeler değişir.
Korunacak Büyüklük Kontrast Oranıdır
Doğru dönüşüm, basamak numaralarını değil kontrast oranlarını taşır. Her rol için açık temada ölçülen oran hedef alınır ve koyu yüzey üzerinde aynı oranı üreten açıklık değeri aranır.
Bu, önce bir karar gerektirir: koyu yüzey ne kadar koyu olacak? Yüzey koyulaştıkça üzerine sığdırılabilecek kontrast payı büyür, ama pay sonsuz değildir.
// korunum.mjs — koyu temanin acik temadaki kontrast oranlarini koruyacak bicimde turetilmesi function hslRgb(h, s, l) { s /= 100; l /= 100; const k = (n) => (n + h / 30) % 12; const a = s * Math.min(l, 1 - l); const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))); return [f(0), f(8), f(4)].map((v) => Math.round(v * 255)); } const onaltilik = (rgb) => "#" + rgb.map((v) => v.toString(16).padStart(2, "0")).join(""); function kanal(v) { const s = v / 255; return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); } const parlaklik = ([r, g, b]) => 0.2126 * kanal(r) + 0.7152 * kanal(g) + 0.0722 * kanal(b); function kontrast(a, b) { const [x, y] = [parlaklik(a), parlaklik(b)].sort((p, q) => q - p); return (x + 0.05) / (y + 0.05); } const ACIKLIK = { "000": 100, "050": 97, 100: 92, 200: 84, 300: 74, 400: 62, 500: 50, 600: 40, 700: 31, 800: 22, 900: 14 }; const TON = { notr: [214, 8], ana: [214, 62], hata: [8, 68] }; const renk = (aile, b) => hslRgb(TON[aile][0], TON[aile][1], ACIKLIK[b]); // 1) Koyu yuzey secimi: ne kadar koyu yuzey, ustunde ne kadar kontrast araligi birakir? console.log("koyu yuzey adayi onaltilik bagil parlaklik ustundeki en yuksek kontrast"); for (const [ad, rgb] of [ ["saf siyah", [0, 0, 0]], ["notr-900", renk("notr", "900")], ["notr-800", renk("notr", "800")], ["notr-700", renk("notr", "700")], ]) { console.log( `${ad.padEnd(17)} ${onaltilik(rgb)} ${parlaklik(rgb).toFixed(4).padStart(15)} ${(1.05 / (parlaklik(rgb) + 0.05)).toFixed(2).padStart(27)}:1` ); } // 2) Korunum: her rol icin acik temadaki kontrast oranini koyu yuzeyde yeniden uret. const ACIK_YUZEY = renk("notr", "000"); const KOYU_YUZEY = renk("notr", "900"); const ROLLER = [ { ad: "metin-birincil", aile: "notr", basamak: "900", doygunluk: 8 }, { ad: "metin-ikincil", aile: "notr", basamak: "600", doygunluk: 8 }, { ad: "kenarlik", aile: "notr", basamak: "500", doygunluk: 8 }, { ad: "eylem-ana", aile: "ana", basamak: "600", doygunluk: 45 }, { ad: "hata-metni", aile: "hata", basamak: "600", doygunluk: 55 }, ]; function aciklikAra(ton, doygunluk, zemin, hedef) { let alt = 0, ust = 100; for (let i = 0; i < 50; i++) { const orta = (alt + ust) / 2; if (kontrast(hslRgb(ton, doygunluk, orta), zemin) < hedef) alt = orta; else ust = orta; } return (alt + ust) / 2; } console.log("\nrol acik hedef koyu aciklik doygunluk koyu renk olculen"); const koyuPalet = {}; for (const r of ROLLER) { const hedef = kontrast(renk(r.aile, r.basamak), ACIK_YUZEY); const ton = TON[r.aile][0]; const l = aciklikAra(ton, r.doygunluk, KOYU_YUZEY, hedef); const rgb = hslRgb(ton, r.doygunluk, l); koyuPalet[r.ad] = rgb; console.log( `${r.ad.padEnd(16)} ${hedef.toFixed(2).padStart(8)}:1 ${l.toFixed(2).padStart(12)}% ${String(r.doygunluk).padStart(9)}% ${onaltilik(rgb)} ${kontrast(rgb, KOYU_YUZEY).toFixed(2).padStart(6)}:1` ); } // 3) Hiyerarsi orani korundu mu? Birincil ve ikincil metin arasindaki fark. const acikB = kontrast(renk("notr", "900"), ACIK_YUZEY); const acikI = kontrast(renk("notr", "600"), ACIK_YUZEY); const koyuB = kontrast(koyuPalet["metin-birincil"], KOYU_YUZEY); const koyuI = kontrast(koyuPalet["metin-ikincil"], KOYU_YUZEY); console.log(`\nacik tema: birincil ${acikB.toFixed(2)}:1, ikincil ${acikI.toFixed(2)}:1, oran ${(acikB / acikI).toFixed(3)}`); console.log(`koyu tema: birincil ${koyuB.toFixed(2)}:1, ikincil ${koyuI.toFixed(2)}:1, oran ${(koyuB / koyuI).toFixed(3)}`); // 4) Aynalanmis surumde ayni oran ne oluyordu? const aynaB = kontrast(renk("notr", "000"), KOYU_YUZEY); const aynaI = kontrast(renk("notr", "200"), KOYU_YUZEY); console.log(`aynalama : birincil ${aynaB.toFixed(2)}:1, ikincil ${aynaI.toFixed(2)}:1, oran ${(aynaB / aynaI).toFixed(3)}`); // 5) Doygunluk serbest, aciklik bagimli: ayni hedefi farkli doygunluklarda tutturmak console.log("\neylem-ana icin hedef 6.50:1 -> doygunluk secimi ve cozulen aciklik"); for (const s of [62, 45, 30, 15]) { const l = aciklikAra(TON.ana[0], s, KOYU_YUZEY, 6.5); const rgb = hslRgb(TON.ana[0], s, l); console.log( `doygunluk ${String(s).padStart(2)}% -> aciklik ${l.toFixed(2).padStart(6)}% ${onaltilik(rgb)} ${kontrast(rgb, KOYU_YUZEY).toFixed(2)}:1` ); } // 6) Koyu temada yukselti: ustteki yuzey daha acik olur. console.log("\nyukselti aciklik onaltilik yuzeye karsi ustundeki en yuksek kontrast"); for (const [ad, hedefAyrim] of [["taban", 1.0], ["kart", 1.3], ["acilir katman", 1.7]]) { const l = hedefAyrim === 1.0 ? ACIKLIK["900"] : aciklikAra(TON.notr[0], TON.notr[1], KOYU_YUZEY, hedefAyrim); const rgb = hslRgb(TON.notr[0], TON.notr[1], l); console.log( `${ad.padEnd(14)} ${l.toFixed(2).padStart(7)}% ${onaltilik(rgb)} ${kontrast(rgb, KOYU_YUZEY).toFixed(3).padStart(12)} ${(1.05 / (parlaklik(rgb) + 0.05)).toFixed(2).padStart(27)}:1` ); }
koyu yuzey adayi onaltilik bagil parlaklik ustundeki en yuksek kontrast saf siyah #000000 0.0000 21.00:1 notr-900 #212327 0.0167 15.74:1 notr-800 #34383d 0.0390 11.80:1 notr-700 #494e55 0.0752 8.39:1 rol acik hedef koyu aciklik doygunluk koyu renk olculen metin-birincil 15.74:1 99.82% 8% #ffffff 15.74:1 metin-ikincil 5.89:1 62.78% 8% #989fa8 5.89:1 kenarlik 4.11:1 52.08% 8% #7b848f 4.15:1 eylem-ana 6.50:1 68.22% 45% #89a9d2 6.50:1 hata-metni 6.51:1 70.24% 55% #dd9589 6.55:1 acik tema: birincil 15.74:1, ikincil 5.89:1, oran 2.670 koyu tema: birincil 15.74:1, ikincil 5.89:1, oran 2.672 aynalama : birincil 15.74:1, ikincil 10.78:1, oran 1.459 eylem-ana icin hedef 6.50:1 -> doygunluk secimi ve cozulen aciklik doygunluk 62% -> aciklik 69.03% #7fa9e1 6.49:1 doygunluk 45% -> aciklik 68.22% #89a9d2 6.50:1 doygunluk 30% -> aciklik 67.38% #93a9c5 6.54:1 doygunluk 15% -> aciklik 66.36% #9ca7b6 6.46:1 yukselti aciklik onaltilik yuzeye karsi ustundeki en yuksek kontrast taban 14.00% #212327 1.000 15.74:1 kart 21.60% #33363b 1.298 12.13:1 acilir katman 28.34% #42484e 1.700 9.26:1
Yüzey Seçimi Kontrast Payını Belirler
Birinci tablo, koyu temanın en temel kararını sayıya bağlıyor. Saf siyah yüzey üzerinde en yüksek kontrast 21:1’e kadar çıkabilir; nötr 900 basamağında 15.74, nötr 700’de yalnızca 8.39.
Saf siyahın verdiği pay en geniş olanıdır, ama bedeli vardır. Saf siyah üzerine beyaz metin 21:1 üretir; bu, açık temadaki 15.74 değerinin belirgin biçimde üzerindedir ve dönüşümün korunum koşulunu ihlal eder. Korunum isteniyorsa yüzey öyle seçilmelidir ki açık temanın en yüksek oranı koyu temada da üretilebilsin, ama zorunlu olarak aşılmasın. Nötr 900 tam bu noktadadır: üzerindeki en yüksek kontrast 15.74, açık temanın birincil metin oranıyla birebir aynı.
İkinci tablo bunu doğruluyor. Birincil metin rolü için çözülen açıklık yüzde 99.82, yani pratikte saf beyaz. Bu bir tesadüf değil, yüzey seçiminin doğrudan sonucudur: nötr 900 yüzeyde 15.74 oranını yalnızca beyaz üretebilir.
Üçüncü blok korunumun sağlandığını gösteriyor. Türetilmiş koyu temada birincil ve ikincil metin arasındaki oran 2.672; açık temadaki 2.670 değeriyle üç ondalık basamağa kadar aynı. Aynalanmış sürümde bu oran 1.459 idi. Hiyerarşi, dönüşümün ölçütü kontrast oranı olduğunda korunuyor, basamak numarası olduğunda korunmuyor.
Doygunluk Serbest, Açıklık Bağımlıdır
Dördüncü tablo dönüşümün ikinci parametresini ele alıyor. Birincil eylem rengi için hedef 6.50:1; bu hedef dört farklı doygunlukta da tutturulabiliyor, yalnızca gereken açıklık değişiyor: yüzde 66.36 ile yüzde 69.03 arasında.
Bunun anlamı, doygunluğun bir serbest parametre olmasıdır. Kontrast korunumu doygunluğu kısıtlamaz; hangi doygunluk seçilirse seçilsin, açıklık ona göre çözülür ve hedef oran yakalanır. Doygunluk kararı bu yüzden ayrı bir gerekçe ister.
Katalog arayüzünde koyu tema için doygunluk açık temadakinden düşük seçildi: ana ailede 62 yerine 45, hata ailesinde 68 yerine 55. Gerekçe, koyu zeminde yüksek doygunluklu ve yüksek açıklıklı bir rengin çevresindeki koyu alanla kurduğu ilişkidir; aynı rengin açık zeminde kapladığı algısal yer ile koyu zeminde kapladığı yer aynı değildir. Bu bir tercih kararıdır ve dönüşümün geri kalanını etkilemez — tablo bunu gösteriyor: doygunluk düşürülse de oran korunuyor.
Burada dikkat edilecek nokta, doygunluğun rol tablosunda değil rampa tanımında
değişmesidir. Koyu tema, rol adlarını hiç değiştirmez; yalnızca rollerin baktığı basamak
değerlerini değiştirir. Bileşenler --eylem-ana yazmaya devam eder ve tema geçişinden
haberdar olmaz. Dördüncü derste rol adlandırmasının bedeli burada geri ödenir.
Yükselti Kontrast Payını Tüketir
Beşinci tablo koyu temaya özgü bir kısıtı ölçüyor. Açık temada bir kartın taban yüzeyden ayrılması gölgeyle yapılır. Koyu zeminde gölge görünmez, çünkü gölge zaten koyudur; ayrım, üstteki yüzeyi açmakla kurulur.
Bunun bedeli hesaplanabilir. Taban yüzey nötr 900’de duruyor ve üzerinde 15.74 kontrast payı var. Tabandan 1.3 oranıyla ayrılan bir kart yüzeyi yüzde 21.60 açıklığa çıkıyor ve üzerindeki pay 12.13’e düşüyor. 1.7 oranıyla ayrılan bir açılır katmanda pay 9.26’ya iniyor.
Yani koyu temada her yükselti katmanı, üzerine yerleşecek metnin kullanabileceği kontrast aralığından pay alır. Üç katmanlı bir arayüzde en üst katmandaki metin, taban katmandaki metinle aynı oranı tutturamaz. Bu, açık temada karşılığı olmayan bir kısıttır: açık temada yükselti beyaza doğru gider ve beyaz zaten üst sınırdır, dolayısıyla pay değişmez.
Pratik sonuç, koyu temada yükselti katmanı sayısının açık temadakinden az tutulmasıdır. Katalog arayüzünde iki katman yeter: taban yüzey ve kayıt ayrıntısının açıldığı katman. Üçüncü bir katman eklenirse, o katmandaki ikincil metin 5.89 oranını tutturamaz ve hiyerarşi orada bozulur.
Tema Geçişinin Yazılı Biçimi
Dönüşüm, rol adlarını sabit tutup basamak atamalarını değiştirerek yazılır:
:root { --yuzey: #ffffff; --metin-birincil: #212327; --metin-ikincil: #5e656e; --kenarlik: #757e8a; --eylem-ana: #275ea5; } @media (prefers-color-scheme: dark) { :root { --yuzey: #212327; --metin-birincil: #ffffff; --metin-ikincil: #989fa8; --kenarlik: #7b848f; --eylem-ana: #89a9d2; } }
Değerler hesapla üretildiği için bu blok elle yazılmış bir liste değil, bir çıktıdır. Sisteme yeni bir rol eklendiğinde koyu karşılığı tahmin edilmez; aynı korunum hesabı yeniden çalıştırılır.
Tema seçimi kullanıcının işletim sistemi tercihinden okunur. Arayüzün ayrıca bir tema düğmesi sunması gerekiyorsa, o düğmenin seçimi sistem tercihini geçersiz kılar ve seçim saklanır; bu, tasarım kararı değil durum yönetimi kararıdır.
Özet
- Koyu tema ikinci bir renk listesi değil, açık temadan türetilen bir dönüşümdür.
- Basamakları başa baş takas etmek eşik uygunluğunu korur ama oran yapısını bozar; birincil ve ikincil metin arasındaki ayrım 2.67 katından 1.46 katına düşer.
- Doğru dönüşümün korunan büyüklüğü kontrast oranıdır; her rol için hedef oranı üreten açıklık değeri aranarak koyu palet türetilir.
- Koyu yüzeyin ne kadar koyu olacağı, üzerine sığdırılabilecek en yüksek kontrastı belirler; yüzey, açık temanın en yüksek oranını tam olarak karşılayacak biçimde seçilir.
- Doygunluk serbest parametredir; hangi doygunluk seçilirse seçilsin açıklık ona göre çözülür ve hedef oran yakalanır.
- Koyu temada yükselti, üstteki yüzeyi açmakla kurulur ve her katman kontrast payından pay alır; bu yüzden katman sayısı açık temadakinden az tutulur.
Sonraki Adım
Tipografi ve renk kararları bittiğinde katalog arayüzünün durağan görüntüsü tamamlanmış olur: hangi metin hangi boyutta, hangi renk hangi rolde, iki temada da denetlenmiş. Ancak arayüz durağan değildir. “Ödünç al” düğmesi üzerine gelindiğinde, klavyeyle odaklanıldığında, basılı tutulduğunda ve kullanılamaz olduğunda farklı görünür. Bu görünümlerin her biri ayrı bir tasarım kararıdır ve bugüne kadar hiçbiri tanımlanmadı. Sonraki konu bileşenlerin durumlarını ele alır; ilk ders beş etkileşim durumunu tanımlar, her birinin hangi kanalda ayrıldığını belirler ve odak göstergesinin neden kaldırılamaz olduğunu ölçütle gösterir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.