Windows 10 Anniversary Update–More on Inking with Wet Ink & Custom Rulers

I posted a bit about DirectInk and the notion of wet/dry ink back in this post and one of the things that I was really interested in from //Build was the notion that there were some additional capabilities here in inking in the upcoming Windows Anniversary Update.

This is talked about in the excellent inking session here;

image

and all of the session is very much worth watching but the relevant piece for this post is the piece that starts at approximately 43m which talks about the notion of simultaneous touch and ink.

The InkCanvas is a fantastic bit of kit but it tends to ‘take over’ in the sense that it covers whatever content sits beneath it from both a presentation point of view and, to some extent, from an event processing point of view.

In my previous post, I wrote about how (today on 10586) you can take control of the drying of ink so as to make it possible to interleave the presentation of ink collected by the InkCanvas with other content. The InkCanvas captures the ink “wet” on its own thread and then it calls your code to “dry” it on your thread and you take control at that point.

On the 14366 preview SDK, there a new class called CoreWetStrokeUpdateSource which seems to start opening up some of that “wet” ink processing, allowing me to hook code into the system’s thread that captures the ink and first draws it fluidly in response to the user’s pen moving before it gets collected and handed over to the UI thread for any custom drying.

This allows for scenarios where we can move the ink around as the user is drawing it and I think it’s what enables the new ruler that’s part of the Anniversary Update.

I wanted to experiment and so I thought I’d take that idea of a ‘ruler’ and extend it to include the idea of ‘ruled paper’. Mine is only a simple demo but I thought it highlighted how flexible and powerful the inking platform is becoming.

I made a simple app that displays a set of XAML-drawn lines;

1

and it’s using the InkToolbar to control the formatting (which, by default, has the ruler on it). This app operates in 3 modes. By default, it’s in freeform mode so I can just ink;

2

but if I tap (specifically with touch) then it changes mode into a SnapX or SnapY mode where it snaps drawn lines to the paper;

3

and I can also pinch to zoom the paper grid up to a larger size if I want a larger snap grid;

4

for the small piece of code I wrote to make this work, it actually works surprisingly well although the code needs updating to handle window resizing Smile

I essentially just placed a Canvas behind an InkCanvas and it’s on that background Canvas that I draw the grid;

<Page
  x:Class="App6.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:App6"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  mc:Ignorable="d">

  <Grid
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
      <RowDefinition
        Height="Auto" />
      <RowDefinition />
    </Grid.RowDefinitions>
    <InkToolbar
      TargetInkCanvas="{Binding ElementName=ink}" />
    <Canvas
      x:Name="drawCanvas"
      Grid.Row="1"
      RenderTransformOrigin="0,0">
      <Canvas.RenderTransform>
        <ScaleTransform
          x:Name="scaleTransform"
          CenterX="0"
          CenterY="0" />
      </Canvas.RenderTransform>
    </Canvas>
    <TextBlock
      Margin="8"
      x:Name="txtMode"
      FontSize="18"
      Grid.Row="1"
      HorizontalAlignment="Left"
      VerticalAlignment="Bottom" />
    <InkCanvas
      x:Name="ink"
      Grid.Row="1"
      Tapped="OnTapped"
      ManipulationMode="Scale"
      ManipulationDelta="OnInkManipulationDelta"/>
  </Grid>
</Page>

and so all we have here are a Canvas, a InkCanvas, a InkToolbar and a TextBlock to display the current status. I married this up with some code-behind which could do with a bit of tidying;

namespace App6
{
  using System;
  using System.Linq;
  using Windows.Devices.Input;
  using Windows.Foundation;
  using Windows.UI;
  using Windows.UI.Input.Inking.Core;
  using Windows.UI.Xaml.Controls;
  using Windows.UI.Xaml.Input;
  using Windows.UI.Xaml.Media;
  using Windows.UI.Xaml.Shapes;

  public sealed partial class MainPage : Page
  {
    enum DrawMode
    {
      FreeForm = 0,
      SnapY = 1,
      SnapX = 2
    }
    static MainPage()
    {
      lineBrush = new SolidColorBrush(Colors.LightBlue);
    }
    public MainPage()
    {
      this.InitializeComponent();

      this.scaledGridSize = BASE_GRID_SIZE;
      this.drawMode = DrawMode.FreeForm;
      this.Loaded += OnLoaded;
    }
    void OnLoaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
    {
      // We want to handle events when wet ink is being processed.
      var source = CoreWetStrokeUpdateSource.Create(this.ink.InkPresenter);

      // We should probably also handle the cancel event too.
      source.WetStrokeStarting += OnWetStrokeStarting;
      source.WetStrokeContinuing += OnWetStrokeContinuing;

      // Draw our grid lines, no resize handling yet.
      for (int i = 0; i < this.drawCanvas.ActualWidth; i += BASE_GRID_SIZE)
      {
        this.AddLine(i, 0, i, this.drawCanvas.ActualHeight);
      }
      for (int j = 0; j < this.drawCanvas.ActualHeight; j += BASE_GRID_SIZE)
      {
        this.AddLine(0, j, this.drawCanvas.ActualWidth, j);
      }
      this.UpdateDrawMode();
    }
    void UpdateDrawMode()
    {
      this.txtMode.Text = this.drawMode.ToString();
    }

    void AddLine(double x1, double y1, double x2, double y2)
    {
      var line = new Line()
      {
        X1 = x1,
        Y1 = y1,
        X2 = x2,
        Y2 = y2,
        Stroke = lineBrush
      };
      this.drawCanvas.Children.Add(line);
    }
    double GetNearestGridMultiple(double coordinate)
    {
      var result = 0.0d;
      var dividand = (int)(coordinate / this.scaledGridSize);
      var lower = dividand * this.scaledGridSize;
      var upper = (dividand + 1) * this.scaledGridSize;
      var lowerDistance = Math.Abs(lower - coordinate);
      var upperDistance = Math.Abs(upper - coordinate);
      if (lowerDistance < upperDistance)
      {
        result = lower;
      }
      else
      {
        result = upper;
      }
      return (result);
    }
    void OnWetStrokeStarting(CoreWetStrokeUpdateSource sender,
      CoreWetStrokeUpdateEventArgs args)
    {
      // NB: We are not on the UI thread and we probably are not meant to do much
      // work here as we could slow the experience. It looks like the API has been
      // designed to avoid us trying to get 'clever' and doing too much work.
      var firstPoint = args.NewInkPoints.First();

      if (this.drawMode != DrawMode.FreeForm)
      {
        if (this.drawMode == DrawMode.SnapY)
        {
          snapY = this.GetNearestGridMultiple(firstPoint.Position.Y);
        }
        else
        {
          snapX = this.GetNearestGridMultiple(firstPoint.Position.X);
        }
        this.SnapPoints(args);
      }
    }

    void OnWetStrokeContinuing(CoreWetStrokeUpdateSource sender, CoreWetStrokeUpdateEventArgs args)
    {
      this.SnapPoints(args);
    }
    void SnapPoints(CoreWetStrokeUpdateEventArgs args)
    {
      for (int i = 0; i < args.NewInkPoints.Count; i++)
      {
        if (snapX != null)
        {
          args.NewInkPoints[i] = new Windows.UI.Input.Inking.InkPoint(
            new Point(snapX.Value, args.NewInkPoints[i].Position.Y), args.NewInkPoints[i].Pressure);
        }
        else if (snapY != null)
        {
          args.NewInkPoints[i] = new Windows.UI.Input.Inking.InkPoint(
            new Point(args.NewInkPoints[i].Position.X, snapY.Value), args.NewInkPoints[i].Pressure);
        }
      }
    }
    bool IsTouchPoint(PointerRoutedEventArgs e)
    {
      return (e.Pointer.PointerDeviceType == PointerDeviceType.Touch);
    }
    void OnInkManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
    {
      var newScale = this.scaledGridSize * e.Delta.Scale;
      
      if ((newScale >= BASE_GRID_SIZE) && (newScale <= (MAX_SCALE * BASE_GRID_SIZE)))
      {
        this.scaleTransform.ScaleX = newScale / BASE_GRID_SIZE;
        this.scaleTransform.ScaleY = newScale / BASE_GRID_SIZE;           
        this.scaledGridSize = newScale;
      }
    }
    void OnTapped(object sender, TappedRoutedEventArgs e)
    {
      if (e.PointerDeviceType == PointerDeviceType.Touch)
      {
        int value = ((int)this.drawMode + 1);
        if (value > (int)DrawMode.SnapX)
        {
          value = 0;
        }
        this.drawMode = (DrawMode)value;

        this.snapX = this.snapY = null;

        this.UpdateDrawMode();
      }
    }
    double? snapX;
    double? snapY;
    DrawMode drawMode;
    static SolidColorBrush lineBrush;
    double scaledGridSize;
    static readonly int BASE_GRID_SIZE = 20;
    static readonly int MAX_SCALE = 4;
  }
}

and the main additions there beyond what I could do in 10586 are (I think);

    1. The new CoreWetStrokeUpdateSource is letting me pick up the ink as it is being input ‘wet’ and manipulate it (in my case, to snap the points to a new place).
    2. The simultaneous touch/ink support is letting me use manipulations on the InkCanvas to apply a scale transform to my underlying Canvas and make the grid larger/smaller with relative ease.

Here’s the app actually running – note that the screen capture doesn’t quite pick everything up here and some of the menus don’t seem to be captured;

Over on the ‘Context’ show, we’ve got an episode coming up around ink so there’ll be more discussion and demo code there in the coming week or so.

3 thoughts on “Windows 10 Anniversary Update–More on Inking with Wet Ink & Custom Rulers

Comments are closed.