iPhone, masanın üstünde
Three.js ile çizilmiş bir masada duran, ekranında 16 uygulamanın çalıştığı bir iPhone 15 Pro Max.
Tarayıcıda çalışan bir iPhone. Three.js ile çizilmiş bir masanın üstünde bir iPhone 15 Pro Max duruyor. Ekranında React ile yazdığım, iOS 26 Liquid Glass görünümlü bir arayüz ve 16 çalışan uygulama var.

- Durum
- Tamamlandı
- Kategori
- Tarayıcıda OS
- Güncelleme
- 5 Ekim 2026
- Yığın
- React 19Three.jsReact Three FiberZustandMotion
- Bağlantılar
- Kaynak kod
Three.js ile çizilmiş bir çalışma masasının üstünde bir iPhone 15 Pro Max duruyor. Ekranında, React ile yazdığım ve iOS 26'nın "Liquid Glass" görünümünü izleyen bir arayüz ve gerçekten çalışan 16 uygulama var. Sürükleyerek kamerayı masanın etrafında döndürebilir, çift tıklayıp ekrana yaklaşabilirsin. 3D modeldeki güç, ses, Action ve Camera Control tuşlarına tıklamak gerçek tuşlara basmak gibi çalışıyor; klavye kısayolları da aynı donanım nesnesini çağırıyor. 3D sahneyi istemezsen F tuşuyla düz moda geçebilirsin. Dokunmatik cihazlarda zaten düz mod açılıyor.
Masada bir lamba, buharı tüten bir kupa, peri ışıkları ve bir MacBook da var; MacBook'un editöründe açık olan da kabaca bu sahnenin kendi kaynak kodu. Telefonun içinde kilit ekranı, ana ekran ve widget'lar, Kontrol Merkezi, Bildirim Merkezi, Spotlight ve uygulama değiştirici çalışıyor. Dynamic Island da Face ID, bildirim, arama, zamanlayıcı ve müzik için canlı etkinlik görünümlerine dönüşüyor, genişlettiğinde kontrolleri açılıyor.
Hareketler iPhone'dakiyle aynı mantıkta: alttan hızlıca yukarı kaydırırsan ana ekrana, yavaşça kaydırırsan uygulama değiştiriciye gidiyorsun; alt kenarda yana kaydırınca önceki uygulamaya geçiyorsun. Her dokunuşun hafif bir titreşim deseni var, daha sert haptikler 3D telefonu da hafifçe sarsıyor. Sahneyi React Three Fiber ve drei ile, arayüzü React 19, TypeScript, zustand, Motion ve @use-gesture ile kurdum; sunucu ve paketleme tarafında Bun var.
Ekranda canlı bir DOM
Telefonun arayüzü sahneden bağımsız, ayrı bir React uygulaması. 3D modda onu bir dokuya çevirmiyorum; drei'nin <Html transform occlude="blending"> bileşeniyle modelin ekran boşluğuna canlı DOM olarak yerleştiriyorum. Üstteki cam katmanı yalnızca yansımayı çiziyor, lambanın ve odanın parlamalarını canlı ekranın üzerine ekliyor. Düz modda aynı arayüz sahne olmadan çiziliyor. Sahneyi lazy() ile yüklediğim için düz modun varsayılan olduğu telefonlarda three.js hiç indirilmiyor. Sahne frameloop="demand" ile çalışıyor, yani kare yalnızca bir şey değiştiğinde çiziliyor.
Bu yaklaşımın bir bedeli vardı. Kamera hareket ederken Chrome, CSS3D ekranı her yeni ölçekte yeniden rasterleştiriyor; harfler her karede farklı bir piksel ızgarasına oturuyor ve yazılar titriyor. Çözüm olarak hareket boyunca will-change: transform ekliyorum, kamera durduktan 180 ms sonra da kaldırıyorum. Böylece görüntü hareket sırasında akıcı kalıyor, durunca da son ölçekte keskinleşiyor:
function usePinnedRasterWhileMoving(layer: RefObject<HTMLDivElement | null>) {
const last = useRef(new Matrix4());
const idle = useRef<ReturnType<typeof setTimeout>>(undefined);
useEffect(() => () => clearTimeout(idle.current), []);
useFrame(({ camera }) => {
const el = layer.current;
if (!el || last.current.equals(camera.matrixWorld)) return;
last.current.copy(camera.matrixWorld);
el.style.willChange = "transform";
clearTimeout(idle.current);
idle.current = setTimeout(() => (el.style.willChange = ""), SETTLE_MS);
});
}Tek store, üç ömür
İşletim sisteminin bütün durumu tek bir zustand store'unda duruyor ve üç bölüme ayrılıyor. Ayarlar (duvar kâğıdı, koyu mod, haptik, ses, yazı ölçeği, parlaklık, Wi-Fi, Odak, Action tuşunun görevi gibi) ve uygulama verisi (notlar, mesajlar, alarmlar, kişiler, takvim, hatırlatıcılar, App Store'dan kurulan projeler) localStorage'a yazılıyor. Üçüncü bölüm sistem durumu: ekranın ve kilidin durumu, açık uygulama, bildirimler, zamanlayıcı ve çalan müzik gibi. Bu kısım yalnızca uygulama çalışırken yaşıyor. Kalıcı kısım bir sürüm numarası ve partialize ile yazılıyor; persist'in sığ birleştirmesi sayesinde eski kayıtlarda olmayan yeni alanlar da başlangıç değerini alıyor. Fotoğraflar localStorage'a sığmayacak kadar büyük, o yüzden idb-keyval ile IndexedDB'de tutuluyor.
Yeni uygulama eklemeyi basit tuttum: bir klasör, bir de kayıt dosyasında bir satır yetiyor. Her kayıtta kimlik, ad, ikon, ikon rengi, koyu arayüz bayrağı ve lazy() ile yüklenen bileşen var. İsteğe bağlı alanlardan biri ikonu canlı içerikle değiştiriyor (Takvim ikonu bugünün tarihini gösteriyor), bir başkası da ana ekrandaki kırmızı sayacı hesaplıyor (Hatırlatıcılar'da bugün ve öncesine ait açık hatırlatıcılar).
Arayüze bağlı olması gerekmeyen mantığı ayrı dosyalara çıkardım: hareket kararları, kilit ekranındaki "sıradaki" etkinlik ya da alarm, hesap makinesi, takvim tarih hesapları ve haptik desenleri. Örneğin hareket modülü, alttan kaydırmanın ana ekrana bir "fırlatma" mı yoksa uygulama değiştirici için bir "duraklama" mı olduğuna mesafe ve hız eşikleriyle karar veriyor.
Uygulamalar ve ağa çıkan parçalar
Telefonda şu uygulamalar çalışıyor: Telefon, Mesajlar, Kamera, Fotoğraflar, Hesap Makinesi, Takvim, Hatırlatıcılar, Kişiler, Saat, Hava Durumu, Notlar, Müzik, Ayarlar, Safari, Projelerim ve App Store. Kamera bilgisayarının kamerasını kullanıyor, çektiğin fotoğraflar da Fotoğraflar'da görünüyor. Saat'te dünya saati, alarmlar, kronometre ve zamanlayıcı var; çalışan bir zamanlayıcı Dynamic Island'a da taşınıyor. Projelerim ve App Store'da kendi projelerim duruyor; App Store'dan "kurduğun" her proje ana ekrana bir ikon olarak ekleniyor.
Dışarıyla konuşan iki uygulama var. Birincisi Safari: bir sayfanın iframe'de açılıp açılamayacağını tarayıcı kendisi bilemiyor, bu yüzden Bun sunucusundaki /api/frame-check ucu sayfanın X-Frame-Options ve CSP frame-ancestors başlıklarına bakıyor. İkincisi Hava Durumu; verisini Open-Meteo'dan alıyor, konum izni verirsen bulunduğun yerin, vermezsen İstanbul'un hava durumunu gösteriyor. Müzik ise hiçbir şey indirmiyor: parçalar Web Audio üzerinde bir adım sıralayıcıyla (step sequencer) sentezleniyor.
Bazı yerler şimdilik yüzeyde kalıyor. Telefon uygulamasındaki aramalar yalnızca arayüzden ibaret: arama ekranı ve süre sayacı açılıyor, kapatınca arama geçmişe kaydediliyor ama gerçek bir bağlantı kurulmuyor. Mesajlar'ın otomatik yanıt kuralları da henüz boş, yani gönderdiğin mesajlara yanıt gelmiyor. Liquid Glass'taki kırılma efekti backdrop-filter: url(#...) gerektirdiği için yalnızca Chromium'da çiziliyor.
iPhone modeli polyman, MacBook modeli jackbaeten imzalı Sketchfab modellerinin (CC BY 4.0) değiştirilmiş halleri; diğer modeller ve dokular Poly Haven'dan.