Ink Geometry Pipeline & Rendering
The different rendering modes have already been introduced. This guide explains how to setup the pipeline and the rendering, step-by-step.
The visual result is dependent on the brush configuration, the pipeline configuration and the configuration of the platform rendering.
Raster Ink
This section describes the best practice for setting up the geometry and rendering pipeline for Raster Ink using a step-by-step approach.
Brush configuration
To setup the raster particle brush, two textures are needed:

The shape and the fill texture:
The shape parameter specifies the shape of the particle.
It accepts an image in which the non-transparent pixels define the particle shape.
The fill parameter specifies the fill pattern of the shape.
It accepts an image from which the pattern is sampled.
For both textures, you can also pass a sequence of images in this parameter. In this case, the method interprets them as mipmaps.
- Kotlin
- C#
- JavaScript
...
fun pencil(context: Context): RasterBrush {
   val opts = BitmapFactory.Options()
   opts.inSampleSize = 1
   opts.inScaled = false
   // Texture for shape
   val shapeTexture =
       BitmapFactory.decodeResource(context.resources, R.drawable.essential_shape, opts)
   // Texture for fill
   val fillTexture =
       BitmapFactory.decodeResource(context.resources, R.drawable.essential_fill_11, opts)
   // Convert to PNG
   val stream = ByteArrayOutputStream()
   shapeTexture!!.compress(Bitmap.CompressFormat.PNG, 100, stream)
   val shapeTextureByteArray = stream.toByteArray()
   val stream2 = ByteArrayOutputStream()
   fillTexture!!.compress(Bitmap.CompressFormat.PNG, 100, stream2)
   val fillTextureByteArray = stream2.toByteArray()
   // Create the raster brush
 var brush = RasterBrush(
     URIBuilder.getBrushURI("raster", "Pencil"), // name of the brush
     0.15f,                                      // spacing
     0.15f,                                      // scattering
     RotationMode.RANDOM,                        // rotation mode
     listOf(shapeTextureByteArray),              // shape texture
     listOf(), fillTextureByteArray,             // fill texture
     "",                                         // fill texture URI
     fillTexture.width.toFloat(),                // width of texture
     fillTexture.height.toFloat(),               // height of texture
     false,                                      // randomized fill
     BlendMode.MAX                               // mode of blending
 )
   shapeTexture.recycle()
   fillTexture.recycle()
   return brush
}
...
public PencilTool(Graphics graphics)
{
    Fill = new PixelInfo(new Uri("ms-appx:///Assets/textures/essential_fill_11.png"));
    Shape = new PixelInfo(new Uri("ms-appx:///Assets/textures/essential_shape.png"));
    Brush.Scattering = 0.05f;
    Brush.RotationMode = ParticleRotationMode.RotateRandom;
    Brush.FillTileSize = new Size(32.0f, 32.0f);
    Brush.FillTexture = graphics.CreateTexture(Fill.PixelData);
    Brush.ShapeTexture = graphics.CreateTexture(Shape.PixelData);
    ParticleSpacing = 0.3f;
}
  /* **************** RASTER BRUSH configuration **************** */
	pencil: new BrushGL(
    URIBuilder.getBrushURI("raster", "Pencil"),
    "/images/textures/essential_shape.png",
    "/images/textures/essential_fill_11.png",
    {
      spacing: 0.15,
      scattering: 0.15
    }),
    ...
PathPoint calculators
With all the relevant brushes defined, we need to configure the PathPoint calculators which influence the way sensor data changes the width, intensity and the tilt of the ink strokes.
First, the layout needs to be configured. Depending on the input provider, the layout can be adapted. For instance for touch input you are not receiving tilt nor pressure information.
- Kotlin
- C#
- JavaScript
...
import com.wacom.ink.PathPointLayout
...
override fun getLayout(): PathPointLayout {
    // Define different layouts for stylus and touch input
    if (isStylus) {
        return PathPointLayout(
            PathPoint.Property.X,
            PathPoint.Property.Y,
            PathPoint.Property.SIZE,
            PathPoint.Property.ALPHA,
            PathPoint.Property.ROTATION,
            PathPoint.Property.OFFSET_X
        )
    } else {
        return PathPointLayout(
            PathPoint.Property.X,
            PathPoint.Property.Y,
            PathPoint.Property.SIZE,
            PathPoint.Property.ALPHA
        )
    }
}
...
...
using Wacom.Ink.Geometry;
using Wacom.Ink.Rendering;
...
public override PathPointLayout GetLayout(Windows.Devices.Input.PointerDeviceType deviceType)
 {
     switch (deviceType)
     {
         case Windows.Devices.Input.PointerDeviceType.Mouse:
         case Windows.Devices.Input.PointerDeviceType.Touch:
             return new PathPointLayout(PathPoint.Property.X,
                                         PathPoint.Property.Y,
                                         PathPoint.Property.Size,
                                         PathPoint.Property.Alpha);
         case Windows.Devices.Input.PointerDeviceType.Pen:
             return new PathPointLayout(PathPoint.Property.X,
                                         PathPoint.Property.Y,
                                         PathPoint.Property.Size,
                                         PathPoint.Property.Alpha,
                                         PathPoint.Property.OffsetX,
                                         PathPoint.Property.OffsetY);
         default:
             throw new Exception("Unknown input device type");
     }
 }
// In the JavaScript implementation layout is predefined within PathPointContext
let context = new PathPointContext();
As touch and stylus input can be optimized for the different kinds of input sensor data, you need to define calculators which receive the previous, current and next sensor data input and computes the appropriate PathPoint.
For instance, touch data includes neither pressure information nor tilt orientation like a stylus.
Thus, to achieve the same difference in alpha intensity or variation of ink stroke width, only speed information can be used.
The function to compute width based on the current speed of the input data is offered by the engine.
Here, the min and max value define the range of the width, while min and max speed define how the width is changed within its range. The result is a normalized value between 0 and 1. Finally, with the remap function the normalized value can be modified, e.g., to reverse the behavior. So that, for instance a slow movement can increase the width of the stroke rather reduce it, while if the finger moves faster the stroke becomes thinner.
- Kotlin
- C#
- JavaScript
...
companion object {
    val uri = URIBuilder.getToolURI("raster", "pencil")
    // Minimum size of the pencil tip
    val MIN_PENCIL_SIZE = 4f
    // Maximum size of the pencil tip
    val MAX_PENCIL_SIZE = 25f
    // Minimum alpha values for the particles
    val MIN_ALPHA = 0.1f
    // Maximum alpha values for the particles
    val MAX_ALPHA = 0.7f
    //  Unit for speed is px/second.
    //  NOTE: This needs to be optimized for different Pixel densities of different devices
    val MAX_SPEED = 15000f
}
...
override val touchCalculator: Calculator = { previous, current, next ->
       // Use the following to compute size based on speed:
       var size = current.computeValueBasedOnSpeed(
           previous,
           next,
           minValue = MIN_PENCIL_SIZE,
           maxValue = MAX_PENCIL_SIZE,
           minSpeed = 80f,
           maxSpeed = MAX_SPEED,
           // reverse behaviour
           remap = { 1f - it}
       )!!
       var alpha = current.computeValueBasedOnSpeed(
           previous,
           next,
           minValue = MIN_ALPHA,
           maxValue = MAX_ALPHA,
           minSpeed = 80f,
           maxSpeed = MAX_SPEED,
           // reverse behaviour
           remap = { 1f - it}
       )
       if (alpha == null) {
           alpha = previousAlpha
       } else {
           previousAlpha = alpha
       }
       PathPoint(current.x, current.y, size = size, alpha = alpha)
   }
...
private static readonly ToolConfig mSizeConfig = new ToolConfig()
{
    initValue = 4,
    finalValue = 10,
    minValue = 4,
    maxValue = 10,
    minSpeed = 80,
    maxSpeed = 1400,
};
private static readonly ToolConfig mAlphaConfig = new ToolConfig()
{
    initValue = 0.1f,
    finalValue = 0.5f,
    minValue = 0.1f,
    maxValue = 0.5f,
    minSpeed = 80,
    maxSpeed = 1400,
};
...
protected override ToolConfig SizeConfig => mSizeConfig;
protected override ToolConfig AlphaConfig => mAlphaConfig;
...
/// <summary>
/// Calculator delegate for input from mouse input
/// Calculates the path point properties based on pointer input.
/// </summary>
/// <param name="previous"></param>
/// <param name="current"></param>
/// <param name="next"></param>
/// <returns>PathPoint with calculated properties</returns>
protected PathPoint CalculatorForMouseAndTouch(PointerData previous, PointerData current, PointerData next)
{
    var size = current.ComputeValueBasedOnSpeed(previous, next,
                SizeConfig.minValue, SizeConfig.maxValue,
                SizeConfig.initValue, SizeConfig.finalValue,
                SizeConfig.minSpeed, SizeConfig.maxSpeed,
                SizeConfig.remap);
    if (size.HasValue)
    {
        PreviousSize = size.Value;
    }
    else
    {
        size = PreviousSize;
    }
    var alpha = current.ComputeValueBasedOnSpeed(previous, next,
                  AlphaConfig.minValue, AlphaConfig.maxValue,
                  AlphaConfig.initValue, AlphaConfig.finalValue,
                  AlphaConfig.minSpeed, AlphaConfig.maxSpeed,
                  AlphaConfig.remap);
    if (alpha.HasValue)
    {
        PreviousAlpha = alpha.Value;
    }
    else
    {
        alpha = PreviousAlpha;
    }
    PathPoint pp = new PathPoint(current.X, current.Y)
    {
        Size = size,
        Alpha = alpha
    };
    return pp;
}
// In the JavaScript implementation you need to define the dynamics for the PathPoint calculators
/* ******* RASTER TOOLS ******* */
  pencil: {
    brush: BrushPalette.pencil,
    dynamics: {
      size: {
        value: {
          min: 4,
          max: 5
        },
        velocity: {
          min: 80,
          max: 1400,
          remap: v => ValueTransformer.reverse(v)
        }
      },
      alpha: {
        value: {
          min: 0.05,
          max: 0.2
        },
        velocity: {
          min: 80,
          max: 1400
        }
      },
      rotation: {
        dependencies: [SensorChannel.Type.ROTATION, SensorChannel.Type.AZIMUTH]
      },
      scaleX: {
        dependencies: [SensorChannel.Type.RADIUS_X, SensorChannel.Type.ALTITUDE],
        value: {
          min: 1,
          max: 3
        }
      },
      scaleY: {
        dependencies: [SensorChannel.Type.RADIUS_Y],
        value: {
          min: 1,
          max: 3
        }
      },
      offsetX: {
        dependencies: [SensorChannel.Type.ALTITUDE],
        value: {
          min: 2,
          max: 5
        }
      }
    }
  }
The configuration for stylus input offers more opportunities to configure a natural behavior of a real world writing tool. Here, pressure sensor data can be used to increase the width of tools or change the intensity of the color. Also, if a stylus is tilted it would naturally change the contact surface area. If you want to implement pencil hatching, water color effects, or fountain pens, you need to implement the effect here.
NOTE: Input sensor quality varies, thus, test if the desired behavior is achieved with different kinds of stylus input.
- Kotlin
- C#
...
var previousAlpha = 0.2f
...
override val stylusCalculator: Calculator = { previous, current, next ->
       // calculate the offset of the pencil tip due to tilted position
       val cosAltitudeAngle = cos(current.altitudeAngle!!)
       val sinAzimuthAngle = sin(current.azimuthAngle!!)
       val cosAzimuthAngle = cos(current.azimuthAngle!!)
       val x = sinAzimuthAngle * cosAltitudeAngle
       val y = cosAltitudeAngle * cosAzimuthAngle
       val offsetY = 5f * -x
       val offsetX = 5f * -y
       // compute the rotation
       val rotation = current.computeNearestAzimuthAngle(previous)
       // now, based on the tilt of the pencil the size of the brush size is increasing, as the
       // pencil tip is covering a larger area
       val size = max(MIN_PENCIL_SIZE,
           min(MAX_PENCIL_SIZE, MIN_PENCIL_SIZE + 20f * abs(current.altitudeAngle!!)))
       // Change the intensity of alpha value by pressure of speed, if available else use speed
       var alpha = if (current.force == -1f) {
           current.computeValueBasedOnSpeed(
               previous,
               next,
               minValue = MIN_ALPHA,
               maxValue = MAX_ALPHA,
               minSpeed = 0f,
               maxSpeed = 3500f,
               // reverse behaviour
               remap = { 1.0f - it}
           )
       } else {
           current.computeValueBasedOnPressure(
               minValue = MIN_ALPHA,
               maxValue = MAX_ALPHA,
               minPressure = 0.0f,
               maxPressure = 1.0f,
               remap = { v: Float -> v.toDouble().pow(1).toFloat() }
           )
       }
       if (alpha == null) {
           alpha = previousAlpha
       } else {
           previousAlpha = alpha
       }
       PathPoint(
           current.x, current.y,
           alpha = alpha, size = size, rotation = rotation,
           offsetX = offsetX, offsetY = offsetY
           )
   }
...
/// <summary>
/// Calculator delegate for input from a stylus (pen)
/// Calculates the path point properties based on pointer input.
/// </summary>
/// <param name="previous"></param>
/// <param name="current"></param>
/// <param name="next"></param>
/// <returns>PathPoint with calculated properties</returns>
protected override PathPoint CalculatorForStylus(PointerData previous, PointerData current, PointerData next)
{
    var cosAltitudeAngle = Math.Cos(current.AltitudeAngle.Value);
    var sinAzimuthAngle = Math.Sin(current.AzimuthAngle.Value);
    var cosAzimuthAngle = Math.Cos(current.AzimuthAngle.Value);
    // calculate the offset of the pencil tip due to tilted position
    var x = sinAzimuthAngle * cosAltitudeAngle;
    var y = cosAltitudeAngle * cosAzimuthAngle;
    var offsetY = -x;
    var offsetX = -y;
    // compute the rotation
    var rotation = current.ComputeNearestAzimuthAngle(previous);
    // now, based on the tilt of the pencil the size of the brush size is increasing, as the
    // pencil tip is covering a larger area
    var size = Math.Max(mSizeConfig.minValue, mSizeConfig.minValue + 20f * Math.Abs(current.AltitudeAngle.Value));
    // Change the intensity of alpha value by pressure of speed, if available else use speed
    var alpha = (!current.Force.HasValue)
        ? current.ComputeValueBasedOnSpeed(previous, next, 0.1f, 0.7f, null, null, 0f, 3500f)
        : ComputeValueBasedOnPressure(current, 0.1f, 0.7f, 0.0f, 1.0f);
    if (!alpha.HasValue)
    {
        alpha = PreviousAlpha;
    }
    else
    {
        PreviousAlpha = alpha.Value;
    }
    PathPoint pp = new PathPoint(current.X, current.Y)
    {
        Size = size,
        Alpha = alpha,
        OffsetX = (float)offsetX,
        OffsetY = (float)offsetY
    };
    return pp;
}
Pipeline
The RasterInkBuilder defines the Raster Ink pipeline:
- Kotlin
- C#
- JavaScript
import com.wacom.ink.*
import com.wacom.ink.format.rendering.RasterBrush
import com.wacom.ink.pipeline.PathProducer
import com.wacom.ink.pipeline.SmoothingFilter
import com.wacom.ink.pipeline.SplineInterpolator
import com.wacom.ink.pipeline.SplineProducer
import com.wacom.ink.pipeline.base.ProcessorResult
...
class RasterInkBuilder() {
    // The pipeline components:
    lateinit var pathProducer: PathProducer
    lateinit var smoother: SmoothingFilter
    lateinit var splineProducer: SplineProducer
    lateinit var splineInterpolator: SplineInterpolator
    lateinit var pathSegment: PathSegment
    lateinit var tool: RasterTool
    ...
    fun updatePipeline(newTool: Tool) {
      if (newTool !is RasterTool) {
          throw IllegalArgumentException("Invalid tool")
      } else {
          tool = newTool
          var layout = tool.getLayout()
          // Path producer needs to know the layout
          pathProducer = PathProducer(layout, tool.getCalculator())
          smoother = SmoothingFilter(layout.size) // Dimension
          splineProducer = SplineProducer(layout) // Layout
          splineInterpolator = SplineInterpolator(
              spacing = tool.brush.spacing, // Spacing between two successive sample.
              splitCount = 1,               // Determines the number of iterations for the
                                            // discretization.
              calculateDerivatives = true,  // Calculate derivatives
              interpolateByLength = true    // Interpolate by length
          )
          pathSegment = PathSegment()
          splineProducer.keepAllData = true
      }
  }
}  
...
/// <summary>
   /// Manages ink geometry pipeline for raster (particle) brushes.
   /// </summary>
   public class RasterInkBuilder : InkBuilder
   {
       #region Fields
       private const float defaultSpacing = 0.15f;
       private const int splitCount = 1;
       #endregion
       #region Constructors
       public RasterInkBuilder()
       {
       }
       #endregion
       #region Properties
       public event EventHandler LayoutUpdated;
       #endregion
       ...
       #region Public Interface
       public void UpdatePipeline(PathPointLayout layout, Calculator calculator, float spacing)
       {
           bool layoutChanged = false;
           bool otherChange = false;
           if ((Layout == null) || (layout.ChannelMask != Layout.ChannelMask))
           {
               Layout = layout;
               layoutChanged = true;
           }
           if (mPathProducer == null || calculator != mPathProducer.PathPointCalculator || layoutChanged)
           {
               mPathProducer = new PathProducer(Layout, calculator, true);
               otherChange = true;
           }
           if (mSmoothingFilter == null || layoutChanged)
           {
               mSmoothingFilter = new SmoothingFilter(Layout.Count)
               {
                   KeepAllData = true
               };
               otherChange = true;
           }
           if (SplineProducer == null || layoutChanged)
           {
               SplineProducer = new SplineProducer(Layout, true);
               otherChange = true;
           }
           if (SplineInterpolator == null || layoutChanged)
           {
               SplineInterpolator = new DistanceBasedInterpolator(Layout, spacing, splitCount, true, true, true);
               otherChange = true;
           }
           ((DistanceBasedInterpolator) SplineInterpolator).Spacing = spacing;
           if (layoutChanged || otherChange)
           {
               LayoutUpdated?.Invoke(this, EventArgs.Empty);
           }
       }       
       #endregion
   }
}
// In the JavaScript implementation the setup of the pipeline within InkBuilderAbstract which is inheritat by InkBuilder
this.builder = new InkBuilder();
Raster View
Next, setup the raster view:
- Kotlin
- C#
- JavaScript
import com.wacom.ink.egl.EGLRenderingContext
import com.wacom.ink.format.tree.data.Stroke
import com.wacom.ink.format.tree.nodes.StrokeNode
import com.wacom.ink.rasterization.InkCanvas
import com.wacom.ink.rasterization.Layer
import com.wacom.ink.rasterization.StrokeRenderer
import com.wacom.ink.rendering.BlendMode
...
/**
 * This is a surface for drawing raster inking.
 * Extends from SurfaceView
 */
class RasterView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : SurfaceView(context, attrs, defStyleAttr) {
...
 private var inkCanvas: InkCanvas? = null
 private lateinit var viewLayer: Layer
 private lateinit var strokesLayer: Layer
 private lateinit var currentFrameLayer: Layer
 private lateinit var strokeRenderer: StrokeRenderer
 private var rasterInkBuilder = RasterInkBuilder() // The ink builder
 var rasterTool: RasterTool = PencilTool(context) // The default tool
 ...
 init {
         rasterInkBuilder.updatePipeline(PencilTool(context))
         setZOrderOnTop(true);
         holder.setFormat(PixelFormat.TRANSPARENT);
         // the drawing is going to be performer on background
         // on a surface, so we initialize the surface
         holder.addCallback(object : SurfaceHolder.Callback {
             override fun surfaceChanged(holder: SurfaceHolder?, format: Int, w: Int, h: Int) {
                 // here, once the surface is crated, we are going to initialize ink canvas
                 // firs we check that there is no other inkCanvas, in case there is we dispose it
                 if (inkCanvas?.isDisposed == false) {
                     releaseResources();
                 }
                 inkCanvas =
                     InkCanvas(holder, EGLRenderingContext.EGLConfiguration(8, 8, 8, 8, 8, 8))
                 viewLayer = inkCanvas!!.createViewLayer(w, h)
                 strokesLayer = inkCanvas!!.createLayer(w, h)
                 currentFrameLayer = inkCanvas!!.createLayer(w, h)
                 inkCanvas!!.clearLayer(currentFrameLayer)
                 strokeRenderer =
                     StrokeRenderer(inkCanvas, PencilTool(context).brush.toParticleBrush(), w, h)
                 drawStrokes(strokeNodeList)
                 renderView()
                 if (listener != null) {
                     listener.onSurfaceCreated()
                 }
             }
             override fun surfaceDestroyed(holder: SurfaceHolder?) {
                 releaseResources();
             }
             override fun surfaceCreated(p0: SurfaceHolder?) {
                 //we don't have to do anything here
             }
         })
     }
}
...
/// <summary>
/// Creates and manages rendering brush and graphics objects and interactions between them;
/// Recieves pointer input events and passes them on to the rendering brush to feed into
/// the ink geometry pipeline
/// </summary>
public class Renderer
{
    #region Fields
    private SwapChainPanel m_swapChainPanel;
    private PointerManager m_pointerManager;
    private Graphics m_graphics = new Graphics();
    #endregion
    ...
    #region Constructor
    ...
    /// <summary>
    /// Constructor. Creates rendering brush; initializes graphics
    /// </summary>
    /// <param name="swapChainPanel">SwapChainPanel on which to render captured ink</param>
    /// <param name="brushType">Type of brush to use</param>
    /// <param name="thickness">Relative thickness of brush</param>
    /// <param name="color">Color of ink</param>
    public Renderer(SwapChainPanel swapChainPanel, RasterBrushStyle style, MediaColor color)
    {
        StrokeHandler = new RasterStrokeHandler(this, style, color, m_graphics);
        m_swapChainPanel = swapChainPanel;
        m_graphics.GraphicsReady += OnGraphicsReady;
        m_graphics.Initialize(m_swapChainPanel, false);
    }
    #endregion
class InkCanvasRaster extends InkCanvas {
	constructor(canvas, width, height) {
		super();
		this.canvas = InkCanvasGL.createInstance(canvas, width, height);
		this.strokesLayer = this.canvas.createLayer();
		this.strokeRenderer = new StrokeRendererGL(this.canvas);
	}
}
Input Handling
Input events from the operating system are handled using the following:
- Kotlin
- C#
- JavaScript
// This function is going to be call when we touch the surface
fun surfaceTouch(event: MotionEvent) {
    // Check the input method
    if (event.resolveToolType() == InkInputType.PEN) {
        if ((newTool) || (!isStylus)) {
            newTool = false
            isStylus = true
            rasterInkBuilder.updateInputMethod(isStylus)
        }
    } else {
        if ((newTool) || (isStylus)) {
            newTool = false
            isStylus = false
            rasterInkBuilder.updateInputMethod(isStylus)
        }
    }
    for (i in 0 until event.historySize) {
        val pointerData = event.historicalToPointerData(i)
        rasterInkBuilder.add(pointerData.phase, pointerData, null)
    }
    val pointerData = event.toPointerData()
    rasterInkBuilder.add(pointerData.phase, pointerData, null)
    val (added, predicted) = rasterInkBuilder.build()
    if (pointerData.phase == Phase.BEGIN) {
        // initialize the sensor data each time a new stroke begin
        val pair = inkEnvironmentModel.createSensorData(event)
        sensorData = pair.first
        channelList = pair.second
    }
    for (channel in channelList) {
        when (channel.type) {
            InkSensorType.X -> sensorData.add(channel, pointerData.x)
            InkSensorType.Y -> sensorData.add(channel, pointerData.y)
            InkSensorType.TIMESTAMP -> sensorData.addTimestamp(
                channel,
                pointerData.timestamp
            )
            InkSensorType.PRESSURE -> sensorData.add(channel, pointerData.force!!)
            InkSensorType.ALTITUDE -> sensorData.add(channel, pointerData.altitudeAngle!!)
            InkSensorType.AZIMUTH -> sensorData.add(channel, pointerData.azimuthAngle!!)
        }
    }
    if ((pointerData.phase == Phase.END) && (rasterInkBuilder.splineProducer.allData != null)) {
        addStroke()
        sensorDataList.add(sensorData)
    }
    if (added != null) {
        drawStroke(event, added, predicted)
    }
    renderView()
}
/// <summary>
/// Initiates capture of an ink stroke
/// </summary>
private void OnPointerPressed(object sender, PointerRoutedEventArgs args)
{
    // If currently there is an unfinished stroke - do not interrupt it
    if (!m_pointerManager.OnPressed(args))
        return;
    m_swapChainPanel.CapturePointer(args.Pointer);
    StrokeHandler.OnPressed(m_swapChainPanel, args);
}
/// <summary>
/// Updates current ink stroke with new pointer input
/// </summary>
private void OnPointerMoved(object sender, PointerRoutedEventArgs args)
{
    // Ignore events from other pointers
    if (!m_pointerManager.OnMoved(args))
        return;
    StrokeHandler.OnMoved(m_swapChainPanel, args);
}
/// <summary>
/// Completes capture of an ink stroke
/// </summary>
private void OnPointerReleased(object sender, PointerRoutedEventArgs args)
{
    // Ignore events from other pointers
    if (!m_pointerManager.OnReleased(args))
        return;
    m_swapChainPanel.ReleasePointerCapture(args.Pointer);
    StrokeHandler.OnReleased(m_swapChainPanel, args);
    if (!StrokeHandler.IsSelecting)
    {
        RenderNewStrokeSegment();
        BlendCurrentStrokesLayerToAllStrokesLayer();
        RenderBackbuffer();
        PresentGraphics();
        var ptrDevice = args.GetCurrentPoint(m_swapChainPanel).PointerDevice;
        StrokeHandler.StoreCurrentStroke(ptrDevice.PointerDeviceType);
    }
}
// StrokeHandler for raster strokes
public class RasterStrokeHandler : StrokeHandler
{
...
  public override void OnPressed(UIElement uiElement, PointerRoutedEventArgs args)
  {
     m_startRandomSeed = (uint)mRand.Next();
     base.OnPressed(uiElement, args);
     ActiveTool.OnPressed(uiElement, args);
  }
  public override void OnMoved(UIElement uiElement, PointerRoutedEventArgs args)
  {
     ActiveTool.OnMoved(uiElement, args);
  }
  public override void OnReleased(UIElement uiElement, PointerRoutedEventArgs args)
  {
     ActiveTool.OnReleased(uiElement, args);
  }
...
}
class InkCanvas extends InkController {
...
	begin(sensorPoint) {
		if (this.forward) return this.inkCanvasRaster.begin(sensorPoint);
		this.reset(sensorPoint);
		this.builder.add(sensorPoint);
		this.builder.build();
	}
	move(sensorPoint) {
		if (this.forward) return this.inkCanvasRaster.move(sensorPoint);
		if (this.requested) {
			this.builder.ignore(sensorPoint);
			return;
		}
		this.builder.add(sensorPoint);
		if (!this.requested) {
			this.requested = true;
			this.builder.build();
			requestAnimationFrame(() => (this.requested = false));
		}
	}
	end(sensorPoint) {
		if (this.forward) return this.inkCanvasRaster.end(sensorPoint);
		this.builder.add(sensorPoint);
		this.builder.build();
	}
  ...
Finally, draw the stroke:
- Kotlin
- C#
- JavaScript
fun drawStrokes(strokeList: MutableList<Pair<StrokeNode, RasterBrush>>) {
    for (stroke in strokeList) {
        drawStroke(stroke.first, stroke.second)
    }
}
...
// Draw stroke
private fun drawStroke(
    event: MotionEvent,
    added: InterpolatedSpline,
    predicted: InterpolatedSpline?
) {
    strokeRenderer.drawPoints(added, defaults, event.action == MotionEvent.ACTION_UP)
    if (predicted != null) {
        strokeRenderer.drawPrelimPoints(predicted, defaults)
    }
    if (event.action != MotionEvent.ACTION_UP) {
        inkCanvas!!.setTarget(currentFrameLayer, strokeRenderer.strokeUpdatedArea)
        inkCanvas!!.clearColor()
        inkCanvas!!.drawLayer(strokesLayer, BlendMode.SOURCE_OVER)
        strokeRenderer.blendStrokeUpdatedArea(currentFrameLayer, rasterTool.getBlendMode())
    } else {
        strokeRenderer.blendStroke(strokesLayer, rasterTool.getBlendMode())
        inkCanvas!!.setTarget(currentFrameLayer)
        inkCanvas!!.clearColor()
        inkCanvas!!.drawLayer(strokesLayer, BlendMode.SOURCE_OVER)
    }
}
...
public override void RenderAllStrokes(RenderingContext context, IEnumerable<Identifier> excluded, Rect? clipRect)
{
    foreach (var stroke in m_dryStrokes)
    {
        // Draw current stroke
        context.SetTarget(mRenderer.CurrentStrokeLayer);
        context.ClearColor(Colors.Transparent);
        DoRenderStroke(context, stroke, mRenderer.TranslationLayerPainted);
        // Blend stroke to Scene Layer
        context.SetTarget(mRenderer.SceneLayer);
        context.DrawLayer(mRenderer.CurrentStrokeLayer, null, Ink.Rendering.BlendMode.SourceOver);
        // Blend Current Stroke to All Strokes Layer
        context.SetTarget(mRenderer.AllStrokesLayer);
        context.DrawLayer(mRenderer.CurrentStrokeLayer, null, Ink.Rendering.BlendMode.SourceOver);
    }
}
...
/// <summary>
/// Draw stroke
/// </summary>
/// <param name="renderingContext">RenderingContext to draw to</param>
/// <param name="o">Cached stroke (as object)</param>
public override void DoRenderStroke(RenderingContext renderingContext, object o, bool translationLayerPainted)
{
    RasterInkStroke stroke = (RasterInkStroke)o;
    renderingContext.DrawParticleStroke(stroke.Path, stroke.StrokeConstants,
                                        ActiveTool.Brush, Ink.Rendering.BlendMode.SourceOver,
                                        stroke.RandomSeed);
}
draw(pathPart) {
		this.drawPath(pathPart);
		if (pathPart.phase == InkBuilder.Phase.END) {
			if (this.strokeRenderer) {
				let stroke = this.strokeRenderer.toStroke(this.builder);
				this.dataModel.add(stroke)
			}
		}
	}
drawPath(pathPart) {
	this.strokeRenderer.draw(pathPart.added, pathPart.phase == InkBuilder.Phase.END);
	if (pathPart.phase == InkBuilder.Phase.UPDATE) {
		this.strokeRenderer.drawPreliminary(pathPart.predicted);
		let dirtyArea = this.canvas.bounds.intersect(this.strokeRenderer.updatedArea);
		if (dirtyArea) {
			this.canvas.clear(dirtyArea);
			this.canvas.blend(this.strokesLayer, {rect: dirtyArea});
			this.strokeRenderer.blendUpdatedArea();
		}
	}
	else if (pathPart.phase == InkBuilder.Phase.END) {
		let dirtyArea = this.canvas.bounds.intersect(this.strokeRenderer.strokeBounds.union(this.strokeRenderer.updatedArea));
		if (dirtyArea) {
			dirtyArea = dirtyArea.ceil();
			if (!this.selector && !this.intersector) {
				if (app.type == app.Type.VECTOR) {
					let size = config.getSize(this.toolID);
					if (size.max < 2 || size.min < 1)
						this.strokeRenderer.draw(this.builder.getInkPath(), true);
				}
				this.strokeRenderer.blendStroke(this.strokesLayer);
				this.canvas.clear(dirtyArea);
				this.canvas.blend(this.strokesLayer, {rect: dirtyArea});
			}
		}
	}
}
Vector Ink
This section describes the best practice for setting up the geometry and rendering pipeline for Vector Ink using a step-by-step approach.
Brush configuration
Vector Ink brush configuration is defined by an arbitrary convex polygon:

- Kotlin
- C#
- JavaScript
import com.wacom.ink.format.enums.RotationMode
import com.wacom.ink.format.rendering.RasterBrush
import com.wacom.ink.rendering.BlendMode
import com.wacom.ink.rendering.BrushPrototype
import com.wacom.ink.rendering.VectorBrush
...
class BrushPalette {
...
fun circle(): VectorBrush {
    return VectorBrush(
        "circle",  // inSampleSize
        arrayOf(
            BrushPrototype(FloatArrayList(ShapeFactory.createCircle(4)), 0f),
            BrushPrototype(FloatArrayList(ShapeFactory.createCircle(8)), 2f),
            BrushPrototype(FloatArrayList(ShapeFactory.createCircle(16)), 6f),
            BrushPrototype(FloatArrayList(ShapeFactory.createCircle(32)), 18f)
        )
    ) // Brush prototypes for different level of details
}
...
class ShapeFactory {
    companion object {
        /**
         * Creates polygon with circle shape
         *
         * @param {int} [n=20] number of polygon segments
         * @param {int} [r=1] radius (0 < r <= 1)
         * @param {Point} [c={x: 0, y: 0}] center
         * @return {Polygon} circle shape points
         */
        @JvmStatic
        fun createCircle(n: Int = 20, r: Float = 1f,
          c: PointF = PointF(0f, 0f)): FloatArray {
            return createEllipse(n, r, r, c);
        }
        ...
    }
}
}
public class VectorBrushFactory
{
  ...
	public static List<Vector2> CreateEllipseBrush(int pointsNum, float width, float height)
	{
		List<Vector2> brushPoints = new List<Vector2>();
		double radiansStep = Math.PI * 2 / pointsNum;
		double currentRadian = 0.0;
		for (var i = 0; i < pointsNum; i++)
		{
			currentRadian = i * radiansStep;
			brushPoints.Add(new Vector2((float)(width * Math.Cos(currentRadian)),
										(float)(height * Math.Sin(currentRadian))));
		}
		return brushPoints;
	}
	 ...
}
circle: new Brush2D(URIBuilder.getBrushURI("vector", "Circle"), [
	BrushPrototype.create(BrushPrototype.Type.CIRCLE, 0, 4),
	BrushPrototype.create(BrushPrototype.Type.CIRCLE, 2, 8),
	BrushPrototype.create(BrushPrototype.Type.CIRCLE, 6, 16),
	BrushPrototype.create(BrushPrototype.Type.CIRCLE, 18, 32)
])
PathPoint calculators
The PathPoint calculators are defined in a similar way as for the raster tools. Again, we need to define the layout of the PathPoints:
- Kotlin
- C#
- JavaScript
override fun getLayout(): PathPointLayout {
    if (isStylus) {
        /**
         * The currently layout will use:
         * - Coordinate values (X and Y)
         * - Size - the size of the brush at any point of the stroke
         * - For tilt effect the rotation, scale y and offset y
         */
        return PathPointLayout(
            PathPoint.Property.X,
            PathPoint.Property.Y,
            PathPoint.Property.SIZE,
            PathPoint.Property.ROTATION,
            PathPoint.Property.SCALE_Y,
            PathPoint.Property.OFFSET_Y
        )
    } else {
        /**
         * The currently layout will use:
         * - Coordinate values (X and Y)
         * - Size - the size of the brush at any point of the stroke
         */
        return PathPointLayout(
            PathPoint.Property.X,
            PathPoint.Property.Y,
            PathPoint.Property.SIZE
        )
    }
}
/// <summary>
/// Vector drawing tool for rendering pen-style output
/// </summary>
class PenTool : VectorDrawingTool
{
...
public override PathPointLayout GetLayout(Windows.Devices.Input.PointerDeviceType deviceType)
{
    switch (deviceType)
    {
        case Windows.Devices.Input.PointerDeviceType.Mouse:
        case Windows.Devices.Input.PointerDeviceType.Touch:
            return new PathPointLayout(PathPoint.Property.X,
                                       PathPoint.Property.Y,
                                      PathPoint.Property.Size);
        case Windows.Devices.Input.PointerDeviceType.Pen:
            return new PathPointLayout(PathPoint.Property.X,
                                       PathPoint.Property.Y,
                                       PathPoint.Property.Size,
                                       PathPoint.Property.OffsetY);
        default:
            throw new Exception("Unknown input device type");
    }
}
TBD
Pipeline
The VectorInkBuilder is a class that handles the path building, using the pipeline.
It processes the input to the whole pipeline in order to produce polygons from the input.
After that, the polygons are converted to native structures.
- Kotlin
- C#
- JavaScript
import com.wacom.ink.PathSegment
import com.wacom.ink.Phase
import com.wacom.ink.PointerData
import com.wacom.ink.Spline
import com.wacom.ink.pipeline.*
import com.wacom.ink.pipeline.base.ProcessorResult
...
class VectorInkBuilder() {
    // The pipeline components:
    internal lateinit var pathProducer: PathProducer
    internal lateinit var smoother: SmoothingFilter
    internal lateinit var splineProducer: SplineProducer
    internal lateinit var splineInterpolator: SplineInterpolator
    internal lateinit var brushApplier: BrushApplier
    internal lateinit var convexHullChainProducer: ConvexHullChainProducer
    internal lateinit var polygonMerger: PolygonMerger
    internal lateinit var polygonSimplifier: PolygonSimplifier
    internal lateinit var bezierPathBuilder: PolygonToBezierPathProducer
    internal lateinit var pathSegment: PathSegment
    lateinit var tool: VectorTool
    fun updatePipeline(newTool: Tool) {
        if (newTool !is VectorTool) {
            throw IllegalArgumentException("Invalid tool")
        } else {
            tool = newTool
            var layout = tool.getLayout()
            pathProducer = PathProducer(layout, tool.getCalculator())
            smoother = SmoothingFilter(
              layout.count(),     // Dimension
              30)                 // Moving average window size
            splineProducer = SplineProducer(layout) // Layout
            splineInterpolator = SplineInterpolator(
              tool.brush.spacing, // Spacing between two successive sample.
              6,                  // Determines the number of iterations for the discretization.
              false,              // Calculate derivatives
              false)              // Interpolate by length
            brushApplier = BrushApplier(
              tool.brush,         // Vector brush
              true)               // Keep all data
            convexHullChainProducer = ConvexHullChainProducer()
            polygonMerger = PolygonMerger()
            polygonSimplifier = PolygonSimplifier(0.1f) // Epsilon
            bezierPathBuilder = PolygonToBezierPathProducer()
            pathSegment = PathSegment()
            splineProducer.keepAllData = true
        }
    }
}    
 ...
/// <summary>
/// Manages ink geometry pipeline for vector brushes.
/// </summary>
public class VectorInkBuilder : InkBuilder
{
...
#region Calculators
public void UpdatePipeline(PathPointLayout layout, Calculator calculator, VectorBrush brush)
{
    bool layoutChanged = false;
    if ((Layout == null) || (layout.ChannelMask != Layout.ChannelMask))
    {
        Layout = layout;
        layoutChanged = true;
    }
    if (mPathProducer == null || calculator != mPathProducer.PathPointCalculator || layoutChanged)
    {
        mPathProducer = new PathProducer(Layout, calculator)
        {
            KeepAllData = true
        };
    }
    if (mSmoothingFilter == null || layoutChanged)
    {
        mSmoothingFilter = new SmoothingFilter(Layout.Count)
        {
            KeepAllData = true
        };
    }
    if (SplineProducer == null || layoutChanged)
    {
        SplineProducer = new SplineProducer(Layout)
        {
            KeepAllData = true
        };
    }
    if (SplineInterpolator == null || layoutChanged)
    {
        SplineInterpolator = new CurvatureBasedInterpolator(Layout)
        {
            KeepAllData = true
        };
    }
    if (BrushApplier == null || (brush != BrushApplier.Prototype) || layoutChanged)
    {
        BrushApplier = new BrushApplier(Layout, brush)
        {
            KeepAllData = true
        };
    }
    if (ConvexHullChainProducer == null)
    {
        ConvexHullChainProducer = new ConvexHullChainProducer()
        {
            KeepAllData = true
        };
    }
    if (mPolygonMerger == null)
    {
        mPolygonMerger = new PolygonMerger()
        {
            KeepAllData = true
        };
    }
    if (PolygonSimplifier == null)
    {
        PolygonSimplifier = new PolygonSimplifier(0.1f)
        {
            KeepAllData = true
        };
    }
}
#endregion
// In the JavaScript implementation the setup of the pipeline within InkBuilderAbstract which is inheritat by InkBuilder
this.builder = new InkBuilder();
Vector View
Next, the vector view needs to be setup. This class is a simple view that is used to draw vector inking. Vector inking is a Path, so it is drawn by extending the platform rendering method and drawing the path there.
- Kotlin
- C#
- JavaScript
import com.wacom.ink.PathPoint
import com.wacom.ink.Phase
import com.wacom.ink.PointerData
import com.wacom.ink.Spline
import com.wacom.ink.format.InkSensorType
import com.wacom.ink.format.enums.InkInputType
import com.wacom.ink.manipulation.ManipulationMode
import com.wacom.ink.manipulation.SpatialModel
import com.wacom.ink.manipulation.callbacks.ErasingCallback
import com.wacom.ink.manipulation.callbacks.SelectingCallback
import com.wacom.ink.model.InkStroke
import com.wacom.ink.model.StrokeAttributes
...
class VectorView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    // Relevant for serialization
    var inkSensorTypes = listOf(InkSensorType.X, InkSensorType.Y, InkSensorType.TIMESTAMP)
    lateinit var inkEnvironmentModel: InkEnvironmentModel
    var drawingTool: VectorTool = PenTool() //default tool
    var inkBuilder = VectorInkBuilder()
    // This will contain the current drawing path before it is finished
    private var currentPath = Path()
    var paint: Paint = Paint()
    // A list of existing strokes
    private var strokes = mutableMapOf<String, WillStroke>()
    ...
    private fun addStroke(event: MotionEvent) {
        val brush = inkBuilder.tool.brush
        val stroke = WillStroke(inkBuilder.splineProducer.allData!!.copy(), brush)
        stroke.sensorData = inkEnvironmentModel.createSensorData(event, inkSensorTypes)
        stroke.strokeAttributes = object : StrokeAttributes {
            override var size: Float = 10f
            override var rotation: Float = 0.0f
            override var scaleX: Float = 1.0f
            override var scaleY: Float = 1.0f
            override var scaleZ: Float = 1.0f
            override var offsetX: Float = 0.0f
            override var offsetY: Float = 0.0f
            override var offsetZ: Float = 0.0f
            override var red: Float = Color.red(paint.color) / 255f
            override var green: Float = Color.green(paint.color) / 255f
            override var blue: Float = Color.blue(paint.color) / 255f
            override var alpha: Float = paint.alpha.toFloat() / 255
        }
        spatialModel.add(stroke)
        stroke.path = currentPath
        strokes[stroke.id] = stroke
    }
}
/// <summary>
/// Creates and manages rendering brush and graphics objects and interactions between them;
/// Recieves pointer input events and passes them on to the rendering brush to feed into
/// the ink geometry pipeline
/// </summary>
public class Renderer
{
    #region Fields
    private SwapChainPanel m_swapChainPanel;
    private PointerManager m_pointerManager;
    private Graphics m_graphics = new Graphics();
    #endregion
    ...
    #region Constructor
    ...
    /// <summary>
    /// Constructor. Creates rendering brush; initializes graphics
    /// </summary>
    /// <param name="swapChainPanel">SwapChainPanel on which to render captured ink</param>
    /// <param name="brushType">Type of brush to use</param>
    /// <param name="thickness">Relative thickness of brush</param>
    /// <param name="color">Color of ink</param>
    /// <param name="style">Shape of brush (VectorBrush only)</param>
    public Renderer(SwapChainPanel swapChainPanel, VectorBrushStyle style, MediaColor color)
    {
        StrokeHandler = new VectorStrokeHandler(this, style, color);
        m_swapChainPanel = swapChainPanel;
        m_graphics.GraphicsReady += OnGraphicsReady;
        m_graphics.Initialize(m_swapChainPanel, false);
    }
    #endregion
class InkCanvasVector extends InkCanvas {
	constructor(canvas, width, height) {
		super();
		this.canvas = InkCanvas2D.createInstance(canvas, width, height);
		this.strokesLayer = this.canvas.createLayer();
		this.strokeRenderer = new StrokeRenderer2D(this.canvas);
		this.canvasTransformer = new CanvasTransformer(width, height);
		this.selection = new SelectionVector(this.dataModel, {
			canvas: this.canvas,
			canvasTransformer: this.canvasTransformer,
			redraw: this.redraw.bind(this)
		});
		this.selection.connect();
	}
Input Handling
Input events from the operating system are handled in the same way as for the raster input (please see Raster Input Handling)
Finally, draw the stroke:
- Kotlin
- C#
- JavaScript
/**
 * In order to make the strokes look nice and smooth, it is recommended to enable:
 *  - Antialiasing
 *  - Dither
 */
val paint = Paint().also {
    it.isAntiAlias = true
    it.isDither = true
}
...
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    selectionBox?.drawSelectionBox(canvas)
    for ((id, stroke) in strokes) {
        if (id in selectedStrokes) {
            canvas.drawPath(stroke.path, paintSelected)
        } else {
            // Selection color
            val color = Color.argb(
                (stroke.strokeAttributes.alpha * 255).toInt(),
                (stroke.strokeAttributes.red * 255).toInt(),
                (stroke.strokeAttributes.green * 255).toInt(),
                (stroke.strokeAttributes.blue * 255).toInt()
            )
            paint.color = color
            canvas.drawPath(stroke.path, paint)
        }
    }
    // draw the current path,
    // because we have not finished the stroke we has not saved it yet
    if (drawingTool.uri() == EraserVectorTool.uri) {
        paint.color = Color.WHITE
    } else if (drawingTool.uri() == EraserWholeStrokeTool.uri) {
        paint.color = Color.RED
    } else {
        paint.color = currentColor
    }
    canvas.drawPath(currentPath, paint)
}
public override void RenderAllStrokes(RenderingContext context,
                                      IEnumerable<Identifier> excluded,
                                      Rect? clipRect)
{
    foreach (var stroke in m_dryStrokes)
    {
        if (excluded == null || !excluded.Contains(stroke.Id))
        {
            // Draw current stroke
            context.SetTarget(mRenderer.CurrentStrokeLayer);
            context.ClearColor(Colors.Transparent);
            DoRenderStroke(context, stroke, mRenderer.TranslationLayerPainted);
            // Blend stroke to Scene Layer
            context.SetTarget(mRenderer.SceneLayer);
            context.DrawLayer(mRenderer.CurrentStrokeLayer, null,
                              Ink.Rendering.BlendMode.SourceOver);
            // Blend Current Stroke to All Strokes Layer
            context.SetTarget(mRenderer.AllStrokesLayer);
            context.DrawLayer(mRenderer.CurrentStrokeLayer, null,
                              Ink.Rendering.BlendMode.SourceOver);
        }
    }
}
/// <summary>
/// Handles brush-specific parts of drawing a new stroke segment
/// </summary>
/// <param name="updateRect">returns bounding rectangle of area requiring update</param>
public override void DoRenderNewStrokeSegment(out Rect updateRect)
{
    var result = ActiveTool.Polygons;
    ConvertPolygon(result.Addition, m_addedPolygon);
    ConvertPolygon(result.Prediction, m_predictedPolygon);
    // Draw the added stroke
    mRenderer.RenderingContext.SetTarget(mRenderer.CurrentStrokeLayer);
    Rect addedStrokeRect = mRenderer.RenderingContext.FillPolygon(m_addedPolygon, BrushColor,
                                                                  Ink.Rendering.BlendMode.Max);
    // Measure the predicted stroke
    Rect predictedStrokeRect = mRenderer.RenderingContext.MeasurePolygonBounds(m_predictedPolygon);
    // Calculate the update rect for this frame
    updateRect = mRenderer.DirtyRectManager.GetUpdateRect(addedStrokeRect, predictedStrokeRect);
    // Draw the predicted stroke
    mRenderer.RenderingContext.SetTarget(mRenderer.PrelimPathLayer);
    mRenderer.RenderingContext.DrawLayerAtPoint(mRenderer.CurrentStrokeLayer, updateRect,
                                                new Point(updateRect.X, updateRect.Y),
                                                Ink.Rendering.BlendMode.Copy);
    mRenderer.RenderingContext.FillPolygon(m_predictedPolygon, BrushColor,
                                           Ink.Rendering.BlendMode.Max);
}
draw(pathPart) {
	this.drawPath(pathPart);
	if (pathPart.phase == InkBuilder.Phase.END) {
		if (this.strokeRenderer) {
			let stroke = this.strokeRenderer.toStroke(this.builder);
			this.dataModel.add(stroke)
		}
	}
}
drawPath(pathPart) {
	this.strokeRenderer.draw(pathPart.added, pathPart.phase == InkBuilder.Phase.END);
	if (pathPart.phase == InkBuilder.Phase.UPDATE) {
		this.strokeRenderer.drawPreliminary(pathPart.predicted);
		let dirtyArea = this.canvas.bounds.intersect(this.strokeRenderer.updatedArea);
		if (dirtyArea) {
			this.canvas.clear(dirtyArea);
			this.canvas.blend(this.strokesLayer, {rect: dirtyArea});
			this.strokeRenderer.blendUpdatedArea();
		}
	}
	else if (pathPart.phase == InkBuilder.Phase.END) {
		let dirtyArea = this.canvas.bounds.intersect(this.strokeRenderer.strokeBounds.union(this.strokeRenderer.updatedArea));
		if (dirtyArea) {
			dirtyArea = dirtyArea.ceil();
			if (!this.selector && !this.intersector) {
				if (app.type == app.Type.VECTOR) {
					let size = config.getSize(this.toolID);
					if (size.max < 2 || size.min < 1)
						this.strokeRenderer.draw(this.builder.getInkPath(), true);
				}
				this.strokeRenderer.blendStroke(this.strokesLayer);
				this.canvas.clear(dirtyArea);
				this.canvas.blend(this.strokesLayer, {rect: dirtyArea});
			}
		}
	}
}