Série 8 – Jokers et animations : corrigé

Partie 1

Le corrigé de la partie 1 vous est fourni sous la forme d'une archive Zip contenant une version modifiée et commentée des classes Lists et ListsTest.

Partie 2

Le corrigé de la partie 2 est disponible sous la forme d'une archive Zip, qui contient également le code de l'énoncé. Les solutions aux différents exercices sont brièvement discutées ci-dessous.

Exercice 1

Avant de définir la méthode rainbow, il faut définir une méthode pour les images constantes, comme le demande l'énoncé. Cela se fait très simplement en ajoutant la méthode constant à l'interface Image, en s'inspirant au besoin de celle de Animation.

public static <T> Image<T> constant(T value) {
    return (x, y) -> value;
}

Cela fait, la définition de rainbow ne pose plus de gros problèmes puisqu'il suffit de lui donner les six paramètres demandés (les trois fréquences et les trois phases) et les utiliser pour calculer la couleur correspondant au temps reçu.

public static Animation<ColorRGB> rainbow(double fR,
                                          double phiR,
                                          double fG,
                                          double phiG,
                                          double fB,
                                          double phiB) {
    double pi2 = 2 * PI;
    return t -> {
        double r = sin(t * fR * pi2 + phiR) / 2d + 0.5;
        double g = sin(t * fG * pi2 + phiG) / 2d + 0.5;
        double b = sin(t * fB * pi2 + phiB) / 2d + 0.5;
        return Image.constant(new ColorRGB(r, g, b));
    };
}

Exercice 2

La méthode compose de Animation fait exactement la même chose que celle de Image mais avec des animations plutôt qu'avec des images statiques. Elle retourne donc une animation qui ne fait rien d'autre qu'obtenir les deux images et le masque pour le temps actuel, puis les mélange au moyen de la méthode compose de Image.

static Animation<ColorRGB> compose(Animation<ColorRGB> bg,
                                   Animation<ColorRGB> fg,
                                   Animation<Double> m) {
    return t -> Image.compose(bg.imageAt(t),
                              fg.imageAt(t),
                              m.imageAt(t));
}

De manière générale, il est très simple de prendre une opération fonctionnant sur les images, comme compose ici, et de la transformer en une opération équivalente sur les animations. Il suffit d'appeler imageAt pour obtenir les images sur lesquelles opérer puis de les passer à l'opération sur les images.

La méthode dissolveMask quant à elle n'est pas très difficile à définir étant donné qu'elle ressemble beaucoup à la méthode linearHorizontalGradientMask de Image. La différence principale entre les deux est que la seconde travaille dans le domaine spatial (l'intensité du masque dépend de la coordonnée x) alors que la première travaille dans le domaine temporel (l'intensité du masque dépend du temps).

public static Animation<Double> dissolveMask(double t1,
                                             double t2) {
    if (! (0 <= t1 && t1 < t2))
        throw new IllegalArgumentException();
    double dt = t2 - t1;
    return t ->
        Image.constant(max(0, min((t - t1) / dt, 1)));
}

Notez ici l'utilisation des méthode min et max de la classe Math, qui permet de simplifier le code — au prix, il est vrai, d'une petite perte d'efficacité.

Une fois dissolveMask écrite, faire un fondu enchaîné entre deux images revient à créer deux animations constantes avec ces images, un masque de flou enchaîné et à combiner le tout avec compose, par exemple :

// Animation d'arrière-plan
Image<ColorRGB> bg =
    Image.chessboard(ColorRGB.BLACK, ColorRGB.WHITE, 0.2);
Animation<ColorRGB> bgAnim = Animation.constant(bg);

// Animation d'avant-plan
Image<ColorRGB> fg =
    Image.target(ColorRGB.RED, ColorRGB.BLUE, 0.3);
Animation<ColorRGB> fgAnim = Animation.constant(fg);

// Animation du masque (ici un fondu enchaîné)
Animation<Double> maskAnim = Animation.dissolveMask(1, 2);

// Animation composite
Animation<ColorRGB> animation =
    Animation.compose(bgAnim, fgAnim, maskAnim);

A noter que compose permet de faire des fondus enchaînés entre des animations quelconques, pas seulement constantes comme ci-dessus.

Exercice 3

Pour faire tourner en boucle une animation entre les temps \(t_1\) et \(t_2\), il suffit de lui « faire croire » que le temps commence en \(t_1\), s'écoule normalement jusqu'en \(t_2\), puis recommence en \(t_1\) et ainsi de suite.

En d'autres termes, sa méthode imageAt doit être appelée avec \(t_1\) au temps \(t = 0\), \(t_1 + \delta\) au temps \(t + \delta\) jusqu'à ce que cette somme soit égale à \(t_2\), puis de nouveau avec \(t_1\), etc. Ce comportement suggère l'utilisation du reste de la division pour calculer, en fonction du temps \(t\), le temps \(t'\) à passer à l'animation à faire tourner en boucle :

\[ t' = t_1 + (t \bmod (t_2 - t_1)) \]

En traduisant cela en Java, on obtient la définition suivante de la méthode par défaut loop, qui tire parti du fait que l'opérateur de calcul du reste de la division en Java (l'opérateur %) s'applique également aux nombres à virgule flottante :

public default Animation<T> loop(double t1, double t2) {
    if (! (0 <= t1 && t1 < t2))
        throw new IllegalArgumentException();
    return t -> imageAt(t1 + t % (t2 - t1));
}