墨水几何结构和渲染管线
我们现在引入了不同的rendering modes。 该指南对管道设置和渲染每一步坐了详细介绍。
视觉结果取决于刷子配置、管道配置以及平台渲染配置。
Raster Ink
本节逐步详细介绍了最佳的几何图形设置方法和栅格墨水的渲染管道。
笔刷配置
要设置光栅颗粒刷子,需要两种纹理:
形状纹理和填充纹理:
shape
参数指定颗粒的形状。
它接受图像,可以不透明像素定义颗粒形状。
fill
参数指定形状的填充图案。
它接受图像,可从中采样图案。
两种纹理都可以在该参数中传递一系列图像。 这种情况下,该方法将它们解析为纹理映射。
- 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 计算器
对于所有相关的刷子,需要配置PathPoint 计算器 ,它会影响传感器数据更改墨水笔划宽度、强度和倾斜度的方式。
首先,需要配置布局。 布局可根据输入提供者调整。 例如,对于触摸输入,不接收倾斜度或压力信息。
- 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();
触摸和触控笔输入可针对不同类型的输入传感器数据进行优化,因此,需要定义接收之前的、当前的和后续的传感器数据输入的计算器,并计算合适的@@@PathPoint
.
例如,触摸数据与触控笔数据一样,不包含压力信息和倾斜方向。
因此,要实现同样的α强度 差值或墨水笔划宽度变动,只能使用速度信息。
宽度计算函数所基于输入数据的当前速度由引擎提供。
在此,最小值和最大值定义宽度范围,而最小速度和最大速度定义在该范围内,宽度如何改变。 其结果是介于0和1之间的标准化值。 最后,可通过重新映射功能修改标准化值,例如,将行为逆转。 因此,例如,缓慢移动会增大笔划的宽度,而非减小;如果手指移动加快,则笔划会变细。
- 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
}
}
}
}
触控笔输入配置可以更方便地配置现实世界书写工具的自然行为。 可在此通过压力传感器数据增加工具的宽度或更改颜色的强度。 另外,如果触控笔倾斜,将自然而然地更改接触面积区域。 如果想要实现笔的影线,水彩效果或墨水笔效果,需要在此设置。
**注意:**输入传感器质量各不相同,因此,需要通过不同的触控笔输入测试是否达到所需的行为效果。
- 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;
}
管线
RasterInkBuilder
定义栅格墨水管道:
- 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();
光栅化视图
接下来,设置光栅视图:
- 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);
}
}
输入处理
操作系统的输入事件使用下列方式进行处理:
// 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
本节将逐步详细介绍最佳的几何图形设置方法和矢量墨水的渲染管道。
笔刷配 置
矢量墨水刷子配置由任意凸多边形定义。
- 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 计算器
PathPoint计算器的定义方法与光栅工具的类似。 同样,我们需要定义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
管线
VectorInkBuilder
是一个使用管道处理路径构建的类。
它可以处理整个管道的输入,并通过输入产生多边形。
之后,多边形被转换为天然结构。
- 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();
矢量化视图
接下来,需要设置矢量视图。 该类是一个用于绘制矢量墨水的简单视图。 矢量墨水是一条路径,它通过扩展平台渲染方法并绘制路径来绘制。
- 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();
}
输入处理
操作系统的输入事件使用与光栅输入相同的方式进行处理 (please see Raster Input Handling)
最后,绘制笔划:
- 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});
}
}
}
}