Manipuler des images

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

Il est temps d'embellir notre application à l'aide d'images ! Dans ce chapitre, nous verrons comment incorporer des images à notre application, comment les afficher sur la scène, en plus de quelques manipulations utiles. Toutefois, nous n'aborderons pas le chargement dynamique d'images dans ce chapitre.

Embarquer des images

Préparation de l'image

Dans un premier temps, il faut préparer l'image que nous voulons incorporer dans notre application. Reprenons l'image de Zozor (voir figure suivante) que nous avons utilisée pour afficher une image dans un champ de texte.

Zozor, l'âne facétieux

Créez un dossier lib s'il n'existe pas déjà dans le répertoire de votre projet, puis créez-y un dossier img. Enfin, copiez dans ce dossier img l'image de Zozor. Voilà ! Notre image de Zozor est prête à être incorporée dans notre application ! :)

Librairie d'images

Une nouvelle classe

Pour que notre projet soit un peu organisé, nous allons embarquer nos image dans une classe qui représentera notre bibliothèque d'images. Créez une nouvelle classe ImageLibrary à côté de notre classe Main :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package  
{
    /**
     * Librairie contenant nos images embarquées.
     * @author Guillaume CHAU
     */
    public class ImageLibrary 
    {

    }

}

Incorporer une image

Pour embarquer une image, nous allons utiliser une instruction spéciale pour le compilateur, de la même manière que nous avons embarqué une police de caractère, grâce au mot-clé Embed :

1
[Embed(source="../lib/img/zozor.png")]

Le code est plus simple : il suffit d'indiquer le chemin de l'image dans le paramètre source. Petite astuce si vous utilisez FlashDevelop : dans l'arborescence du projet, faite un clic droit sur l'image à incorporer, puis sélectionnez Generate Embed Code, comme à la figure suivante.

FlashDevelop facilite l'incorporation d'images

FlashDevelop insère alors l'instruction spéciale automatiquement, à l'endroit où votre curseur d'insertion se trouve dans la classe.

Puis, sur la ligne suivante, il faut déclarer un attribut statique de type Class, comme pour les polices de caractères embarquées :

1
public static var Zozor:Class;

Voici le code complet de la classe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package  
{
    /**
     * Librairie contenant nos images embarquées.
     * @author Guillaume CHAU
     */
    public class ImageLibrary 
    {

        [Embed(source="../lib/img/zozor.png")]
        public static var Zozor:Class;

    }

}

Notre librairie d'images embarquées est maintenant prête à l'emploi ! :)

Afficher des images

La classe Bitmap

Pour afficher une image, nous allons utiliser la classe Bitmap (voir figure suivante).

La classe Bitmap dans l'arbre des classes d'affichage

Si nous utilisons maintenant le constructeur de la classe Bitmap, nous obtiendrons une image vide :

1
2
// Image vide
var zozor:Bitmap = new Bitmap();

À la place, nous allons utiliser la classe contenu dans l'attribut statique que nous avons déclaré dans notre classe ImageLibrary :

1
var zozor:Bitmap = new ImageLibrary.Zozor();

Voici le code complet pour afficher notre image de Zozor :

1
2
3
4
5
6
7
// Création de l'image
var zozor:Bitmap = new ImageLibrary.Zozor();
addChild(zozor);

// Alignement au centre
zozor.x = (stage.stageWidth - zozor.width) / 2;
zozor.y = (stage.stageHeight - zozor.height) / 2;

Ce qui nous donne à l'écran la figure suivante.

Zozor !

Redimensionnement

Je vous propose d'essayer de redimensionner notre image de Zozor : doublez sa taille sans regarder la solution, pour vérifier que vous vous souvenez bien des attributs nécessaires.

Vous devriez obtenir la figure suivante.

Zozor a pris du poids.

Voici une solution possible :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Création de l'image
var zozor:Bitmap = new ImageLibrary.Zozor();
addChild(zozor);

// Redimensionnement
zozor.scaleX = 2;
zozor.scaleY = zozor.scaleX;

// Alignement au centre
zozor.x = (stage.stageWidth - zozor.width) / 2;
zozor.y = (stage.stageHeight - zozor.height) / 2;

À l'aide des attributs scaleX et scaleY de la classe DisplayObject dont hérite la classe Bitmap, nous pouvons très facilement appliquer un coefficient multipliant la taille de notre objet d'affichage.

L'image est devenue affreuse avec ces gros pixels… Peux-on y remédier ?

Plus ou moins. :D En vérité, il n'y a pas de formule magique pour rendre l'image aussi nette qu'elle était à sa taille d'origine, mais il est néanmoins possible d'améliorer le rendu obtenu lors de tels redimensionnements jusqu'à une certaine mesure. Pour cet effet, la classe Bitmap dispose d'un attribut smoothing qui indique si les pixels de l'image doivent être lissés si elle est redimensionnée :

1
2
// Lissage des pixels
zozor.smoothing = true;

Ce qui nous donne à l'écran la figure suivante.

Les pixels sont lissés.

C'est déjà un peu mieux ! :) Mais l'effet de cet attribut se fait surtout ressentir lorsque l'on diminue la taille des images. Au lieu de doubler la taille de Zozo, mettons-la à 70%, sans utiliser le lissage :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Création de l'image
var zozor:Bitmap = new ImageLibrary.Zozor();
addChild(zozor);

// Lissage des pixels
//zozor.smoothing = true;

// Redimensionnement
zozor.scaleX = 0.7;
zozor.scaleY = zozor.scaleX;

// Alignement au centre
zozor.x = (stage.stageWidth - zozor.width) / 2;
zozor.y = (stage.stageHeight - zozor.height) / 2;

Ce qui nous donne la figure suivante.

Taille à 70% de l'original

Comme vous pouvez le voir, le rendu que nous obtenu est loin d'être agréable. Pour remédier à cela, réactivons le lissage :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Création de l'image
var zozor:Bitmap = new ImageLibrary.Zozor();
addChild(zozor);

// Lissage des pixels
zozor.smoothing = true;

// Redimensionnement
zozor.scaleX = 0.7;
zozor.scaleY = zozor.scaleX;

// Alignement au centre
zozor.x = (stage.stageWidth - zozor.width) / 2;
zozor.y = (stage.stageHeight - zozor.height) / 2;

Ce qui nous donne à l'écran la figure suivante.

Taille à 70% avec lissage

C'est tout de même beaucoup mieux ! :) Pour mieux voir les différences entre les deux rendus, voici à la figure suivante une comparaison avec zoom.

Comparaison avec et sans lissage

N'abusez pas du lissage si vous affichez beaucoup d'images : sinon, cela dégradera les performances de votre application.

Opérations sur les images

La classe BitmapData

Tout objet de la classe Bitmap dispose d'un attribut bitmapData de la classe BitmapData (voir figure suivante). Cet objet représente les pixels de l'image stockés en mémoire. L'instance de la classe Bitmap ne contient pas de pixels : elle n'est là que pour les afficher.

Tout objet de la classe Bitmap dispose d'un attribut bitmapData de la classe BitmapData

Il est possible d'utiliser un seul objet BitmapData pour plusieurs images Bitmap. N'hésitez pas à le faire si vous devez afficher plusieurs fois la même image, car cette astuce améliore grandement les performances d'affichage des images concernées.

Créer notre première image

Pour créer une image vide, nous pouvons créer une instance de la classe Bitmap sans lui passer de paramètre :

1
2
3
// Création de l'image
var image:Bitmap = new Bitmap();
addChild(image);

Notre objet image n'a aucun pixel à sa disposition, donc il n'affiche rien et a une taille nulle :

1
trace("taille de l'image : " + image.width + " x " + image.height);

Nous obtenons ceci dans la console :

1
taille de l'image : 0 x 0

Ne nous arrêtons pas là et créons notre première image grâce à la classe BitmapData :

1
2
// Création des données de l'image (les pixels)
var imageData:BitmapData = new BitmapData(300, 300, false, 0x0000ff);

Le premier paramètre est la largeur du tableau de pixel (donc de l'image), puis le deuxième est sa hauteur. Ensuite, un booléen nous permet d'indiquer si l'image vide est transparente ou non ; enfin, nous passons la couleur de remplissage initiale. Ici, notre image sera donc un carré de 300 pixels de côté, rempli de couleur bleue.

Ensuite, il faut dire à notre objet image d'utiliser les données contenues dans notre objet imageData :

1
2
// Données pour l'image
image.bitmapData = imageData;

Nous avons désormais la figure suivante à l'écran.

Un carré bleu

Nous avons créé une image à partir de rien ! :)

Dessiner sur des images

Nous allons maintenant nous atteler à rendre ce morne carré bleu un peu plus intéressant, en manipulant les pixels qui le composent.

Pipette

Nous avons à disposition d'une sorte de pipette, comme dans les logiciels de dessin, qui nous permet de connaître la couleur d'un pixel. Cette pipette est la méthode getPixel(x, y) de la classe BitmapData. En paramètre, nous lui fournissons les coordonnées du pixel dont nous voulons la couleur, et elle nous la renvoie (si nous donnons des coordonnées qui sortent de l'image, elle nous renvoie 0).

1
2
3
// Prenons la couleur d'un de nos pixels (celui du centre)
var couleur:uint = imageData.getPixel(150, 150);
trace("couleur du pixel : " + couleur.toString(16)); // Affiche: ff (équivalent de 0000ff)

Il existe une deuxième pipette qui nous renvoie la couleur d'un pixel avec son opacité : getPixel32(x, y).

1
2
3
// Avec l'opacité
couleur = imageData.getPixel32(150, 150);
trace("couleur ARVB du pixel : " + couleur.toString(16)); // Affiche: ff0000ff

Comme vous pouvez le remarquer, il y a deux caractères supplémentaires par rapport à une couleur classique : ce sont les deux premiers. Ils représentent l'opacité de la couleur en quelque sorte, notée en hexadécimal de 00 à ff, comme pour le rouge, le vert et le bleu. Ainsi, 0x00000000 est une couleur totalement transparente, 0x880000ff représente du bleu à moitié transparent et 0xff0000ff du bleu totalement opaque.

Colorier un pixel

Il est grand temps de barbouiller notre carré de couleur ! :) Commençons avec la méthode setPixel(x, y, couleur), qui permet do colorier un pixel aux coordonnées que nous passons en paramètre, avec une certaine couleur (sans gestion de l'opacité) :

1
2
3
// Colorions le pixel du centre en rouge
couleur = 0xff0000;
imageData.setPixel(150, 150, couleur);

Nous obtenons la figure suivante (j'ai agrandi l'image pour que l'on puisse mieux voir).

Pixel rouge au milieu de pixels bleus.

Comme tout à l'heure, il existe également la méthode setPixel32(x, y, couleur) qui permet de colorier un pixel avec une couleur contenant une valeur de transparence.

Exercice

Essayons-nous à plus de fantaisies ! Je vous propose un petit exercice : coloriez chaque pixel de notre carré avec une couleur aléatoire opaque. Essayez de ne pas regarder la solution trop vite ! ;)

Quelques conseils :

  • utilisez deux boucles imbriquées pour "parcourir" les pixels de l'image,
  • utilisez la méthode Math.random() pour générer un nombre aléatoire de 0 à 1 (que vous pouvez multiplier par 0xffffff pour avoir une couleur aléatoire),
  • la classe BitmapData dispose des attributs width et height permettant d'obtenir la taille de l'image stockée dans l'objet, un peu comme pour les objets d'affichage.

J'obtiens la figure suivante.

Un beau tas de pixels.

Ne regardez pas tout de suite la solution que je vous propose si vous n'avez pas réfléchi au code qu'il faut écrire. Essayez d'obtenir un bon résultat, et même si vous n'y arrivez pas, le fait d'avoir essayé est déjà un grand pas vers la maîtrise du langage. ;)

Si vous avez réussi (ou que vous êtes sur le point de vous jeter par la fenêtre), vous pouvez comparer votre code avec le mien (je n'ai vraiment pas envie d'avoir votre mort sur la conscience) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Création de l'image
var image:Bitmap = new Bitmap();
addChild(image);

// Création des données de l'image (les pixels)
var imageData:BitmapData = new BitmapData(300, 300, false, 0x0000ff);

// Données pour l'image
image.bitmapData = imageData;

///// Barbouillage !
// On parcourt chaque pixel de l'image
// Lignes
for (var x:int = 0; x < imageData.width; x ++)
{
    // Colonnes
    for (var y:int = 0; y < imageData.height; y ++)
    {
        // Couleur aléatoire
        couleur = Math.random() * 0xffffff;

        // Colorions le pixel actuel
        imageData.setPixel32(x, y, couleur);
    }
}

Et c'est tout ! Finalement, c'est plutôt simple non ?

Exercice : variante

Et si nous corsions un peu les choses ? Modifions l'exercice : il faut désormais que tous les pixels de l'image soient remplis avec une couleur choisie aléatoirement parmi un choix limité : rouge, vert, bleu.

Quelques conseils :

  • utilisez un tableau pour stocker les couleurs autorisées,
  • réutilisez la méthode Math.random() et la propriété length de votre tableau pour choisir aléatoirement une couleur parmi celle du tableau.

J'obtiens cette fois-ci la figure suivante.

Pixels de trois couleurs

Solution :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
///// Barbouillage v 2.0
// On définit d'abord les couleurs possibles
var couleurs:Array = [0xff0000, 0x00ff00, 0x0000ff];

// On parcourt chaque pixel de l'image
// Lignes
for (var x:int = 0; x < imageData.width; x ++)
{
    // Colonnes
    for (var y:int = 0; y < imageData.height; y ++)
    {
        // Choix de la couleur
        var choix:int = Math.random() * couleurs.length;

        // Couleur choisie
        couleur = couleurs[choix];

        // Colorions le pixel actuel
        imageData.setPixel32(x, y, couleur);
    }
}

Remplissage un rectangle de couleur

Une autre méthode bien utile permet de remplir une zone rectangulaire avec une couleur (avec transparence), ce qui nous simplifie tout-de-même la vie. Il s'agit de fillRect(rectangle, couleur) de la classe BitmapData, qui prend en paramètre un objet de la classe Rectangle et une couleur avec transparence.

1
2
// Traçons un rectangle rouge
imageData.fillRect(new Rectangle(0, 0, 150, 100), 0xffff0000);

Notre image ressemble désormais à la figure suivante.

Un rectangle rouge sur fond bleu

Exercice : deuxième variante

Hop ! Encore le même exercice ! Mais cette fois-ci, remplissons notre image avec de gros carrés de 25 pixels de côté, d'une couleur choisie aléatoirement parmi le même choix limité : rouge, vert, bleu.

Quelques conseils :

  • utilisez un objet de la classe Rectangle pour stocker les coordonnées et la taille des carrés,
  • et bien entendu, utilisez la méthode que nous venons de voir, fillRect() !

J'obtiens la figure suivante.

Le rendu

Et voici ce que j'ai écrit :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
///// Barbouillage v 3.0
// On définit d'abord les couleurs possibles
var couleur:uint;
var couleurs:Array = [0xff0000, 0x00ff00, 0x0000ff];

// On définit la taille des rectangles
var rectangle:Rectangle = new Rectangle(0, 0, 25, 25);

// On parcourt l'image pour la remplir de rectangles
// Lignes
for (rectangle.x = 0; rectangle.x < imageData.width; rectangle.x += rectangle.width)
{
    // Colonnes
    for (rectangle.y = 0; rectangle.y < imageData.height; rectangle.y += rectangle.height)
    {
        // Choix de la couleur
        var choix:int = Math.random() * couleurs.length;

        // Couleur choisie
        couleur = couleurs[choix];

        // Colorions le rectangle actuel
        imageData.fillRect(rectangle, couleur);
    }
}

Comme vous pouvez le remarquer, j'ai utilisé l'objet rectangle directement dans les boucles for : le code en est d'autant plus logique et lisible. ;)


En résumé

  • Pour embarquer une image, il suffit d'utiliser l'instruction spéciale Embed et définir un attribut de type Class, comme pour les polices embarquées.
  • L'affichage d'une image se fait via la classe Bitmap, dont la définition peut se faire grâce aux images embarquées.
  • La classe Bitmap dispose d'une propriété smoothing qui permet de lisser les pixels d'une image, principalement utile après une transformation.
  • L'ensemble des pixels d'une image sont stockés en mémoire à l'aide une instance de la classe BitmapData.
  • La classe Bitmap possède donc une propriété nommée bitmapData de type BitmapData, dont l'édition est possible.