Adobe Acrobat 3D, JavaScript i animacje. Część 2.
marzec 23rd, 2009. Brak komentarzy.
Kategorie: Acrobat, JavaScript, Software, Zabawy z kodem.
Witam ponownie.
W poprzedniej części tej jakże zacnej serii przedstawiłem szkic kodu ułatwiającego zarządzanie animacjami w Acrobacie 3D. Teraz czas na coś bardziej zaawansowanego.
Podstawowym problemem jest opracowanie takiej sekwencji animacji, aby przejście pomiędzy dwoma dowolnymi wariantami produktu odbywało się za pomocą możliwie jak namniejszej ich (animacji) liczby. W naszych rozważaniach z pomocą może przyjść algorytm przeszukiwania w głąb, niemniej jednak nie jest to do końca to, czego oczekujemy.
Poprzednio utworzyliśmy listę animacji. Lista ta w momencie dodawania nowej animacji uaktualniała ogólną listę punktów (naszych wariantów produktu), pomiędzy którymi “poruszają się” nasze animacje. Lista punktów znajduje się we właściwości klasy:
myList.points; // Array
No to połowę (heh) pracy mamy za sobą. Teraz tylko trzeba przeszukać graf (bo punkty są połączone animacjami), jako punkt najwyższy ustalając aktualnie wybrany produkt.
W tym celu tworzymy klasę, która reprezentować będzie punkt na grafie. Punkt ten będzie pośród swoich właściwości zawierał listę punktów podrzędnych. Paskudna ta rekurencja…
// obiekt punktu animacji function myPoint(id, par, nest, been, target) { // brak animacji if(id == target && !nest) return false; // pseudostatyczna wlasciwosc klasy, lista punktow na sciezce od-do if(typeof myPoint.way == 'undefined' ) myPoint.way = Array(); // metoda tworzaca liste animacji na podstawie listy punktow this.makeAnim = function() { myPoint.anim = Array(); for(var a = 0; a < myPoint.way.length - 1; a++) myPoint.anim.push(myList.getAnim(myPoint.way[a], myPoint.way[a+1])); } var point = id; // id danego punktu var level = nest; // stopien zagniezdzenia (kolejny numer punktu na sciezce) var upper = par; // punkt nadrzedny var subs = new Array(); // lista podrzednych punktow na sciezce var visited = new Array(); // przebyta droga // obcinamy nadmiarowe wartosci do poziomu aktualnego zagniezdzenia for(var a = 0; a <= nest; a++) visited[a] = been[a]; // ...i zastepujemy wartosc odpowiadajaca aktualnemu zagniezdzeniu visited[nest] = id; // jesli aktualny punkt jest naszym celem if(id == target) { // sprawdz, czy przebyta sciezka jest krotsza od ostatnio zapisanej // (jesli taka istnieje) if(visited.length < myPoint.way.length || !myPoint.way[0]) { // zapisz aktualna sciezke jako najkrotsza, utworz liste animacji myPoint.way = visited; this.makeAnim(); } // nie szukaj glebiej return; } // szukaj animacji, ktorej... -----------------------. for(var a = 0; a < myList.count(); a++) { if(myList.anims[a].animStart == point && // ...start pokrywa sie z aktualnym punktem myList.anims[a].animEnd != upper && // ...koniec nie jest nadrzednym punktem danego punktu !in_array(myList.anims[a].animEnd, visited)) // ...koniec nie znajduje sie na przebytej juz sciezce { // nowa, przekazywana punktowi podrzednemu lista "tu bylem", zaktualizowana o nowy punkt var x = visited; x.push(myList.anims[a].animEnd); subs.push(new myPoint(myList.anims[a].animEnd, point, (level + 1), x, target)); } } }
Wszystko zrozumiałe, prawda? :)
Polega to na tym, iż tworzymy jeden obiekt klasy myPoint, który robi za nas resztę, to znaczy:
– przeszukuje w liście animacji wszystkich animacji, które mogą z niego “wyjść”
– dopisuje do swojej listy punktów podrzędnych wszystkie punkty końcowe tych animacji (sam jest punktem początkowym). Dlatego właśnie przy dodawaniu do listy dodajemy tez wersję odwróconą animacji. Żeby teraz było szybciej.
– ponieważ jednym z argumentów “konstruktora” jest tablica już przebytych punktów, wyszukiwanie w danej gałęzi grafu jest przerywane w momencie, gdy aktualnym punktem jest punkt szukany. Wtedy zapisywana jest przebyta droga.
– przeszukiwanie w innych gałęziach jest kontynuowane i droga jest nadpisywana, jeśli znaleziona zostanie krótsza
Warto zauważyć, ze niewielka modyfikacja skryptu w miejscu zapisywania drogi pozwoli na uzyskanie algorytmu biorącego pod uwagę długość animacji, a nie tylko ilość.
Żeby to wszystko zobrazować, użyję przykładu utworzenia takiej “drogi” pomiędzy dwoma punktami:
if(typeof actual == 'undefined') var actual = 'zatyczka, wklad';
// na wszelki wypadek resetowanie statycznej listy punktow. myPoint.way = new Array(); // TU OBLICZANA JEST NAJKROTSZA SCIEZKA graph = new myPoint(actual, null, 0, been, cRtn);
Zmienna cRtn to aktualnie wybrany punkt docelowy. Nie pytajcie, czemu tak się nazywa – zapraszam do Acrobat JS Reference :)
Teraz słówko o uruchamianiu animacji. Jeśli wydaje Wam się, że wystarczy napisać
Context().Animation1.play(); Context().Animation2.play();
…to jesteście w błędzie :) Po prostu uruchomienie jednej animacji nie sprawia, że skrypt będzie na nas czekał, aż animacja się skończy. W wyniku uruchomienia takiego potworka otrzymujemy losowe efekty, z reguły odtwarza się ostatnia animacja z listy.
Jak wobec tego “odczekać” do końca animacji? Tu sprawa jest lekko skomplikowana, ale podam to jak na tacy :) Przetestowałem kilka sposobów i tylko jeden działał, więc może ktoś się ucieszy na jego widok.
Na początek należy zauważyć, że po obliczeniu drogi mamy gotową nie tylko listę punktów, przez które ona prowadzi, ale też listę łączących je animacji:
myPoint.anim; // Array
Czyli generalnie mamy wszystko. Teraz wystarczy w sumie przeiterować tę listę i odpalać animacje po kolei. Czyli należy sprawdzać, czy któraś (poprzednia) animacja nie jest aktualnie w toku. Tu pomoże nam ustawianie interwałów czasowych – app.setInterval().
Ponieważ nasza lista animacji jest zwykłą tablicą, najpierw resetujemy jej indeks (zmienna poniekąd globalna):
var movieIndex = 0;
Następnie musimy uruchomić interwał:
anim_timeout = app.setInterval("playMe()", 10);
Teraz już pozostaje tylko stworzyć funkcję, która całą resztę za nas załatwi:
// funkcja uruchamiania animacji function playMe() { // jesli nie istnieje lista animacji, wyjdz if(myPoint.anim == null) return false; // jesli ktoras animacja jest uruchomiona, wyjdz for(var a = 0; a < myPoint.anim.length; a++) if(myPoint.anim[a][0].getRunning()) return false; // ustaw kierunek odtwarzania oraz predkosc i uruchom animacje o danym indeksie myPoint.anim[movieIndex][0].setPlayForward(!myPoint.anim[movieIndex][1]); myPoint.anim[movieIndex][0].setPlaySpeed(1); myPoint.anim[movieIndex][0].play(); // jesli to ostatnia animacja, usun timer, wyzeruj indeks, pokaz nazwe produktu if(movieIndex == myPoint.anim.length - 1) { app.clearInterval(anim_timeout); movieIndex = 0; this.getField("Text1").value = actual; } else // jesli to nie ostatnia animacja, inkrementuj indeks movieIndex++; }
Co robi ta funkcja, powinno być jasne:
– najpierw sprawdza, czy któraś z animacji nie jest już uruchomiona. Jeśli tak, kończy działanie
– konfiguruje animację (kierunek odtwarzania) i uruchamia ją
– jeśli to ostatnia animacja, usuwa interwał. Jeśli nie, inkrementuje indeks animacji
Cała filozofia :)
W następnym odcinku dowiemy się, jak to wszystko połączyć w całość za pomocą ładnego popUp menu.