OpenSiv3D v0.4.2 集中線描画機能のリファレンス

先日リリースされたOpenSiv3Dv0.4.2のHamFrameworkに追加された新機能について紹介します。

# どんな機能？

見てもらった方が早いので、スクリーンショット等を載せます。

<img width="1276" alt="スクリーンショット 2019-11-30 23.34.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212031/5b260f0d-2911-2ba5-7500-ccc3f69fa965.png">

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">v0.4.2, 集中線描画機能 (by <a href="https://twitter.com/co_para7_?ref_src=twsrc%5Etfw">@co_para7_</a> さん) と、縁取り文字描画機能が追加されるので、これを作るしかないと思った <a href="https://twitter.com/hashtag/OpenSiv3D?src=hash&amp;ref_src=twsrc%5Etfw">#OpenSiv3D</a> <a href="https://t.co/6KNUSFnyoe">pic.twitter.com/6KNUSFnyoe</a></p>&mdash; Ryo Suzuki (@Reputeless) <a href="https://twitter.com/Reputeless/status/1200069264155131904?ref_src=twsrc%5Etfw">November 28, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">集中線 <a href="https://twitter.com/hashtag/OpenSiv3D?src=hash&amp;ref_src=twsrc%5Etfw">#OpenSiv3D</a> <a href="https://t.co/3Ou6xUE0KG">pic.twitter.com/3Ou6xUE0KG</a></p>&mdash; para7 (@co_para7_) <a href="https://twitter.com/co_para7_/status/1200740202651639808?ref_src=twsrc%5Etfw">November 30, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

よくある集中線です。とりあえず使うだけで地味な画面を派手さにしてくれます。使い方は無限大。

# 基本的な使い方

　集中線の生成に関わる数値を設定して、draw関数を呼ぶだけです。
　drawを呼び出したときに、数値に変更があれば集中線が自動で再生成されます。ユーザ側での事前処理は一切必要ありません。具体的にどんな数値が設定できるかは以下のリファレンスにあります。

　毎フレームエフェクトを変えたい！など、意図的に集中線を再生成したい場合はgenerate関数を呼び出してください。

# リファレンス

指定できる要素

- 集中線が注目する範囲（図形）:TargetShape
- 描画するウインドウの大きさ:OuterRect
- 線の本数:LineCount
- 線の太さ:MinThickness, MaxThickness
- 線が出現する座標のばらつき具合:OffsetRange
- 線の色
- 乱数の設定

## 集中線が注目する範囲(図形):TargetShape

　テンプレートを使っているのでRect, Circle, EllipseなどSiv3Dの様々な図形に対応しています。
　デフォルトではEllipseを使う設定になっています。Ellipse以外の形で集中線を作りたい場合はコンストラクタを呼び出すときに設定してください。

```cpp:Rectにする例
//中心の座標
const Vec2 targetShapeCenter(800, 400);
//RectFをつくる
const auto targetShape = RectF(Arg::center = targetShapeCenter, 300, 200);
//コンストラクタで渡す
SaturatedLinework linework(targetShape);

//途中で変えたいとき、図形を取得したいとき
//set
linework.setTargetShape(RectF(Arg::center = targetShapeCenter, 200, 100));
//get
auto shape = linework.getTargetShape();
//どの数値もsetHoge, getHogeの形で変更・取得ができるようになっていますので、
//以降この2つの機能(set, get)については省略します。
//setはメソッドチェーンに対応しています。
```

## 描画するウインドウの大きさ:OuterRect

　何も指定しなければ画面全体になるので、特に指定する必要はありません。
　画面に枠があったり（一番最後のサンプルでは枠があります）、画面以外に描画する際はサイズを指定してください。
　setOuterRect以外に、コンストラクタでも指定することができます。

```cpp
//画面の0.9倍の範囲に描画する
SaturatedLinework linework(targetShape, Scene::Rect().scaled(0.9));
```

## 線の本数:LineCount

線の本数です。
他の数値は全てdouble型ですが、これはint型です。

## 線の太さ:MinThickness, MaxThickness

集中線は内側が細く、外側が太くなっています。内側の太さはゼロで固定なので、ここでは外側の幅を指定できます。
線の太さはmin~maxの範囲内でランダムに決まります。全て均一の太さで生成したい場合は、minとmaxに同じ数値を設定してください。

```cpp
//個別設定もできる
linework.setMinThickness(minThick)
        .setMaxThickness(maxThick);

//一度に設定することもできる
linework.setThickness(minThick, maxThick);

//太さを固定したいときは、両方に同じ値を入れればよい
linework.setThickness(maxThick, maxThick);
```

min<=maxにならない数値を設定すると例外を出します。

## 線が出現する座標のばらつき具合:OffsetRange

　集中線の出現位置が図形そのままだと、集中線の密度によっては輪郭がはっきり見えてしまいます。
　それを嫌う場合などはこのOffsetRangeを設定することで、0~OffsetRangeの範囲内で外側に座標がばらけるようになります。
　初期値は0なので図形そのままの形に沿って生成します。

　注意：v0.4.2時点では、負の値を入れた場合、draw関数内で呼び出しているgenerate関数にて乱数生成器が例外を送出します。記事作成中に気づいたエラー処理の入れ忘れなので、次期バージョンではsetOffsetRangeに0未満を渡した時点で例外が出るように修正しておきます。

## 線の色

　色を黒以外にしたい場合は、draw関数を呼ぶときに引数で色を渡して下さい。

```cpp
linework.draw(color);
```

## 乱数の設定

　指定しなければランダムなシード値が設定されます。毎回同じ集中線を生成したいときはシード値を指定してください。

# サンプルコード

　開発中にデバッグで使っていたソースコードを一部編集して記載しました。OpenSiv3Dのヘッダにある公式サンプルよりもリッチなサンプルになっていますので、実際に集中線を調整するときなどにも活用してもらえたらと思います。

```cpp:Sample.cpp
#include <Siv3D.hpp>
#include <HamFramework.hpp>

//数値保存用
struct
{
    double value;
    double max;
    double min = 0;
} lineCount, offsetRange, minThick, maxThick;

void Main()
{
    HSV lineColor = ColorF(0.11), frameColor = ColorF(0.2), windowColor = ColorF(0.9);

    Window::Resize(1280, 720);

    const Font font(90, Typeface::Default, FontStyle::BoldItalic);

    const Vec2 targetShapeCenter(800, 400);
    const auto targetShape = Ellipse(targetShapeCenter, 200, 100);
    // Rectにする場合
    // const auto targetShape = RectF(Arg::center = targetShapeCenter, 300, 200);
    SaturatedLinework linework(targetShape);

    // シード値を設定したいとき
    // linework.SetSeed(0);

    // Lineworkで設定される初期値を取得
    lineCount.value = static_cast<int>(linework.getLineCount());
    offsetRange.value = linework.getOffsetRange();
    minThick.value = linework.getMinThickness();
    maxThick.value = linework.getMaxThickness();

    // シード値を表示
    Print(U"seed:", linework.getSeed());

    lineCount.max = 300;
    offsetRange.max = 400;
    minThick.max = 50;
    maxThick.max = 120;

    // Slider GUIのサイズ
    constexpr int32 label = 150;
    constexpr int32 slider = 250;

    // GUIのベース座標
    constexpr int32 x = 20;
    constexpr int32 y = 40;

    const Font colorFont(20);

    // デフォルトだと画面全体に描画されるので、画面より少し小さめの領域に描画するように再設定する
    const Rect virtualWindow = Scene::Rect().scaled(0.92);
    linework.setOuterRect(virtualWindow);

    while (System::Update())
    {
        //ウインドウ本体
        virtualWindow.draw(windowColor);

        //集中線を描画
        linework.draw(lineColor);

        //枠
        virtualWindow.drawFrame(0, 100, frameColor);

        font(U"集中線").drawAt(Geometry2D::Center(linework.getTargetShape()), Palette::Black);

        SimpleGUI::Slider(U"LineCount{:.0f}"_fmt(lineCount.value), lineCount.value, lineCount.min, lineCount.max, Vec2(x, y), label, slider);
        SimpleGUI::Slider(U"RandomPos{:.0f}"_fmt(offsetRange.value), offsetRange.value, offsetRange.min, offsetRange.max, Vec2(x, y + 40 * 1), label, slider);

        if (SimpleGUI::Slider(U"MinThick{:.0f}"_fmt(minThick.value), minThick.value, minThick.min, minThick.max, Vec2(x, y + 40 * 2), label, slider))
        {
            maxThick.value = Max(maxThick.value, minThick.value);
        }

        if (SimpleGUI::Slider(U"MaxThick{:.0f}"_fmt(maxThick.value), maxThick.value, maxThick.min, maxThick.max, Vec2(x, y + 40 * 3), label, slider))
        {
            minThick.value = Min(maxThick.value, minThick.value);
        }


        //メソッドチェーンで設定できる
        linework.setLineCount(static_cast<int>(lineCount.value))
            .setOffsetRange(offsetRange.value)
            .setThickness(minThick.value, maxThick.value);

        // シード値を変える
        if (SimpleGUI::Button(U"SeedReset", Vec2(x + 160, y + 40 * 4)))
        {
            ClearPrint();
            linework.setSeed(RandomUint64());
            Print(U"seed:",linework.getSeed());
        }

        // 値をランダムにセット
        if (SimpleGUI::Button(U"RandomSet", Vec2(x, y + 40 * 4)))
        {
            lineCount.value = Random(lineCount.min, lineCount.max);
            offsetRange.value = Random(offsetRange.min, offsetRange.max);
            minThick.value = Random(minThick.min, minThick.max);
            maxThick.value = Random(minThick.value, maxThick.max);

            linework.setLineCount(static_cast<size_t>(lineCount.value))
                .setThickness(static_cast<double>(minThick.value), static_cast<double>(maxThick.value))
                .setOffsetRange(static_cast<double>(offsetRange.value));
        }

        // 再生成
        if (SimpleGUI::Button(U"Generate", Vec2(x, y + 40 * 5)))
        {
            linework.generate();
        }

        // 色変更GUI

        const int width = 160;
        Rect(x, y + (int)(40 * 6.35), width * 3, 27 * 1).draw(Palette::White).drawFrame(0, 1, Palette::Black);

        colorFont(U"LineColor").draw(x + width * 0, y + 40 * 6.35, Palette::Black);
        colorFont(U"frameColor").draw(x + width * 1, y + 40 * 6.35, Palette::Black);
        colorFont(U"windowColor").draw(x + width * 2, y + 40 * 6.35, Palette::Black);

        SimpleGUI::ColorPicker(lineColor, Vec2(x + 170 * 0, y + 40 * 7));
        SimpleGUI::ColorPicker(frameColor, Vec2(x + width * 1, y + 40 * 7));
        SimpleGUI::ColorPicker(windowColor, Vec2(x + width * 2, y + 40 * 7));
    }
}

```

# 今後やるかもしれない更新

<img width="171" alt="スクリーンショット 2019-12-02 17.25.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212031/f7404cbd-93f6-95a5-f0cb-e2a6c1007c9f.png">

実装会にて、角度によっては端の部分がちょっと汚くなるので、描画の仕組みを変えようか？という話が出ました。
v0.4.2に間に合わせるため変更せずリリースしましたので、時間があったら今後調整しておきます。
~~宣言しておかないと永遠にやらなさそうなのでここで宣言した~~