Exempel på bitmapp: animerad snurrande måne

Flash Player 9 och senare, Adobe AIR 1.0 och senare

Exemplet med den animerade snurrande månen visar tekniker för hur man arbetar med Bitmap-objekt och bitmappsbilddata (BitmapData-objekt). I exemplet skapas en animering av en snurrande sfärisk måne med en platt bild av månens yta som råbilddata. Följande tekniker visas:

  • Läsa in en extern bild och använda dess råbilddata.

  • Skapa animeringar genom att kopiera pixlar från olika delar av en källbild upprepade gånger.

  • Skapa en bitmappsbild genom att ange pixelvärden

Programfilerna för det här exemplet finns på www.adobe.com/go/learn_programmingAS3samples_flash_se . Programfiler för den animerade snurrande månen finns i mappen Samples/SpinningMoon. Programmet består av följande filer:

Fil

Beskrivning

SpinningMoon.mxml

eller

SpinningMoon.fla

Huvudfilen för Flex (MXML) eller Flash (FLA).

com/example/programmingas3/moon/MoonSphere.as

En klass som utför funktionerna att läsa in, visa och animera månen.

moonMap.png

En bildfil som innehåller ett fotografi av månens yta. Bildfilen läses in och används för att skapa den animerade, snurrande månen.

Läsa in en extern bild som bitmappsdata

Den första huvudsakliga uppgiften som utförs av det här exemplet är att en extern bildfil läses in. Bilden är ett foto av månens yta. Inläsningen hanteras av två metoder i klassen MoonSphere: konstruktorn MoonSphere() som initierar inläsningsprocessen och metoden imageLoadComplete() som anropas när den externa bilden har lästs in helt.

En extern bildfil läses in på ungefär samma sätt som en extern SWF-fil. I båda fallen används en instans av klassen flash.display.Loader för att utföra inläsningen. Koden i metoden MoonSphere() som börjar läsa in bilden är följande:

var imageLoader:Loader = new Loader(); 
imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, imageLoadComplete); 
imageLoader.load(new URLRequest("moonMap.png"));

På den första raden deklareras en Loader-instans med namnet imageLoader . Det är den tredje raden som faktiskt startar inläsningen genom att anropa Loader-objektets load() -metod och skicka en URLRequest-instans som representerar URL:en för bilden som ska läsas in. På den andra raden ställs händelseavlyssnaren in, som ska aktiveras när bilden har lästs in. Observera att metoden addEventListener() inte anropas för själva Loader-instansen. I stället anropas den för Loader-objektets contentLoaderInfo -egenskap. Loader-instansen i sig skickar inte händelser som relaterar till innehållet som läses in. Dess contentLoaderInfo -egenskap innehåller däremot en referens till LoaderInfo-objektet som är associerat med innehållet som läses in i Loader-objektet (den externa bildfilen i det här fallet). Det LoaderInfo-objektet innehåller flera händelser som relaterar till förloppet och slutförandet för det externa innehållet, inklusive complete -händelsen ( Event.COMPLETE ) som utlöser ett anrop till metoden imageLoadComplete() när bilden är helt inläst.

Att starta inläsningen av den externa bilden är en viktig del av processen, men det är lika viktigt att veta vad som ska göras när inläsningen är klar. Som du kan se i koden ovan anropas funktionen imageLoadComplete() när bilden har lästs in. Funktionen gör flera saker med inlästa bilddata, vilket beskrivs längre fram. För att kunna använda bilddata behöver funktionen först åtkomst till dessa data. När ett Loader-objekt används för att läsa in en extern bild blir den inlästa bilden en Bitmap-instans som kopplas som ett underordnat visningsobjekt till Loader-objektet. I det här fallet är Loader-instansen tillgänglig för händelseavlyssnarmetoden som en del av händelseobjektet som skickas till metoden som en parameter. De första raderna i metoden imageLoadComplete() är följande:

private function imageLoadComplete(event:Event):void 
{ 
    textureMap = event.target.content.bitmapData; 
    ... 
}

Observera att händelseobjektsparametern heter event och är en instans av klassen Event. Alla instanser av klassen Event har egenskapen target , som refererar till det objekt som utlöser händelsen (i det här fallet LoaderInfo-instansen som metoden addEventListener() anropades för, vilket beskrevs tidigare). LoaderInfo-objektet i sin tur har egenskapen content (så snart inläsningen är klar) som innehåller Bitmap-instansen med den inlästa bitmappsbilden. Om du vill visa bilden direkt på skärmen kan du koppla den här Bitmap-instansen ( event.target.content ) till en visningsobjektsbehållare. (Du kan också koppla objektet Loader till en visningsobjektsbehållare). I det här exemplet används emellertid det inlästa innehållet som en källa till råbilddata i stället för att visas på skärmen. Det innebär att den första raden i metoden imageLoadComplete() läser egenskapen bitmapData i den inlästa Bitmap-instansen ( event.target.content.bitmapData ) och sparar den i instansvariabeln textureMap , vilken används som källa till bilddata för att skapa animeringen av den roterande månen. Detta beskrivs härnäst.

Skapa animering genom att kopiera pixlar

En grundläggande definition av animering är illusionen av rörelse eller förändring som skapas av att bilden ändras över tiden. I det här exemplet är målet att skapa en illusion av att en sfärisk måne roterar runt sin lodräta axel. För den här animeringen kan du emellertid bortse från den sfäriska förvrängningsaspekten av exemplet. Titta på den faktiska bilden som läses in och används som källa till månens bilddata:

Som du ser är bilden inte en eller flera sfärer. Den är ett rektangulärt foto av månens yta. Eftersom fotot tagits exakt vid månens ekvator är de delar av bilden som är överst och underst utsträckta och förvrängda. För att bli av med förvrängningen och ge bilden ett sfäriskt utseende använder vi ett förskjutningsfilter, vilket beskrivs senare. Eftersom källbilden är en rektangel behöver koden bara förflytta månytan i vågrät riktning för att skapa en illusion av att sfären roterar.

Observera att bilden faktiskt innehåller två kopior av månytebilden bredvid varandra. Det är den här bilden som är källbilden som bilddata kopieras från flera gånger för att skapa ett intryck av rörelse. Genom att placera två kopior av bilden bredvid varandra skapas lättare en kontinuerlig, oavbruten rullningseffekt. Låt oss nu gå igenom animeringsprocessen steg för steg för att se hur detta fungerar.

I processen används egentligen två separata ActionScript-objekt. Det första är den inlästa källbilden, som är den kod som representeras av BitmapData-instansen textureMap . textureMap fylls i med bilddata så snart den externa bildfilen läses in. För detta används följande kod:

textureMap = event.target.content.bitmapData;

Innehållet i textureMap är den rektangulära månbilden. Det andra objektet som används är Bitmap-instansen sphere , som används för att skapa den animerade rotationen och som är det faktiska visningsobjekt som visar månbilden på skärmen. Precis som textureMap skapas objektet sphere och fylls i med sina ursprungliga bilddata i metoden imageLoadComplete() . För detta används följande kod:

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));

Som du kan se i koden skapas en instans av sphere . Dess bitmapData -egenskap (de råbilddata som visas av sphere ) skapas med samma höjd och halva bredden för textureMap . Med andra ord kommer innehållet i sphere att ha samma storlek som ett månfoto (eftersom bilden textureMap innehåller två månfoton bredvid varandra). Därefter fylls bitmapData -egenskapen i med bilddata med metoden copyPixels() . Parametrarna i anropet till metoden copyPixels() anger flera saker:

  • Den första parametern indikerar att bilddata kopieras från textureMap .

  • Den andra parametern, som är en ny Rectangle-instans, anger från vilken del av textureMap ögonblicksbilden ska tas. I det här fallet är ögonblicksbilden en rektangel som börjar i övre vänstra hörnet av textureMap (anges av de första två Rectangle() -parametrarna: 0, 0 ) och rectangle-ögonblicksbildens bredd och höjd matchar egenskaperna width och height för sphere .

  • Den tredje parametern, som är en ny Point-instans med x- och y-värdena 0 , definierar målet för pixeldata – i det här fallet det övre vänstra hörnet (0, 0) av sphere.bitmapData .

Visuellt sett kopierar koden pixlarna från textureMap som markeras i följande bild och klistrar in dem på sphere . Med andra ord är BitmapData-innehållet i sphere den del av textureMap som markeras här:

Kom ihåg att detta bara är utgångsläget för sphere – det första bildinnehållet som kopieras till sphere .

När källbilden har lästs in och sphere har skapats är den sista uppgiften som utförs av metoden imageLoadComplete() att ställa in animeringen. Animeringen drivs av en Timer-instans som heter rotationTimer . Den skapas och startas av följande kod:

var rotationTimer:Timer = new Timer(15); 
rotationTimer.addEventListener(TimerEvent.TIMER, rotateMoon); 
rotationTimer.start();

Koden skapar först Timer-instansen rotationTimer . Den parameter som skickas till Timer() -konstruktorn anger att rotationTimer ska utlösa dess timer -händelse var femtonde millisekund. Därefter anropas metoden addEventListener() som anger, att när händelsen timer ( TimerEvent.TIMER ) inträffar, ska metoden rotateMoon() anropas. Slutligen startas timern genom att dess start() -metod anropas.

På grund av hur rotationTimer är definierad anropar Flash Player metoden rotateMoon() i klassen MoonSphere var femtonde millisekund. Det är där månen animeras. Källkoden för metoden rotateMoon() är följande:

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(); 
}

Koden gör tre saker:

  1. Värdet för variabeln sourceX (som ursprungligen har värdet 0) ökas med 1.

    sourceX += 1;

    Som du ser används sourceX för att avgöra var i textureMap pixlarna ska kopieras från till sphere . Den här kodens effekt är alltså att flytta rektangeln en pixel till höger i textureMap . Om vi går tillbaka till den visuella återgivningen kommer källrektangeln att ha flyttats flera pixlar till höger efter några animeringscykler:

    Efter ytterligare några cykler kommer rektangeln att ha rört sig ännu längre:

    Denna gradvisa, konstanta förflyttning av varifrån pixlarna kopieras är nyckeln till animeringen. Genom att källplatsen sakta men säkert flyttas till höger, kommer bilden som visas på skärmen i sphere att se ut som om den hela tiden rör sig åt vänster. Det är därför källbilden ( textureMap ) behöver ha två kopior av fotot av månytan. Eftersom rektangeln hela tiden rör sig åt höger är den för det mesta inte över ett enda månfoto, utan överlappar båda månfotona.

  2. Det finns ett problem med att källrektangeln hela tiden rör sig åt höger. Så småningom kommer rektangeln att komma till högerkanten av textureMap . Då kommer det inte att finnas fler månpixlar att kopiera till sphere :

    Nästa kodrader åtgärdar detta problem:

    if (sourceX >= textureMap.width / 2) 
    { 
        sourceX = 0; 
    }

    Koden kontrollerar om sourceX (rektangelns vänstra kant) har nått mitten av textureMap . I så fall återställs sourceX till 0 så att den flyttas tillbaka till vänstra kanten av textureMap och börjar om cykeln:

  3. När rätt värde för sourceX har beräknats är det sista steget för animeringen att faktiskt kopiera den nya källrektangelns pixlar till sphere . Koden som gör detta liknar koden som ursprungligen fanns i sphere (beskrivs ovan) väldigt mycket. Den enda skillnaden är att i det här fallet placeras rektangelns vänsterkant vid sourceX i konstruktoranropet new Rectangle() :

    sphere.bitmapData.copyPixels(textureMap, 
                                new Rectangle(sourceX, 0, sphere.width, sphere.height), 
                                new Point(0, 0));

Kom ihåg att den här koden anropas om och om igen var femtonde millisekund. Eftersom källrektangelns plats flyttas hela tiden och pixlarna kopieras till sphere , blir effekten på skärmen att månfotot som representeras av sphere hela tiden rör sig. Med andra ord ser månen ut att rotera oavbrutet.

Skapa ett sfäriskt utseende.

Månen är förstås en sfär och inte en rektangel. Därför måste exemplet ta det rektangulära fotot av månytan som animeras kontinuerligt och konvertera det till en sfär. Detta görs i två steg: en mask används för att dölja allt innehåll förutom ett cirkelformat område av månytefotot, och ett förskjutningsfilter används för att förvränga månfotots utseende så att det ser tredimensionellt ut.

Först används en cirkelformad mask för att dölja allt innehåll i MoonSphere-objektet förutom den sfär som skapas av filtret. I följande kod skapas en mask som en Shape-instans. Den tillämpas som mask för MoonSphere-instansen:

moonMask = new Shape(); 
moonMask.graphics.beginFill(0); 
moonMask.graphics.drawCircle(0, 0, radius); 
this.addChild(moonMask); 
this.mask = moonMask;

Observera, att eftersom MoonSphere är ett visningsobjekt (det är baserat på klassen Sprite) kan masken tillämpas direkt på MoonSphere-instansen med den ärvda egenskapen mask .

Att bara dölja delar av fotot med en cirkelformad mask räcker inte för att skapa en realistisk effekt av en roterande sfär. På grund av det sätt som fotot av månens yta tagits är inte dess dimensioner proportionerliga. De delar av bilden som är högst upp och längst ned är mer förvrängda och utsträckta jämfört med delarna nära ekvatorn. För att månfotot ska se tredimensionellt ut förvränger vi det med ett förskjutningsfilter.

Ett förskjutningsfilter är ett filter som används för att förvränga en bild. I det här fallet förvrängs fotot så att det ser mer realistiskt ut genom att bildens övre och nedre del trycks ihop i vågrät riktning medan mittpartiet inte ändras. Förutsatt att filtret används på en fyrkantig del av fotot, kommer det att bli en cirkel när övre och nedre delen kläms ihop utan att mittpartiet påverkas. En sidoeffekt av animeringen av den här förvrängda bilden blir att mitten av bilden verkar röra sig längre i faktiskt pixelavstånd än de övre och nedre delarna, vilket ger intrycket av att cirkeln faktiskt är ett tredimensionellt objekt (en sfär).

I följande kod skapas förskjutningsfiltret displaceFilter :

var displaceFilter:DisplacementMapFilter; 
displaceFilter = new DisplacementMapFilter(fisheyeLens, 
                                new Point(radius, 0),  
                                BitmapDataChannel.RED, 
                                BitmapDataChannel.GREEN, 
                                radius, 0);

Den första parametern fisheyeLens kallas för mappningsbilden. I det här fallet är den ett BitmapData-objekt som skapas programmatiskt. Hur den bilden skapas beskrivs i avsnittet Skapa en bitmappsbild genom att ange pixelvärden . Den andra parametern anger vid vilken position i den filtrerade bilden som filtret ska tillämpas, vilka färgkanaler som ska användas för att styra förskjutningseffekten och i vilken utsträckning de ska påverka förskjutningen. När förskjutningsfiltret har skapats tillämpas det på sphere , fortfarande inom metoden imageLoadComplete() :

sphere.filters = [displaceFilter];

Den färdiga bilden med både mask och förskjutningsfilter tillämpade ser ut så här:

För varje cykel i animeringen av den roterande månen skrivs BitmapData-innehållet i sphere över av en ny ögonblicksbild av källbilden. Filtret behöver däremot inte tillämpas igen varje gång. Det beror på att filtret tillämpas på Bitmap-instansen (visningsobjektet) i stället för på bitmappsdata (råa pixeldata). Kom ihåg att Bitmap-instansen egentligen inte är faktiska bitmappsdata, den är ett visningsobjekt som visar bitmappsdata på skärmen. Man skulle kunna säga att en Bitmap-instans är som projektorn som används för att visa diabilder på en duk, och att BitmapData-objektet är den diabild som visas av projektorn. Ett filter kan tillämpas direkt på ett BitmapData-objekt, vilket skulle motsvara att rita direkt på en diabild för att ändra bilden. Ett filter kan också tillämpas på visningsobjekt, till exempel en Bitmap-instans. Det skulle motsvara att placera ett filter framför projektorns lins för att förvränga den bild som visas på skärmen (utan att ändra diabilden i sig). Eftersom råa bitmappsdata är tillgängliga via bitmapData-egenskapen för en Bitmap-instans, skulle man ha kunnat tillämpa filtret direkt på dessa data. I det här fallet är det dock bättre att tillämpa filtret på Bitmap-visningsobjektet i stället för på bitmappsdata.

Mer information om hur du använder förskjutningsfiltret i ActionScript finns i Filtrera visningsobjekt .

Skapa en bitmappsbild genom att ange pixelvärden

En viktig aspekt av förskjutningsfiltret är att det faktiskt används två bilder i det. Den ena bilden (källbilden) är den bild som faktiskt ändras av filtret. I det här exemplet är källbilden Bitmap-instansen sphere . Den andra bilden som används av filtret kallas mappningsbilden. Mappningsbilden visas aldrig på skärmen. I stället används färgen för var och en av dess pixlar som indata till förskjutningsfunktionen. Pixelns färg vid en viss x, y-koordinat i mappningsbilden avgör hur mycket förskjutning (fysisk lägesändring) som ska tillämpas på pixeln vid den x, y-koordinaten i källbilden.

För att ett förskjutningsfilter ska kunna användas för att skapa en sfäreffekt måste alltså rätt mappningsbild finnas i exemplet. Den ska ha grå bakgrund och en cirkel som är ifylld med en övertoning av en enda färg (röd) som övergår från mörk till ljus i vågrät riktning, vilket visas här:

Eftersom bara en mappningsbild och ett filter används i exemplet skapas bara mappningsbilden en gång, i metoden imageLoadComplete() (d.v.s. när den externa bilden har lästs in). Mappningsbilden, som heter fisheyeLens , skapas genom att metoden createFisheyeMap() i klassen MoonSphere anropas:

var fisheyeLens:BitmapData = createFisheyeMap(radius);

Inuti metoden createFisheyeMap() ritas mappningsbilden en pixel i taget med metoden setPixel() i klassen BitmapData. Den fullständiga koden för metoden createFisheyeMap() listas här, följd av en steg-för-steg-diskussion om hur den fungerar:

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; 
}

När metoden först anropas får den parametern radius som anger radien för den cirkelformade bild som ska skapas. Sedan skapar koden det BitmapData-objekt som cirkeln ska ritas på. Det objektet, som heter result , skickas sedan tillbaka som returvärde för metoden. Som du kan se i följande kodavsnitt skapas BitmapData-instansen result med en bredd och höjd som är lika med cirkelns diameter, utan genomskinlighet (den tredje parametern är false ) och i förväg ifylld med färgen 0x808080 (mellangrå):

var result:BitmapData = new BitmapData(diameter, 
                                    diameter, 
                                    false, 
                                    0x808080);

Sedan används två slingor för att iterera genom alla pixlar i bilden. Den yttre slingan går igenom varje kolumn i bilden från vänster till höger (variabeln i representerar den vågräta positionen för den pixel som manipuleras). Den inre slingan går igenom varje pixel i den aktuella kolumnen uppifrån och ner (variabeln j representerar den lodräta positionen för den pixel som manipuleras). Koden för slingorna (den inre slingans innehåll har utelämnats) visas här:

for (var i:int = 0; i < diameter; i++) 
{ 
    for (var j:int = 0; j < diameter; j++) 
    { 
        ... 
    } 
}

När slingan går igenom pixlarna en i taget beräknas ett värde för varje pixel (färgvärdet för den pixeln i mappningsbilden). Detta görs i fyra steg:

  1. Den aktuella pixelns avstånd från cirkelns mitt längs x-axeln (i - radius) beräknas. Detta värde divideras med radien så att det blir en procentandel av radien i stället för ett absolut avstånd ( (i - radius) / radius ). Procentvärdet sparas i en variabel som heter pctX och motsvarande värde för y-axeln beräknas och sparas i variabeln pctY , vilket visas i följande kod:

    var pctX:Number = (i - radius) / radius; 
    var pctY:Number = (j - radius) / radius;
  2. Med en vanlig trigonometrisk formel (Pythagoras sats) beräknas det linjära avståndet mellan cirkelns mitt och den aktuella punkten från värdena pctX och pctY . Det värdet sparas i en variabel som heter pctDistance , vilket visas här:

    var pctDistance:Number = Math.sqrt(pctX * pctX + pctY * pctY);
  3. Sedan kontrolleras om avståndet är mindre än 1 (vilket innebär 100 % av radien, med andra ord om pixeln är inom cirkelns radie eller inte). Om pixeln är inuti cirkeln tilldelas den ett beräknat färgvärde (utelämnas här, men beskrivs i steg 4). Om pixeln är utanför cirkeln händer inget mer med den pixeln och dess färg förblir mellangrå:

    if (pctDistance < 1) 
    { 
        ... 
    }
  4. För pixlar som är inuti cirkeln beräknas ett färgvärde. Den slutliga färgen blir en röd nyans som ligger mellan svart (0 % röd) längst till vänster i cirkeln och klarröd (100 % röd) längst till höger i cirkeln. Färgvärdet beräknas ursprungligen i tre delar (röd, grön och blå) vilket visas här:

    red = 128 * (1 + 0.75 * pctX * pctX * pctX / (1 - pctY * pctY)); 
    green = 0; 
    blue = 0;

    Observera att bara den röda delen av färgen (variabeln red ) faktiskt har ett värde. De gröna och blå värdena (variablerna green och blue ) visas här av tydlighetsskäl, men de kan utelämnas. Eftersom syftet med den här metoden är att skapa en cirkel som innehåller en röd övertoning behövs inga gröna eller blå värden.

    När de tre enskilda färgvärdena har bestämts kombineras de till ett enda heltalsfärgvärde med en standardalgoritm för bitflyttning, som visas i följande kod:

    rgb = (red << 16 | green << 8 | blue);

    När färgvärdet slutligen har beräknats kan det tilldelas den aktuella pixeln med metoden setPixel() i BitmapData-objektet result , vilket visas här:

    result.setPixel(i, j, rgb);