GUI - Animowane przyciski

0

Witam. Napisałem prototyp przycisku w C++ SFML. Potrzebuję dodać dwie animacje do przycisku animacje "najechania kursorem" i animacje "zdjęcia kursora".

#include <SFML/Graphics.hpp>

class Button {
public:
	
	sf::Vector2f position;
	sf::Vector2f size;
	sf::Color normalColor;
	sf::Color mouseOverColor;
	sf::Color currentColor;
	int colorTimer;
	int maxTimer;
	bool isMouseOver;
	sf::RectangleShape* rect;
	
	Button(sf::Vector2f position, sf::Vector2f size, sf::Color normalColor, sf::Color mouseOverColor) {
		this->position = position;
		this->size = size;
		this->normalColor = normalColor;
		this->mouseOverColor = mouseOverColor;

		isMouseOver = false;

		colorTimer = 0;
		maxTimer = 8;
	}

	bool mouseOvering(sf::Vector2i mousePosition) {
		if (mousePosition.x > position.x - size.x / 2.f &&
			mousePosition.x < position.x + size.x / 2.f &&
			mousePosition.y > position.y - size.y / 2.f &&
			mousePosition.y < position.y + size.y / 2.f ) {
			
			isMouseOver = true;
			colorTimer += 1;
			
			if (colorTimer > maxTimer)
				colorTimer = maxTimer;
		}
		else {
			isMouseOver = false;
			colorTimer -= 1;

			if (colorTimer < 0)
				colorTimer = 0;
		}
			

		return isMouseOver;
	}

	virtual void render(sf::RenderWindow* window) {
		rect = new sf::RectangleShape(size);
		rect->setOrigin(size.x / 2.0f, size.y / 2.0f);
		rect->setPosition(position);
		(isMouseOver) ? rect->setFillColor(mouseOverColor) : rect->setFillColor(normalColor);
		rect->setFillColor(
			sf::Color(
				normalColor.r + (mouseOverColor.r - normalColor.r) * colorTimer / maxTimer,
				normalColor.g + (mouseOverColor.g - normalColor.g) * colorTimer / maxTimer,
				normalColor.b + (mouseOverColor.b - normalColor.b) * colorTimer / maxTimer)
		);
		window->draw(*rect);
	}
};


2

Animacje są przereklamowane. Natychmiastowa zmiana koloru będzie łatwiejsza i wystarczająca.

Ale jak chcesz...

Dodaj dla każdego przycisku zmienną transitionRate. Od niej będzie zależała interpolacja między kolorem idle, a kolorem mouseOver.
Kiedy myszka jest nad przyciskiem, to w każdej klatce zwiększasz transitionRate z pożądaną szybkością.
Kiedy myszka nie jest nad przyciskiem, to zmniejszasz transitionRate.
Po każdej zmianie wartości transitionRate odświeżasz kolor przycisku.

0

Animacje są przereklamowane. Natychmiastowa zmiana koloru będzie łatwiejsza i wystarczająca.

Też tak myślałem, ale bez animacji przyciski wyglądają słabo. Potrzebuję jakiejś ulepszonej animacji.
Na chwilę obecną mam takie coś:
link

2

Jeśli chodzi o gry, to o ile sama gra nie udaje stylem jakiegoś prymitywnego retro-stylu, to powinna mieć animacje — wszędzie gdzie się da. Nie mówię, że interfejs cały czas ma pływać i morphować, ale takie rzeczy jak scrollowanie, aktywowanie i deaktywowanie kontrolek powinno być animowane.

Żeby to miało prawo działać, powinieneś wykrywać moment otrzymania fokusu oraz jego utraty — te akcje powinny odpalać odliczanie w zadanym kierunku. Musisz raz na klatkę popychać animację do przodu — czyli twoje liczniki. W każdej klatce gry, stan kontrolki (czyli liczniki animacji) powinien być aktualizowany jednokrotnie.

Czyli w skrócie, powinieneś mieć cztery ”miejsca”, w których dziać się powinny cztery rzeczy:

  • focus in — wykrycie momentu otrzymania fokusu i rozpoczęcie odliczania kroków animacji w górę,
  • focus out — wykrycie momentu utraty fokusu i rozpoczęcie odliczania kroków animacji w dół,
  • update — aktualizacja stanu liczników animacji (inkrementacja lub dekrementacja licznika),
  • render — wyrenderowanie kontrolki na podstawie aktualnych stanów liczników.

Pamiętaj, że w przypadku animacji otrzymania czy utraty fokusu, należy odwrócić kierunek odliczania licznika. To powinien być jeden licznik — jeden na obsługę animacji fokusu, jeden dla animacji zmiany widoczności, jeden dla animacji zmiany stanu odblokowania itd. Jeśli aktywna kontrolka ma np. cały czas migać (płynnie się rozjaśniać i przyciemniać, np. wedle sinusoidy) to potrzebujesz kolejnego licznika.


Pamiętaj też, że im więcej właściwości ma kontrolka i im więcej animacji wspiera dla różnych swoich stanów, tym więcej rzeczy trzeba wziąć pod uwagę np. podczas obliczania finalnego koloru przycisku. Niżej przykład.

  1. Przycisk ma stan widoczności oraz animację pojawiania się i znikania. Stan widoczności kontrolki wizualizowany jest za pomocą kanału alpha — kontrolka niewidoczna to alpha 0 (całkowita przezroczystość), a w pełni widoczna to alpha 255 (pełne przykrycie). Animacja wizualizowana jest jako stopniowe wyłanianie się kontrolki lub jej zanikanie.

  2. Ma też stan odblokowania oraz animację blokowania i odblokowywania. Przycisk zablokowany to pełna skala szarości jego koloru, a w pełni odblokowany to oryginalny jej kolor. Animacja blokowania/odblokowywania kontrolki to płynne przejście z koloru do skali szarości.

  3. Kontrolka ma też animację fokusu — animacja fokusowania to płynne rozjaśnianie koloru przycisku, a defokusowania to płynne przyciemnianie koloru. Przycisk bez fokusu ma oryginalny kolor, natomiast z fokusem, to oryginalny kolor rozjaśniony o zadany poziom (np. 30% jaśniejszy niż jej oryginalny kolor).

Aby teraz poprawnie wyrenderować taki przycisk, do obliczenia finalnego koloru musisz uwzględnić stan wszystkich tych trzech liczników. Najpierw wziąć oryginalny kolor i na podstawie animacji fokusu odpowiednio go rozjaśnić (im licznik tej animacji bliżej zera, tym słabsze rozjaśnienie). Następnie odczytujesz postęp animacji blokowania i obliczasz odcień skali szarości — im mniejsza wartość licznika tej animacji, tym odcień musi być bliższy pełnej szarości. Na koniec odczytujesz stan licznika widoczności i obliczasz kanał alpha. Tak otrzymany kolor używasz do wyrenderowania przycisku, np. prostokąta.

Oczywiście warto jest najpierw sprawdzić, czy animacja trwa, bo jeśli nie trwa, to część obliczeń należy pominąć. Np. jeśli kontrolka nie jest w ogóle sfokusowana i animacja fokusu jest zakończona, to obliczanie jaśniejszego odcienia nie ma sensu, bo wynikiem będzie ten sam kolor co oryginalny. I to samo, jeśli animacja pojawiania się/zanikania nie jest wykonywana, czyli kontrolka ma pełną widoczność, to obliczanie kanału alpha nie ma sensu, bo i tak wyjdzie 255. Tak więc najpierw sprawdzaj czy animacja trwa i jeśli tak, to wykonaj dodatkowe obliczenia.

1

Warto też wspomnieć o jednej rzeczy, czyli o timerach odliczających czas trwania różnych wydarzeń, np. animacji. Już teraz, na potrzeby jednej animacji — fokusowania — używasz dwóch luźnych zmiennych, czyli colorTimer z aktualnym postępem odliczania oraz maxTimer, z maksymalną wartością timera.

Nie masz natomiast zmiennej informującej o kierunku odliczania, więc logika przycisku nie wie czy licznik colorTimer ma być inkrementowany czy dekrementowany (nie da się odwrócić biegu odliczania). Do tego potrzebna jest jeszcze jedna zmienna, np. dirTimer — powinna przyjmować wartość -1 do odliczania w dół lub 1 do odliczania w górę i być używana jako mnożnik postępu.

Zakładając, że chcesz zaktualizować stan animacji, to bierzesz nową deltę (u ciebie jest to 1), mnożysz ją przez wartość kierunku i dodajesz do obecnego licznika. Na koniec sprawdzasz zakresy i to tyle. Czyli aktualizacja colorTimer, z uwzględnieniem kierunku, może wyglądać tak:

colorTimer += 1 * dirTimer; // inkrementacja lub dekrementacja

if (colorTimer < 0)        colorTimer = 0;
if (colorTimer > maxTimer) colorTimer = maxTimer;

Jeśli chcesz sprawdzić czy animacja trwa, to porównaj licznik z wartościami brzegowymi:

if (colorTimer > 0 && colorTimer < maxTimer)
  // animacja trwa

Jeśli chcesz odwrócić bieg animacji, wystarczy prosty trik:

dirTimer *= -1; // z "1" zrobi się "-1", a z "-1" zrobi się "1"

W razie gdybyś potrzebował zmienić kierunek biegu animacji, najpierw go zmień, a potem zaktualizuj colorTimer.


Te trzy zmienne, które wymieniłem, czyli colorTimer i maxTimer oraz dodatkowa dirTimer służą wyłącznie jednej animacji. Są luzem, więc musisz je za każdym razem aktualizować ręcznie. Jeśli będziesz chciał aby kontrolka posiadała wiele animacji, będziesz miał więcej luźnych zmiennych i każdy ich zestaw będziesz aktualizował dokładnie w ten sam sposób — czyli trochę DRY cierpi.

Dlatego też dobrze by było, abyś te wszystkie liczniki dotyczące jednej animacji opakował sobie w jakąś prostą strukturkę czy klasę i przygotował funkcje/metody do wygodnego używania takiego licznika. Dzięki temu, jeśli twoja kontrolka będzie wykorzystywała pięć różnych animacji, wystarczy że dodasz do jej klasy pięć strukturek animacji, a każdą z nich będziesz mógł aktualizować tymi samymi funkcjami/metodami. Unikniesz DRY i skrócisz ilość kodu logiki kontrolki.

Dodatkowo, taka klasa może być używana do odliczania czegokolwiek — poszczególnych animacji kontrolki, przejścia pomiędzy stage'ami gry, wszelkiej maści liczników używanych w rozgrywce itd. Wszędzie to samo, używane i aktualizowane tak samo. Warto sobie taki wrapper napisać.

1

@furious programming: Można też użyć czegoś do "tween'owania".
Pierwsze z brzegu: https://github.com/mobius3/tweeny

2

Fajne, choć takie rzeczy wolałbym zaimplementować sobie samodzielnie. Po prostu preferuję klepanie kodu, zamiast rozwijania drzewka zależności i pakowania się w abstrakcje. U mnie drzewko zależności zawiera tylko jeden element — SDL-a. 😉

Aby przeprowadzić jakąkolwiek transformację od początku do końca (czyli np. animację), wystarczy zwykły licznik. Nie ma za bardzo znaczenia ile kroków (jak długo) ma trwać transformacja — na koniec bierze się jej obecny postęp, normalizuje i używa. W razie czego zawsze można ją przepuścić przez funkcję kształtującą (shaping functions), jeśli zwykły lerp nie wystarczy.

IMO lepiej takie rzeczy implementować samodzielnie — są bardzo proste, więc lepiej zająć się nimi po swojemu, zaimplementować to co faktycznie jest potrzebne i mieć nad tym pełną kontrolę. W przeciwnym razie skończy się to tak jak w przypadku left-pada i będzie wstyd na cały Internet.

1 użytkowników online, w tym zalogowanych: 0, gości: 1