ベジェ曲線で図形を描く

ベジェ曲線で図形を描くサンプルプログラムを作成していたら、意外と面白かったので色々作ってみました。

HTML5 Javascript版

文字で説明しても判りにくいにので、下にある実行ボタンを押してみてください。ランダムに色々な模様が表示されます。
(ブラウザが対応していない、Javascriptの実行を禁止している等、環境によって表示されない場合もあります)
図形を表示するには、canvasタグをサポートしたブラウザが必要です。
点の数
ステップ
制御線長さ1
制御線長さ2
制御線角度1
制御線角度2
設定値
線の数
前データ
値を転送
どのような処理をしているかというと
1:円周上に等間隔に複数の点を設定する。
2:開始点と指定値分隣の点をベジエ曲線で継ぐ。
3:さらにその点から指定分隣の点を同じ曲線で継ぐ。
4:開始点に戻るまで続ける。
5:使用されなかった点がある場合は、開始点を1個ずらして同じ処理を繰り返す。
6:全ての点が使用されるまで処理を続ける。

となります。
設定値については
点の数:円周上に配置される点の数。2以上、ランダム設定の場合20まで
ステップ:いくつ隣の点と継ぐか。1がすぐ隣の点を表します。1以上点の数未満。
制御線長さ1&2:ベジェ曲線の始点終点の制御線の長さを設定します。ランダム設定の場合-100から100。
制御線角度1&2:ベジェ曲線の始点終点の制御線の角度を設定します。ランダム設定の場合0から359。 0を指定した場合制御線は円の中心から点に引かれた線と直角に(円の接線)、線の中央に向かう方向になります。

上のスクリプトでは値を入れずに実行ボタンを押した場合、ランダムな値が割り当てられます。
値を入れていても、条件に合致しない場合は無視してランダムな値を割り当てます。
設定値の欄は描画時に使用された値が表示されます。5 ,2 ,0 ,0 ,0 ,0 のように値のみがカンマ区切りで表示されます。
線の数の欄に線が何本あるかが表示されます。
前データの欄には、前のデータが保存されます。

値を転送の欄に設定値データを入れて転送ボタンを押すと上の6個のテキストボックスに値が転送されます。
一度表示したデータをもう一度見たい場合等に使用してください。

Windows版

VB2010を使用して作成しました。

基本的に上のスクリプトと同じですが、拡張メタファイルとしての保存とクリップボードへのコピーができます。
ベジェ曲線の描画方法が閉じた曲線が描けなかったので、開始点と終点の接続がきれいになりませんでした。
仕方ないので最初の線に最後に上書きするようにしてみました。チェックボックスで選択できます。
両方チェックを入れるとおかしな図形ができます。理由が知りたい方は下の「おまけ」を見てください。

遊んでみたい方はこちらからどうぞ
BezierVb.zip




見るようなものはないと思いますがコードの一部です。興味のある方はご覧ください。
変数の宣言です。
    Dim PN As Integer '点の数
    Dim ST As Integer 'ステップ
    Dim L1 As Integer '制御線長さ1
    Dim L2 As Integer '制御線長さ2
    Dim S1 As Integer '制御線角度1
    Dim S2 As Integer '制御線角度2
    Dim DM As Integer '直径
    Dim RD As Integer '半径
    Dim LN As Integer '線の数

    Dim paArray()() As Point '点配列の配列


設定値から座標を計算して配列に保存する部分です。
たぶんもっと賢い方法があるんでしょうが考えたくなかったので書いたままで。
    Private Sub CalcPoint()

        '頂点とステップの最大公約数
        Dim A As Integer = PN
        Dim B As Integer = ST
        Dim C
        For I = 0 To 1 Step 0
            C = A Mod B
            A = B
            B = C
            If B = 0 Then
                Exit For
            End If
        Next I

        LN = A

        '描画データ作成
        '最小公倍数が1でない場合 2から最小公倍数-1までの点が使用されないので
        '開始点を1ずつずらして最小公倍数回描画する
        Dim sin0 As Double
        Dim sin1 As Double
        Dim sin2 As Double
        Dim cos0 As Double
        Dim cos1 As Double
        Dim cos2 As Double
        Dim X0 As Integer
        Dim Y0 As Integer
        Dim XS As Integer '開始点
        Dim YS As Integer '開始点
        ReDim paArray(A - 1)
        For K = 0 To A - 1
            Dim plist As New List(Of Point)
            XS = Math.Round(Math.Sin(K * 6.283 / PN) * RD) + RD + 50
            YS = RD + 50 - Math.Round(Math.Cos(K * 6.283 / PN) * RD)
            plist.Add(New Point(XS, YS)) '開始点保存
            For I = 0 To PN / A
                cos0 = Math.Cos(((ST * I) + K) * 6.283 / PN)
                sin0 = Math.Sin(((ST * I) + K) * 6.283 / PN)
                X0 = Math.Round(sin0 * RD) + RD + 50
                Y0 = RD + 50 - Math.Round(cos0 * RD)
                cos1 = Math.Cos(((ST * I) + K) * 6.283 / PN + S1 * 3.142 / 180)
                sin1 = Math.Sin(((ST * I) + K) * 6.283 / PN + S1 * 3.142 / 180)
                cos2 = Math.Cos(((ST * I) + K) * 6.283 / PN + S2 * 3.142 / 180)
                sin2 = Math.Sin(((ST * I) + K) * 6.283 / PN + S2 * 3.142 / 180)
                If I > 0 Then '終点とその制御点を保存
                    If I = PN / A Then '最後の頂点は最初と同じ
                        plist.Add(New Point(XS - Math.Round(cos2 * L2), YS - Math.Round(sin2 * L2)))
                        plist.Add(New Point(XS, YS))
                        'plist.Add(New Point(X0 - Math.Round(cos2 * L2), Y0 - Math.Round(sin2 * L2)))
                    Else
                        plist.Add(New Point(X0 - Math.Round(cos2 * L2), Y0 - Math.Round(sin2 * L2)))
                        plist.Add(New Point(X0, Y0))
                        'plist.Add(New Point(X0, Y0))
                    End If
                End If
                If I < PN / A Then '始点の制御点を保存
                    plist.Add(New Point(X0 + Math.Round(cos1 * L1), Y0 + Math.Round(sin1 * L1)))
                End If
            Next I
            If CheckBox2.Checked Then '開始点上書きのため最初の線を繰り返す
                plist.Add(New Point(plist(1).X, plist(1).Y))
                plist.Add(New Point(plist(2).X, plist(2).Y))
                plist.Add(New Point(plist(3).X, plist(3).Y))
            End If
            paArray(K) = plist.ToArray
        Next K

    End Sub



メタファイルを作成してファイルに保存/クリップボードにコピーするサブルーチンです。
SaveMetafile(0)でファイルに保存、SaveMetafile(1)でクリップボードにコピーします。
MemoryStreamを使っているのは、最初これをクリップボードにコピーすれば良いかと思っていたからです。
実際にはAPIを呼ばないとメタファイルをクリップボードに転送できませんでした。

    Private Sub SaveMetafile(ByVal sw As Integer)
        Dim ms As MemoryStream = New MemoryStream
        Dim mf As Metafile ' = New Metafile("metafiletest.wmf", ipHdc, EmfType.EmfPlusDual)
        Dim grfx As Graphics = CreateGraphics()
        Dim ipHdc As IntPtr = grfx.GetHdc
        mf = New Metafile(ms, ipHdc, EmfType.EmfPlusDual)
        grfx.ReleaseHdc(ipHdc)
        grfx.Dispose()
        Dim grfxMeta As Graphics = Graphics.FromImage(mf)

        Dim LW As Double '線幅
        Try
            LW = Double.Parse(TextBox11.Text)
            If LW < 0 Then Throw New Exception("LW<0")
        Catch ex As Exception
            LW = 1
        End Try
        Dim CL As Color '線色
        Try
            If TextBox12.Text = "" Then Throw New Exception("TextBox12.Text=""""")
            CL = ColorTranslator.FromHtml(TextBox12.Text)
        Catch ex As Exception
            CL = Color.Black
        End Try
        Dim myPen As New Pen(CL, LW) 'ペンを作成


        If CheckBox1.Checked Then 'ベジェ曲線を1本ずつ描く
            Dim myPath As New GraphicsPath
            For i = 0 To paArray.Length - 1
                myPath.StartFigure() '新しい図形を開始する
                For j = 0 To Int(paArray(i).Length / 3) - 1
                    myPath.AddBezier(paArray(i)(j * 3), paArray(i)(j * 3 + 1), paArray(i)(j * 3 + 2), paArray(i)(j * 3 + 3))
                Next j
                myPath.CloseFigure()
            Next i
            grfxMeta.DrawPath(myPen, myPath) 'パス図形を描画する()
        Else 'ベジェ曲線をまとめて描く
            For i = 0 To paArray.Length - 1
                grfxMeta.DrawBeziers(myPen, paArray(i))
            Next i
        End If

      


        grfxMeta.Dispose()

        If sw = 0 Then 'ファイルに保存
            SaveFileDialog1.Filter = "拡張メタファイル|*.emf|メタファイル|*.wmf|すべてのファイル|*.*"
            If SaveFileDialog1.ShowDialog = Windows.Forms.DialogResult.OK Then
                Dim fsm As FileStream = New FileStream(SaveFileDialog1.FileName, FileMode.Create, FileAccess.Write)
                fsm.Write(ms.GetBuffer, 0, CType(ms.Length, Integer))
                fsm.Close()
            End If
        ElseIf sw = 1 Then 'クリップボードにコピー
            ClipboardMetafileHelper.PutEnhMetafileOnClipboard(Me.Handle, mf)
            'この呼び出しには下記ページのコードが必要です
            '「クリップボード上のメタファイルは、すべてのアプリケーションには表示されません」https://support.microsoft.com/ja-jp/kb/323530
        End If

    End Sub

Adobe Photoshop & Illustrator JavaScript 版

調子に乗ってPhotoshop と Illustrator の JavaScript も作ってみました。
JavaScriptを実行すると値を入力するダイアログが表示されますので、設定値をカンマ区切りで入れてください。
設定値は上と同じで6個の値に加えて、直径も指定できます。
各値を省略した場合はランダムな値が割り当てられます。直径を省略した場合は200が設定されます。
このページで表示したものと同じ図形がほしければ、上のスクリプトで作成した設定値をこぴぺでOKです。
初めて作ったので、最初は Photoshop & Illustrator は同じスクリプトで良いのかと思っていましたが、違いました。
違うソフトなので当然といえば当然ですが。
あとY軸の座標系が逆でした。なので座標計算部分のコードが若干違います。
たぶんバージョンとか文字コードとかで動かないことがあります。適当に手直しして使ってください。

Adobe Photoshop 用
bezier-p.js

Adobe Illustrator 用
bezier-i.js



おまけ VB .NET でのベジェ曲線描画について

VB .NET のプログラムに変なチェックボックスがある理由の説明です。
ベジェ曲線を描く時は、DrawBezierを使います。ひし形を描く場合
        Dim P1 As New Point(100, 20)
        Dim P2 As New Point(180, 100)
        Dim P3 As New Point(100, 180)
        Dim P4 As New Point(20, 100)

        G.DrawBezier(myPen, P1, P1, P2, P2)
        G.DrawBezier(myPen, P2, P2, P3, P3)
        G.DrawBezier(myPen, P3, P3, P4, P4)
        G.DrawBezier(myPen, P4, P4, P1, P1)
のようにするわけですが、これだと
こんな風に、線の接続点が離れてしまいます。

そこでDrawBeziersを使ってみます。
        Dim pa() As Point = {P1, P1, P2, P2, P2, P3, P3, P3, P4, P4, P4, P1, P1}

        G.DrawBeziers(myPen, pa)
これでも、開始点と終了点の接続がきれいになりません。

それで1本目の線を上書きすることにします。
        Dim pa1() As Point = {P1, P1, P2, P2, P2, P3, P3, P3, P4, P4, P4, P1, P1, P1, P2, P2}

        G.DrawBeziers(myPen, pa)
これできれいになりました。

また、GraphicsPathを使う方法もあります。これだと始点と終点を自動的に接続してくれるみたいです。
        Dim myPath As New GraphicsPath
        myPath.StartFigure() '新しい図形を開始する
        myPath.AddBezier(P1, P1, P2, P2)
        myPath.AddBezier(P2, P2, P3, P3)
        myPath.AddBezier(P3, P3, P4, P4)
        myPath.AddBezier(P4, P4, P1, P1)
        myPath.CloseFigure()
        G.DrawPath(myPen, myPath) 'パス図形を描画する
こうなります。
上との違いは見た目では判りませんが、上が曲がった線であるのに対して、これは閉じた多角形になっているはずです。
ただし、クリップボードを介してコピーして使う場合は、受け側のソフトの処理によってここで書いたようにはならないことがあります。






【参考にしたページ】
DOBON.NET 曲線を描く
VB .net でのベジェ曲線の描き方の参考にしました。

メタファイル生成方法(Metafileクラス)
VB .net でのメタファイル生成方法の参考にしました。

6.8 メタファイル
VB .net でのメタファイル生成方法の参考にしました。

クリップボード上のメタファイルは、すべてのアプリケーションには表示されません(Metafiles on Clipboard Are Not Visible to All Applications)
VB .net でメタファイルをクリップボードに転送する参考にしました。

JavaScript
JavaScriptの使い方の参考にしました。

<canvas>タグで図形を描く
HTML5のcanvasの使い方の参考にしました。

Scripting Illustrator
IllustratorのJavaScriptの参考にしました。

Illustrator CS自動化作戦
IllustratorのJavaScriptの参考にしました。

クリエイター手抜きプロジェクト 数学曲線を描く
PhotoshopのJavaScriptの参考にしました。





TOPに戻る
2016/6/16