- Ü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
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
Diçin bir adi diferansiyel denklem olarak yorumlarsak belki çözülebilir. Üstel yumuşatma,dD/dt=-C*Ddenkleminin 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.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
sqrtya 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.
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
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.
input:checked + .slider:active:before { transform: translateX(8px); transition: 1s; }input:not(:checked) + .slider:active:before { transform: translateX(18px); transition: 1s; }Bu yazıyı sevdim. Neredeyse birebir aynı tekniği yaklaşık 10 yıl önce yazmıştım; o zaman
lazy-easydiyordum 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
requestAnimationFramedö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ü?
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.