白板アプリケーションでは、様々な種類のペンを使用して描画することがあります。例えば、書道用の筆やマーカーなどです。ここでは、チョークの描画機能について説明します。
インターネットで検索しても、チョーク描画に関する情報はほとんど見つかりません。参考にできるのは、毛筆や楷書の描画に関する記事がいくつかありますが、それらは以下のリンクを参照してください:
- WPF InkCanvas CustomInk 毛筆効果
- WPF InkCanvas で毛筆効果
チョーク描画のアイデアは、GitHub の mychalkboard/MyChalkBoard リポジトリから得られました。このリポジトリは、チョークで速やかにスケッチを行うためのアプリケーションです。
基本的な考え方:
- 一般的なペン先(ImageSource)を使用し、Stroke でキャプチャされた StylusPoints の点を基に、対応する座標を生成し、drawingContext.DrawImage を使用して画像を描画します。
1. ペン先の生成
UIデザイナーが作成したチョーク形状のPNG画像を使用します。
2. 色の変更
以下のブログ記事を参考に、WriteableBitmap の RGBA 値を変更することで色を置き換えます。
public static unsafe ImageSource ChangeImageColor(Color newColor, WriteableBitmap writableBitmap)
{
if (writableBitmap == null)
{
return null;
}
writableBitmap.Lock();
int length = writableBitmap.PixelWidth * writableBitmap.PixelHeight * writableBitmap.Format.BitsPerPixel / 8;
byte* backBuffer = (byte*)writableBitmap.BackBuffer;
byte[] byteList = new byte[length];
for (int i = 0; i + 4 < length; i += 4)
{
byteList[i] = newColor.B;
byteList[i + 1] = newColor.G;
byteList[i + 2] = newColor.R;
byteList[i + 3] = backBuffer[i + 3];
}
writableBitmap.Unlock();
writableBitmap = new WriteableBitmap(writableBitmap.PixelWidth, writableBitmap.PixelHeight, 96, 96, writableBitmap.Format, writableBitmap.Palette);
writableBitmap.Lock();
writableBitmap.WritePixels(new Int32Rect(0, 0, writableBitmap.PixelWidth, writableBitmap.PixelHeight), byteList, writableBitmap.BackBufferStride, 0);
writableBitmap.AddDirtyRect(new Int32Rect(0, 0, writableBitmap.PixelWidth, writableBitmap.PixelHeight));
writableBitmap.Unlock();
return writableBitmap;
}
3. 点の収集
マウスやタッチ入力から得られる点を収集するため、独自に開発したアルゴリズムを使用します。具体的には、Down、Move、Up イベントと区間ごとの集計を行い、点を収集します。
4. 点の補完
収集された点が疎密によって異なるため、点が少ない部分では補完を行います。これにより、ギザギザが少なくなるように調整できます。
var previousPoint = new Point(double.NegativeInfinity, double.NegativeInfinity);
for (int i = 0; i < stylusPoints.Count; i++)
{
var pressureFactor = stylusPoints[i].PressureFactor * 2;
var currentPoint = stylusPoints[i].ToPoint();
var vector = previousPoint - currentPoint;
var newWidth = width * pressureFactor;
var baseWidth = newWidth / 1.5;
if (!double.IsInfinity(vector.Length) && vector.Length > baseWidth)
{
var w2 = newWidth;
if (newWidth - vector.Length > newWidth)
w2 = newWidth - vector.Length;
var newPointCount = (int)(vector.Length / (baseWidth)) * 2;
var dx = (currentPoint.X - previousPoint.X) / newPointCount;
var dy = (currentPoint.Y - previousPoint.Y) / newPointCount;
for (int pointCount = 0; pointCount < newPointCount; pointCount++)
{
var newX = previousPoint.X + dx * (pointCount + 1);
var newY = previousPoint.Y + dy * (pointCount + 1);
drawingContext.DrawImage(imageSource, new Rect(newX - w2, newY - w2, w2 * 2, w2 * 2));
}
}
else
{
Rect rectangle = new Rect(currentPoint.X - newWidth, currentPoint.Y - newWidth, newWidth * 2, newWidth * 2);
drawingContext.DrawImage(imageSource, rectangle);
}
previousPoint = currentPoint;
}
5. 描画処理
上記の4つのステップを経て、描画のサイズを計算し、drawingContext.DrawImage を使用してチョークの画像を描画します。
drawingContext.DrawImage(imageSource, new Rect(newX - w2, newY - w2, w2 * 2, w2 * 2));
6. 実際の描画結果:
通常の書き込み
重力による遅い書き込み(チョークを強く押すような感覚):