Manipulation of Digital Ink
Manipulations are handled differently for Vector and Raster Ink. Vector manipulations modify the generated geometry, while the raster eraser only "overdraws" the strokes.
Raster Ink
For Raster Ink so far only partial eraser functionality is offered.
Eraser
The eraser is configured as a tool, similar to the brush.
This tool uses a specific blending mode BlendMode.DESTINATION_OUT
to erase the strokes by overlapping them.
- Kotlin
- JavaScript
import com.wacom.ink.Calculator
import com.wacom.ink.PathPoint
import com.wacom.ink.PathPointLayout
import com.wacom.ink.rendering.BlendMode
...
class EraserRasterTool(context: Context) : RasterTool(context) {
companion object {
val uri = URIBuilder.getToolURI("raster", "eraser")
}
override var brush = BrushPalette.eraser(context)
override fun getLayout(): PathPointLayout {
return PathPointLayout(
PathPoint.Property.X,
PathPoint.Property.Y,
PathPoint.Property.SIZE,
PathPoint.Property.RED,
PathPoint.Property.GREEN,
PathPoint.Property.BLUE,
PathPoint.Property.ALPHA
)
}
override val touchCalculator: Calculator = { previous, current, next ->
// Use the following to compute size based on speed:
var size = current.computeValueBasedOnSpeed(
previous,
next,
minValue = 8f,
maxValue = 112f,
minSpeed = 720f,
maxSpeed = 3900f
)
if (size == null) size = 8f
PathPoint(current.x, current.y, size = size,
red = 1f, green = 1f, blue = 1f, alpha = 1f)
}
override val stylusCalculator: Calculator = { previous, current, next ->
// Use the following to compute size based on speed:
var size = current.computeValueBasedOnSpeed(
previous,
next,
minValue = 8f,
maxValue = 112f,
minSpeed = 720f,
maxSpeed = 3900f
)
if (size == null) size = 8f
PathPoint(current.x, current.y, size = size,
red = 1f, green = 1f, blue = 1f, alpha = 1f)
}
override fun getBlendMode(): BlendMode {
return BlendMode.DESTINATION_OUT
}
}
...
eraserRaster: {
brush: BrushPalette.circle,
blendMode: BlendMode.DESTINATION_OUT,
dynamics: {
size: {
value: {
min: 8,
max: 112
},
velocity: {
min: 720,
max: 3900
}
}
},
...
Note: Strokes are not removed from the model, just overdrawn.
Vector Ink
Vector ink offers more capabilities of manipulating strokes with:
- Partial stroke eraser
- Whole stroke eraser
- Manipulation options
Partial Eraser / Splitter
The eraser tool works in a similar way to the brush tools. Thus, you need to define its calculators. Here, you can decide if the width of the eraser changes with more pressure or with more speed. Moreover, you can define a different behavior for finger or stylus input.
- Kotlin
- C#
- JavaScript
class EraserVectorTool : VectorTool() {
companion object {
val uri = "tool@erase_vector"
}
override var brush = BrushPalette.circle()
override fun getLayout(): PathPointLayout {
return PathPointLayout(
PathPoint.Property.X,
PathPoint.Property.Y,
PathPoint.Property.SIZE,
PathPoint.Property.RED,
PathPoint.Property.GREEN,
PathPoint.Property.BLUE
)
}
override var drawingMode = DrawingMode.ERASING_PARTIAL_STROKE
override val touchCalculator: Calculator = { previous, current, next ->
// Use the following to compute size based on speed:
var size = current.computeValueBasedOnSpeed(
previous,
next,
minValue = 8f,
maxValue = 112f,
minSpeed = 720f,
maxSpeed = 3900f
)
if (size == null) size = 1.0f
PathPoint(current.x, current.y, size = size, red = 1f, green = 1f, blue = 1f, alpha = 0.5f)
}
override val stylusCalculator: Calculator = { previous, current, next ->
// Use the following to compute size based on speed:
var size = current.computeValueBasedOnSpeed(
previous,
next,
minValue = 8f,
maxValue = 112f,
minSpeed = 720f,
maxSpeed = 3900f
)
if (size == null) size = 1.0f
PathPoint(current.x, current.y, size = size, red = 1f, green = 1f, blue = 1f)
}
}
/// <summary>
/// Vector "drawing" tool for erasing ink storkes
/// </summary>
public class VectorEraserTool : VectorSelectionTool
{
private static readonly ToolConfig mConfig = new ToolConfig()
{
initValue = 2,
minSpeed = 100,
maxSpeed = 4000,
minValue = 2,
maxValue = 24
};
public VectorEraserTool(ManipulationMode mode)
: base(mode)
{
}
public override bool BlendCurrentStroke => false;
public override VectorBrush Shape => mCircleBrush;
protected override ToolConfig SizeConfig => mConfig;
protected override float PreviousSize { get; set; } = 2f;
public override void OnReleased(UIElement uiElement, PointerRoutedEventArgs args)
{
base.OnReleased(uiElement, args);
}
public override PathPointLayout GetLayout(Windows.Devices.Input.PointerDeviceType deviceType)
{
switch (deviceType)
{
case Windows.Devices.Input.PointerDeviceType.Mouse:
case Windows.Devices.Input.PointerDeviceType.Touch:
case Windows.Devices.Input.PointerDeviceType.Pen:
return new PathPointLayout(PathPoint.Property.X,
PathPoint.Property.Y,
PathPoint.Property.Size);
default:
throw new Exception("Unknown input device type");
}
}
public override Calculator GetCalculator(PointerDeviceType deviceType)
{
switch (deviceType)
{
case Windows.Devices.Input.PointerDeviceType.Mouse:
case Windows.Devices.Input.PointerDeviceType.Touch:
case Windows.Devices.Input.PointerDeviceType.Pen:
return CalculatorForMouseAndTouch;
default:
throw new Exception("Unknown input device type");
}
}
}
}
eraserVector: {
brush: BrushPalette.circle,
intersector: new Intersector(Intersector.Mode.PARTIAL_STROKE),
dynamics: {
size: {
value: {
min: 8,
max: 12
},
velocity: {
min: 720,
max: 3900
}
}
},
Whole Stroke Eraser
- Kotlin
- C#
- JavaScript
class EraserWholeStrokeTool : VectorTool() {
companion object {
val uri = URIBuilder.getToolURI("vector", "eraser_whole_stroke")
}
override var brush = BrushPalette.basic()
override fun getLayout(): PathPointLayout {
return PathPointLayout(
PathPoint.Property.X,
PathPoint.Property.Y,
PathPoint.Property.SIZE
)
}
override var drawingMode = DrawingMode.ERASING_WHOLE_STROKE
override val touchCalculator: Calculator = { previous, current, next ->
//Use the following to compute size based on speed:
PathPoint(current.x, current.y, size = 3f)
}
override val stylusCalculator: Calculator = { previous, current, next ->
//Use the following to compute size based on speed:
PathPoint(current.x, current.y, size = 3f)
}
}
/// <summary>
/// Vector "drawing" tool for erasing ink storkes
/// </summary>
public class VectorEraserTool : VectorSelectionTool
{
private static readonly ToolConfig mConfig = new ToolConfig()
{
initValue = 2,
minSpeed = 100,
maxSpeed = 4000,
minValue = 2,
maxValue = 24
};
public VectorEraserTool(ManipulationMode mode)
: base(mode)
{
}
public override bool BlendCurrentStroke => false;
public override VectorBrush Shape => mCircleBrush;
protected override ToolConfig SizeConfig => mConfig;
protected override float PreviousSize { get; set; } = 2f;
public override void OnReleased(UIElement uiElement, PointerRoutedEventArgs args)
{
base.OnReleased(uiElement, args);
}
public override PathPointLayout GetLayout(Windows.Devices.Input.PointerDeviceType deviceType)
{
switch (deviceType)
{
case Windows.Devices.Input.PointerDeviceType.Mouse:
case Windows.Devices.Input.PointerDeviceType.Touch:
case Windows.Devices.Input.PointerDeviceType.Pen:
return new PathPointLayout(PathPoint.Property.X,
PathPoint.Property.Y,
PathPoint.Property.Size);
default:
throw new Exception("Unknown input device type");
}
}
public override Calculator GetCalculator(PointerDeviceType deviceType)
{
switch (deviceType)
{
case Windows.Devices.Input.PointerDeviceType.Mouse:
case Windows.Devices.Input.PointerDeviceType.Touch:
case Windows.Devices.Input.PointerDeviceType.Pen:
return CalculatorForMouseAndTouch;
default:
throw new Exception("Unknown input device type");
}
}
}
}
eraserWholeStroke: {
brush: BrushPalette.basic2,
intersector: new Intersector(Intersector.Mode.WHOLE_STROKE),
statics: {
size: 3,
red: 255,
green: 255,
blue: 255,
alpha: 0.5
}
},
Selection
If you need to manipulate ink strokes with operations such as:
- Translation
- Rotation
- Scale
you mostly use a lasso tool to select either parts of the stroke or whole strokes. With the technology we differentiate between partial selection and whole stroke selection.
Partial Selection
Again we need to define tools.
- Kotlin
- C#
- JavaScript
class SelectorPartialStrokeTool : VectorTool() {
companion object {
val uri = "tool@selector_parcial_stroke"
}
override var brush = BrushPalette.basic()
override fun getLayout(): PathPointLayout {
return PathPointLayout(
PathPoint.Property.X,
PathPoint.Property.Y,
PathPoint.Property.SIZE,
PathPoint.Property.RED,
PathPoint.Property.GREEN,
PathPoint.Property.BLUE
)
}
override var drawingMode = DrawingMode.SELECTING_PARTIAL_STROKE
override val touchCalculator: Calculator = { previous, current, next ->
// Use the following to compute size based on speed:
PathPoint(current.x, current.y, size = 3f, red = 1f, green = 1f, blue = 1f)
}
override val stylusCalculator: Calculator = { previous, current, next ->
// Use the following to compute size based on speed:
PathPoint(current.x, current.y, size = 3f, red = 1f, green = 1f, blue = 1f)
}
}
ManipulationMode manipulation = ManipulationMode.PartialStroke
..
/// <summary>
/// Vector "drawing" tool for selecting ink strokes
/// </summary>
public class VectorManipulationTool : VectorSelectionTool
{
private static readonly ToolConfig mConfig = new ToolConfig()
{
minSpeed = 5,
maxSpeed = 210,
minValue = 0.5f,
maxValue = 1.6f,
remap = v => (1 + 0.62f) * v / ((float)Math.Abs(v) + 0.62f)
};
private Point m_startPosition;
private Point m_prevPosition;
private bool m_isTranslating = false;
private VectorStrokeHandler m_strokeHandler;
public VectorManipulationTool(VectorStrokeHandler strokeHandler, ManipulationMode mode)
: base(mode)
{
m_strokeHandler = strokeHandler;
}
public override bool BlendCurrentStroke => false;
public override VectorBrush Shape => mCircleBrush;
protected override ToolConfig SizeConfig => mConfig;
protected override float PreviousSize { get; set; } = 1.5f;
public Rect DestRect { get; set; } = Rect.Empty;
public Rect SourceRect { get; set; } = Rect.Empty;
public event EventHandler OnTranslate;
public event EventHandler<Matrix3x2> TranslateFinished;
public override void OnPressed(UIElement uiElement, PointerRoutedEventArgs args)
{
var currentPt = args.GetCurrentPoint(uiElement).Position;
m_isTranslating = CurrentPointIntersectsSourceRect(currentPt);
if (m_isTranslating)
{
m_startPosition = currentPt;
m_prevPosition = m_startPosition;
OnTranslate?.Invoke(this, null);
}
else
{
base.OnPressed(uiElement, args);
}
}
public override void OnMoved(UIElement uiElement, PointerRoutedEventArgs args)
{
if (m_isTranslating)
{
var pt = args.GetCurrentPoint(uiElement).Position;
double dX = pt.X - m_prevPosition.X;
double dY = pt.Y - m_prevPosition.Y;
Matrix3x2 translation = Matrix3x2.CreateTranslation((float)dX, (float)dY);
m_prevPosition = pt;
Vector2 newPosition = Vector2.Transform(new Vector2((float)DestRect.X, (float)DestRect.Y), translation);
DestRect = new Rect(newPosition.X, newPosition.Y, DestRect.Width, DestRect.Height);
OnTranslate?.Invoke(this, null);
}
else
{
base.OnMoved(uiElement, args);
}
}
public override void OnReleased(UIElement uiElement, PointerRoutedEventArgs args)
{
if (m_isTranslating)
{
//m_isTranslating = false;
Matrix3x2 translation = Matrix3x2.Identity;
var pt = args.GetCurrentPoint(uiElement).Position;
if (!m_strokeHandler.TransformationMatrix.IsIdentity)
{
bool res = Matrix3x2.Invert(m_strokeHandler.TransformationMatrix, out Matrix3x2 modelTransformationMatrix);
if (!res)
{
throw new InvalidOperationException("Transform matrix could not be inverted.");
}
Vector2 currentPointPos = new Vector2((float)pt.X, (float)pt.Y);
Vector2 startPos = new Vector2((float)m_startPosition.X, (float)m_startPosition.Y);
Vector2 modelCurrentPointPos = Vector2.Transform(currentPointPos, modelTransformationMatrix);
Vector2 modelStartPos = Vector2.Transform(startPos, modelTransformationMatrix);
double dX = modelCurrentPointPos.X - modelStartPos.X;
double dY = modelCurrentPointPos.Y - modelStartPos.Y;
translation = Matrix3x2.CreateTranslation((float)dX, (float)dY);
}
else
{
double dX = pt.X - m_startPosition.X;
double dY = pt.Y - m_startPosition.Y;
translation = Matrix3x2.CreateTranslation((float)dX, (float)dY);
}
TranslateFinished?.Invoke(this, translation);
}
else
{
base.OnReleased(uiElement, args);
}
m_isTranslating = false;
}
private bool CurrentPointIntersectsSourceRect(Point currentPoint)
{
if (SourceRect.IsEmpty)
return false;
return SourceRect.Contains(currentPoint);
}
public override PathPointLayout GetLayout(Windows.Devices.Input.PointerDeviceType deviceType)
{
switch (deviceType)
{
case Windows.Devices.Input.PointerDeviceType.Mouse:
case Windows.Devices.Input.PointerDeviceType.Touch:
case Windows.Devices.Input.PointerDeviceType.Pen:
return new PathPointLayout(PathPoint.Property.X,
PathPoint.Property.Y,
PathPoint.Property.Size);
default:
throw new Exception("Unknown input device type");
}
}
public override Calculator GetCalculator(PointerDeviceType deviceType)
{
switch (deviceType)
{
case Windows.Devices.Input.PointerDeviceType.Mouse:
case Windows.Devices.Input.PointerDeviceType.Touch:
case Windows.Devices.Input.PointerDeviceType.Pen:
return CalculatorForMouseAndTouch;
default:
throw new Exception("Unknown input device type");
}
}
}
selector: {
brush: BrushPalette.circle,
selector: new Selector(Selector.Mode.PARTIAL_STROKE),
statics: {
size: 2,
red: 0,
green: 151,
blue: 212,
alpha: 1
}
}
Whole Stroke Selector
- Kotlin
- C#
- JavaScript
class SelectorWholeStrokeTool : VectorTool() {
companion object {
val uri = "tool@selector_whole_stroke"
}
override var brush = BrushPalette.basic()
override fun getLayout(): PathPointLayout {
return PathPointLayout(
PathPoint.Property.X,
PathPoint.Property.Y,
PathPoint.Property.SIZE,
PathPoint.Property.RED,
PathPoint.Property.GREEN,
PathPoint.Property.BLUE
)
}
override var drawingMode = DrawingMode.SELECTING_WHOLE_STROKE
override val touchCalculator: Calculator = { previous, current, next ->
//Use the following to compute size based on speed:
PathPoint(current.x, current.y, size = 3f, red = 1f, green = 1f, blue = 1f)
}
override val stylusCalculator: Calculator = { previous, current, next ->
//Use the following to compute size based on speed:
PathPoint(current.x, current.y, size = 3f, red = 1f, green = 1f, blue = 1f)
}
}
// Only use a different manipulation mode for the manipluation tool
ManipulationMode manipulation = ManipulationMode.WholeStroke
selectorWholeStroke: {
brush: BrushPalette.circle,
selector: new Selector(Selector.Mode.WHOLE_STROKE),
statics: {
size: 2,
red: 0,
green: 151,
blue: 212,
alpha: 1
}
}