WPF でチョーク描画の実装

白板アプリケーションでは、様々な種類のペンを使用して描画することがあります。例えば、書道用の筆やマーカーなどです。ここでは、チョークの描画機能について説明します。

インターネットで検索しても、チョーク描画に関する情報はほとんど見つかりません。参考にできるのは、毛筆や楷書の描画に関する記事がいくつかありますが、それらは以下のリンクを参照してください:

  • 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. 実際の描画結果:

通常の書き込み

重力による遅い書き込み(チョークを強く押すような感覚):

タグ: WPF InkCanvas ChalkDrawing WriteableBitmap DrawingContext

8月5日 10:51 投稿