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