Exemple d’objet Bitmap : lune en rotation animée

Flash Player 9 et les versions ultérieures, Adobe AIR 1.0 et les versions ultérieures

L’exemple de lune en rotation animée illustre les techniques d’utilisation des objets Bitmap et des données d’image bitmap (objets BitmapData). L’exemple crée une animation d’une lune sphérique en rotation et utilise comme données d’image brutes une image plane de la surface de la lune. Les techniques suivantes sont illustrées :

  • Chargement d’une image externe et accès aux données d’image brutes correspondantes

  • Création d’une animation par copie répétée des pixels de différentes parties d’une image source

  • Création d’une image bitmap par définition de la valeur des pixels

Pour obtenir les fichiers d’application de cet exemple, voir www.adobe.com/go/learn_programmingAS3samples_flash_fr . Les fichiers d’application de la lune en rotation animée résident dans le dossier Samples/SpinningMoon. L’application se compose des fichiers suivants :

Fichier

Description

SpinningMoon.mxml

ou

SpinningMoon.fla

Le fichier d’application principal dans Flex (MXML) ou Flash (FLA).

com/example/programmingas3/moon/MoonSphere.as

Classe exécutant le chargement, l’affichage et l’animation de la lune.

moonMap.png

Fichier image contenant une photographie de la surface de la lune, chargé et utilisé pour créer la lune en rotation animée.

Chargement d’une image externe comme données bitmap

La première tâche à exécuter dans cet exemple consiste à charger une image externe, une photographie de la surface de la lune. Le chargement est géré par deux méthodes de la classe MoonSphere : le constructeur MoonSphere() , qui lance le processus de chargement, et la méthode imageLoadComplete() , qui est appelée au terme du chargement de l’image externe.

Le chargement d’une image externe est similaire à celui d’un fichier SWF externe : il est réalisé par une occurrence de la classe flash.display.Loader. Le code ci-après de la méthode MoonSphere() commence à charger l’image :

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

La première ligne déclare l’occurrence de Loader appelée imageLoader . La troisième ligne commence le processus de chargement à proprement parler en appelant la méthode load() de l’objet Loader. Cette méthode transmet une occurrence URLRequest représentant l’URL de l’image à charger. La deuxième ligne définit l’écouteur d’événement qui se déclenchera à l’issue du chargement de l’image. Observez que la méthode addEventListener() n’est pas appelée sur l’occurrence de Loader elle-même, mais sur la propriété contentLoaderInfo de l’objet Loader. L’occurrence de Loader n’envoie pas d’événements en rapport avec le contenu chargé. En revanche, sa propriété contentLoaderInfo contient une référence à l’objet LoaderInfo qui est associé au contenu chargé dans l’objet Loader (en l’occurrence, l’image externe). L’objet LoaderInfo génère des événements en rapport avec le déroulement et la fin du chargement du contenu externe, notamment l’événement complete ( Event.COMPLETE ) qui déclenche un appel à la méthode imageLoadComplete() au terme du chargement de l’image.

S’il est essentiel de lancer le chargement de l’image externe, il est tout aussi important de savoir comment procéder au terme de cette opération. Comme l’illustre le code ci-dessus, la fonction imageLoadComplete() est appelée une fois l’image chargée. Cette fonction exécute diverses opérations sur les données chargées, comme indiqué ultérieurement. Cependant, pour utiliser les données d’image, elle doit pouvoir y accéder. Une image externe chargée par le biais d’un objet Loader devient une image Bitmap jointe en tant qu’objet d’affichage enfant de l’objet Loader. Dans ce cas, la méthode écouteur d’événement a accès à l’occurrence de Loader dans l’objet événement transmis en tant que paramètre à la méthode. Les premières lignes de la méthode imageLoadComplete() sont les suivantes :

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

Notez que le paramètre de l’objet événement s’appelle event et que c’est une occurrence de la classe Event. Chaque occurrence de la classe Event possède une propriété target , qui fait référence à l’objet déclenchant l’événement (en l’occurrence, l’occurrence de LoaderInfo sur laquelle la méthode addEventListener() a été appelée, comme indiqué plus haut). De même, l’objet LoaderInfo possède une propriété content qui, à l’issue du chargement, contient une occurrence de Bitmap comportant l’image bitmap chargée. Pour afficher l’image directement à l’écran, vous pouvez joindre cette occurrence de Bitmap ( event.target.content ) à un conteneur d’objet d’affichage (vous pouvez aussi joindre l’objet Loader à un conteneur d’objet d’affichage). Toutefois, dans cet exemple, le contenu chargé n’est pas affiché à l’écran, il est utilisé en tant que données d’image brutes. Par conséquent, la première ligne de la méthode imageLoadComplete() lit la propriété bitmapData de l’occurrence de Bitmap chargée ( event.target.content.bitmapData ) et la stocke dans la variable d’occurrence de textureMap , qui est utilisée en tant que source des données d’image pour créer l’animation de la lune en rotation. Ce processus est décrit ci-après.

Création d’une animation par copie de pixels

Une animation, dans sa définition la plus simple, est l’illusion d’un mouvement ou d’un changement, créée par la modification graduelle d’une image. Cet exemple a pour but de créer l’illusion d’une lune sphérique tournant sur son axe vertical. Cependant, pour les besoins de l’animation, vous pouvez ne pas tenir compte de l’aspect de distorsion sphérique de l’exemple. Examinez l’image chargée et utilisée comme source des données d’image de la lune :

Comme vous pouvez le constater, l’image ne représente pas une ou plusieurs sphères ; c’est une photographie rectangulaire de la surface de la lune. La photographie ayant été prise à l’emplacement exact de l’équateur de la lune, les parties supérieures et inférieures de l’image sont donc étirées et déformées. Pour supprimer la distorsion de l’image et lui redonner son aspect sphérique, nous utiliserons un filtre Mappage de déplacement (voir plus bas). Toutefois, l’image source étant un rectangle, il suffit que le code fasse glisser horizontalement la photographie de la surface de la lune pour créer l’illusion d’une sphère en rotation.

Observez que l’image contient en fait deux copies juxtaposées de la photographie de la surface de la lune. Cette image représente l’image source dans laquelle des données ont été copiées plusieurs fois pour créer un effet de mouvement. La juxtaposition de deux copies de l’image facilite la création d’un effet de défilement continu. Examinons en détail le processus d’animation afin de mieux le comprendre.

Le processus s’applique à deux objets ActionScript distincts. Le premier de ces objets est l’image source chargée qui, dans le code, est représentée par l’occurrence de BitmapData textureMap Comme nous l’avons vu, les données d’image sont insérées dans textureMap dès le chargement de l’image externe à l’aide de ce code :

textureMap = event.target.content.bitmapData;

Le contenu de textureMap correspond à l’image de la lune rectangulaire. En outre, pour créer la rotation animée, le code utilise l’occurrence de Bitmap sphere , qui représente l’objet d’affichage qui affiche l’image de la lune à l’écran. A l’instar de textureMap , l’objet sphere contient les données d’image initiales de la méthode imageLoadComplete() , ainsi que le stipule le code suivant :

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

Comme vous pouvez le constater, sphere est instancié. La hauteur et la largeur de sa propriété bitmapData (les données d’image brutes qui sont affichées par sphere ) sont identiques à celles de textureMap . Autrement dit, le contenu de sphere a la même taille qu’une seule photographie de la lune (puisque l’image textureMap contient deux photographies juxtaposées). Des données d’image sont ensuite insérées dans la propriété bitmapData à l’aide de sa méthode copyPixels() . Les paramètres de l’appel de la méthode copyPixels() donnent plusieurs indications :

  • Le premier paramètre indique que les données d’image copiées proviennent de textureMap .

  • Le deuxième paramètre, une nouvelle occurrence de Rectangle, détermine quelle partie de textureMap est copiée. En l’occurrence, le cliché est un rectangle dont l’origine coïncide avec le coin supérieur gauche de textureMap (ce qu’indiquent les deux premiers paramètres de Rectangle() : 0, 0 ) et dont la largeur et la hauteur correspondent aux propriétés width et height de sphere .

  • Le troisième paramètre, une nouvelle occurrence de Point avec des valeurs de x et y égales à 0 , définit la destination des données de pixel ; en l’occurrence, le coin supérieur gauche (0, 0) de sphere.bitmapData .

Représenté visuellement, le code copie les pixels de textureMap mis en évidence ci-dessous et les colle sur sphere . Autrement dit le contenu BitmapData de sphere correspond à la partie de textureMap mise en évidence :

Pour rappel, il s’agit seulement de l’état initial de sphere , le contenu de la première image copiée sur sphere .

Une fois l’image source chargée et sphere créé, il ne reste plus à la méthode imageLoadComplete() qu’à définir l’animation. L’animation est pilotée par une occurrence de Timer, rotationTimer , créée et lancée par le code suivant :

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

Le code commence par créer l’occurrence de Timer rotationTimer . Le paramètre passé au constructeur Timer() indique que rotationTimer doit déclencher son événement timer toutes les 15 millisecondes. La méthode addEventListener() qui est appelée ensuite stipule que le déclenchement de l’événement timer ( TimerEvent.TIMER ) entraîne l’appel de la méthode rotateMoon() . Enfin, l’appel de la méthode start() du timer entraîne le démarrage de celui-ci.

De par la définition de rotationTimer , Flash Player appelle la méthode rotateMoon() dans la classe MoonSphere environ toutes les 15 millisecondes, ce qui se traduit par l’animation de la lune. Le code source de la méthode rotateMoon() est le suivant :

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

Ce code effectue trois opérations :

  1. La valeur de la variable sourceX (initialement fixée à 0) est incrémentée d’une unité.

    sourceX += 1;

    sourceX permet de déterminer d’où proviennent, dans textureMap , les pixels copiés sur sphere . Ce code déplace donc le rectangle d’un pixel vers la droite sur textureMap . Comme le montre l’illustration suivante, après plusieurs cycles d’animation, le rectangle source s’est déplacé de plusieurs pixels vers la droite :

    Après plusieurs autres cycles, le rectangle se trouve encore plus à droite.

    C’est sur ce déplacement progressif constant de l’emplacement d’origine des pixels copiés que repose l’animation. Par un déplacement lent mais continu de l’emplacement source vers la droite, l’image affichée à l’écran dans sphere semble continuellement glisser vers la gauche. C’est pourquoi l’image source ( textureMap ) doit contenir deux copies de la photographie de la surface de la lune. Comme le rectangle se déplace continuellement vers la droite, il chevauche généralement les deux photographies et non pas seulement l’une d’elles.

  2. Ce lent déplacement vers la droite donne cependant lieu à un problème. Le rectangle finira par atteindre le bord droit de textureMap et ne trouvera plus de pixels à copier sur sphere :

    Les lignes suivantes du code permettent de résoudre ce problème :

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

    Le code vérifie si sourceX (le bord gauche du rectangle) a atteint le milieu de textureMap . Si tel est le cas, il remet la variable sourceX à 0 ; autrement dit, il la ramène au bord gauche de textureMap , et le cycle recommence :

  3. Une fois la valeur de sourceX appropriée calculée, la dernière étape du processus d’animation consiste à copier les pixels du nouveau rectangle source sur sphere . Pour ce faire, nous reprenons le code qui a initialement rempli sphere (voir plus haut), à la différence près que, dans l’appel du constructeur new Rectangle() , le bord gauche du rectangle est placé à sourceX :

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

Pour rappel, ce code est appelé toutes les 15 millisecondes. Comment l’emplacement du rectangle source change constamment et que les pixels sont copiés sur sphere , à l’écran, la photographie de la lune représentée par sphere semble glisser continuellement. En d’autres termes, la lune semble tourner sur elle-même continuellement.

Définition de l’aspect sphérique

La lune est bien entendu sphérique , ce n’est pas un rectangle. La photographie rectangulaire de la surface lunaire, qui fait l’objet d’une animation constante, doit donc être convertie en sphère. Cette opération comprend deux étapes : un masque cache tout le contenu excepté une partie circulaire de la photographie et un filtre Mappage de déplacement déforme l’apparence de la photographie, lui donnant un aspect tridimensionnel.

Dans un premier temps, un masque circulaire cache entièrement le contenu de l’objet MoonSphere excepté la sphère créée par le filtre. Le code suivant crée le masque, une occurrence de Shape, et l’applique à l’occurrence de MoonSphere :

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

Comme MoonSphere est un objet d’affichage (fondé sur la classe Sprite), il est possible d’appliquer directement le masque à l’occurrence de MoonSphere à l’aide de sa propriété mask héritée.

Il ne suffit pas d’occulter des parties de la photographie à l’aide d’un masque circulaire pour créer un effet réaliste de sphère en rotation. En raison de la façon dont la photographie de la surface lunaire a été prise, ses dimensions ne sont pas proportionnelles . les parties supérieures et inférieures de l’image sont déformées et étirées par rapport aux zones équatoriales. Pour déformer l’apparence de la photographie et lui donner un aspect tridimensionnel, nous allons utiliser un filtre Mappage de déplacement.

Ce type de filtre permet de déformer une image. En l’occurrence, nous allons déformer la photographie de la lune pour lui donner un aspect plus réaliste, en compressant horizontalement les parties supérieures et inférieures de l’image sans toucher à son milieu. En supposant que le filtre intervienne sur une partie carrée de la photographie, la compression du haut et du bas mais pas du milieu aura pour effet de convertir le carré en cercle. L’animation de cette image déformée a un effet secondaire : la distance en pixels parcourue par le milieu de l’image semble supérieure à celle couverte par les parties supérieure et inférieure, d’où l’impression que le cercle est en fait un objet tridimensionnel (une sphère).

Le code suivant permet de créer un filtre Mappage de déplacement appelé displaceFilter :

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

Le premier paramètre, fisheyeLens , est l’image de mappage ; en l’occurrence, un objet BitmapData créé par programmation. La création de cette image est décrite dans Création d’une image bitmap par définition de la valeur des pixels . Les autres paramètres décrivent l’emplacement d’application du filtre au sein de l’image filtrée, les canaux colorimétriques utilisés pour régir l’effet de déplacement et leur impact sur celui-ci. Une fois le filtre Mappage de déplacement créé, il est appliqué à sphere , toujours dans la méthode imageLoadComplete() :

sphere.filters = [displaceFilter];

L’image finale, une fois le masque et le filtre appliqués, se présente comme suit :

A chaque cycle du processus d’animation de la lune en rotation, le contenu BitmapData de sphere est remplacé par un nouveau cliché des données d’image source. Il est cependant inutile de réappliquer le filtre à chaque fois car il est appliqué à l’occurrence de Bitmap (l’objet d’affichage) plutôt qu’aux données bitmap (données de pixel brutes). Pour rappel, l’occurrence de Bitmap ne correspond pas aux données bitmap. C’est un objet d’affichage qui affiche ces données à l’écran. Une occurrence de Bitmap peut être assimilée à un projecteur de diapositives, tandis qu’un objet BitmapData serait une diapositive présentée par le biais du projecteur. Il est possible d’appliquer un filtre directement à un objet BitmapData, ce qui reviendrait à dessiner sur une diapositive pour modifier l’image. Vous pouvez aussi appliquer un filtre à tout objet d’affichage, y compris une occurrence de Bitmap, ce qui équivaudrait à placer un filtre devant l’objectif du projecteur pour déformer l’image à l’écran sans modifier la diapositive d’origine. Comme les données bitmap brutes sont accessibles par le biais de la propriété bitmapData d’une occurrence de Bitmap, rien n’empêche de leur appliquer directement le filtre. Dans ce cas, cependant, il est préférable d’appliquer directement le filtre à l’objet d’affichage Bitmap plutôt qu’aux données bitmap.

Pour plus d’informations sur l’utilisation du filtre Mappage de déplacement en ActionScript, voir Filtrage des objets d’affichage .

Création d’une image bitmap par définition de la valeur des pixels

Le fait qu’un filtre Mappage de déplacement implique en réalité deux images est un facteur important. L’image source est modifiée par le filtre. Dans cet exemple, il s’agit de l’occurrence de Bitmap sphere . L’autre image utilisée par le filtre est appelée l’image de mappage. Elle n’apparaît pas à l’écran. En revanche, la couleur de ses pixels est utilisée en entrée par la fonction de déplacement : la couleur d’un pixel se trouvant à des coordonnées x, y spécifiques détermine le déplacement (changement physique de position) à appliquer au pixel à ces coordonnées x, y dans l’image source.

Dans cet exemple, pour utiliser le filtre Mappage de déplacement en vue de créer un effet sphérique, il est donc nécessaire d’utiliser l’image de mappage appropriée, c’est-à-dire une image au fond gris comportant un cercle rempli d’un dégradé d’une seule couleur (rouge) qui passe, horizontalement, du foncé au clair, comme illustré ci-dessous :

Comme une image de mappage et un filtre uniques sont utilisés dans cet exemple, l’image de mappage est créée une seule fois, dans la méthode imageLoadComplete() (autrement dit, à l’issue du chargement de l’image externe). L’image de mappage, fisheyeLens , est créée par appel de la méthode createFisheyeMap() de la classe MoonSphere :

var fisheyeLens:BitmapData = createFisheyeMap(radius);

Au sein de la méthode createFisheyeMap() , l’image de mappage est dessinée pixel par pixel à l’aide de la méthode setPixel() de la classe BitmapData. Vous trouverez le code complet de la méthode createFisheyeMap() ci-dessous, suivi d’une présentation détaillée de son fonctionnement :

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

En premier lieu, la méthode reçoit un paramètre, radius , qui indique le rayon de l’image circulaire à créer. Le code crée ensuite l’objet BitmapData sur lequel sera tracé le cercle. Cet objet, appelé result , est renvoyé comme valeur résultante de la méthode. Comme illustré par l’extrait de code ci-dessous, la largeur et la hauteur de l’occurrence de BitmapData result créée sont égales au diamètre du cercle. En outre, cette occurrence n’a pas de transparence (le troisième paramètre correspond à false ) et elle est pré-remplie par la couleur 0x808080 (gris moyen) :

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

Le code utilise ensuite deux boucles pour itérer par-dessus chaque pixel de l’image. La boucle extérieure parcourt de droite à gauche chaque colonne de l’image (la variable i représentant la position horizontale du pixel manipulé), alors que la boucle intérieure intervient sur chaque pixel de la colonne actuelle, de bas en haut (la variable j représentant la position verticale du pixel actuel). Le code des boucles (le contenu de la boucle intérieure étant omis) est illustré ci-dessous :

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

Au fur et à mesure de la manipulation des pixels par les boucles, une valeur est calculée à chacun d’eux (la valeur colorimétrique de ce pixel dans l’image de mappage). Ce processus comporte quatre étapes :

  1. Le code calcule la distance séparant le pixel actuel du centre du cercle, le long de l’axe x ( i - radius ). Cette valeur est divisée par le rayon pour obtenir un pourcentage de celui-ci plutôt qu’une distance absolue ( (i - radius) / radius ). Ce pourcentage est stocké dans une variable appelée pctX . La valeur équivalente sur l’axe y est calculée et stockée dans la variable pctY , comme illustré dans le code ci-dessous :

    var pctX:Number = (i - radius) / radius; 
    var pctY:Number = (j - radius) / radius;
  2. Une formule trigonométrique standard (le théorème de Pythagore) est utilisée pour calculer la distance linéaire entre le centre du cercle et le point actuel, à partir de pctX et pctY . Cette valeur est stockée dans une variable, pctDistance , comme illustré ci-dessous :

    var pctDistance:Number = Math.sqrt(pctX * pctX + pctY * pctY);
  3. Le code vérifie ensuite si la distance en pourcentage est inférieure à 1 (ou 100 % du rayon ; autrement dit, si le pixel concerné se trouve sur le rayon du cercle). Si le pixel figure dans le cercle, une valeur colorimétrique calculée lui est affectée (voir la description à l’étape 4). Dans le cas contraire, ce pixel ne fait l’objet d’aucune manipulation et conserve la couleur par défaut, c’est-à-dire le gris moyen.

    if (pctDistance < 1) 
    { 
        ... 
    }
  4. Une valeur colorimétrique est calculée pour tout pixel se trouvant dans le cercle. La couleur finale est une nuance de rouge qui va du noir (0 % de rouge) sur le bord gauche du cercle au rouge vif (100 % de rouge) sur le bord droit du cercle. La valeur colorimétrique comprend initialement trois parts de couleur (rouge, vert et bleu), comme illustré ci-dessous) :

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

    Observez que seule la part rouge de la couleur (variable red ) possède une valeur. Les valeurs vert et bleu (variables green et blue ) sont illustrées ici par souci de clarté, mais il est possible de les omettre. Cette méthode ayant pour but de créer un cercle contenant un dégradé de rouge, les valeurs vertes et bleues sont superflues.

    Une fois les trois valeurs colorimétriques individuelles déterminées, elles sont conjuguées dans une valeur entière unique à l’aide d’un algorithme de décalage de bits, comme illustré ci-dessous :

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

    En dernier lieu, la valeur colorimétrique calculée est affectée au pixel actuel à l’aide de la méthode setPixel() de l’objet BitmapData result , comme illustré ci-dessous :

    result.setPixel(i, j, rgb);