インクジオメトリパイプラインとレンダリング
さまざまなrendering modesが既に導入されています。 本ガイドでは、パイプラインとレンダリングのセットアップ方法を段階的に説明します。
視覚的な結果はブラシ設定、パイプライン設定、およびプラットフォームレンダリングの設定により変化します。
Raster Ink
本セクションでは、ラスターインクのジオメトリおよびレンダリングパイプラインをセットアップする場合のベストプラクティスについて、順を追って説明します。
ブラシ設定
ラスターパーティクルブラシをセットアップするには、2つのテクスチャが必要です。
図形と 塗りつぶしのテクスチャ:
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 calculators を設定する必要があります。
最初に、レイアウトの設定が必要です。 レイアウトは、入力元に応じて調整することができます。 たとえば、タッチ入力の場合は傾きも筆圧情報も取得しません。
- 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の間で正規化された値となります。 最後に、remap関数で正規化された値を修正して、動作を逆にすることなどができます。 その結果、たとえばゆっくりとした動きでは線の幅を太くできる一方で、指の動きを速めた場合は線を細くできます。
- 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);
}
}
入力処理
オペレーティングシステムからの入力イベントは、下記によって処理されます。
- 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();
}
...
最後に、ストロークを描いてください。
- 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計算機は、ラスターツールの場合と同様の方法で定義されます。 この場合にも、PathPointのレイアウトを定義する必要があります。
- 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});
}
}
}
}