Serialization of Ink
Ink Serializationモジュールは、インクコンテンツと関連メタデータのエンコードおよびデコードを行うアルゴリズムを提供します。 これらのアルゴリズムは最適化されて、と連動します。Universal Ink Model.
アルゴリズムは次の機能をサポートします。
- 高速なエンコードおよびデコード
- コンパクトなファイルサイズ
- オペレーティングシステムやデバイス間での移植
- 各種コンテナ形式に組み込むことができるバイナリ表現
エンコードスキームの詳細を確認するには、here.
モデルの処理
The following illustrates how to manage the data used within the Universal Ink Modelをクリックしてください。 当該使用事例で情報が必要とされる場合、インクモデル内では、複数のデータレポジトリを、インクツリー、ビュー、およびKnowledge Graphとともに管理する必要があり ます。
Ink Model
最初のステップは、InkModel
.
- Kotlin
- C#
- JavaScript
inkModel = InkModel()
public class Serializer
{
...
public void Init()
{
InkDocument = new InkModel();
InkDocument.InkTree.Root = new StrokeGroupNode(Identifier.FromNewGuid());
}
class DataModel {
constructor() {
this.inkModel = new InkModel();
this.repository = new DataRepository();
this.manipulationsContext = new SpatialContext();
}
...
入力コンテキストリポジトリ
入力コンテキストはセンサーデータの取得方法を記述します。 この情報はアプリケーションによって追加する必要がありますが、アプリケーションの使用事例によっては不要な場合もあります。 オプションで、センサーデータの保存もできます。
以下のセクションでは、コンテキストの定義方法の例をいくつか示します。
使用環境
センサーデータを生成したenvironment(オペレーティングシステムなど)は、次のように定義することができます。
- Kotlin
- C#
import com.wacom.ink.format.input.*
...
// Initialize environment
environment = Environment()
environment.putProperty("os.name", "android")
environment.putProperty("os.version.name", Build.VERSION.CODENAME)
environment.putProperty("os.version.code", Build.VERSION.SDK_INT.toString())
environment.putProperty("os.version.incremental", Build.VERSION.INCREMENTAL)
environment.putProperty("os.version.release", Build.VERSION.RELEASE)
environment.putProperty("wacom.ink.sdk.name", activity.getString(R.string.sdk_name))
environment.putProperty("wacom.ink.sdk.version", activity.getString(R.string.sdk_version))
// Init environment
mEnvironment.Properties["os.name"] = m_eas.OperatingSystem;
mEnvironment.Properties["os.version.code"] = System.Environment.OSVersion.Version.ToString();
mEnvironment.Seal();
InkInputProvider
ink input provider は一般的な入力データソースを表し、これによってデータの生成方法(タッチ入力、マウス、スタイラス、ハードウェアコントローラの使用など)が特定されます。 アプリケーションは、プラットフォームおよびデバイスごとに、アプリケーション内で使用される入力デバイスを決定し、インクモデル内でシリアル化する必要があります。
- Kotlin
- C#
- JavaScript
import com.wacom.ink.format.input.*
...
val toolType = when (event.getToolType(0)) {
MotionEvent.TOOL_TYPE_STYLUS -> InkInputType.PEN
MotionEvent.TOOL_TYPE_FINGER -> InkInputType.TOUCH
MotionEvent.TOOL_TYPE_MOUSE -> InkInputType.MOUSE
else -> InkInputType.PEN
}
val provider = InkInputProvider(toolType)
TDB
TBD
入力デバイス
センサーデータの生成に使用したinput device です。 プラットフォームにより、使用可能な入力デバイスは複数存在する場合があります。 一部では、既にスタイラスを備えたタブレット端末やアクセサリとして使用可能な端末もあります。 次の属性は、プラットフォーム自体などによって提供された場合に取得できます。
- Kotlin
- C#
- JavaScript
import com.wacom.ink.format.input.*
...
// Initialize InputDevice
inputDevice = InputDevice()
inputDevice.putProperty("dev.id", Build.ID)
inputDevice.putProperty("dev.manufacturer", Build.MANUFACTURER)
inputDevice.putProperty("dev.brand", Build.BRAND)
inputDevice.putProperty("dev.model", Build.MODEL)
inputDevice.putProperty("dev.board", Build.BOARD)
inputDevice.putProperty("dev.hardware", Build.HARDWARE)
inputDevice.putProperty("dev.codename", Build.DEVICE)
inputDevice.putProperty("dev.display", Build.DISPLAY)
...
{
InputDevice inputDevice = new InputDevice();
inputDevice.Properties["dev.name"] = System.Environment.MachineName;
inputDevice.Seal();
Identifier inputDeviceId = inputDevice.Id;
bool res = InkDocument.InputConfiguration.Devices.Any((device) => device.Id == inputDeviceId);
if (!res)
{
InkDocument.InputConfiguration.Devices.Add(inputDevice);
}
return inputDevice;
}
/**
* Creates default input device instance, based on system information
*
* @param {Properties} envProps User defined env properties, like app.id for example
* @return {InkInput.InputDevice} default instance
*/
static async createInstance(envProps) {
let device = new this(...Array.from(arguments).slice(1));
if (typeof sysInfo == "undefined")
device.props["dev.graphics.resolution"] = `${screen.width}x${screen.height}`;
else {
let system = await sysInfo.system();
let cpu = await sysInfo.cpu();
let graphics = await sysInfo.graphics();
let display = graphics.displays.filter(d => d.main)[0];
let adapter = graphics.controllers[0];
device.props["dev.id"] = system.uuid.toLowerCase();
device.props["dev.manufacturer"] = system.manufacturer;
device.props["dev.model"] = system.model;
device.props["dev.cpu"] = `${cpu.manufacturer} ${cpu.brand} ${cpu.speed} - ${cpu.cores} core(s)`;
device.props["dev.graphics.display"] = `${display.model} ${display.currentResX}x${display.currentResY} (${display.pixeldepth} bit)`;
device.props["dev.graphics.adapter"] = `${adapter.model} ${adapter.vram} GB`;
}
device.environment = await Environment.createInstance(envProps);
return device;
}
...
let device = await InputDevice.createInstance({"app.id": "will3-sdk-for-ink-web-demo", "app.version": "1.0.0"});
SensorContext
sensor contextは、デジタルインク入力の取得に使用される、センサチャンネルコンテキストの一意の組み合わせです。 この情報は、データを正規化する必要のある機械学習においてセンサーデータが使用される場合に役立つことがあります。
- Kotlin
- C#
// Define which channels are used for sensor data collection
var inkSensorTypes = listOf(InkSensorType.X, InkSensorType.Y, InkSensorType.TIMESTAMP)
...
// Register the channels which are used for sensor data collection
val channels = registerChannels(inkSensorTypeUris)
// Define the context for the sensor channels
val sensorChannelsContext = SensorChannelsContext(provider.id, inputDevice.id, channels)
val sensorContext = SensorContext()
sensorContext.addSensorChannelsContext(sensorChannelsContext)
// Maintain a map of contexts by id
sensorContexts[sensorContext.id] = sensorContext
// Input context is combining environment and sensor context
val inputContext = InputContext(environment.id, sensorContext.id)
// Maintain a map of input context
inputContexts[inputContext.id] = inputContext
inputProviderToInputContextMapping[provider.id] = inputContext.id
...
private fun registerChannels(inkSensorTypeUris: List<String>): MutableList<SensorChannel> {
val precision = 2
val channels = mutableListOf<SensorChannel>()
val dimensions = getScreenDimensions()
for (type in inkSensorTypeUris) {
val channel = when (type) {
InkSensorType.X -> SensorChannel(
InkSensorType.X,
InkSensorMetricType.LENGTH,
ScalarUnit.INCH,
0.0f,
dimensions.x,
precision
)
InkSensorType.Y -> SensorChannel(
InkSensorType.Y,
InkSensorMetricType.LENGTH,
ScalarUnit.INCH,
0.0f,
dimensions.y,
precision
)
InkSensorType.Z -> SensorChannel(
InkSensorType.Z,
InkSensorMetricType.LENGTH,
ScalarUnit.DIP,
0.0f,
0.0f,
precision
)
InkSensorType.TIMESTAMP -> SensorChannel(
InkSensorType.TIMESTAMP,
InkSensorMetricType.TIME,
ScalarUnit.MILLISECOND,
0.0f,
0.0f,
precision
)
InkSensorType.PRESSURE -> SensorChannel(
InkSensorType.PRESSURE,
InkSensorMetricType.NORMALIZED,
ScalarUnit.NORMALIZED,
0.0f,
1.0f,
precision
)
InkSensorType.RADIUS_X -> SensorChannel(
InkSensorType.RADIUS_X,
InkSensorMetricType.LENGTH,
ScalarUnit.DIP,
0.0f,
0.0f,
precision
)
InkSensorType.RADIUS_Y -> SensorChannel(
InkSensorType.RADIUS_Y,
InkSensorMetricType.LENGTH,
ScalarUnit.DIP,
0.0f,
0.0f,
precision
)
InkSensorType.ALTITUDE -> SensorChannel(
InkSensorType.ALTITUDE,
InkSensorMetricType.ANGLE,
ScalarUnit.RADIAN,
0.0f,
(Math.PI/2).toFloat(),
precision
)
InkSensorType.AZIMUTH -> SensorChannel(
InkSensorType.AZIMUTH,
InkSensorMetricType.ANGLE,
ScalarUnit.RADIAN,
-(Math.PI/2).toFloat(),
(Math.PI/2).toFloat(),
precision
)
InkSensorType.ROTATION -> SensorChannel(
InkSensorType.ROTATION,
InkSensorMetricType.ANGLE,
ScalarUnit.RADIAN,
0.0f,
0.0f,
precision
)
else -> {
throw Exception("Unknown channel type.")
}
}
channels.add(channel)
}
return channels
}
// Init sensor channels contexts
SensorChannelsContext mouseSensorChannelsContext = SensorChannelsContext.CreateDefault(mouseInputProvider, m_currentInputDevice); //new SensorChannelsContext(mouseInputProvider, m_currentInputDevice, m_sensorChannels);
SensorChannelsContext touchSensorChannelsContext = SensorChannelsContext.CreateDefault(touchInputProvider, m_currentInputDevice); //new SensorChannelsContext(touchInputProvider, m_currentInputDevice, m_sensorChannels);
SensorChannelsContext penSensorChannelsContext = SensorChannelsContext.CreateDefault(penInputProvider, m_currentInputDevice); //new SensorChannelsContext(penInputProvider, m_currentInputDevice, m_sensorChannels);
// Cache sensor channels contexts
m_sensorChannelsContexts.Add(mouseSensorChannelsContext.Id, mouseSensorChannelsContext);
m_sensorChannelsContexts.Add(touchSensorChannelsContext.Id, touchSensorChannelsContext);
m_sensorChannelsContexts.Add(penSensorChannelsContext.Id, penSensorChannelsContext);
SensorData リポジトリ
センサーデータを追跡するには、すべてのプラットフォーム上のInkInputProvider
を知ることが重要ですが、これはイベントから推測することができます。
さらに、たとえばタッチ入力の場合とスタイラス入力の場合とでは、レンダリング動作が異なります。
- Kotlin
- C#
- JavaScript
...
val touchChannels = listOf(InkSensorType.X, InkSensorType.Y, InkSensorType.TIMESTAMP)
val penChannels = listOf(InkSensorType.X, InkSensorType.Y, InkSensorType.TIMESTAMP,
InkSensorType.PRESSURE, InkSensorType.ALTITUDE, InkSensorType.AZIMUTH)
...
fun createSensorData(event: MotionEvent): Pair<SensorData, List<SensorChannel>> {
// MotionEvent tool type helps to decide on the appropriate ink input provider
val toolType = when (event.getToolType(0)) {
MotionEvent.TOOL_TYPE_STYLUS -> InkInputType.PEN
MotionEvent.TOOL_TYPE_FINGER -> InkInputType.TOUCH
MotionEvent.TOOL_TYPE_MOUSE -> InkInputType.MOUSE
else -> InkInputType.PEN
}
var provider: InkInputProvider? = null
// First, check if exist an input provider of the desired type
for ((_, existingProvider) in inputProviders) {
existingProvider.type == toolType
provider = existingProvider
break
}
if (provider == null) {
provider = InkInputProvider(toolType)
// It is possible to add custom define properties to the input provider
// for example PenID in case exists
if (toolType == InkInputType.PEN) {
provider.putProperty("penType", "s-pen") // Assuming using Samsung S Pen
}
}
var inputContextId = if (!inputProviders.containsKey(provider.id)) {
inputProviders[provider.id] = provider
// Build the list of channels
val channels = registerChannels(if (toolType == InkInputType.PEN) penChannels else touchChannels)
channelsForInput.put(provider.id, channels)
val sensorChannelsContext = SensorChannelsContext(
provider.id, // Reference to input input provider
inputDevice.id, // Reference to input device
channels) // Channels for the registered channels
val sensorContext = SensorContext()
sensorContext.addSensorChannelsContext(sensorChannelsContext)
sensorContexts[sensorContext.id] = sensorContext
val inputContext = InputContext(
environment.id, // Reference to environment
sensorContext.id) // Reference to sensor context
inputContexts[inputContext.id] = inputContext
inputProviderToInputContextMapping[provider.id] = inputContext.id
inputContext.id
} else {
val provider = inputProviders[provider.id]!!
inputProviderToInputContextMapping[provider.id]!!
}
var channelList: List<SensorChannel>? = null
for ((id, channels) in channelsForInput) {
if (provider.id == id) {
channelList = channels
}
}
if (channelList == null) {
channelList = listOf() // empty list to avoid null pointer exceptions
}
// Create the SensorData object with a new unique ID and its input context reference id
return Pair<SensorData,
List<SensorChannel>>(SensorData(UUID.randomUUID().toString(), inputContextId, InkState.PLANE),
channelList)
}
override fun onEvent(pointerData: PointerData, inkToolType: InkInputType) {
when (pointerData.phase) {
// Set the appropriate input provider
Phase.BEGIN -> {
if ((inputProvider.type != inkToolType)) {
inputProvider = InkInputProvider(inkToolType)
}
}
// Adding sensor data
Phase.UPDATE, Phase.END -> {
sensorData.add(sensorChannels[InkSensorType.X]!!, pointerData.x)
sensorData.add(sensorChannels[InkSensorType.Y]!!, pointerData.y)
sensorData.addTimestamp(sensorChannels[InkSensorType.TIMESTAMP]!!,
pointerData.timestamp)
}
}
}
...
}
public Identifier AddSensorData(PointerDeviceType deviceType, List<PointerData> pointerDataList)
{
Identifier inputContextId = m_deviceTypeMap[deviceType];
InputContext inputContext = m_inputContexts[inputContextId];
SensorContext sensorContext = m_sensorContexts[inputContext.SensorContextId];
// Create sensor data using the input context
SensorData sensorData = new SensorData(
Identifier.FromNewGuid(),
inputContext.Id,
InkState.Plane);
PopulateSensorData(sensorData, sensorContext, pointerDataList);
m_sensorDataMap.TryAdd(sensorData.Id, sensorData);
return sensorData.Id;
}
...
/// <summary>
/// Make the current stroke permanent
/// </summary>
/// <remarks>Copies the output of the render pipeline from InkBuilder to dry strokes</remarks>
public override void StoreCurrentStroke(PointerDeviceType deviceType)
{
var allData = RasterInkBuilder.SplineInterpolator.AllData;
var points = new List<float>();
if (allData != null)
{
for (int i = 0; i < allData.Count; i++)
{
points.Add(allData[i]);
}
if (points.Count > 0)
{
var dryStroke = new RasterInkStroke(RasterInkBuilder,
deviceType,
points,
m_startRandomSeed,
CreateSerializationBrush($"will://examples/brushes/{Guid.NewGuid().ToString()}"),
mStrokeConstants.Clone(),
mSerializer.AddSensorData(deviceType, InkBuilder.GetPointerDataList()));
m_dryStrokes.Add(dryStroke);
}
}
}
add(stroke) {
this.manipulationsContext.add(stroke);
return this.inkModel.addPath(stroke);
}
...
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 (app.downsampling && 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();
}
Stroke リポジトリ
rendering pipelineの視覚的出力も、Universal Ink Modelの極めて重要な要素であるため、保存することが必要です。 この場合は、次の構造を追加する必要があります。
- Stroke - インクストロークのジオメトリ
- Style - ストロークのレンダリング用のカスタマイズパラメータ
- StrokeNode - インクツリーの論理ノード
- Kotlin
- C#
- JavaScript
import com.wacom.ink.format.rendering.PathPointProperties
import com.wacom.ink.format.rendering.Style
import com.wacom.ink.format.tree.data.Stroke
import com.wacom.ink.format.tree.nodes.StrokeNode
import com.wacom.ink.model.IdentifiableImpl
...
fun surfaceTouch(event: MotionEvent) {
...
if ((pointerData.phase == Phase.END) &&
(rasterInkBuilder.splineProducer.allData != null)) {
addStroke(event)
}
...
}
...
private fun addStroke(event: MotionEvent) {
// Adding the style
val style = Style(
rasterTool.uri(), // Style URI
1, // Particle random seed
props = PathPointProperties( // Coloring path properties
red = defaults.red,
green = defaults.green,
blue = defaults.blue,
alpha = defaults.alpha
),
renderModeUri = rasterTool.getBlendMode().name
)
// Adding stroke to the Stroke Repository
val path = Stroke(
IdentifiableImpl.generateUUID(), // Generated UUID
rasterInkBuilder.splineProducer.allData!!.copy(), // Spline
style // Style
)
// Adding a node to the Ink tree
val node = StrokeNode(IdentifiableImpl.generateUUID(),
path)
...
strokeNodeList.add(node)
}
private void EncodeStrokeCommon(Identifier id, Spline spline , PathPointLayout layout, Identifier sensorDataId, Style style)
{
Stroke stroke = new Stroke(
id,
spline.Clone(),
style,
layout,
sensorDataId);
StrokeNode strokeNode = new StrokeNode(Identifier.FromNewGuid(), stroke);
InkDocument.InkTree.Root.Add(strokeNode);
if (sensorDataId != Identifier.Empty)
{
SensorData sensorData = m_sensorDataMap[sensorDataId];
AddSensorDataToModel(sensorData);
}
}
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)
}
}
}
Brush リポジトリ
あらゆるブラシを組み込むことで、さまざまなプラットフォームやアプリケーションにおいてインクストロークを確実にレンダリングできるようにする必要があります。
- Kotlin
- C#
fun setTool(view: View, tool: Tool) {
drawingTool = tool
if (drawingTool is VectorTool) {
vectorDrawingView.setTool(drawingTool as VectorTool)
} else {
val dt = drawingTool as RasterTool
val brush = dt.brush
if (inkModel.brushRepository.getBrush(brush.name) == null) {
// Adding a raster brush if it is not within the repository yet
inkModel.brushRepository.addRasterBrush(brush as RasterBrush)
}
rasterDrawingSurface.setTool(dt)
}
highlightTool(view)
}
private void AddRasterBrushToInkDoc(PointerDeviceType deviceType, RasterBrush rasterBrush,
Style rasterStyle, StrokeConstants strokeConstants,
uint startRandomSeed)
{
rasterStyle.RenderModeUri = $"will3://rendering//{deviceType.ToString()}";
if (!InkDocument.Brushes.TryGetBrush(rasterBrush.Name, out Brush foundBrush))
{
InkDocument.Brushes.AddRasterBrush(rasterBrush);
}
}
private void AddVectorBrushToInkDoc(string pointerDeviceType, Wacom.Ink.Serialization.Model.VectorBrush vectorBrush, Style style)
{
style.RenderModeUri = $"will3://rendering//{pointerDeviceType}";
if (!InkDocument.Brushes.TryGetBrush(vectorBrush.Name, out Brush foundBrush))
{
InkDocument.Brushes.AddVectorBrush(vectorBrush);
}
}
持続性
インクモデルを持続させ、プラットフォーム間で共有するには、データストリーミングでエンコードする必要があります。