1 puan yazan GN⁺ 2024-03-09 | 1 yorum | WhatsApp'ta paylaş
  • Üstel yumuşatma, aç/kapa düğmeleri, kamera, UI öğeleri ve ses seviyesi gibi hedef değeri yumuşak biçimde takip etmesi gereken animasyonlarda geniş biçimde kullanılabilen basit bir tekniktir
  • Temel denklem position += (target - position) * (1 - exp(- speed * dt)) olup yalnızca mevcut konum ve hedef konumla hedef değişikliklerine doğal biçimde tepki verir
  • Doğrusal hareket ve yaygın easing yöntemleri, araya giren tıklamalar, hızlı kamera girdileri ve büyük dt değerlerinde sıçrama, titreme veya girdi kuyruğu gibi ek işlemler gerektirirken, üstel yumuşatma aynı yapıyla karmaşıklığı azaltır
  • 1 - exp(- speed * dt), orantılı güncelleme formülünün çözdüğü diferansiyel denklemin çözümünden gelir; küçük dt değerlerinde eski formülle neredeyse aynıdır ve büyük dt değerlerinde de overshoot'u önler
  • Hedef değere matematiksel olarak tam isabetle ulaşmasa da, kayan nokta hassasiyeti ve kullanıcının algılayabildiği değişim miktarı sınırı nedeniyle pratik animasyonlarda yeterince tamamlanmış gibi davranır

Aç/kapa düğmesinde görülen sorun

  • Aç/kapa düğmesinin anahtar konumu turned_on ? max_x : min_x gibi basitçe hesaplanabilir, ancak durum değiştiğinde konum anında yer değiştirir ve canlılık hissi eksik kalır
  • Doğrusal animasyon, mevcut konumu sabit hızla güncelleyip aralığı sınırlandırma yöntemiyle uygulanır
position.x += (turned_on ? 1 : -1) * speed * dt;
position.x = clamp(position.x, min_x, max_x);
  • Sabit hızlı hareket, konumun zamana göre doğrusal bir fonksiyon olmasına yol açar ve hareket donuk görünebilir
  • Üzerine easing fonksiyonları eklenirse hareket daha yumuşak hale getirilebilir
    • Klasik cubic smoothstep: 3t² - 2t³
    • Karekök easing: sqrt(t)
  • smoothstep, 1 - f(t) = f(1 - t) simetrisine sahip olduğu için ileri ve geri animasyonda aynı kod kullanılabilir
  • sqrt için ise yöne göre farklı formüller gerekir
    • Açılırken: sqrt(t)
    • Kapanırken: 1 - sqrt(1 - t)
  • sqrt, başlangıçta hızlı hareket edip hedefe yaklaştıkça yumuşakça yavaşlar, ancak basit bir 2 durumlu aç/kapa için bile t, yön ve easing hesabı gibi durum yönetimi gerektirir
  • Kullanıcı animasyonun ortasında yeniden tıklarsa, mevcut easing yaklaşımı konumda ani sıçramaya yol açan bir süreksizlik oluşturabilir

Üstel yumuşatma formülü

  • Üstel yumuşatma, hedef konumu belirledikten sonra mevcut konumu hedefe doğru azar azar çeken bir yöntemdir
target = (state.value ? max_x : min_x);
position.x += (target - position.x) * (1 - exp(- dt * speed));
  • Güncelleme miktarı, mevcut konum ile hedef konum arasındaki fark olan target - position.x temel alınarak belirlenir
  • 1 - exp(- dt * speed), her karede hedefe doğru ne kadar ilerleme yapılacağını belirleyen interpolasyon katsayısıdır
  • Hedef arada değişse bile ek bir ilerleme süresi veya animasyon yönü durumu olmadan, hareket yeni hedefe doğru anında devam eder
  • Aç/kapa düğmesi örneğinde, sqrt easing gibi hızlı başlayıp hedefe yaklaşınca yavaşlarken, aradaki tıklamalarda sıçrama sorunu da azalır

Kamera hareketinde daha da büyüyen avantaj

  • Harita üzerinde gezen kamerada da aynı sorun ortaya çıkar
  • Sabit hızlı interpolasyonu basitçe uygulamak için eksen bazında hedef yönünün işareti kullanılır
position.x += sign(target.x - position.x) * speed * dt;
position.y += sign(target.y - position.y) * speed * dt;
  • Animasyon tamamlanma noktasında target - position işareti artı ile eksi arasında gidip gelerek titreme oluşturabilir
  • Bunu önlemek için delta değerini max_delta aralığıyla sınırlayan ayrı bir güncelleme fonksiyonu gerekir
float update(float & value, float target, float max_delta)
{
    float delta = target - value;
    delta = min(delta,  max_delta);
    delta = max(delta, -max_delta);
    value += delta;
}
  • Cubic easing'i kamera hareketine uygulamak için istenen hareket olaylarını kuyruğa alıp tek tek işlemek gerekir; bu da yapıyı karmaşıklaştırır
  • Animasyon sırasında kullanıcı girdisini yok saymak, kullanıcı açısından oldukça sinir bozucu hissettirir
  • Üstel yumuşatma kullanıldığında kamera hareketi, aç/kapa düğmesiyle neredeyse aynı kodla işlenebilir
position.x += (target.x - position.x) * (1.0 - exp(- speed * dt));
position.y += (target.y - position.y) * (1.0 - exp(- speed * dt));
  • Kullanıcı hızlı hızlı tıklarsa, hedef ile mevcut konum arasındaki fark büyüdüğü için kamera doğal biçimde daha da hızlanır

Neden 1 - exp(- speed * dt)?

  • Basit orantılı güncelleme şu denklemle yazılabilir
position += (target - position) * speed * dt;
  • Bu denklem, hedef ile mevcut konum arasındaki fark büyüdükçe daha hızlı hareket eder ve mevcut konum ile hedef konum dışında ek durum gerektirmez
  • lerp olarak yazıldığında da aynı denkleme karşılık gelir
position = lerp(position, target, speed * dt);
  • speed * dt 0'a yakınsa yavaş hareket edilir, 1'e yakınsa hedefe hızlı yaklaşılır
  • speed * dt 1'den büyükse interpolasyon hedefi aşan bir overshoot üretir
    • Örnekte speed = 220, dt = 1 / 125 olduğunda speed * dt 1'den büyük olur ve titreme görülür
    • speed * dt < 2 ise mutlak fark azalabilir, ancak speed * dt > 2 olduğunda artık kullanışlı bir davranış vermez
  • İnterpolasyon katsayısı min(1, speed * dt) ile sınırlandırılabilir, ancak bu da dt büyüdüğünde sorunu pürüzsüzce çözen bir yöntem değildir
    • Kod yavaş çalıştığı için kare hızı düşebilir
    • Kullanıcı başka bir sekmeye veya pencereye geçip kod durduktan sonra, birkaç saniyelik dt ile geri dönülebilir
  • Fizik simülasyonlarında büyük dt sınırlandırılabilir ya da birkaç güncellemeye bölünebilir; ancak animasyonda kamera ve düğmelerin büyük dt değerlerinde bile doğal davranması kullanıcı deneyimi açısından daha iyidir

Diferansiyel denklem açısından çözüm

  • A += B * dt biçimi genellikle dA/dt = B şeklindeki bir diferansiyel denklemin sayısal çözümüne karşılık gelir
  • Basit güncelleme denklemi şu denklemi çözer
d(position) / dt = (target - position) * speed
  • Değişkenleri x = position, a = target, c = speed alırsak denklem şu hale gelir
dx / dt = (a - x) * c
  • Bu denklem doğrudan çözülürse şu biçim elde edilir
x = x0 + (a - x0) * (1 - exp(-c * t))
  • Dolayısıyla küçük dt için doğru olan formül position += (target - position) * speed * dt ise, rastgele herhangi bir dt için kullanılabilecek formül şudur
position += (target - position) * (1 - exp(- speed * dt));
  • Taylor açılımında exp(x) ≈ 1 + x olduğundan, küçük dt için 1 - exp(-speed * dt) ≈ speed * dt olur ve mevcut basit formülle aynı sonuca varır
  • speed * dt çok büyük olsa bile exp(-speed * dt) 0'a yaklaşır ve 1 - exp(...) 1'e yaklaşarak hedefe yakın bir değere kararlı biçimde ilerler
  • Aynı denklem lerp ile de yazılabilir
position = lerp(position, target, 1 - exp(- speed * dt));
position = lerp(target, position, exp(- speed * dt));

Hız değerini seçmek

  • Tipik animasyonlar genelde “0,125 saniyede gitsin” gibi bir süre ile düşünülür; ancak teknik olarak üstel yumuşatmanın hedefe tam olarak ulaşması için sonsuz zaman gerekir
  • exp(- speed * time) zaman ilerledikçe küçülür ama 0 olmaz; bu yüzden başlangıç değeri ile hedef farklıysa position da matematiksel olarak hiçbir zaman target ile tamamen aynı olmaz
  • Pratikte ise ya kayan nokta hassasiyeti sınırına ulaşılır ya da kamera konumu farkı kullanıcının fark edemeyeceği kadar küçülür ve animasyon bitmiş gibi görünür
  • speed değerinin anlamı, 1 / speed süresi içinde position değerinin targeta tam olarak e = 2.71828... kat daha yakın hale gelmesidir
  • Pratik kullanımda speed genelde 5..50 aralığında seçilir; benzer his veren doğrusal veya cubic animasyon hızlarına kıyasla üstel yumuşatma için yaklaşık 2 * speed uygun gelebilir

Sinyal işleme ile bağlantı

  • “exponential smoothing” veya “exponential moving average” diye aratırsanız Wikipedia’daki Exponential smoothing maddesini bulabilirsiniz
  • dt sabitken ve target her yinelemede değişirken, değer yineleme indisine göre aşağı yukarı şöyle güncellenir
    • factor = 1 - exp(- speed * dt)
    • Genelde factor doğrudan 0 ile 1 arasında bir değer olarak seçilir
  • Ayrık üstel yumuşatma, animasyonda kullanılan yaklaşımın ayrık analoğudur
  • Sinyal işlemede de önceki değerlerin listesini veya karmaşık bir durumu tutmadan, yalnızca mevcut ortalama değerle çalışabildiği için kullanılır
  • Dijital seste dt genelde örnekleme frekansının tersi olan 1 / freq olarak sabittir
    • Örneğin: 1/44100, 1/48000

1 yorum

 
GN⁺ 2024-03-09
Hacker News görüşleri
  • Burada asıl noktanın yeterince ele alınmadığını düşünüyorum. Bu, 0 ile 1 arasında başka bir easing eğrisi ya da smoothstep() değil; neredeyse her tür girdiyi düzenli biçimde işleyen durumsuz bir yöntem olduğu için gerçekten kullanışlı.
    CSS geçişlerini kullandıysanız bu sorun tanıdık gelecektir. Süreyi 400 ms olarak belirlediyseniz neden özellikle 400 ms? Gidilmesi gereken mesafeye göre değişmesi gerekmez mi?
    Başkalarının da söylediği gibi üstel yumuşatmada hedefe asimptotik olarak yaklaşma ama asla ulaşmama sorunu var. Adım eşik değerinin altına indiğinde animasyonu durdurmak gibi bariz bir çözüm var, ama zarif değil.
    Ataletli kaydırmada benzer bir yöntem kullanırken, yalancı bir sürtünme terimi eklemek işe yaramıştı. Bu terim üstel terimi dengeliyor ve fiilen minimum hız gibi davranıyor. Desmos örneği burada: https://www.desmos.com/calculator/98ufbuzxhj

    • Üstel yumuşatmayı hedefe olan mesafe D için bir adi diferansiyel denklem olarak yorumlarsak belki çözülebilir. Üstel yumuşatma, dD/dt=-C*D denkleminin Euler güncellemesidir; çözümü D(t)=A*exp(-C*t) olduğundan 0’a asimptotik yaklaşır ama ulaşmaz.
      Bunu sonlu zamanda 0’a giden bir ifadeyle, örneğin dD/dt=-C*sqrt(D) ile değiştirmek yeterli. Çözüm, yarım bir ikinci dereceden fonksiyon gibi hareket eder ve 0’a ulaştığında orada kalır. Bu ifadenin Euler güncellemesi de istendiği gibi durumsuzdur.
    • Adım eşik değerinin altına indiğinde animasyonu durdurmanın neden zarif olmadığını anlamıyorum. Basit, uygulaması ve doğrulaması kolay; istenen görsel etkiyi de sağlıyor, bu yüzden yeterince temiz görünüyor.
    • CSS geçişlerindeki 400 ms sorunu varsa, üstel fonksiyonun hız parametresi de aynı soruna sahip değil mi diye düşünüyorum.
  • Bir oyun geliştiricisi olarak, çoğu UI için önceden belirlenmiş süreye sahip easing tween’lerinin daha iyi olduğunu düşünüyorum. Ancak başlangıç ve bitiş noktası net olmayan, sürekli ve öngörülemez hareketleri yumuşatmak istediğinizde bu diğer animasyon türü çok kullanışlı.
    Örneğin oyuncu fareyle ızgara üzerindeki bir karoyu sürüklerken ızgaraya snap etmesini sağlamak ya da yazıdaki örnekte olduğu gibi kamerayı hareket ettirmek buna girer.
    Bu tür durumlarda üstel enterpolasyon püf noktası çok kullanışlıdır ama pek yaygın bilinmez. Pek çok oyun daha hatalı doğrusal enterpolasyon kullanır ve 60 fps’nin standart olduğu dönemden farklı olarak biri oyunu 240 Hz monitörde çalıştırınca animasyon hissinin tamamen tuhaflaşması sorununu yaşar.
    Bu yüzden bu yazıyı görmek sevindirici. Bu tür aşırı spesifik bilgiye erişmek genelde zordur; çoğunlukla ekipteki kıdemli birinin juniore usta-çırak usulü sözlü aktarmasıyla yayılır.

  • Yazıyı beğendim, ama toggle switch konusunda yazarın sqrt’nin kübik fonksiyondan daha iyi olduğu yargısının nesnel olarak yanlış olduğunu söylemek isterim. Gerçek toggle switch’lerin genelde çalışma biçimine bakınca bu durum için kübik fonksiyon daha iyi bir seçim.
    Evdeki elektrik şalterlerini ya da analog synth ve ses ekipmanlarında sık görülen anahtarları düşünün. Bunlar belirli bir estetik hedefleyen ekipmanlar; bende bulunan küçük Hughes & Kettner gitar amfisinde bile basması hoş iki anahtar var.
    Bu tür anahtarlarda başta biraz direnç olur, sonra yay mekanizması nedeniyle aniden yeni konuma tak diye geçer. Bu hareketi sqrt ya da üstel yumuşatmadan çok kübik fonksiyon daha iyi modeller.
    Bu küçük itiraz dışında yazı çok iyiydi. Uygun bir easing fonksiyonu gibi animasyonun iyi kullanıldığında kullanıcı deneyimini iyileştirdiğini, ama doğrusal enterpolasyon örneğinde olduğu gibi dikkatsiz uygulanırsa rahatsız edici olup deneyimi bozabileceğini iyi gösteriyor.
    [0] Toggle switch türüne göre değişir elbette. Ancak Minimoog gibi cihazlarda görülen tür de “direnip sonra yeni konuma tak diye geçme” hareketi sergiler ve kullanması keyiflidir. Bu arada ekipmanla hava atmak istemiyorum; Minimoog’um yok.

    • Sezgisel olarak da kübik fonksiyon tarafının hissi daha iyiydi. Mekanik toggle’da bir miktar atalet vardır ve bu, kübik yaklaşımda görülüyor gibi.
  • Basit doğrusal olmayan püf noktalarının çevrimiçi etkileşimlere ne kadar sık keyif kattığı beni şaşırtmaya devam ediyor. Renk algısında ise iki rengin bir kişi için neden yeterince ayırt edilemediğini anlamada da kritik.
    Tuhaf olan, insanların ivmeyi her zaman iyi kavrayamaması. Bir yangının düz arazideki gibi neredeyse sabit hızla ilerlediğine inanıp yokuş yukarı kaçmamak gerekir. Yangın yokuş çıkarken hızlanır.
    Çocuklar atılan bir topun zemin boyunca ne hızla hareket ettiğini hızla öğrenir, ama yerçekimi nedeniyle ellerine çarptığında ne kadar hızlı hareket edeceğini her zaman iyi anlayamaz.

  • Bu yazının büyük bölümünün sonunda easinge bağlanması ilginç. Sanki her yeni neslin bunu kendi başına yeniden keşfetmesi gerekiyor gibi görünüyor.
    90’ların sonunda Yugo Nakamura’nın deneysel web sitelerine hayran kaldığımı hatırlıyorum. Easing’i özgürce kullanarak organik bir his veren, gördüğüm ilk web sitelerinden biriydi: https://www.youtube.com/watch?v=NLt7Gwnt3WY

    • Yugo’nun konuştuğu bir konferansta en ön sırada oturuyordum; demoları hızlıca gösterdiğinde diğer Flash uzmanlarının tamamen coştuğunu hatırlıyorum.
  • Nedense şöyle bir toggle olsa hoşuma giderdi: Dokunulduğunda ya da tıkılı tutulduğunda hedefin yaklaşık %75’ine kadar yavaşça ilerlesin, bırakılınca kalanını tak diye tamamlasın.
    UX açısından bunun ne anlama geleceğinden emin değilim. Ayarın en sonda gerçekten uygulanması ya da kaydedilmesi anlamına gelebilir.
    Ya da “Gerçekten emin misiniz?” diyalogunun bir parçası olabilir. Basılı tutulurken ayar uygulanır, ama yerine tak diye oturmadan önce Escape ile geri alınabilir gibi.

    • https://jsfiddle.net/u1vybhqg/
      input:checked + .slider:active:before { transform: translateX(8px); transition: 1s; }
      input:not(:checked) + .slider:active:before { transform: translateX(18px); transition: 1s; }
    • Pek çok yaşlı kullanıcının toggle’ı itmeye çalışıp hiçbir şey olmadığını gördüm. O yüzden böyle yapacaksanız toggle’ın gerçekten itilmesine de izin vermelisiniz. Yoksa sadece checkbox kullanmak daha iyi.
  • Bu yazıyı sevdim. Neredeyse birebir aynı tekniği yaklaşık 10 yıl önce yazmıştım; o zaman lazy-easy diyordum ve hâlâ kullanıyorum.
    Bazen tüm durum yönetimini yapmadan sadece akıcı bir animasyon istersiniz: https://www.hailpixel.com/articles/lazy-animation-with-lazy-...

  • Yazının kendisi gerçekten iyi. Demo Chrome’da düzgün çalışıyor gibi görünüyor ama Firefox’ta kaydırma sırasında donuyor ve sayfanın render edilmesi tamamen duruyor.

  • Aslında oldukça iyi bir yaklaşım ve animasyon/easing teknikleri için bir kavram kanıtı olarak da iyi. Bana epey Flickity’yi hatırlatıyor.
    https://metafizzy.co/blog/initial-demos/
    https://metafizzy.co/blog/math-time-resting-position/
    https://metafizzy.co/blog/particle-to-slider/
    https://metafizzy.co/blog/flickity-begins/
    Özellikle şu demo: https://codepen.io/desandro/pen/myXdej
    Bu teknik yalnızca switch’ler için kullanışlı değil. Zaten gerçek hayatta switch’te böyle bir öğe kullanmazsınız; site genelinde paralel çalışan 20 requestAnimationFrame döngüsü de çalıştırmazsınız. Bilerek bozuk bir öğe de koymazsınız.
    Ayrıca delta yeterince küçüldüğünde render etmeyi durduran bir optimizasyon da yok; bunu prodüksiyon seviyesine getirmek için muhtemelen onlarca küçük ayar daha gerekir.
    Buradaki tepkilere bakınca insanlar yazıyı okumadan varsayım yapıyor, ağaçlara bakıp ormanı göremiyor ya da akıllı görünmek için fazlasıyla taraflı ve alaycı, uzmanlıktan uzak yargılar paylaşıyor gibi görünüyor.
    HN ne zamandan beri Reddit’e döndü?

    • Bu durumda birçok kişi yazıyı okuyamıyor. Çünkü sayfanın yaklaşık üçte birine inildiğinde çoğu Chromium dışı tarayıcı donup kalıyor.
    • “HN ne zamandan beri Reddit’e döndü?” diye soruyorsanız https://news.ycombinator.com/newsguidelines.html sayfasının en azından son paragrafını okuyabilirsiniz.
    • Bu değişim bir süredir devam ediyor, ancak HN yönergeleri böyle şeyleri doğrudan söylemeyi yasakladığı için insanlar konuşmuyor. Gerçekten tuhaf.
    • Bu durumda alaycılık pek adil görünmüyor.
      HN’nin Reddit’e dönüşmesi kısmına gelince, belki de teknoloji genelinde belirsiz bir alaycı hava vardır. Bunun altında, hükümetlerin ve şirketlerin ortaya çıkan güçlü teknolojileri nasıl kullanmak istediğine; bunları bireysel özgürlüğü artırmak yerine bireyleri kısıtlayıp yönetmek amacıyla mı inşa ettiklerine dair bir güvensizlik yatıyor.
  • Duygusal tasarımın (https://en.wikipedia.org/wiki/Emotional_Design) özüne yakın. Çok küçük bir animasyonun arkasında bile söylenecek çok şey var.