Adobe Acrobat 3D, JavaScript i animacje. Część 2.

No Comments

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.

Leave a Reply

Your email address will not be published. Required fields are marked *