Przykład z bitmapą: animowany obracający się księżycFlash Player 9 i nowsze wersje, Adobe AIR 1.0 i nowsze wersje Przykład animowanego wirującego księżyca ilustruje techniki pracy z obiektami Bitmap oraz z danymi obrazów bitmapowych (obiektami BitmapData). Ten przykład ilustruje tworzenie animacji ruchu wirowego sferycznego księżyca za pomocą płaskiego obrazu powierzchni księżyca wykorzystywanego jako dane nieprzetworzone. Poniższe techniki ilustrują:
Aby pobrać pliki tej przykładowej aplikacji, należy przejść na stronę www.adobe.com/go/learn_programmingAS3samples_flash_pl. Pliki aplikacji Animowany wirujący księżyc można znaleźć w folderze Samples/SpinningMoon. Aplikacja składa się z następujących plików:
Ładowanie obrazu zewnętrznego w formie danych bitmapyPierwszym, głównym zadaniem w tym przykładzie jest załadowanie zewnętrznego pliku obrazu, stanowiącego zdjęcie powierzchni księżyca. Operacja ładowania odbywa się za pomocą dwu metod klasy MoonSphere: konstruktora MoonSphere(), gdzie inicjowany jest proces ładowania, metody imageLoadComplete(), która jest wywoływana po całkowitym załadowaniu obrazu zewnętrznego. Ładowanie obrazu zewnętrznego przypomina ładowanie zewnętrznego pliku SWF; w obu przypadkach do wykonania operacji ładowania używana jest instancja klasy flash.display.Loader. Rzeczywisty kod w metodzie MoonSphere(), która rozpoczyna ładowanie obrazu, jest następujący: var imageLoader:Loader = new Loader();
imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, imageLoadComplete);
imageLoader.load(new URLRequest("moonMap.png"));
W pierwszej linii znajduje się deklaracja instancji Loader o nazwie imageLoader. Dopiero trzecia linia faktycznie rozpoczyna proces ładowania, wywołując metodę obiektu Loader load(), i przekazując instancję URLRequest reprezentującą adres URL obrazu do załadowania. Druga linia powoduje ustawienie detektora zdarzeń, który zostanie wyzwolony w przypadku całkowitego załadowania obrazu. Należy zauważyć, że metoda addEventListener() nie jest wywoływana w instancji Loader samoistnie; zamiast tego jest ona wywoływana w zależności od właściwości contentLoaderInfo obiektu Loader. Instancja Loader nie przekazuje samoistnie zdarzeń związanych z ładowaną zawartością. Jej właściwość contentLoaderInfo zawiera jednak odniesienie do obiektu LoaderInfo, który jest skojarzony z ładowaną do obiektu Loader zawartością (w tym wypadku z obrazem zewnętrznym). Ten obiekt LoaderInfo udostępnia kilka zdarzeń związanych z postępem i ukończeniem ładowania zawartości zewnętrznej, w tym ze zdarzeniem complete (Event.COMPLETE), które wyzwala wywołanie metody imageLoadComplete() po całkowitym załadowaniu obrazu. Chociaż uruchomienie ładowania obrazu zewnętrznego stanowi ważną część procesu, tak samo ważna jest wiedza, co należy zrobić po ukończeniu ładowania. Zgodnie z kodem pokazanym powyżej funkcja imageLoadComplete() jest wywoływana po załadowaniu obrazu. Funkcja ta odpowiada za kilka kwestii związanych z danymi ładowanego obrazu, opisanymi w dalszej części podręcznika. Jednak aby można było użyć danych obrazu, funkcja ta musi mieć do nich dostęp. W przypadku korzystania z obiektu Loader do ładowania obrazu zewnętrznego ładowany obraz staje się instancją Bitmap dołączoną jako potomny obiekt wyświetlania do obiektu Loader. W takim przypadku dla metody detektora zdarzeń dostępna jest instancja Loader przekazywana następnie do metody jako parametr. Pierwsze wiersze metody imageLoadComplete() są następujące: private function imageLoadComplete(event:Event):void
{
textureMap = event.target.content.bitmapData;
...
}
Należy zauważyć, że parametr obiektu zdarzenia ma nazwę event, oraz że jest to instancja klasy Event. Każda instancja klasy Event ma właściwość target, która odnosi się do obiektu wyzwalającego zdarzenie (w tym przypadku, jest to instancja LoaderInfo, dla której wywołano metodę addEventListener(), zgodnie z opisem powyżej). Obiekt LoaderInfo z kolei ma właściwość content, która (po ukończeniu procesu ładowania) zawiera instancję Bitmap z załadowanym obrazem bitmapowym. W celu wyświetlenia obrazu bezpośrednio na ekranie można wówczas dołączyć tę instancję Bitmap (event.target.content) do kontenera obiektu wyświetlanego. (Można również dołączyć obiekt Loader do kontenera obiektu wyświetlanego). W tym przypadku jednak załadowana treść stanowi źródło nieprzetworzonych danych obrazu, nie jest zaś wyświetlana na ekranie. Konsekwentnie, pierwsza linia metody imageLoadComplete() odczytuje właściwość bitmapData załadowanej instancji Bitmap (event.target.content.bitmapData) i zapisuje ją w zmiennej instancji o nazwie textureMap, która stanowi źródło danych obrazu użytych do utworzenia animacji obracającego się księżyca. To zagadnienie zostało opisane w następnej sekcji. Tworzenie animacji przez kopiowanie pikseliPodstawowa definicja animacji to złudzenie ruchu, lub zmiany, utworzone w wyniku zmiany obrazu w czasie. W tym przykładzie celem jest utworzenie iluzji wirowania sferycznego księżyca wokół jego pionowej osi. Dla celów tej animacji można jednak zignorować kwestię zniekształcenia sferycznego w tym przykładzie. Należy uwzględnić rzeczywisty obraz ładowany jako źródło i używany w charakterze źródła dla danych obrazu księżyca: Jak można zobaczyć, obraz nie ma kształtu sfery ani kilku sfer; jest to tylko prostokątna fotografia powierzchni księżyca. Ponieważ jednak zdjęcie zostało zrobione dokładnie na równiku księżyca, części obrazu w górnej i dolnej jego części są rozciągnięte i zniekształcone. W celu usunięcia zniekształceń i uzyskania kształtu sferycznego użyjemy filtra mapy przemieszczeń, opisanego poniżej. Ponieważ jednak obraz źródłowy ma kształt prostokąta, w celu wytworzenia iluzji, że sfera się obraca, konieczne jest po prostu przesunięcie zdjęcia powierzchni księżyca w poziomie przez kod. Należy przy tym zauważyć, że obraz w istocie zawiera dwie kopie powierzchni księżyca, ułożone obok siebie. Obraz ten jest obrazem źródłowym, z którego dane obrazu są kopiowane w sposób cykliczny w celu utworzenia efektu ruchu. Dysponując umieszczonymi obok siebie dwiema kopiami obrazu, można łatwiej uzyskać niezakłócony niczym efekt przewijania. Przejdźmy teraz krok po kroku przez proces animacji, aby zobaczyć, jak to działa. Proces w istocie obejmuje dwa różne obiekty ActionScript. Pierwszy z nich to ładowany obraz źródłowy, reprezentowany w kodzie przez instancję BitmapData o nazwie textureMap. Zgodnie ze wcześniejszym opisem, dane obrazu są wypełniane instancją textureMap, gdy tylko zewnętrzny obraz zostanie załadowany z wykorzystaniem tego kodu: textureMap = event.target.content.bitmapData; Zawartość obiektu textureMap jest prostokątnym obrazem księżyca. Ponadto, w celu utworzenia animowanego ruchu w przykładzie zastosowano instancję Bitmap o nazwie sphere, która jest rzeczywistym obiektem wyświetlanym — księżycem. Podobnie jak textureMap, obiekt sphere jest tworzony i wypełniany początkowymi danymi obrazu w metodzie imageLoadComplete() za pomocą następującego kodu: sphere = new Bitmap();
sphere.bitmapData = new BitmapData(textureMap.width / 2, textureMap.height);
sphere.bitmapData.copyPixels(textureMap,
new Rectangle(0, 0, sphere.width, sphere.height),
new Point(0, 0));
Zgodnie z treścią kodu tworzona jest instancja sphere. Jej właściwość bitmapData (nieprzetworzone dane obrazu wyświetlane przez instancję sphere) jest tworzona przy użyciu tej samej wysokości oraz połowy szerokości instancji textureMap. Innymi słowy, zawartość instancji sphere będzie odpowiadała rozmiarom zdjęcia księżyca (ponieważ obraz textureMap zawiera dwa zdjęcia księżyca umieszczone obok siebie). Następnie właściwość bitmapData jest wypełniana danymi obrazu przy wykorzystaniu metody copyPixels(). Parametry należące do metody copyPixels() mogą stanowić o kilku kwestiach:
Można opisać to tak, że kod kopiuje piksele z instancji textureMap opisanej na poniższym obrazie i wkleja je do instancji sphere. Innymi słowy, zawartość BitmapData instancji sphere jest fragmentem instancji textureMap (patrz podświetlenie): Należy jednak pamiętać, że jest to tylko początkowy stan instancji sphere — zawartość pierwszego obrazu, kopiowana do instancji sphere. Po załadowaniu obrazu źródłowego oraz utworzeniu instancji sphere ostatnim zadaniem wykonywanym przez metodę imageLoadComplete() jest skonfigurowanie animacji. Animacja jest sterowana przez instancję Timer zwaną rotationTimer, tworzoną i uruchamianą następującym kodem: var rotationTimer:Timer = new Timer(15); rotationTimer.addEventListener(TimerEvent.TIMER, rotateMoon); rotationTimer.start(); Kod tworzy najpierw instancję Timer o nazwie rotationTimer; parametr ten, przekazywany do konstruktora Timer(), wskazuje, że instancja rotationTimer powinna wyzwalać zdarzenie timer co 15 milisekund. Następnie wywoływana jest metoda addEventListener(), określająca, że wystąpienie zdarzenia timer (TimerEvent.TIMER) powoduje wywołanie metody rotateMoon(). Ostatecznie instancja timer jest faktycznie uruchamiana przez wywołanie jej metody start(). Ze względu na sposób, w jaki zdefiniowano instancję rotationTimer, co około 15 milisekund Flash Player wywołuje metodę rotateMoon() w klasie MoonSphere, to jest tam, gdzie faktycznie ma miejsce animacja księżyca. Kod źródłowy metody rotateMoon() brzmi następująco: private function rotateMoon(event:TimerEvent):void
{
sourceX += 1;
if (sourceX > textureMap.width / 2)
{
sourceX = 0;
}
sphere.Data.copyPixels(textureMap,
new Rectangle(sourceX, 0, sphere.width, sphere.height),
new Point(0, 0));
event.updateAfterEvent();
}
Kod realizuje trzy funkcje:
Należy pamiętać, że ten kod jest wywoływany cyklicznie, w odstępach 15-milisekundowych. w miarę przesuwania lokalizacji prostokąta źródłowego oraz kopiowania pikseli do instancji sphere na ekranie widać, jak zdjęcie księżyca reprezentowane przez instancję sphere przesuwa się. Innymi słowy, wygląda to tak, jakby księżyc stale się obracał. Tworzenie efektu sferycznegoKsiężyc to oczywiście sfera, a nie prostokąt. W naszym przykładzie konieczne jest zatem użycie prostokątnego zdjęcia powierzchni księżyca, w stanie ciągłej animacji, i przekonwertowanie go do formy sferycznej. Wymaga to wykonania dwu osobnych kroków: użycia maski do ukrycia wszystkiego poza okrągłym obszarem zdjęcia powierzchni księżyca, oraz filtra mapy przemieszczenia służącego do odkształcenia zdjęcia księżyca tak, aby wyglądało ono na trójwymiarowe. Po pierwsze, maska o kształcie okręgu służy do przesłonięcia całej zawartości obiektu MoonSphere, z wyjątkiem sfery utworzonej przez filtr. Poniższy kod umożliwia utworzenie maski w postaci instancji Shape, oraz zastosowanie jej w formie maski instancji MoonSphere: moonMask = new Shape(); moonMask.graphics.beginFill(0); moonMask.graphics.drawCircle(0, 0, radius); this.addChild(moonMask); this.mask = moonMask; Należy pamiętać, że ponieważ MoonSphere jest obiektem wyświetlanym (opartym na klasie Sprite), maskę można nałożyć bezpośrednio na instancję MoonSphere za pomocą jej dziedziczonej właściwości mask. ![]() Do utworzenia realistycznego efektu wirującej sfery nie wystarczy niestety proste ukrycie fragmentu zdjęcia. Z uwagi na sposób, w jaki zrobiono zdjęcie księżyca, jego wymiary są nieproporcjonalne; fragmenty obrazu przesunięte w górę oraz w dół obrazy są znacznie bardziej zniekształcone i wyciągnięte w porównaniu z fragmentami w obszarze równika. W celu odkształcenia zdjęcia księżyca tak, aby miało ono charakter trójwymiarowy, użyjemy filtra mapy przemieszczeń. Filtr mapy przemieszczeń jest to rodzaj filtra używany do odkształcania obrazu. W naszym przypadku zdjęcie księżyca zostanie odkształcone tak, aby wyglądało bardziej „realistycznie”. W tym celu górna i dolna część obrazu zostaną ściśnięte w poziomie, podczas gdy środkowa część obrazu pozostanie niezmieniona. Przyjmując, że filtr działa na fragmencie o kształcie kwadratu, ściśnięcie górnej i dolnej części, a pozostawienie niezmienionej środkowej części, umożliwi uzyskanie kształtu okręgu. Efektem „ubocznym” w przypadku tej akurat animacji jest fakt, że środek obrazu wydaje się przemieszczać znacznie dalej, niż obszary w górnej i dolnej części — to złudzenie dodatkowo wzmacnia efekt trójwymiarowości (złudzenie sferyczności księżyca). Poniższy kod służy do utworzenia filtra mapy przemieszczeń o nazwie displaceFilter: var displaceFilter:DisplacementMapFilter;
displaceFilter = new DisplacementMapFilter(fisheyeLens,
new Point(radius, 0),
BitmapDataChannel.RED,
BitmapDataChannel.GREEN,
radius, 0);
Pierwszy z parametrów, fisheyeLens, jest znany jako obraz mapy; w tym przypadku jest to obiekt BitmapData tworzony programowo. Tworzenie obrazu opisano w sekcjiTworzenie obrazu bitmapowego przez ustawienie wartości pikseli. Pozostałe parametry opisują położenie filtrowanego obrazu, w którym powinien zostać nałożony filtr, oraz to, które kanały kolorów zostaną użyte do kontroli efektu przemieszczenia, a także, w jakim stopniu będą one miały wpływ na przemieszczenie. Po utworzeniu filtra mapy przemieszczeń jest on nakładany na instancję sphere (nadal w metodzie imageLoadComplete()): sphere.filters = [displaceFilter]; Ostateczny obraz, wraz z maską i filtrem mapy przemieszczeń wygląda następująco: ![]() Wraz z każdym cyklem animacji wirującego księżyca zawartość obiektu BitmapData sfery jest nadpisywana nową migawką danych obrazu źródłowego. Filtr nie wymaga jednak ponownego zastosowania za każdym razem. Dzieje się tak, ponieważ filtr jest nakładany na instancję Bitmap (obiekt wyświetlany), nie zaś na dane bitmapowe (nieprzetworzone informacje o pikselach). Należy przy tym pamiętać, że instancja Bitmap nie stanowi faktycznych danych o bitmapie; jest to obiekt wyświetlany przedstawiający dane bitmapy na ekranie. Posługując się analogią, instancję Bitmap można przyrównać do projektora slajdów wyświetlającego je na ekranie, a obiekt BitmapData do faktycznego slajdu, który można wyświetlić za pomocą projektora. Filtr można nałożyć bezpośrednio na obiekt BitmapData, co byłoby równoważne rysowaniu po samym slajdzie. Można również nałożyć filtr tylko na obiekt wyświetlany, w tym na instancję Bitmap; oznaczałoby to umieszczenie filtra przed obiektywem rzutnika, tak aby działanie filtra dotyczyło wyłącznie obrazu wyświetlanego na ekranie (bez ingerencji w oryginalne zdjęcie). Ponieważ nieprzetworzone dane bitmapowe są dostępne za pośrednictwem instancji Bitmap właściwości bitmapData, filtr może zostać nałożony bezpośrednio na nieprzetworzone dane bitmapy. W tym przypadku sens ma raczej nałożenie filtra na obiekt wyświetlany Bitmap niż na dane bitmapy. Szczegółowe informacje dotyczące używania filtra mapy przemieszczeń języka ActionScript można znaleźć w sekcji Filtrowanie obiektów wyświetlanych. Tworzenie obrazu bitmapowego przez ustawienie wartości pikseliJedną z istotnych kwestii dotyczących filtra mapy przemieszczeń jest to, że w rzeczywistości wymaga on użycia dwu obrazów. Jeden z nich, obraz źródłowy, jest obrazem naprawdę zmienionym w wyniku działania filtru. W tym przykładzie obraz źródłowy jest to instancja obiektu Bitmap pod nazwą sphere. Drugi obraz używany przez filtr zwany jest obrazem mapy. Obraz mapy nie jest wyświetlany na ekranie. Zamiast tego, kolor każdego z jego pikseli jest używany jako dane wejściowe dla funkcji przemieszczania — kolor piksela o danych współrzędnych x, y na obrazie mapy determinuje, jak duże przemieszczenie (fizyczne przesunięcie) jest stosowane wobec piksela o tych współrzędnych x, y w obrazie źródłowym. Konsekwentnie, w celu użycia filtra mapy przemieszczeń do utworzenia efektu sferycznego konieczne jest zastosowanie odpowiedniego obrazu mapy — o szarym tle i okręgu z wypełnieniem gradientowym jednego koloru (czerwonego) rozjaśniającym się w poziomie, tak jak przedstawiono poniżej: ![]() Ponieważ w przykładzie tym użyto wyłączeni jednego obrazu mapy i filtra, obraz mapy jest tworzony tylko raz, w metodzie imageLoadComplete() (innymi słowy, po zakończeniu ładowania obrazu zewnętrznego). Tworzony jest obraz mapy o nazwie fisheyeLens przez wywołanie klasy MoonSphere metody createFisheyeMap(): var fisheyeLens:BitmapData = createFisheyeMap(radius); Wewnątrz metody createFisheyeMap() odbywa się faktyczne kreślenie obrazu mapy, po jednym pikselu naraz, z wykorzystaniem metody setPixel() klasy BitmapData. Kompletny kod dla metody createFisheyeMap() zamieszczono poniżej; opatrzono go omówieniem krok po kroku wyjaśniającym jego działanie: private function createFisheyeMap(radius:int):BitmapData
{
var diameter:int = 2 * radius;
var result:BitmapData = new BitmapData(diameter,
diameter,
false,
0x808080);
// Loop through the pixels in the image one by one
for (var i:int = 0; i < diameter; i++)
{
for (var j:int = 0; j < diameter; j++)
{
// Calculate the x and y distances of this pixel from
// the center of the circle (as a percentage of the radius).
var pctX:Number = (i - radius) / radius;
var pctY:Number = (j - radius) / radius;
// Calculate the linear distance of this pixel from
// the center of the circle (as a percentage of the radius).
var pctDistance:Number = Math.sqrt(pctX * pctX + pctY * pctY);
// If the current pixel is inside the circle,
// set its color.
if (pctDistance < 1)
{
// Calculate the appropriate color depending on the
// distance of this pixel from the center of the circle.
var red:int;
var green:int;
var blue:int;
var rgb:uint;
red = 128 * (1 + 0.75 * pctX * pctX * pctX / (1 - pctY * pctY));
green = 0;
blue = 0;
rgb = (red << 16 | green << 8 | blue);
// Set the pixel to the calculated color.
result.setPixel(i, j, rgb);
}
}
}
return result;
}
Przede wszystkim wywołanie tej metody wiąże się z otrzymaniem parametru radius, wskazującego promień obrazu w kształcie okręgu, który ma zostać utworzony. Następnie kod tworzy obiekt BitmapData, w którym zostanie narysowany okrąg. Obiekt ten o nazwie result, jest następnie przekazywany z powrotem jako wartość zwracana metody. Zgodnie z tym, co pokazano w poniższym fragmencie kodu, tworzoną instancję result obiektu BitmapData charakteryzują szerokość i wysokość odpowiadające średnicy okręgu, bez przezroczystości (trzeci parametr równy false), oraz ze wstępnym wypełnieniem kolorem 0x808080 (średnio szary): var result:BitmapData = new BitmapData(diameter,
diameter,
false,
0x808080);
Następnie w kodzie wykorzystywane są dwie pętle służące do uzyskania iteracji cyklu wykonywanego na kolejnych pikselach. Pętla zewnętrzna przechodzi przez każdą z kolumn obrazu od lewej do prawej (korzystając przy tym ze zmiennej i do reprezentowania położenia poziomego piksela, którego dotyczy manipulacja), podczas gdy pętla wewnętrzna przechodzi przez każdy z pikseli z bieżącej kolumny od góry do dołu (korzystając przy tym ze zmiennej j reprezentującej pionowe położenie bieżącego piksela). Kod dla pętli (pominięto środkowy fragment pętli) jest następujący: for (var i:int = 0; i < diameter; i++)
{
for (var j:int = 0; j < diameter; j++)
{
...
}
}
W miarę wykonywania pętli na kolejnych pikselach obliczane są kolejne wartości (wartości koloru tego piksela na obrazie mapy). Ten proces obejmuje cztery kroki:
|
|