Introduction à Win2D pour un développeur XAML

Win2D est une API Windows Runtime dont le but est de faciliter l’accès et l’utilisation de Direct2D pour les applications universelles 8.1 et 10 (UWP).

Comment ? Tout d’abord en permettant de s’affranchir du principal frein d’adoption pour certaines personnes : l’obligation d’utiliser du C++ ! En effet, Win2D est en réalité une librairie universelle, écrite en C++. Elle est donc accessible aux applications XAML, que ce soit en C# ou en VB.
De plus, Win2D cherche à simplifier les opérations possibles en Direct2D à l’aide de structures de code et de manipulations qui se rapprochent beaucoup plus de ce à quoi sont habitués les développeurs XAML.

Direct2D est une API graphique faisant partie de la famille DirectX permettant de dessiner des primitives simples, telles que des formes géométriques en deux dimensions, des bitmaps et du texte. Elle est dite Immediate. Cela implique que, contrairement aux technologies XAML qui sont Retained, il est à la charge du développeur de décrire de manière procédurale toutes les opérations de dessin, et ceci à chaque fois qu’une nouvelle frame doit être dessinée.
C’est donc une API assez bas niveau, mais cela implique également de meilleures performances, notamment car elle est accélérée matériellement (par le GPU), de plus grandes libertés et également une empreinte mémoire réduite.

Win2D va s’intégrer aux applications Xaml par le biais de contrôles. Ces contrôles seront des surfaces de rendu Direct2D, tout en se conformant aux comportements habituels des contrôles : layout, transforms, etc.
Cette approche fournit un grand confort dans son utilisation et rend ces surfaces de rendu non intrusives. Il devient dès lors possible de les combiner du Xaml traditionnel avec très peu de compromis.

Le projet Win2D, né en septembre 2014, a atteint la version stable 1 en juillet 2015.
Le projet est open source et son code source est hébergé sur GitHub. L’équipe en charge du développement publie les modifications apportées à chaque fin de sprint. Cela permet donc de suivre les évolutions au fil de l’eau, d’en profiter et d’apporter du feedback très tôt. Les pulls requests sont également acceptées !
Il est également disponible en tant que package Nuget.

Ce que propose Win2D s’apparente grandement à la librairie SharpDX.
Il y a toutefois des différences importantes :

  • Win2D n’est pas simplement un « wrapper managé ». Il propose un niveau d’abstraction supplémentaire et des contrôles pour Xaml et est lui-même écrit en C++.
  • Il est développé par les équipes de Microsoft.
  • Il est déjà compatible avec les dernières versions de Windows et Windows Phone.

Les bases

Numerics

Sur Windows 10, les projets Win2D exploitent la librairie System.Numerics.Vectors. Celle-ci, gérée par l’équipe .NET CoreFX, a été récemment intégrée au sein de tous les projets Windows 10.
Celle-ci expose des types mathématiques (Matrix, Vector) sous différentes formes.

Sur Windows 8 en revanche, une particularité réside dans le fait que les méthodes des librairies prennent en paramètre des types dont l’espace de nom est Microsoft.Graphics.Canvas.Numerics. Toutefois, l’équipe met à disposition System.Numerics (.NET) et Windows::Foundation::Numerics (C++), qui sont à utiliser de préférences.
Les types présents dans ces namespaces disposent de Casts implicites, afin de remplacer facilement les appels attendant Microsoft.Graphics.Canvas.Numerics.

Sur Windows 10, les opérations sur ces types sont compatibles SIMD ! C’est la promesse de performances plus élevées sur les opérations multiples entre vecteurs et matrices.

Il est également important de noter que dans le mode Win2D, tout ce qui concerne le positionnement est float, et non double. Cette limitation est principalement due aux cartes graphiques un peu anciennes.

Les contrôles

La librairie est actuellement livrée avec trois contrôles : CanvasControl, CanvasAnimatedControl, CanvasVirtualControl et CanvasSwapChainPanel.

Ils sont utilisables en XAML grâce au namespace « using:Microsoft.Graphics.Canvas.UI.Xaml« .

Les contrôles CanvasContol et CanvasAnimatedControl utilisent en interne un CanvasSwapChainPanel, dont le but est de gérer la permutation entre les frames lors de l’affichage.
Le CanvasVirtualControl, quant à lui, intègre de la virtualisation, permettant de ne dessiner que ce qui est visible.

Le CanvasControl vous permettra d’effectuer du rendu de manière très simple.
Il faut ajouter le contrôle, qui s’intégrera parfaitement au layout :

<canvas:CanvasControl Draw="CanvasControl_Draw"/>

Dans le handler, vous aurez accès dans les events args à une instance de CanvasDrawingSession. Celle-ci sera votre point d’entrée pour toutes les opérations de rendu.

private void CanvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
    args.DrawingSession.DrawCircle(100, 100, 50, Colors.Blue);
}

Ce qui donnera pour résultat :

simple circle

Il est également possible de dessiner d’autres formes géométriques, des Paths, du texte, des images, etc.
Lorsque vous souhaiterez effectuer du rendu dont la taille est fonction de la taille du contrôle, vous devrez utiliser ActualWidth et ActualHeight et bien sûr effectuer les calculs vous-mêmes, en n’oubliant pas de transformer les tailles et coordonnées en float.
Lorsque vous souhaitez rafraîchir le dessin, il faut appeler la méthode Invalidate() sur le contrôle, qui déclenchera l’événement Draw.

Le contrôle CanvasAnimatedControl fournit, quant à lui, en plus d’un événement Draw, un événement Update dont le but est d’effectuer des calculs sans faire de rendu.
Le contrôle va créer une horloge interne qui se chargera de déclencher les deux événements sur des threads séparés du thread UI, afin d’une part de distinguer le rendu pur des calculs, et d’autre part de libérer le thread UI afin de proposer un rendu fluide.
Cette approche parlera aux développeurs XNA, qui disposaient de méthodes identiques.
Cela permet de facilement traquer le temps (afin de faire des animations, par exemple) et de s’abstraire des problématiques de multithreading. Le contrôle nous prévient en plus lorsque le rendu est ralenti ! C’est donc une solution très simple à mettre en œuvre dès lors que vous souhaitez faire des animations.

Si vous souhaitez accéder aux ressources UI depuis les handlers de Draw et Update, vous ne pourrez pas le faire directement. Pour y accéder, vous pouvez soit passer par le dispatcher (ce qui est dangereux étant donné le nombre d’appels), soit utiliser simplement un DispatcherTimer, avec un intervalle suffisamment longue pour ne pas impacter les performances mais suffisamment courte pour avoir un temps de réaction correct.
Heureusement, la taille de la surface de rendu, qui fait partie des informations les plus importantes, est exposée sur le contrôle, sous la forme d’une propriété simple, la rendant accessible depuis un thread non UI.

Input

Concernant la gestion des entrées de l’utilisateur, le CanvasAnimatedControl est un peu particulier.
Tous les contrôles se basent sur les événements Xaml classiques, mais le CanvasAnimatedControl requière l’utilisation de la méthode « RunOnGameLoopThreadAsync » si l’on veut interagir avec des éléments qui sont modifiés par les threads du contrôle. Cela évitera notamment d’avoir des ressources partagées entre threads, ce qu’on souhaite généralement éviter.

Voici un exemple simple :

private void animatedControl_PointerPressed(object sender, PointerRoutedEventArgs e)
{
    var position = e.GetCurrentPoint(animatedControl).Position;
    var action = animatedControl.RunOnGameLoopThreadAsync(() =>
    {
        HandlePointerPressed(position);
    });
}

Animations

Les animations sont entièrement gérées à la main. Si vous utilisez le CanvasAnimatedControl, vous avez accès au temps dans les EventArgs de Draw et d’Update.
Vous pouvez ainsi intégrer cette notion de temps dans vos calculs, ou simplement utiliser l’événement Update comme un compteur (time-dependent ou non).

Si vous souhaitez effectuer des opérations de transformations (Scale, Translate, Skew), vous pouvez utiliser le Transform2DEffect.

Par exemple, pour animer un élément de droite à gauche à l’aide de ses coordonnées, vous pouvez le faire comme ceci :

private double _progression = 0;
private static readonly TimeSpan _totalDuration = TimeSpan.FromSeconds(30);

private void CanvasAnimatedControl_Draw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
{
  var x = (float)(_progression * sender.Size.Width);
  var yMiddle = (float)(sender.Size.Height / 2);
  args.DrawingSession.FillEllipse(centerPoint: new Vector2(x, yMiddle), radiusX: 20, radiusY: 30, color: Colors.Red);
}

private void CanvasAnimatedControl_Update(ICanvasAnimatedControl sender, CanvasAnimatedUpdateEventArgs args)
{
  _progression = args.Timing.TotalTime.TotalSeconds / _totalDuration.TotalSeconds;
}

Dans ce cas, l’ellipse prendra 30 secondes pour traverser l’écran, quelque soit le taux de rafraîchissement.

Voici un autre exemple illustrant une balle rebondissante, en effectuant les calculs simplifiés de la Vélocité Verlet :

private const float _ballRadius = 20f;
private Vector2 _ballPosition = new Vector2(_ballRadius, _ballRadius);
private float vx = 5f;
private float vy = 0f;

private void CanvasAnimatedControl_Draw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
{
    args.DrawingSession.DrawRectangle(
        new Rect(0, 0, sender.Size.Width, sender.Size.Height),
        Colors.Blue,
        1);
    args.DrawingSession.FillCircle(_ballPosition, _ballRadius, Colors.Blue);
}

private void CanvasAnimatedControl_Update(ICanvasAnimatedControl sender, CanvasAnimatedUpdateEventArgs args)
{
    var dt = (float)args.Timing.ElapsedTime.TotalSeconds;

    var x = _ballPosition.X;
    var y = _ballPosition.Y;

    // Air resistance, gravity
    var fx = -.1f * (float)Math.PI * vx * vx;
    var fy = 9.81f - .1f * (float)Math.PI * vy * vy;

    // Verlet integration
    var dx = vx * dt * 100f;
    var dy = vy * dt * 100f;

    x += dx;
    y += dy;

    vx += 0.5f * fx * dt;
    vy += 0.5f * fy * dt;

    if (y + _ballRadius > sender.Size.Height && vy > 0)
    {
        vy *= -.7f;
        vx *= .9f;

        y = (float)sender.Size.Height - _ballRadius;
    }

    if (x + _ballRadius > sender.Size.Width && vx > 0)
    {
        vx *= -.7f;

        x = (float)sender.Size.Width - _ballRadius;
    }

    if (x - _ballRadius &lt; 0 && vx &lt; 0)
    {
        vx *= -.7f;

        x = _ballRadius;
    }

    _ballPosition = new Vector2(x, y);
}

Et voici le résultat (ici rendu à 40fps dans un gif manquant un peu de fluidité) :

bouncing ball

Brushes

Win2D gère tous les brushes classiques : Solid, LinearGradient, RadialGradient (qui n’est pas disponible en WinRT 8) et Image.
La surcouche intègre la possibilité d’utiliser les instances de Color, présentes dans le namespace Windows.UI, afin de faciliter leur utilisation.

Pour les dégradés, de nombreuses options supplémentaires sont présentes.

En voici deux exemples :

private void CanvasAnimatedControl_Draw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
{
    var gradientStops = new CanvasGradientStop[]
    {
        new CanvasGradientStop { Position = 0, Color = Colors.PaleVioletRed },
        new CanvasGradientStop { Position = 1, Color = Colors.LightSteelBlue }
    };

    var middle = new Vector2((float)(sender.Size.Width / 2), (float)(sender.Size.Height / 2));

    var brush = new CanvasLinearGradientBrush(
        args.DrawingSession,
        gradientStops,
        CanvasEdgeBehavior.Clamp,
        CanvasAlphaMode.Premultiplied
        )
    {
        StartPoint = new Vector2(middle.X - 100, middle.Y - 100),
        EndPoint = new Vector2(middle.X + 100, middle.Y + 100),
    };

    args.DrawingSession.FillRectangle(0, 0, (float)sender.Size.Width, (float)sender.Size.Height, brush);

}

linear gradient brush

Et avec ce brush :

var brush = new CanvasRadialGradientBrush(args.DrawingSession,
    gradientStops,
    CanvasEdgeBehavior.Mirror,
    CanvasAlphaMode.Premultiplied
    )
{
    Center = new Vector2(middle.X, middle.Y),
    RadiusX = 100,
    RadiusY = 100,
};

radial gradient brush

On peut même faire des animations des plus étranges :

private void CanvasAnimatedControl_Draw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
{
    var gradientStops = new CanvasGradientStop[]
    {
        new CanvasGradientStop { Position = 0, Color =  MakeColorGradient(args.Timing.TotalTime.TotalSeconds, 0, 2, 4) },
        new CanvasGradientStop { Position = .25f, Color = MakeColorGradient(args.Timing.TotalTime.TotalSeconds * 2, 0, 2, 4) },
        new CanvasGradientStop { Position = .5f, Color = MakeColorGradient(args.Timing.TotalTime.TotalSeconds * 4, 0, 2, 4) },
        new CanvasGradientStop { Position = .75f, Color = MakeColorGradient(args.Timing.TotalTime.TotalSeconds * 8, 0, 2, 4) },
        new CanvasGradientStop { Position = 1, Color = MakeColorGradient(args.Timing.TotalTime.TotalSeconds * 16, 0, 2, 4) },
    };

    var middle = new Vector2((float)(sender.Size.Width / 2), (float)(sender.Size.Height / 2));

    var brush = new CanvasRadialGradientBrush(args.DrawingSession,
        gradientStops,
        CanvasEdgeBehavior.Mirror,
        CanvasAlphaMode.Premultiplied
        )
    {
        Center = new Vector2(middle.X, middle.Y),
        RadiusX = 100 + 50 * (float)Math.Sin(args.Timing.TotalTime.TotalSeconds),
        RadiusY = 100 + 50 * (float)Math.Sin(args.Timing.TotalTime.TotalSeconds),
    };

    args.DrawingSession.FillRectangle(0, 0, (float)sender.Size.Width, (float)sender.Size.Height, brush);
}

private Color MakeColorGradient(double frequency, double rphase, double gphase, double bphase)
{
    var r = Math.Sin(frequency + rphase) * 127 + 128;
    var g = Math.Sin(frequency + gphase) * 127 + 128;
    var b = Math.Sin(frequency + bphase) * 127 + 128;
    return Color.FromArgb(255, (byte)r, (byte)g, (byte)b);
}

radial animation

Traitement des images

La librairie permet bien sûr de traiter les images. Le point d’entrée peut être le CanvasBitmap, qui permet de les charger et les enregistrer.

Mais il existe aussi un moyen de créer une image sur laquelle on va pouvoir facilement dessiner, avec une CanvasDrawingSession, comme pour l’événement Draw : le CanvasRenderTarget.
Voici comment vous pouvez l’utiliser :

private void CanvasAnimatedControl_Draw(ICanvasAnimatedControl sender, CanvasAnimatedDrawEventArgs args)
{
    var crt = new CanvasRenderTarget(args.DrawingSession, 100, 100);

    using (var ds = crt.CreateDrawingSession())
    {
        ds.DrawLine(0, 0, 50, 50, Colors.Aquamarine);
    }

    args.DrawingSession.DrawImage(crt);
}

Le CanvasRenderTarget apporte entre autres deux possibilités :

  • Pouvoir simplement dessiner dessus, pour, par exemple, ensuite l’exporter.
  • Pouvoir utiliser les effets !

Effets

Win2D se charge de mettre à disposition tous les effets présents dans Direct2D. Il en existe beaucoup et ils sont très performants.
On retrouve des effets de couleur, de filtre (flous), de transformation, etc.

Les effets sont composables, la plupart du temps grâce à leur propriété Source.

La plupart des effets sont présentés dans un des samples, que vous pourrez trouver à cette adresse.

Vous trouverez également une liste exhaustive ici.

Conclusion

Nous avons passé en revue de nombreuses fonctionnalités. Et pourtant il y en a beaucoup d’autres ! En voici quelques-unes, non exhaustives :

  • Geometry (combinaisons, détourages, intersections, cache dans le GPU etc.)
  • Geometry Path, pour créer des courbes de Bézier
  • Gradient meshes, pour faire des dégradés sur des courbes de Bézier
  • Masques d’opacité
  • Text, polices personnalisées, text layouts
  • Effets sur des flux live (webcam)
  • Gestion des stylets

Ces APIs m’enthousiasment beaucoup ! Elles rendent facilement accessibles des primitives très performantes et faciliteront grandement des tâches qui étaient lourdes ou impossibles à faire en Windows 8.1. Bien sûr, d’autres opérations seront plus simples à réaliser directement avec du XAML. A vous de trouver la bonne combinaison.

Win2D est de plus très versatile. Etant donné la plage très variée de ses compétences, la librairie pourra vous aider à créer :

  • Des applications de retouche photo et vidéo
  • Des tuiles dynamiques ou tout autre type d’images à générer
  • Des surfaces de dessin riches, accessibles au touch, au clavier et au stylet
  • Des jeux
  • Des effets de particules
  • Des contrôles riches, dynamiques et à 60 fps

L’application de démonstration est également disponible sur le store.

Si vous souhaitez aller plus loin, je vous conseille ces lectures :
Le blog de l’équipe
La documentation officielle
Les source sur Github

Tags: win2d,

Pas de commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *