前記事での反省を踏まえつつ JavaScript を使って計算します。
生成する正多面体
原点を中心として、頂点が半径1の球上にある正多面体のデータを求めます。
算出される座標は単位ベクトルになります。
正多面体の面と頂点
正多面体の面数 M と、頂点数 V と面の角数 N と頂点に接する面の数 F の対応は以下のとおり。
M | 正多面体 | N | F | 対 | V |
---|---|---|---|---|---|
4 | 正四面体 | 3 | 3 | 正四面体 | 4 |
6 | 正六面体 | 4 | 3 | 正八面体 | 8 |
8 | 正八面体 | 3 | 4 | 正六面体 | 6 |
12 | 正十二面体 | 5 | 3 | 正二十面体 | 20 |
20 | 正二十面体 | 3 | 5 | 正十二面体 | 12 |
V | 対 | F | N | 正多面体 | M |
正多面体の面の法線(単位ベクトル)は「対となる正多面体」の頂点(単位ベクトル)にすることができます。
また、正多面体と「対となる正多面体」は
- 面数 M と頂点数 V
- 面の角数 N と頂点に接する面の数 F
を入れ替えたものになっています。
面数に対する序数
面数に対する序数を
面数 | 4 | 4 | 6 | 8 | 12 | 20 |
---|---|---|---|---|---|---|
序数 | 0 | 1 | 2 | 3 | 4 | 5 |
とします。正四面体は対も正四面体なので重複させると、対の序数は $\left( M \oplus 1 \right)$ とできます。
全ての頂点を求める
最初の3頂点を求める
最初の3頂点を
\begin{eqnarray}
\phi & = & \frac{2\pi}{F} \\
\cos{\theta} & = & 2 \cos^2{ \frac{\pi}{N} } \csc^2{ \frac{\pi}{F} } - 1 \\
\\
P_1 & = & \left( 0,\ 0,\ 1 \right) \\
P_2 & = & \left( \sin{\theta},\ 0,\ \cos{\theta} \right) \\
P_3 & = & \left( \sin{\theta} \cos{\phi},\ \sin{\theta} \sin{\phi},\ \cos{\theta} \right) \\
\end{eqnarray}
とします。これは対となる正多面体の法線にもなります。
新たな頂点を求める
この3頂点から隣の頂点を求めるため
\begin{eqnarray}
v_x & = & P_1 \\
v_y & = & \frac{ v_x \times P_2 }{ \left| v_x \times P_2 \right| } \\
v_z & = & \frac{ v_x \times v_y }{ \left| v_x \times v_y \right| } \\
X_{cos} & = & v_x \cdot P_3 \\
Y_{cos} & = & v_y \cdot P_3 \\
Z_{cos} & = & v_z \cdot P_3 \\
\end{eqnarray}
および、隣接する2頂点の判定情報
P_{cos} = P_1 \cdot P_2
を用意します。
これで隣接する2頂点 $P_1,P_2$ から $v_x,v_y,v_z$ を求め、これと $X_{cos},Y_{cos},Z_{cos}$ から $P_3$ が求まります。
P_3 = X_{cos} v_x + Y_{cos} v_y + Z_{cos} v_z
同様に、順番を逆にした $P_2,P_1$ と $X_{cos},Y_{cos},Z_{cos}$ からは新たな頂点 $P_4$ を求めることができます。あとは、隣接する2頂点(内積が $P_{cos}$ と一致するもの)の総当たりで新たな頂点を重複排除しつつ追加していき、増えなくなるまで繰り返します。
全ての法線を求める
最初の3法線を求める
最初の3法線は、最初の3頂点を使って
\begin{eqnarray}
Rot_{xy} & = &
\begin{pmatrix}
\cos{\phi} & -\sin{\phi} & 0 \\
\sin{\phi} & \cos{\phi} & 0 \\
0 & 0 & 1 \\
\end{pmatrix} \\
N_1 & = & \frac{ \left( P_1 - P_3 \right) \times \left( P_2 - P_1 \right) }{ \left| \left( P_1 - P_3 \right) \times \left( P_2 - P_1 \right) \right| } \\
N_2 & = & Rot_{xy}\ N_1 \\
N_3 & = & Rot_{xy}\ N_2 \\
\end{eqnarray}
とします。
あとは、$P_1,P_2,P_3$ を $N_1,N_2,N_3$ に置換えて頂点と同じ方法で、全ての法線を求めます。
多角形のデータを作る
一つの法線が、一つの多角形の面向きを表していて、多角形の頂点と法線の角度は一定なので
PN_{cos} = P_1 \cdot N_1
が判定情報になります。
多角形の頂点表
一つの多角形の頂点表は、正多面体の頂点の中で多角形の法線との内積が $PN_{cos}$ と一致するものになります。
多角形を描画するには、頂点の並びも重要です。正多面体の頂点の中から必要な頂点を抽出しただけでは順番がバラバラなので、並び替える必要があります。並び替えるには、多角形の頂点に隣接する頂点(頂点間の内積が $P_{cos}$ と一致)は2つしかないという性質を使います。
頂点 $p_0$ に対して、$p_1,p_2$ が頂点表から見つかったとすると
p_1 \to p_0 \to p_2
または
p_2 \to p_0 \to p_1
の並びしかありません。判定は、次の外積を求めて
n = \frac{ \left( p_0 - p_1 \right) \times \left( p_2 - p_0 \right) }{ \left| \left( p_0 - p_1 \right) \times \left( p_2 - p_0 \right) \right| }
$n$ が多角形の法線と一致すれば $\left( p_1 \to p_0 \to p_2 \right)$ で、一致しないと逆向き $\left( p_1 \to p_0 \to p_2 \right)$ になります。これで頂点 $p_0$ の次の頂点が分かり、その次も同様なので、一周するように並び替えることができます。
これで、多角形描画用の 3D データが出来上がります。
プログラム(HTML+JavaScript)
正多面体の計算に加えて、GIF による 3D 動画も作成しています。
動作確認は以下のブラウザで行ってます。
- Google Chrome (Mac版) バージョン: 97.0.4692.71(Official Build) (x86_64)
プログラムは短くないので折りたたみ
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" >
<title>正多面体の生成プログラム</title>
<style>
body {
margin-left: auto;
margin-right: auto;
width: 750px;
}
table {
border: solid 1px gray;
border-collaspe: collaspe;
border-spacing: 0;
}
th, td {
border: solid 1px gray;
padding: 6px;
}
</style>
</head>
<body>
<hr><h2>正4面体 (対も正4面体)</h2><hr>
<blockquote id="RegularTetrahedron">
</blockquote>
<hr><h2>正6面体 (対は正8面体)</h2><hr>
<blockquote id="RegularHexahedron">
</blockquote>
<hr><h2>正8面体 (対は正6面体)</h2><hr>
<blockquote id="RegularOctahedron">
</blockquote>
<hr><h2>正12面体 (対は正20面体)</h2><hr>
<blockquote id="RegularDodecahedron">
</blockquote>
<hr><h2>正20面体 (対は正12面体)</h2><hr>
<blockquote id="RegularIcosahedron">
</blockquote>
<script type="text/javascript">
<!--
/* ********************************** */
/*
* 正多面体のデータを作る
*/
class MyRegularPolyhedron {
/* ベクトルのソート用(逆順比較) */
static vecCmpR(a, b) {
const va = a.vector;
const vb = b.vector;
const zc = va[2] - vb[2]; if (zc < 0) return +1; if (zc > 0) return -1;
const yc = va[1] - vb[1]; if (yc < 0) return +1; if (yc > 0) return -1;
const xc = va[0] - vb[0]; if (xc < 0) return +1; if (xc > 0) return -1;
return 0;
};
/* ベクトルの一致を調査: 演算誤差を考慮して、1.0 との一致比較はしない */
static vecEq = ((a, b) => (Math.abs(1.0 - a.dot(b)) < (1 / (1 << 20))));
/* ベクトルの内積と比較値の一致を調査: 演算誤差を考慮して、1.0 との一致比較はしない */
static vecDotEq = ((a, b, c) => (Math.abs(c - a.dot(b)) < (1 / (1 << 20))));
/* 重複を排除しながら追加する */
static vecAppend(l, v) {
for (let q of l)
if (MyRegularPolyhedron.vecEq(v, q))
return false;
l.push(v);
return true;
};
/* 3ベクトルから残りのベクトルを求める */
static vecFrom(p1, p2, p3) {
const S = MyRegularPolyhedron;
const vecDotEq = S.vecDotEq;
const vecs = [p1, p2, p3];
// 最初の2ベクトルから新しいベクトルを求めるための準備.
const v1 = p1.normalize();
const v2 = p1.cross(p2).normalize();
const v3 = v1.cross(v2).normalize();
const x_cos = v1.dot(p3);
const y_cos = v2.dot(p3);
const z_cos = v3.dot(p3);
const p_cos = p1.dot(p2);
// 2ベクトルに隣接するベクトルの追加関数.
const vecAdd = function(q1, q2) {
const x = q1.normalize();
const y = q1.cross(q2).normalize();
const z = x.cross(y).normalize();
const v = x.mul(x_cos).add(y.mul(y_cos)).add(z.mul(z_cos));
S.vecAppend(vecs, v.normalize());
};
// 総当たりで求める.
let last_veclen = 0;
let new_veclen = vecs.length;
do {
for (let i1 = last_veclen; i1 < new_veclen; i1++) {
const q1 = vecs[i1];
for (let i2 = 0; i2 < new_veclen; i2++) {
if (i1 == i2)
continue;
const q2 = vecs[i2];
if (!vecDotEq(q1, q2, p_cos))
continue;
vecAdd(q1, q2);
vecAdd(q2, q1);
};
};
last_veclen = new_veclen;
new_veclen = vecs.length;
} while (last_veclen != new_veclen);
vecs.sort(S.vecCmpR);
return vecs;
};
// 面情報の一覧を作成.
static polyFrom(vecs, norms, pp_cos, np_cos) {
const vecEq = MyRegularPolyhedron.vecEq;
const vecDotEq = MyRegularPolyhedron.vecDotEq;
const nl = new Array();
for (const n of norms) {
// 面に属する頂点を抽出(とインデックス)
const plist = vecs.filter(v => vecDotEq(v, n, np_cos));
const pindex = [...Array(plist.length)].map((_, i) => vecs.indexOf(plist[i]));
// 頂点の接続表を作成する.
const plink = new Array();
for (let i1 = 0; i1 < pindex.length; i1++) {
const vc = plist[i1];
let [vp, vn] = plist.filter(v => vecDotEq(v, vc, pp_cos));
if (!vecEq(n, (vc.sub(vp)).cross(vn.sub(vc)).normalize()))
[vp, vn] = [vn, vp];
const pi = plist.indexOf(vp);
const ni = plist.indexOf(vn);
plink.push([pi, ni]);
};
// 接続表から順に整列したインデックス表に変換.
const ilist = new Array();
let p = 0;
do {
ilist.push(pindex[p]);
p = plink[p][1];
} while (p != 0);
nl.push(ilist);
};
return nl;
};
// 辺情報の一覧を作成.
static edgeFrom(poly) {
const edge = new Array();
for (let l of poly) {
let p = l[l.length - 1];
for (let q of l) {
const r = (Math.min(p, q) << 8) + Math.max(p, q);
if (edge.indexOf(r) < 0) edge.push(r);
p = q;
};
};
return [...Array(edge.length)].map(
(_, i) => [(edge[i] >> 8), (edge[i] & 255)]);
};
// コンストラクタ.
constructor(M) {
const S = MyRegularPolyhedron;
const vecEq = S.vecEq;
const vecAppend = S.vecAppend;
const vecFrom = S.vecFrom;
this.S = S;
this.vecEq = vecEq;
this.vecAppend = vecAppend;
this.vecFrom = vecFrom;
const mIndex = { 4:1, 6:2, 8:3, 12:4, 20:5 }[M];
const V = [4, 4, 6, 8, 12, 20][mIndex^1];
const F = [3, 3, 3, 4, 3, 5][mIndex];
const N = [3, 3, 4, 3, 5, 3][mIndex];
const PiDivF = Math.PI / F; // π÷F
const PiDivN = Math.PI / N; // π÷N
// const D = 1 / Math.tan(PiDivF) / Math.tan(PiDivN); // 中心から面までの距離.
// 最初の3頂点を求める.
const rotXY = 2 * PiDivF;
const cosXY = Math.cos(rotXY);
const sinXY = Math.sin(rotXY);
const cosZX = 2 * Math.pow(Math.cos(PiDivN) / Math.sin(PiDivF), 2) - 1;
const sinZX = Math.sqrt(1 - Math.pow(cosZX, 2));
const p1 = new MyVector([0, 0, 1]);
const p2 = new MyVector([sinZX, 0, cosZX]);
const p3 = new MyVector([sinZX*cosXY, sinZX*sinXY, cosZX]);
// 頂点の一覧を作成する.
this.vertex = vecFrom(p1, p2, p3);
// 最初の3法線を求める.
const rm = new MyMatrix().rotateZ(rotXY);
const n1 = p1.sub(p3).cross(p2.sub(p1), 1).normalize();
const n2 = rm.mul(n1).normalize();
const n3 = rm.mul(n2).normalize();
// 法線の一覧を作成する.
this.normal = vecFrom(n1, n2, n3);
const pp_cos = p1.dot(p2); // 頂点間の角度情報.
const np_cos = p1.dot(n1); // 頂点・法線間の角度情報.
const nn_cos = n1.dot(n2); // 法線間の角度情報.
// 正多面体と対の両方の多角形データを作成する.
this.polygon1 = S.polyFrom(this.vertex, this.normal, pp_cos, np_cos);
this.polygon2 = S.polyFrom(this.normal, this.vertex, nn_cos, np_cos);
// 正多面体と対の両方の辺データを作成する.
this.edge1 = S.edgeFrom(this.polygon1);
this.edge2 = S.edgeFrom(this.polygon2);
};
};
/* ********************************** */
/*
* ベクトル
*/
class MyVector {
// コンストラクタ.
constructor() {
this.S = MyVector;
this.M = MyMatrix;
const args = [...Array(arguments.length)].map((_, i) => arguments[i]);
let vector;
if (args.length == 0)
vector = Array(4).fill(0);
else if (args.length == 1) {
vector = args[0];
if (vector.length == 3)
vector.push(1);
while (vector.length < 4)
vector.push(0);
vector = vector.slice(0, 4);
} else if (args.length == 3) {
vector = args;
vector.push(1);
} else {
vector = args;
while (vector.length < 4)
vector.push(0);
vector = vector.slice(0, 4);
}
this.vector = vector;
this.isMatrix = false;
this.isVector = true;
};
// ベクトルの大きさ (w=false で 3 要素)
abs(w=false) {
return Math.sqrt(this.dot(this, w));
}
// 逆 (w=false で[3]はそのまま)
neg(rhs, w=false) {
const vl = this.vector;
return new this.S([
-vl[0],
-vl[1],
-vl[2],
(w ? -vl[3] : vl[3]),
]);
};
// 和 (w=false で[3]はそのまま)
add(rhs, w=false) {
const vl = this.vector;
const vr = rhs.vector;
return new this.S([
vl[0] + vr[0],
vl[1] + vr[1],
vl[2] + vr[2],
(w ? (vl[3] + vr[3]) : vl[3]),
]);
};
// 差 (w=false で[3]はそのまま)
sub(rhs, w=false) {
const vl = this.vector;
const vr = rhs.vector;
return new this.S([
vl[0] - vr[0],
vl[1] - vr[1],
vl[2] - vr[2],
(w ? (vl[3] - vr[3]) : vl[3]),
]);
};
// 乗算 (w=false で 3 要素)
mul(rhs, w=false) {
return ((typeof(rhs) == "number")
? this.nMul(rhs, w)
: this.mMul(rhs));
};
// 実数倍 (w=false で[3]はそのまま)
nMul(n, w=false) {
const vl = this.vector;
return new this.S([
vl[0] * n,
vl[1] * n,
vl[2] * n,
(w ? (vl[3] * n): vl[3]),
]);
};
// 行列との積 (w=false で[3]はそのまま)
mMul(rhs, w=false) {
const vl = this.vector;
const mr = rhs.matrix;
return this.M([
vl[0] * mr[0] + vl[1] * mr[4] + vl[2] * mr[ 8] + vl[3] * mr[12],
vl[0] * mr[1] + vl[1] * mr[5] + vl[2] * mr[ 9] + vl[3] * mr[13],
vl[0] * mr[2] + vl[1] * mr[6] + vl[2] * mr[10] + vl[3] * mr[14],
(w ? vl[0] * mr[3] + vl[1] * mr[7] + vl[2] * mr[11] + vl[3] * mr[15] : vl[3]),
]);
};
// 内積 (w=false で 3 要素).
dot(rhs, w=false) {
const vl = this.vector;
const vr = rhs.vector;
return (vl[0] * vr[0] +
vl[1] * vr[1] +
vl[2] * vr[2] +
(w ? vl[3] * vr[3] : 0));
};
// 外積 (3要素のみ)
cross(rhs, w=0) {
const vl = this.vector;
const vr = rhs.vector;
return new this.S([
vl[1] * vr[2] - vl[2] * vr[1],
vl[2] * vr[0] - vl[0] * vr[2],
vl[0] * vr[1] - vl[1] * vr[0],
w,
]);
};
// 実数除算 (w=false で[3]はそのまま).
div(n, w=false) {
const vl = this.vector;
if (n != 0)
return new this.S([
vl[0] / n,
vl[1] / n,
vl[2] / n,
(w ? (vl[3] / n): vl[3]),
]);
throw 'division by zero';
};
// 単位ベクトルを得る (w=false で 3 要素)
normalize(w=false) {
return this.div(this.abs(w), w);
};
// 整数に近い数値を整数にする.
fixInt() {
const thr = 1 / (1 << 20);
const fix = function(x) {
const xi = Math.trunc(x);
return ((Math.abs(x - xi) < thr) ? xi : x);
};
const vector = this.vector;
return new this.S([
fix(vector[0]),
fix(vector[1]),
fix(vector[2]),
fix(vector[3]),
]);
};
};
/*
* 行列
*/
class MyMatrix {
// コンストラクタ.
constructor() {
const S = MyMatrix;
this.S = S;
this.V = MyVector;
const args = [...Array(arguments.length)].map((_, i) => arguments[i]);
let matrix;
if (args.length == 0)
matrix = S.identity();
else if (args.length == 1)
matrix = args[0];
else if (args.length == 4) {
matrix = [
args[0], args[1], 0, 0,
args[2], args[3], 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
];
} else if (args.length == 9) {
matrix = [
args[0], args[1], args[2], 0,
args[3], args[4], args[5], 0,
args[6], args[7], args[8], 0,
0, 0, 0, 1,
];
} else {
matrix = args;
while (matrix.length < 16)
matrix.push(0);
matrix = matrix.slice(0, 16);
}
this.matrix = matrix;
this.isMatrix = true;
this.isVector = false;
};
// 単位行列.
static identity() {
return [...Array(16)].map((_, i) => (((i % 5) == 0) ? 1 : 0));
};
// 転置.
transpose() {
const ml = this.matrix;
return new this.S([
ml[0], ml[4], ml[ 8], ml[12],
ml[1], ml[5], ml[ 9], ml[13],
ml[2], ml[6], ml[10], ml[14],
ml[3], ml[7], ml[11], ml[15],
]);
};
// 和.
add(rhs) {
const mr = rhs.matrix;
return new this.S(this.matrix.map((l, i) => l + mr[i]));
};
// 差.
sub(rhs) {
const mr = rhs.matrix;
return new this.S(this.matrix.map((l, i) => l - mr[i]));
};
// 配列[16]との積で配列[16]を返す.
aMulI(mr) {
const ml = this.matrix;
return [
ml[ 0] * mr[0] + ml[ 1] * mr[4] + ml[ 2] * mr[ 8] + ml[ 3] * mr[12],
ml[ 0] * mr[1] + ml[ 1] * mr[5] + ml[ 2] * mr[ 9] + ml[ 3] * mr[13],
ml[ 0] * mr[2] + ml[ 1] * mr[6] + ml[ 2] * mr[10] + ml[ 3] * mr[14],
ml[ 0] * mr[3] + ml[ 1] * mr[7] + ml[ 2] * mr[11] + ml[ 3] * mr[15],
ml[ 4] * mr[0] + ml[ 5] * mr[4] + ml[ 6] * mr[ 8] + ml[ 7] * mr[12],
ml[ 4] * mr[1] + ml[ 5] * mr[5] + ml[ 6] * mr[ 9] + ml[ 7] * mr[13],
ml[ 4] * mr[2] + ml[ 5] * mr[6] + ml[ 6] * mr[10] + ml[ 7] * mr[14],
ml[ 4] * mr[3] + ml[ 5] * mr[7] + ml[ 6] * mr[11] + ml[ 7] * mr[15],
ml[ 8] * mr[0] + ml[ 9] * mr[4] + ml[10] * mr[ 8] + ml[11] * mr[12],
ml[ 8] * mr[1] + ml[ 9] * mr[5] + ml[10] * mr[ 9] + ml[11] * mr[13],
ml[ 8] * mr[2] + ml[ 9] * mr[6] + ml[10] * mr[10] + ml[11] * mr[14],
ml[ 8] * mr[3] + ml[ 9] * mr[7] + ml[10] * mr[11] + ml[11] * mr[15],
ml[12] * mr[0] + ml[13] * mr[4] + ml[14] * mr[ 8] + ml[15] * mr[12],
ml[12] * mr[1] + ml[13] * mr[5] + ml[14] * mr[ 9] + ml[15] * mr[13],
ml[12] * mr[2] + ml[13] * mr[6] + ml[14] * mr[10] + ml[15] * mr[14],
ml[12] * mr[3] + ml[13] * mr[7] + ml[14] * mr[11] + ml[15] * mr[15],
];
};
// 乗算で更新.
aMulUpdate(mr) {
this.matrix = this.aMulI(mr);
};
// 配列[4]との積で配列[4]を返す.
vMulI(vr) {
const ml = this.matrix;
return [
ml[ 0] * vr[0] + ml[ 1] * vr[1] + ml[ 2] * vr[ 2] + ml[ 3] * vr[3],
ml[ 4] * vr[0] + ml[ 5] * vr[1] + ml[ 6] * vr[ 2] + ml[ 7] * vr[3],
ml[ 8] * vr[0] + ml[ 9] * vr[1] + ml[10] * vr[ 2] + ml[11] * vr[3],
ml[12] * vr[0] + ml[13] * vr[1] + ml[14] * vr[ 2] + ml[15] * vr[3],
];
};
// 実数倍の配列を返す.
nMulI(r) {
return [
ml[ 0] * r, ml[ 1] * r, ml[ 2] * r, ml[ 3] * r,
ml[ 4] * r, ml[ 5] * r, ml[ 6] * r, ml[ 7] * r,
ml[ 8] * r, ml[ 9] * r, ml[10] * r, ml[11] * r,
ml[12] * r, ml[13] * r, ml[14] * r, ml[15] * r,
];
};
// 積.
mul(rhs) {
if (rhs.isMatrix) return new this.S(this.aMulI(rhs.matrix));
if (rhs.isVector) return new this.V(this.vMulI(rhs.vector));
if (typeof(rhs) == 'number') return new this.S(this.nMulI(rhs));
throw 'unknown parameter.';
};
// 行列式を得る.
det() {
const rval = 0;
const [m00, m01, m02, m03,
m10, m11, m12, m13,
m20, m21, m22, m23,
m30, m31, m32, m33,
] = this.matrix;
rval += m00 * m11 * m22 * m33; rval -= m00 * m11 * m23 * m32;
rval += m00 * m12 * m23 * m31; rval -= m00 * m12 * m21 * m33;
rval += m00 * m13 * m21 * m32; rval -= m00 * m13 * m22 * m31;
rval += m01 * m12 * m20 * m33; rval -= m01 * m12 * m23 * m30;
rval += m01 * m13 * m22 * m30; rval -= m01 * m13 * m20 * m32;
rval += m01 * m10 * m23 * m32; rval -= m01 * m10 * m22 * m33;
rval += m02 * m13 * m20 * m31; rval -= m02 * m13 * m21 * m30;
rval += m02 * m10 * m21 * m33; rval -= m02 * m10 * m23 * m31;
rval += m02 * m11 * m23 * m30; rval -= m02 * m11 * m20 * m33;
rval += m03 * m10 * m22 * m31; rval -= m03 * m10 * m21 * m32;
rval += m03 * m11 * m20 * m32; rval -= m03 * m11 * m22 * m30;
rval += m03 * m12 * m21 * m30; rval -= m03 * m12 * m20 * m31;
return rval;
};
// 逆行列を得る.
inverse() {
const det = this.det();
return ((det != 0) ? this.mul(1 / det) : undefined);
};
// 単位行列を設定する.
loadIdentity() {
this.matrix = this.S.identity();
return this;
};
// X 軸回転を乗算.
rotateX(rad) {
const rc = Math.cos(rad);
const rs = Math.sin(rad);
this.aMulUpdate([
1, 0, 0, 0,
0, rc, -rs, 0,
0, rs, +rc, 0,
0, 0, 0, 1,
]);
return this;
};
degRotateX(deg) {
return this.rotateX(deg * Math.PI / 180);
};
// Y 軸回転を乗算.
rotateY(rad) {
const rc = Math.cos(rad);
const rs = Math.sin(rad);
this.aMulUpdate([
+rc, 0, +rs, 0,
+ 0, 1, 0, 0,
-rs, 0, +rc, 0,
+ 0, 0, 0, 1,
]);
return this;
};
degRotateY(deg) {
return this.rotateY(deg * Math.PI / 180);
};
// Z 軸回転を乗算.
rotateZ(rad) {
const rc = Math.cos(rad);
const rs = Math.sin(rad);
this.aMulUpdate([
rc, -rs, 0, 0,
rs, +rc, 0, 0,
+0, 0, 1, 0,
+0, 0, 0, 1,
]);
return this;
};
degRotateZ(deg) {
return this.rotateZ(deg * Math.PI / 180);
};
// 任意軸回転を乗算.
rotate(rad, vec) {
const [x, y, z, w] = vec.normalize().vector;
const rc = Math.cos(rad);
const rs = Math.sin(rad);
const xs = x * sin;
const ys = y * sin;
const zs = z * sin;
const nc1 = 1 - rc;
const x1c = x * nc1;
const y1c = y * nc1;
const z1c = z * nc1;
this.aMulUpdate([
x * x1c + rc, x * y1c - zs, x * z1c + ys, 0,
y * x1c + zs, y * y1c + rc, y * z1c - xs, 0,
z * x1c - ys, z * y1c + xs, z * z1c + rc, 0,
0, 0, 0, 1,
]);
return this;
};
degRotate(deg, vec) {
return this.rotate(deg * Math.PI / 180, vec);
};
// 平行移動を乗算.
translate(x, y, z) {
this.aMulUpdate([
1, 0, 0, x,
0, 1, 0, y,
0, 0, 1, z,
0, 0, 0, 1,
]);
return this;
};
// 各軸に係数を乗算.
scale(x, y, z, w=1) {
this.aMulUpdate([
x, 0, 0, 0,
0, y, 0, 0,
0, 0, z, 0,
0, 0, 0, w,
]);
return this;
};
};
/*
* 3D オブジェクト用行列
*/
class MyObject3D extends MyMatrix {
// コンストラクタ.
constructor(...args) {
super(...args);
this.S = MyObject3D;
this.matrix_stack = [];
this.loadIdentity();
};
// 行列を複写してスタックに積み上げる.
pushMatrix(m) {
this.matrix_stack.push(this.matrix.map((d) => d));
if (m != undefined) this.aMulUpdate(m.matrix);
return this;
};
// スタックから行列を取り出す.
popMatrix() {
this.matrix = this.matrix_stack.pop();
return this;
};
};
/*
* 透視変換用行列
*/
class MyProjection3D extends MyObject3D {
// コンストラクタ.
constructor(...args) { super(...args); };
// 拡大・縮小表示設定を乗算
setZoom(zoom) {
this.aMulUpdate([
zoom, 0, 0, 0,
+ 0, zoom, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1,
]);
return this;
};
// 奥行範囲設定を乗算.
setOrtho(near, far) {
const fmn = far - near;
const m33 = 2 / fmn;
const m34 = - (far + near) / fmn;
this.aMulUpdate([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, m33, m34,
0, 0, 0, 1,
]);
return this;
};
// 透視図設定を乗算.
setPerspective(znear, zfar, zoom, aspect) {
const zfmn = zfar - znear;
const m11 = zoom * aspect;
const m22 = zoom;
const m33 = (zfar + znear) / zfmn;
const m34 = - (2 * zfar * znear) / zfmn;
this.aMulUpdate([
m11, 0, 0, 0,
+ 0, m22, 0, 0,
+ 0, 0, m33, m34,
+ 0, 0, 1, 0,
]);
return this;
};
};
/*
* ビューポート変換用行列
*/
class MyViewport {
// コンストラクタ.
constructor(width, height, depth) {
this.width = width;
this.height = height;
this.depth = depth;
this.half_width = width / 2;
this.half_height = height / 2;
this.half_depth = depth / 2;
};
getScreen(vertex) {
const hw = this.half_width;
const hh = this.half_height;
const hd = this.half_depth;
const vector = vertex.vector;
const w = vector[3];
return new MyVector(
Math.round(hw + vector[0] / w * hw),
Math.round(hh - vector[1] / w * hh),
Math.round(hd + vector[2] / w * hd),
);
};
};
/*
* 頂点処理用
*/
class MyVertex3D extends MyVector {
// コンストラクタ.
constructor(...args) {
super(...args);
const [x, y, z, w] = this.vector;
const wpx = w + x; const wmx = w - x;
const wpy = w + y; const wmy = w - y;
const wpz = w + z; const wmz = w - z;
this.wclip = [wpz, wpx, wpy, wmy, wmx, wmz];
this.fclip = (((wpz < 0) ? 0x01 : 0) |
((wpx < 0) ? 0x02 : 0) |
((wpy < 0) ? 0x04 : 0) |
((wmy < 0) ? 0x08 : 0) |
((wmx < 0) ? 0x10 : 0) |
((wmz < 0) ? 0x20 : 0));
};
};
/* ********************************** */
/*
* GIF 形式データを生成するクラス
*/
class MyGIF {
static signature = [0x47, 0x49, 0x46];
static version87a = [0x38, 0x37, 0x61];
static version89a = [0x38, 0x39, 0x61];
// コンストラクタ.
constructor(width, height, resolution, background, global_color, aspect) {
const S = MyGIF;
this.S = S;
this.signature = S.signature;
this.version = S.version89a;
this.logicalScreen = S.createLogicalScreenDescriptor(
width, height, resolution, background, global_color, aspect);
this.descriptor = [];
};
// GIF形式のバイナリを取得する.
get_binary() {
return [
this.signature,
this.version,
this.logicalScreen.get_binary(),
this.descriptor.map((d, _) => d.get_binary()).flat(),
].flat();
};
// 任意の Descriptor を追加する.
appendDescriptor(descriptor) {
this.descriptor.push(descriptor);
};
// Image Descriptor を追加する.
appendImageDescriptor(x, y, w, h, data, color, interlace) {
this.descriptor.push(this.S.createImageDescriptor(x, y, w, h, data, color, interlace));
};
// Graphic Control Extension を追加する.
appendGraphicControlExtension(delay, method, transparent, input) {
this.descriptor.push(this.S.createGraphicControlExtension(delay, method, transparent, input));
};
// Comment Extension を追加する.
appendCommentExtension(text) {
this.descriptor.push(this.S.createCommentExtension(text));
};
// Plain Text Extension を追加する.
appendPlainTextExtension(x, y, w, h, cw, ch, fc, bc, text) {
this.descriptor.push(this.S.createPlainTextExtension(x, y, w, h, cw, ch, fc, bc, text));
};
// Application Extension を追加する.
appendApplicationExtension(id, code, data) {
this.descriptor.push(this.S.createApplicationExtension(id, code, data));
};
// アニメーションのループ回数を設定する.
appendApplicationExtensionForLoop(count) {
this.appendApplicationExtension('NETSCAPE', '2.0', [1, (count & 0xff), ((count >> 8) & 0xff)]);
};
// Trailer を追加する.
appendTrailer() {
this.descriptor.push(this.S.createTrailer());
};
// 8 ビット形式の取得.
static uint8el(x) {
return ((!x ? 0 : x) & 0xff);
};
// 16 ビット・リトル・エンディアン形式の取得.
static uint16el(x) {
const y = (!x ? 0 : x);
return [(y & 0xff), ((y >> 8) & 0xff)];
};
// 色情報の過不足を調整.
static fixColorTable(depth, color) {
const colen = (1 << (depth + 1)) * 3;
let table = color.flat();
return ((table.length > colen) ? table.slice(0, colen) :
table.concat(Array(colen - table.length).fill(0)));
};
// 文字の UTF-8 バイナリ化.
static charToUTF8(c) {
if (c < 0x0080) return c;
if (c < 0x0800) return [
(0xc0 | (c >> 6)),
(0x80 | (c & 0x3f)),
];
return [
(0xe0 | (c >> 12)),
(0x80 | ((c >> 6) & 0x3f)),
(0x80 | (c & 0x3f)),
];
};
static stringToUTF8(s) {
return [...Array(s.length)].map((_, i) => MyGIF.charToUTF8(s.charCodeAt(i))).flat();
};
// Data Sub-blocks の生成.
static createDataSubBlocks(data) {
let block = [];
let pos = 0;
let length = data.length;
while (length) {
let slen = (length < 256) ? length : 255;
block.push(slen);
block.push(data.slice(pos, pos + slen));
pos += slen;
length -= slen;
};
block.push(0);
return block.flat();
};
// Logical Screen Descriptor の生成.
static createLogicalScreenDescriptor(width, height, resolution, background, color, sort, aspect) {
const S = MyGIF;
const D = {
logical_screen_width: width,
logical_screen_height: height,
size_of_global_color_table: 0,
sort_flag: sort,
color_resolution: resolution,
global_color_table_flag: false,
background_color_index: background,
pixel_aspect_ratio: ((aspect == undefined) ? 49 : aspect),
global_color_table: undefined,
set_color: (function(table) {
const flag = (table != undefined);
D.size_of_global_color_table =
(!flag ? 0 : (31 - Math.clz32(table.length-1)));
D.global_color_table_flag = flag;
D.global_color_table = table;
}),
get_binary: (function() {
const sgct = (!D.size_of_global_color_table ? 0 : (7 & D.size_of_global_color_table));
const sort = (!D.sort_flag ? 0 : 1);
const res = (!D.color_resolution ? 0 : (7 & D.color_resolution));
const gcf = (!D.global_color_table_flag ? 0 : 1);
const bgc = (!D.background_color_index ? 0 : D.background_color_index);
const aspect = ((D.pixel_aspect_ratio == undefined) ? 49 : D.pixel_aspect_ratio);
const color = (!gcf ? [] : S.fixColorTable(sgct, D.global_color_table));
return [
S.uint16el(D.logical_screen_width),
S.uint16el(D.logical_screen_height),
(sgct | (sort << 3) | (res << 4) | (gcf << 7)),
S.uint8el(bgc),
S.uint8el(aspect),
color.map((v, _) => S.uint8el(v)),
].flat();
}),
};
D.set_color(color);
return D;
};
// Image Descriptor の生成.
static createImageDescriptor(x, y, w, h, data, color, interlace, sort) {
const S = MyGIF;
const D = {
descriptor: 0x2c,
image_left_position: x,
image_top_position: y,
image_width: w,
image_height: h,
size_of_local_color_table: 0,
sort_flag: sort,
interlace_flag: interlace,
local_color_table_flag: false,
local_color_table: undefined,
table_based_image_data: S.createTableBasedImageData(data),
set_color_table: (function(table) {
const flag = (table != undefined);
D.size_of_local_color_table =
(!flag ? 0 : (31 - Math.clz32(table.length-1)));
D.local_color_table_flag = flag;
D.local_color_table = table;
}),
get_binary: (function() {
const slct = (!D.size_of_local_color_table ? 0 : (7 & D.size_of_local_color_table));
const sort = (!D.sort_flag ? 0 : 1);
const interlace = (!D.interlace ? 0 : 1);
const lcf = (!D.local_color_table_flag ? 0 : 1);
const color = (!lcf ? [] : S.fixColorTable(slct, D.local_color_table));
return [
D.descriptor,
S.uint16el(D.image_left_position),
S.uint16el(D.image_top_position),
S.uint16el(D.image_width),
S.uint16el(D.image_height),
(slct | (sort << 5) | (interlace << 6) | (lcf << 7)),
color.map((v, _) => S.uint8el(v)),
D.table_based_image_data.lzw_minimum_code_size,
D.table_based_image_data.data,
].flat();
}),
};
D.set_color_table(color);
return D;
};
// Table Based Image の生成 (LZW圧縮)
static createTableBasedImageData(source) {
const S = MyGIF;
const source_size = source.length;
// const src_max = Math.max.apply(null, source); // 'Maximum call stack size exceeded' がでる.
const src_max = function() { let m = 0; for (const d of source) if (m < d) m = d; return m; }();
const src_max_clz = (32 - Math.clz32(src_max));
const bits_init = ((src_max_clz < 2) ? 2 : src_max_clz);
let bits_curr = (bits_init + 1);
const code_base = (1 << bits_init);
const code_clear = code_base;
const code_end = code_base + 1;
const code_max = ((1 << 12) - 1);
let code_curr = code_end;
let code_step = (1 << bits_curr);
const tree = [...Array(code_max + 2)].map((_, i) => ({ code: i, next: -1, down: -1, data: 0 }));
const buffer = [];
let bs_pos = 0;
const write = function(data) {
const bits = bits_curr;
let idxs = (bs_pos >> 3);
const idxe = ((bs_pos + bits - 1) >> 3);
data <<= (bs_pos & 7);
if (idxs < buffer.length) {
buffer[idxs] |= (data & 0xff);
data >>= 8;
idxs++;
};
while (idxs <= idxe) {
buffer.push(data & 0xff);
data >>= 8;
idxs++;
};
bs_pos += bits;
};
write(code_clear);
if (!source_size) {
write(code_end);
return {
lzw_minimum_code_size: bits_init,
data: S.createDataSubBlocks(buffer),
};
};
let siter = source.values();
let sdat = siter.next();
lzw: for (;;) {
let data, next;
let node = tree[(data = sdat.value)];
scan: for (;;) {
if ((sdat = siter.next()).done) {
write(node.code);
break lzw;
};
data = sdat.value;
if ((next = tree[node.down]) == undefined)
break;
while (data != next.data)
if ((next = tree[next.next]) == undefined)
break scan;
node = next;
};
write(node.code);
{
const next = tree[++code_curr];
// next.code = code_curr;
next.next = node.down;
next.down = -1;
next.data = data;
node.down = code_curr;
}
if (code_curr < code_step)
continue;
if (code_curr < code_max) {
bits_curr++;
if ((code_step <<= 1) < code_max)
continue;
code_step = code_max;
continue;
};
write(code_clear);
bits_curr = bits_init + 1;
code_step = (1 << bits_curr);
code_curr = code_end;
for (let i = 0; i < code_base; i++) {
const node = tree[i];
// node.code = i;
node.next = -1;
node.down = -1;
// node.data = 0;
};
};
write(code_end);
return {
lzw_minimum_code_size: bits_init,
data: S.createDataSubBlocks(buffer),
};
};
// Graphic Control Extension の生成.
static createGraphicControlExtension(delay, method, transparent, input) {
const S = MyGIF;
const D = {
descriptor: 0x21,
label: 0xf9,
disposal_method: method,
user_input_flag: input,
transparent_color_flag: false,
delay_time: delay,
transparent_color_index: 0,
set_transparent: (function(index) {
D.transparent_color_flag = (index != undefined);
D.transparent_color_index = (!index ? 0 : index);
}),
get_binary: (function() {
const method = (!D.disposal_method ? 0 : (7 & D.disposal_method));
const input = (!D.user_input_flag ? 0 : 1);
const ftrans = (!D.transparent_color_flag ? 0 : 1);
return [
D.descriptor,
D.label,
4, /* Data Sub-blocks */
((method << 3) | (input << 6) | (ftrans << 7)),
S.uint16el(D.delay_time),
S.uint8el(D.transparent_color_index),
0, /* Data Sub-blocks */
].flat();
}),
};
D.set_transparent(transparent);
return D;
};
// Comment Extension の生成.
static createCommentExtension(text) {
const S = MyGIF;
const D = {
descriptor: 0x21,
label: 0xfe,
text: text,
get_binary: (() => [
D.descriptor,
D.label,
S.createDataSubBlocks(S.stringToUTF8(D.tex)),
].flat()),
};
return D;
};
// Plain Text Extension の生成.
static createPlainTextExtension(x, y, w, h, cw, ch, fc, bc, text) {
const S = MyGIF;
const D = {
descriptor: 0x21,
label: 0x01,
text_grid_left_position: x,
text_grid_top_position: y,
text_grid_width: w,
text_grid_height: h,
character_cell_width: cw,
character_cell_height: ch,
text_foreground_color_index: fc,
text_background_color_index: bc,
plain_text_data: text,
get_binary: (() => [
D.descriptor,
D.label,
12, /* Data Sub-blocks */
S.uint16el(D.text_grid_left_position),
S.uint16el(D.text_grid_top_position),
S.uint16el(D.text_grid_width),
S.uint16el(D.text_grid_height),
S.uint8el(D.character_cell_width),
S.uint8el(D.character_cell_height),
S.uint8el(D.text_foreground_color_index),
S.uint8el(D.text_background_color_index),
S.createDataSubBlocks(S.stringToUTF8(D.plain_text_data)),
].flat()),
};
return D;
};
// Application Extension の生成.
static createApplicationExtension(id, code, data) {
const S = MyGIF;
const D = {
descriptor: 0x21,
label: 0xff,
application_identifier: id,
application_authentication_code: code,
application_data: data,
get_binary: (() => [
D.descriptor,
D.label,
11, /* Data Sub-blocks */
S.stringToUTF8(D.application_identifier + ' ').slice(0, 8),
S.stringToUTF8(D.application_authentication_code + ' ').slice(0, 3),
S.createDataSubBlocks(D.application_data.map((v, _) => S.uint8el(v))),
].flat()),
};
return D;
};
// Trailer の生成.
static createTrailer() {
return {
descriptor: 0x3b,
get_binary: (() => 0x3b),
};
};
};
/* **************************************** */
/*
* Base 64 符号化
*/
class MyBase64 {
static EncodeTable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
static encode(b) {
const table = MyBase64.EncodeTable;
const blen = b.length;
const brem = blen % 3;
const bcnt = blen - brem;
let s = '';
let i = 0;
while (i < bcnt) {
const d0 = b[i++];
const d1 = b[i++];
const d2 = b[i++];
const d = (d0 << 16) | (d1 << 8) | d2;
s += table[(d >> 18) & 0x3f];
s += table[(d >> 12) & 0x3f];
s += table[(d >> 6) & 0x3f];
s += table[d & 0x3f];
};
if (brem) {
const b2 = (brem == 2);
const d0 = b[i++];
const d1 = (b2 ? b[i++] : 0);
const d = (d0 << 16) | (d1 << 8);
s += table[(d >> 18) & 0x3f];
s += table[(d >> 12) & 0x3f];
s += (b2 ? table[(d >> 6) & 0x3f] : '=');
s += '=';
};
return s;
};
};
/* ********************************** */
/*
* GIF 画像生成用
*/
class MyPixelBuffer {
static grayScaleColor = function(count) {
const m = 255;
const n = count - 1;
return [...Array(count)].map(function(_, k) {
const l = Math.trunc((k * m) / n);
return [l, l, l];
});
};
static grayScaleColor4 = MyPixelBuffer.grayScaleColor(4);
static grayScaleColor8 = MyPixelBuffer.grayScaleColor(8);
static grayScaleColor256 = MyPixelBuffer.grayScaleColor(256);
static simpleColorBase = [...Array(8)].map((_, i) => Math.trunc(i * 255 / 7));
static simpleColor = [...Array(256)].map((_, i) => [
MyPixelBuffer.simpleColorBase[(i >> 5) & 7],
MyPixelBuffer.simpleColorBase[(i >> 2) & 7],
[0, 85, 170, 255][i & 3],
]);
static interlaceMap = new Map();
static createInterlaceTable(h) {
const m = MyPixelBuffer.interlaceMap;
if (m.has(h))
return m.get(h);
const ph = [
[0, 8, ((h + 7) >> 3)],
[4, 8, ((h + 3) >> 3)],
[2, 4, ((h + 1) >> 2)],
[1, 2, (h >> 1)],
];
const tab = [...Array(4)].map(
(_, i) => [...Array(ph[i][2])].map(
(_, j) => ph[i][0] + ph[i][1] * j)).flat();
m.set(h, tab);
return tab;
};
// コンストラクタ.
constructor(color, width, height, defcol=0) {
this.S = MyPixelBuffer;
this.GIF = MyGIF;
this.color = color;
this.width = width;
this.height = height;
this.pixel = Array(width * height).fill(defcol);
};
// 点を打つ.
setPoint(x, y, c) {
const w = this.width;
if ((0 <= x) && (x < w) &&
(0 <= y) && (y < this.height)) {
this.pixel[p] = c;
}
};
// 線を引く.
setLine(x1, y1, x2, y2, c, sep) {
const dx = Math.trunc(Math.abs(x1 - x2));
const dy = Math.trunc(Math.abs(y1 - y2));
if (!dx)
this.setVirticalLine(x1, y1, y2, c);
else if (!dy)
this.setHorizontalLine(x1, x2, y1, c);
else if (dx >= dy)
this.setLineH(x1, y1, x2, y2, dx, dy, c, sep);
else
this.setLineV(x1, y1, x2, y2, dx, dy, c, sep);
};
setLineH(x1, y1, x2, y2, dx, dy, c, sep) {
const S = this.S;
const w = this.width;
const h = this.height;
let xs, ys, xe, ye;
if (x1 < x2) {
xs = x1; ys = y1;
xe = x2; ye = y2;
} else {
xs = x2; ys = y2;
xe = x1; ye = y1;
};
if ((xe < 0) || (w <= xs))
return;
const ay = ((ys < ye) ? +1 : -1);
const ap = (ay < 0) ? -w: +w;
let k = (dy >> 1);
dx++;
if (sep) {
dy++;
k = 0;
};
if (xs < 0) {
const kt = k - (dy * xs);
const yh = Math.trunc(kt / dx);
xs = 0;
ys += ((ay < 0) ? -yh : +yh);
k = (kt % dx);
};
if (w <= xe)
xe = w - 1;
const yt = Math.min(ys, ye);
const yb = Math.max(ys, ye);
if ((yb < 0) || (h <= yt))
return;
if (ay < 0) {
if (ye < 0)
ye = 0;
if (h <= ys) {
const yh = ys - h + 1;
const kt = k + (dx * yh);
const xw = Math.trunc(kt / dy);
xs += xw;
ys = h - 1;
k = kt % dy;
};
} else {
if (h <= ye)
ye = h - 1;
if (ys < 0) {
const kt = k - (dx * ys);
const xw = Math.trunc(kt / dy);
xs += xw;
ys = 0;
k = kt % dy;
};
};
ye += ay;
const pixel = this.pixel;
let p = ys * w + xs;
while (xs++ <= xe) {
pixel[p++] = c;
if ((k += dy) >= dx) {
k -= dx;
p += ap;
if ((ys += ay) == ye)
break;
};
};
};
setLineV(x1, y1, x2, y2, dx, dy, c, sep) {
const S = this.S;
const w = this.width;
const h = this.height;
let xs, ys, xe, ye;
if (y1 < y2) {
xs = x1; ys = y1;
xe = x2; ye = y2;
} else {
xs = x2; ys = y2;
xe = x1; ye = y1;
};
if ((ye < 0) || (h <= ys))
return;
const ax = ((xs < xe) ? +1 : -1);
let k = (dy >> 1);
dy++;
if (sep) {
dx++;
k = 0;
};
if (ys < 0) {
const kt = k - (dx * ys);
const xw = Math.trunc(kt / dy);
xs += ((ax < 0) ? -xw : +xw);
ys = 0;
k = (kt % dy);
};
if (h <= ye)
ye = h - 1;
const xl = Math.min(xs, xe);
const xr = Math.max(xs, xe);
if ((xr < 0) || (w <= xl))
return;
if (ax < 0) {
if (xe < 0)
xe = 0;
if (w <= xs) {
const xw = xs - w + 1;
const kt = k + (dy * xw);
const yh = Math.trunc(kt / dx);
xs = w - 1;
ys += yh;
k = kt % dx;
};
} else {
if (w <= xe)
xe = w - 1;
if (xs < 0) {
const kt = k - (dy * xs);
const yh = Math.trunc(kt / dx);
xs = 0;
ys += yh;
k = kt % dx;
};
};
xe += ax;
const pixel = this.pixel;
let p = ys * w + xs;
while (ys++ <= ye) {
pixel[p] = c; p += w;
if ((k += dx) >= dy) {
k -= dy;
p += ax;
if ((xs += ax) == xe)
break;
};
};
};
// 水平線を引く.
setHorizontalLine(x1, x2, y, c) {
const w = this.width;
const h = this.height;
const pixel = this.pixel;
const xs = Math.trunc(Math.min(x1, x2));
const xe = Math.trunc(Math.max(x1, x2));
if ((xe < 0) || (w <= xs) || (y < 0) || (h <= y))
return;
const us = Math.trunc(Math.max(xs, 0));
const ue = Math.trunc(Math.min(xe, w-1));
const yp = y * w;
pixel.fill(c, yp + us, yp + ue + 1);
}
// 垂直線を引く.
setVirticalLine(x, y1, y2, c) {
const w = this.width;
const h = this.height;
const pixel = this.pixel;
const ys = Math.trunc(Math.min(y1, y2));
const ye = Math.trunc(Math.max(y1, y2));
if ((x < 0) || (w <= x) || (ye < 0) || (h <= ys))
return;
const vs = Math.trunc(Math.max(ys, 0));
const ve = Math.trunc(Math.min(ye, h-1));
const q = ve * w + x;
for (let p = vs * w + x; p <= q; p += w)
pixel[p] = c;
}
// 四角形を描く.
setBox(x1, y1, x2, y2, c) {
const xs = Math.trunc(Math.min(x1, x2));
const xe = Math.trunc(Math.max(x1, x2));
const ys = Math.trunc(Math.min(y1, y2));
const ye = Math.trunc(Math.max(y1, y2));
this.setHorizontalLine(xs, xe, ys, c);
if (ys != ye) {
this.setVirticalLine(xs, ys+1, ye-1, c);
if (xs != xe)
this.setVirticalLine(xe, ys+1, ye-1, c);
this.setHorizontalLine(xs, xe, ye, c);
};
};
// 四角形で塗りつぶす.
setBoxFill(x1, y1, x2, y2, c) {
const w = this.width;
const h = this.height;
const pixel = this.pixel;
const xs = Math.min(x1, x2);
const xe = Math.max(x1, x2);
const ys = Math.min(y1, y2);
const ye = Math.max(y1, y2);
if ((xe < 0) || (w <= xs) ||
(ye < 0) || (h <= ys))
return;
const us = Math.trunc(Math.max(xs, 0));
const ue = Math.trunc(Math.min(xe, w-1));
const vs = Math.trunc(Math.max(ys, 0));
const ve = Math.trunc(Math.min(ye, h-1));
const nu = (ue - us) + 1;
const nv = (ve - vs) + 1;
let vp = vs * w + us;
const vq = nv * w + vp;
for (; vp < vq; vp += w)
pixel.fill(c, vp, vp + nu);
};
// 円描画用の座標情報を生成する.
static getCirclePos(radius) {
if (radius <= 0)
return [[0, 0]];
let x = radius;
let y = 0;
let s = 0;
let t = 1;
let u = (radius << 1) - 1;
let v = u;
/* 大きな円は(上下左右)端の直線を少し短くする */
u -= ((u >> 2) + (u >> 3) + (u >> 4));
let otmp = new Array();
const out1 = new Array();
const out2 = new Array();
while (x > y) {
otmp.push(y);
out2.push([x]);
y = y + 1;
s = s + t;
t = t + 2;
if (s < u)
continue;
out1.push(otmp);
x = x - 1;
v = v - 2;
u = u + v;
otmp = new Array();
};
if (x == y)
out2.push([x]);
if (otmp.length)
out1.push(otmp);
out1.reverse();
return out2.concat(out1);
};
// 円を描く.
setCircle(x, y, r, c) {
const w = this.width;
const h = this.height;
if (((x + r + 1) < 0) || (w < (x - r)) ||
((y + r + 1) < 0) || (h < (y - r)))
return;
const pixel = this.pixel;
const ypos = this.S.getCirclePos(r);
const ylen = ypos.length;
{
let yi = 0;
let ys = y;
if (ys < 0) {
yi = -ys;
ys = 0;
};
let yp = ys * w;
while (yi < ylen) {
if (ys >= h)
break;
for (const xp of ypos[yi]) {
const xl = x - xp;
const xr = x + xp;
if (w <= xl) continue;
if (xr < 0) continue;
if (xr < w) pixel[yp + xr] = c;
if (0 <= xl) pixel[yp + xl] = c;
};
yi++;
ys++;
yp += w;
};
};
{
let yi = 0;
let ys = y;
if (ys >= h) {
yi += (ys - h + 1);
ys = h - 1;
};
let yp = ys * w;
while (yi < ylen) {
if (ys < 0)
break;
for (const xp of ypos[yi]) {
const xl = x - xp;
const xr = x + xp;
if (w <= xl) continue;
if (xr < 0) continue;
if (xr < w) pixel[yp + xr] = c;
if (0 <= xl) pixel[yp + xl] = c;
};
ys--;
yi++;
yp -= w;
};
};
};
// 円で塗りつぶす.
setCircleFill(x, y, r, c) {
const w = this.width;
const h = this.height;
if (((x + r + 1) < 0) || (w < (x - r)) ||
((y + r + 1) < 0) || (h < (y - r)))
return;
const pixel = this.pixel;
const ypos = this.S.getCirclePos(r);
const ylen = ypos.length;
{
let yi = 0;
let ys = y;
if (ys < 0) {
yi = -ys;
ys = 0;
};
let yp = ys * w;
while (yi < ylen) {
if (ys >= h)
break;
const yl = ypos[yi];
const xp = yl[yl.length - 1];
let xl = x - xp;
let xr = x + xp;
if ((0 <= xr) && (xl < w)) {
if (xl < 0) xl = 0;
if (xr >= w) xr = w - 1;
pixel.fill(c, yp + xl, yp + xr + 1);
}
yi++;
ys++;
yp += w;
};
};
{
let yi = 0;
let ys = y;
if (ys >= h) {
yi += (ys - h + 1);
ys = h - 1;
};
let yp = ys * w;
while (yi < ylen) {
if (ys < 0)
break;
const yl = ypos[yi];
const xp = yl[yl.length - 1];
let xl = x - xp;
let xr = x + xp;
if ((0 <= xr) && (xl < w)) {
if (xl < 0) xl = 0;
if (xr >= w) xr = w - 1;
pixel.fill(c, yp + xl, yp + xr + 1);
}
ys--;
yi++;
yp -= w;
};
};
};
// 凸多角形を描画する.
setPolygon(vertices, c) {
const w = this.width;
const h = this.height;
const pixel = this.pixel;
const vlen = vertices.length;
if (!vlen) return;
let xmin = w, xmax = -1;
let ymin = h, ymax = -1;
for (const [x, y] of vertices) {
xmin = Math.min(xmin, x);
xmax = Math.max(xmax, x);
ymin = Math.min(ymin, y);
ymax = Math.max(ymax, y);
};
xmin = Math.trunc(xmin);
xmax = Math.trunc(xmax);
ymin = Math.trunc(ymin);
ymax = Math.trunc(ymax);
if ((xmax < 0) || (w <= xmin) ||
(ymax < 0) || (h <= ymin))
return;
const raster = [...Array(h)].map(() => []);
let [nx, ny] = vertices[vlen - 1];
for (const np of vertices) {
const lx = nx;
const ly = ny;
[nx, ny] = np;
const ay = (ny < ly) ? -1 : +1;
const dx = nx - lx;
const dy = (ay < 0) ? (ly - ny) : (ny - ly);
if (dy == 0) {
// if ((0 <= ly) && (ly < h))
// raster[ly].push(lx, nx);
continue;
};
let ys = ly;
let ye = ny;
let yp = 0;
if (ay >= 0) {
if ((ye < 0) || (h <= ys))
continue;
if (ys < 0) {
yp -= ys;
ys = 0;
};
if (ye >= h)
ye = h - 1;
} else {
if ((ys < 0) || (h <= ye))
continue;
if (ys >= h) {
yp += ys - h + 1;
ys = h - 1;
};
if (ye < 0)
ye = -1;
};
for (; ys != ye; ys += ay, yp++)
raster[ys].push(lx + Math.trunc(dx * yp / dy));
};
const rcmp = ((a, b) => (a - b));
let rs = Math.trunc(Math.max(ymin, 0));
let re = Math.trunc(Math.min(ymax, h - 1));
for (let yp = rs * w; rs <= re; yp += w) {
const rp = raster[rs++];
const rplen = rp.length;
if (rplen == 1) continue;
rp.sort(rcmp);
for (let np = 0; np < rplen; np += 2) {
let xl = rp[np];
let xr = rp[np + 1];
if (xl > xr)
[xl, xr] = [xr, xl];
if ((xr < 0) || (w <= xl))
continue;
if (xl < 0) xl = 0;
if (xr >= w) xr = w - 1;
pixel.fill(c, yp + xl, yp + xr + 1);
};
};
};
// インターレース画像を作成する.
createInterlaced() {
const tab = this.S.createInterlaceTable(h);
const w = this.width;
const h = this.height;
const pixel = this.pixel;
const line = [...Array(h)].map(
function(_, y) { const p = y * w; return pixel.slice(p, p + w); });
return [...Array(h)].map((_, y) => line[tab[y]]).flat();
};
// GIF 処理用オブジェクトを作成する.
createGIF(global_color) {
const color = (global_color ? this.color : undefined);
return new this.GIF(this.width, this.height, (8 - 1), 0, color);
};
// Image Descriptor を作成する.
createImageDescriptor(local_color, x, y, interlace) {
const color = (local_color ? this.color : undefined);
const w = this.width;
const h = this.height;
const pixel = this.pixel;
const data = (!interlace ? pixel : this.createInterlaced());
return this.GIF.createImageDescriptor(x, y, w, h, data, color, interlace);
};
};
/*
* 3D 処理用
*/
class MyImage3DGIF {
// コンストラクタ.
constructor(color, width, height, defcol=0) {
this.S = MyImage3DGIF;
this.pixel = new MyPixelBuffer(color, width, height, defcol);
this.viewport = new MyViewport(width, height, 1);
this.projection = new MyProjection3D();
this.projection.setPerspective(1, 1000, 1, (height / width));
};
// 座標変換済み頂点情報を生成する.
createVertex(vectors) {
const matrix = this.projection;
return [...Array(vectors.length)].map(
(_,i) => new MyVertex3D(matrix.mul(vectors[i], true).vector));
};
// 線分をクリッピングする.
clipLine(p1, p2) {
if (!(p1.fclip | p2.fclip))
return [p1, p2];
if ((p1.fclip & p2.fclip))
return undefined;
let xf = p1.fclip ^ p2.fclip;
for (let cf = 1, cn = 0; xf; cf <<= 1, cn++) {
if (!(xf & cf))
continue;
xf -= cf;
const c1 = p1.wclip[cn];
const c2 = p2.wclip[cn];
if ((p1.fclip & cf)) {
const s = c2 / (c2 - c1);
p1 = new MyVertex3D(p1.nMul(s, true).add(p2.nMul((1 - s), true), true).vector)
} else {
const s = c1 / (c1 - c2);
p2 = new MyVertex3D(p2.nMul(s, true).add(p1.nMul((1 - s), true), true).vector)
};
if ((p1.fclip & p2.fclip))
return undefined;
};
return [p1, p2];
};
// 線を引く(3D)
setLine(v1, v2, color) {
const np = this.clipLine(v1, v2);
if (!np) return;
const view = this.viewport;
const s1 = view.getScreen(np[0]);
const s2 = view.getScreen(np[1]);
const [x1, y1] = s1.vector;
const [x2, y2] = s2.vector;
this.pixel.setLine(x1, y1, x2, y2, color);
};
};
/* ********************************** */
/*
* DOM の操作など
*/
function createImgWithBase64(b64s, id) {
const priority = 'important';
const img = document.createElement('img');
if (id != undefined)
img.setAttribute('id', id);
img.setAttribute('class', 'b64img');
img.setAttribute('src', 'data:image/gif;charset=utf-8;base64,' + b64s);
img.style.setProperty('margin', '0', priority);
// img.style.setProperty('padding', '8px', priority);
// img.style.setProperty('image-rendering', 'pixelated', priority);
return img;
}
function createFrameBuffer(color) {
const fb = new MyImage3DGIF(color, 640, 320);
const proj = fb.projection;
proj.scale(8, 8, -1);
proj.translate(0, 0, -12);
proj.degRotateX(+20);
return fb;
}
function renderPolyhedronEdge(fb, vertex, edge, deg, movx, color) {
const pixel = fb.pixel;
const proj = fb.projection;
const mobj = new MyObject3D();
mobj.translate(movx, 0, 0);
mobj.degRotateY(deg);
mobj.aMulUpdate([
0, 1, 0, 0, // Y → X
0, 0, 1, 0, // Z → Y
1, 0, 0, 0, // X → Z
0, 0, 0, 1,
]);
proj.pushMatrix(mobj);
{
const vertex3d = fb.createVertex(vertex);
for (const e of edge)
fb.setLine(vertex3d[e[0]], vertex3d[e[1]], color);
}
proj.popMatrix();
return pixel;
}
function renderPolyhedronPolyEdge(fb, vertex, polygon, deg, movx) {
const pixel = fb.pixel;
const proj = fb.projection;
const mobj = new MyObject3D();
mobj.translate(movx, 0, 0);
mobj.degRotateY(deg);
mobj.aMulUpdate([
0, 1, 0, 0, // Y → X
0, 0, 1, 0, // Z → Y
1, 0, 0, 0, // X → Z
0, 0, 0, 1,
]);
proj.pushMatrix(mobj);
{
const vertex3d = fb.createVertex(vertex);
const bcol = 2;
const fcol = 3;
const fgp = new Array();
const bgp = new Array();
for (const p of polygon) {
const v0 = vertex3d[p[0]];
const v1 = vertex3d[p[1]];
const v2 = vertex3d[p[2]];
const nv = v1.sub(v0).cross(v2.sub(v1));
if (v0.dot(nv) < 0)
bgp.push(p);
else
fgp.push(p);
}
for (const [pl, col] of [[bgp, bcol] /*, [fgp, fcol] */]) {
for (const p of pl) {
let vp = vertex3d[p[p.length - 1]];
for (const n of p) {
const vn = vertex3d[n];
fb.setLine(vp, vn, col);
vp = vn;
}
}
}
for (const p of fgp) {
let vc = new MyVertex3D();
for (const n of p) {
const vn = vertex3d[n];
vc = vc.add(vn);
}
const small = 0.95;
vc = vc.div(p.length);
let vp = vertex3d[p[p.length - 1]];
vp = new MyVertex3D(vp.sub(vc).mul(small).add(vc).vector);
for (const n of p) {
let vn = vertex3d[n]
vn = new MyVertex3D(vn.sub(vc).mul(small).add(vc).vector);
fb.setLine(vp, vn, fcol);
vp = vn;
}
}
}
proj.popMatrix();
return pixel;
}
function setRegularPolyhedron(M) {
const mIndex = { 4:1, 6:2, 8:3, 12:4, 20:5 }[M];
const id_table = [
'RegularTetrahedron',
'RegularTetrahedron',
'RegularHexahedron',
'RegularOctahedron',
'RegularDodecahedron',
'RegularIcosahedron',
];
const name_table = ['正4面体', '正4面体', '正6面体', '正8面体', '正12面体', '正20面体'];
const dual_table = ['正4面体', '正4面体', '正8面体', '正6面体', '正20面体', '正12面体'];
const loop_table = [3, 3, 3, 4, 3, 5];
const nbsp = '\xa0';
const rp = new MyRegularPolyhedron(M);
const table = document.createElement('table');
const set_vector = function(title, pmsg, table, vectors, poly, dual) {
{
const tr = document.createElement('tr');
const th1 = document.createElement('th');
th1.setAttribute('colspan', '4');
th1.innerText = title;
tr.append(th1);
const th2 = document.createElement('th');
th2.innerText = pmsg;
tr.append(th2);
table.append(tr);
}
for (let i in vectors) {
const v = vectors[i].fixInt().vector;
const tr = document.createElement('tr');
const tdn = document.createElement('td');
tdn.style.setProperty('text-align', 'right');
tdn.innerText = i;
tr.append(tdn);
for (let j = 0; j < 3; j++) {
const s = v[j].toFixed(6);
const tdv = document.createElement('td');
tdv.innerText = nbsp + ((s[0] != '-') ? '+' : '') + s + nbsp;
tr.append(tdv);
}
const tdp = document.createElement('td');
tdp.innerText = (nbsp + dual[i] + nbsp).replaceAll(',', ',' + nbsp);
tr.append(tdp);
table.append(tr);
}
}
set_vector('頂点表(対の多角形の法線表)', '(対)' + dual_table[mIndex] + '\n対の頂点番号表',
table, rp.vertex, rp.polygon1, rp.polygon2);
set_vector('多角形の法線表(対の頂点表)', name_table[mIndex] + '\n頂点番号表',
table, rp.normal, rp.polygon2, rp.polygon1);
{
const bcol = 1;
const fcol = 3;
const nloop1 = loop_table[mIndex];
const nloop2 = nloop1;
const angle1 = Math.trunc(360 / nloop1);
const angle2 = Math.trunc(360 / nloop2);
const step = (150 / nloop1);
const speed = 3;
const color_table = [
[ 0, 0, 0],
[ 90, 90, 0],
[ 85, 85, 85],
[255, 255, 255],
]
const mfb = createFrameBuffer(color_table);
const gif = mfb.pixel.createGIF(true);
gif.appendApplicationExtensionForLoop(0);
for (let s = 0; s < step; s++) {
const deg1 = angle1 * s / step;
const deg2 = angle2 * s / step;
gif.appendGraphicControlExtension(speed);
const sfb = createFrameBuffer(color_table);
// const pix3 = renderPolyhedronEdge(sfb, rp.normal, rp.edge2, deg2, -1.25, bcol);
const pix4 = renderPolyhedronEdge(sfb, rp.vertex, rp.edge1, deg1, +1.25, bcol);
const pix1 = renderPolyhedronPolyEdge(sfb, rp.vertex, rp.polygon1, deg1, -1.25, fcol);
const pix2 = renderPolyhedronPolyEdge(sfb, rp.normal, rp.polygon2, deg2, +1.25, fcol);
gif.appendDescriptor(pix1.createImageDescriptor());
}
gif.appendTrailer();
const tr = document.createElement('tr');
const td = document.createElement('td');
const span = document.createElement('span');
span.innerText = '左は' + name_table[mIndex] + '、右は' + name_table[mIndex^1];
td.setAttribute('colspan', '5');
td.style.setProperty('text-align', 'center');
td.append(span);
td.append(document.createElement('br'));
td.append(createImgWithBase64(MyBase64.encode(gif.get_binary())));
tr.append(td);
table.append(tr);
}
document.getElementById(id_table[mIndex]).append(table);
}
function onLoad() {
for (let M of [4, 6, 8, 12, 20])
setRegularPolyhedron(M);
}
onLoad();
/* ********************************** */
// !-->
</script>
</body>
</html>
HTML を開く(少し時間がかかる)と、座標と多角形のデータとともに GIF 画像を表示します。
生成される画像
GIF 画像は、上から見て反時計回りに回転しています。
時計回りに見える場合は、奥行方向を逆に錯覚しています。