n次ベジェを描けるようになろう
※この記事は前のブログで2018年12月17日に投稿されたものです。最新の情報ではない可能性があります
こんにちは。最近ジーンズの膝の部分に穴が空いてダメージジーンズになった水珈琲です。
この記事はアドベントカレンダーに参加してるそうです。
突然ですが、みなさんはベジェ曲線を知ってますか?canvasなんかで使った事がある人も多いと思います。
しかし、大半のソフトウェアでは2次ベジェ、3次ベジェのみに対応しており、n次ベジェ(nは4以上の自然数)に対応していないことが多いです。
この記事では、ベジェ曲線の仕組みを知り、自分で描けるようになるのを目標にしています。
n次ベジェのイメージを掴みたい方はこちらで遊んでみてください。
第一章 ベジェ曲線って何?
この章では、ベジェ曲線のうっすら概要を説明します。
簡単に言うとベジェ曲線というのはコンピュータで扱いやすい曲線の描画方法です。
Wikipediaでは
ベジエ曲線とは、N 個の制御点から得られる N − 1 次曲線である。「ベジェ曲線」『ウィキペディア日本語版』 2018年2月3日(土)01:00 UTC
と表現されています。
よくわからない人も多いと思うので、私なりの言葉にすると、「めっちゃ短い直線をくねくね繋げたら曲線に見える気がする」です。更に意味不明になったと思うのでgif画像を見てもらいたいと思います。下記画像は所謂2次ベジェ曲線です。
上の画像を見ると20辺りから直線感は無くなっていると思います。
この章では、ベジェ曲線っていうのは直線の集合なんだな〜というのが分かってもらえたら十分です。
第二章 ベジェ曲線を描こう
この章では、実際にベジェ曲線を(出来れば皆さんの手で)書いてみたいと思います。
2次ベジェ曲線
まず、2次ベジェ曲線は
前提条件: 0 < n < 1
- 各辺の長さ*n の場所同士を直線で結ぶ
- 結んだ直線の長さ*n の場所にマークを打つ
- 滑らかになるまで1と2を繰り返した後、それらのマークを直線で繋げる
という手順で描くことができます。
nの分割を細かくするとするほど滑らかになります。
今回は、手書きがしやすいので4分割(分割点は3つ)にしたいと思います。
分割点は、0.25・0.5・0.75になります。分かりやすいので、0.5(=中心)から始めます。
下準備
まず、「制御点」というものを配置します。今回は2次ベジェ曲線なので、3個の制御点が必要です。
そして、それらを直線で繋ぎます。
n = 0.5
手順1. 各辺の長さ*n の場所同士を直線で結ぶ
手順2. 結んだ直線の長さ*n の場所にマークを打つ
n = 0.25
手順1. 各辺の長さ*n の場所同士を直線で結ぶ
手順2. 結んだ直線の長さ*n の場所にマークを打つ
n = 0.75
手順1. 各辺の長さ*n の場所同士を直線で結ぶ
手順2. 結んだ直線の長さ*n の場所にマークを打つ
仕上げ
手順3. 滑らかになるまで1と2を繰り返した後、それらのマークを直線で繋げる
これが曲線?と思う人も多いと思いますが、定義的にはベジェ曲線(と呼んで大丈夫なはず)です。上のサンプルは4分割ですが、例えば8分割にすれば
結構曲線っぽくなります。
n(>=3)次ベジェ曲線
n(>=3)次以降のベジェ曲線は、2次ベジェ曲線の描き方+αで描くことができます。
3次ベジェ曲線は、
前提条件: 0 < n < 1
- 各辺の長さ*n の場所同士を直線で結ぶ
- もし、その直線が複数ある(折れている)場合、その線に対して上の処理をする
- 結んだ直線の長さ*n の場所にマークを打つ
- 滑らかになるまで1と2を繰り返した後、それらのマークを直線で繋げる
となります。
また同じように手順を追うと大変なので、1つだけサンプルを置いておきます。
n = 0.25
手順1. 各辺の長さ*n の場所同士を直線で結ぶ
手順2. もし、その直線が複数ある(折れている)場合、その線に対して上の処理をする
該当するので、手順1に戻ります(続きの内容は下です)
手順1. 各辺の長さ*n の場所同士を直線で結ぶ
手順2. もし、その直線が複数ある(折れている)場合、その線に対して上の処理をする
今度は該当しないので、手順3に進みます。
手順3. 結んだ直線の長さ*n の場所にマークを打つ
この処理を、繰り返せばn(>=3)次以降のベジェ曲線も描く事ができます!
第三章 プログラムに起こそう
サンプルソース
module.exports = (points, fineness) => {
if (points.length < 3) return []
if (fineness <= 0) return []
if (points.some(p => p.length < 2 || !Number.isFinite(p[0]) || !Number.isFinite(p[1]))) return []
let lines = []
for (let i = 0; i < points.length - 1; i++) lines.push(lineSplit(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1], fineness))
const bezier = [];
bezier.push([points[0][0], points[0][1]]);
[...Array(fineness)].forEach((n, index) => {
let l = lines.map(e => e[index])
while (l.length > 1) {
let t = []
for (let i = 0; i < l.length - 1; i++) {
t.push(lineSplit(l[i][0], l[i][1], l[i + 1][0], l[i + 1][1], fineness)[index])
}
l = t.slice()
}
bezier.push([l[0][0], l[0][1]])
})
bezier.push([points[points.length - 1][0], points[points.length - 1][1]])
return bezier
}
const lineSplit = (x1, y1, x2, y2, split) => [...Array(split)].map((e, i) => ([x1 + ((x2 - x1) / (split + 1)) * (i + 1), y1 + ((y2 - y1) / (split + 1)) * (i + 1)]))
pointsが[ [x,y], [x,y] … ]という形式の制御点の配列、finenessが分割数です。
本当は説明を付けたかったのですが、時間と体力の都合上省かせていただきます…すみません…
ソースを読んで解読してみてください…分からない部分はコメントで質問してください!
このプログラムは、あくまでも制御点の配列から直線の配列に変換するだけなので、基本的にプログラムを移植してしまえばx,yを指定して直線を描画できる環境であればどこでもn次ベジェ曲線を描くことができます!
まとめ
駆け足で解説してみましたが、分かりましたでしょうか…
ベジェ曲線が描けると、表現の幅が少しですが広がります。いざ描いてみるとそれほど難しいものでもないので、是非プログラムに取り入れてみてください!
Discussion
New Comments
No comments yet. Be the first one!