Série 4 – Animations : corrigé

Introduction

Le code du corrigé 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.

A noter que pour simplifier la présentation des différentes classes, le code des constructeurs — très répétitif et peu intéressant — a été omis ci-dessous. Il se trouve bien entendu dans l'archive référencée ci-dessus.

Exercice 1

Avant de définir la classe RainbowAnimation, il faut définir une classe pour les images constantes, comme dit dans l'énoncé. Cela peut se faire de deux manières.

La manière simple mais peu générale consiste à définir une classe implémentant l'interface Image<ColorRGB>, prenant en paramètre une couleur et retournant cette couleur pour tous les points.

La manière un peu plus complexe mais aussi plus générale consiste à écrire une classe générique implémentant l'interface Image<T>, prenant en paramètre une valeur de type T et retournant cette valeur pour tous les points. C'est cette solution que nous retenons ici pour définir la classe ConstantImage ci-dessous :

public final class ConstantImage<T> implements Image<T> {
    private final T value;

    public ConstantImage(T value) {
        this.value = value;
    }

    @Override
    public T valueAt(double x, double y) {
        return value;
    }
}

Une fois cela fait, la définition de RainbowAnimation ne pose plus de gros problèmes puisqu'il suffit de lui donner un constructeur qui accepte les six paramètres demandés (les trois fréquences et les trois phases) et qui, dans sa méthode imageAt, calcule la couleur correspondant au temps reçu. On peut alléger un peu le code en plaçant la fonction calculant une composante couleur dans une méthode privée séparée, et on obtient alors :

public final class RainbowAnimation implements Animation<ColorRGB> {
    private final double fR, phiR, fG, phiG, fB, phiB;

    public RainbowAnimation(double fR, double phiR,
                            double fG, double phiG,
                            double fB, double phiB) {
        // ... voir le code du corrigé fourni.
    }

    @Override
    public Image<ColorRGB> imageAt(double time) {
        double r = sine(fR, phiR, time);
        double g = sine(fG, phiG, time);
        double b = sine(fB, phiB, time);
        return new ConstantImage<>(new ColorRGB(r, g, b));
    }

    private static double sine(double f, double phi, double t) {
        return Math.sin(t * f * 2.0 * Math.PI + phi) / 2.0 + 0.5;
    }
}

Exercice 2

La classe AnimationCompositor fait exactement la même chose que ImageCompositor mais avec des animations plutôt qu'avec des images statiques. Sa méthode imageAt est dès lors très simple et commence par obtenir les deux images couleur et le masque à composer, puis laisse le soin à ImageCompositor de les mélanger.

public final class AnimationCompositor
    implements Animation<ColorRGB> {
    private final Animation<ColorRGB> bg, fg;
    private final Animation<Double> mask;

    public AnimationCompositor(Animation<ColorRGB> bg,
                               Animation<ColorRGB> fg,
                               Animation<Double> mask) {
        // ... voir le code du corrigé fourni.
    }

    @Override
    public Image<ColorRGB> imageAt(double time) {
        return new ImageCompositor(bg.imageAt(time),
                                   fg.imageAt(time),
                                   mask.imageAt(time));
    }
}

De manière générale, il est très simple de prendre une opération fonctionnant sur les images, comme ImageCompositor 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 classe DissolveMask quant à elle n'est pas très difficile à définir étant donné qu'elle ressemble beaucoup à la classe LinearHorizontalGradientMask. 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). A noter que, contrairement à ce qui est dit dans l'énoncé, il n'est pas forcément nécessaire de définir une classe pour les masques constants si on a pris soin de rendre la classe ConstantImage générique, comme nous l'avons fait. On peut alors simplement l'utiliser en l'instanciant avec le type Double pour produire un masque constant.

public final class DissolveMask implements Animation<Double> {
    private final double t1, t2;

    public DissolveMask(double t1, double t2) {
        // ... voir le code du corrigé fourni.
    }

    @Override
    public Image<Double> imageAt(double time) {
        double clampedTime = Math.max(t1, Math.min(time, t2));
        return new ConstantImage<>((clampedTime - t1) / (t2 - t1));
    }
}

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é (c-à-d une instance de DissolveMask) et à combiner le tout avec AnimationCompositor, par exemple :

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

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

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

// Animation composite
final Animation<ColorRGB> animation =
    new AnimationCompositor(bgAnim, fgAnim, maskAnim);

A noter que AnimationCompositor 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 classe AnimationLooper, 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 final class AnimationLooper<T> implements Animation<T> {
    private final double t1, t2;
    private final Animation<T> a;

    public AnimationLooper(double t1, double t2, Animation<T> a) {
        // ... voir le code du corrigé fourni.
    }

    @Override
    public Image<T> imageAt(double time) {
        return a.imageAt(t1 + time % (t2 - t1));
    }
}