/*
どうなっているのか?
App()
App.onloadx() htmlのonloadで呼ばれる。画像のプリロードからも呼ばれ、loadをカウントしてる。
App.onloadx1() htmlとimageのloadが完了したら呼ばれる。固定的な内容。onloadx2を呼ぶ。
App.onresizex() 固定的な内容。通常は変更しないと思う。
App.onloadx2() ★ここから書く★
App.start() stopをクリックした後再開するとき呼ばれる。
App.run() 書いたもの。消しても良い
App.draw() 書いたもの。消しても良い
【説明】
このファイルを html ファイルの script タグの src 属性で指定するだけで、動作開始するように作ったつもり。
目的のソフトウェアを App というクラスにまとめてある。
コメントで、「以下 変更しない部分」、「以下 変更する部分」のようにファイルを大きく2分してある。
このファイルを再利用するためです。
html の読み込みが完了した時点で、App クラスの onloadx1() 関数が呼ばれる。
onloadx1() と onloadx2() は処理的に分ける必要はないが、変更する必要のない部分と、
目的のソフトウェアを実装する部分とに分別するために分けた。
リサイズされたら、目的の部分(canvas 要素)はサイズを自動で合わせるようになっている。
【目的のソフトウェア 部分】
ページのロードが完了すると、このプログラムは画面に大きく、多数の四角形の 3DCG を描画する。
右上の STOP ボタンで停止と再開を行う。
アニメを行う基本的なプログラムになっている。
onloadx2() で 3DCG の各モデルの定義を行い、その後すぐに draw() している。
つづく start() でアニメが開始される。( setInterval() による run() の定期実行を行う)
run() は各アニメの数値の推移を行い、
"すい星"(大きな四角形の後に小さな四角形がパラパラと続く様子)
の尾の作成、消去など行い、最後に draw() している。
draw() は 3DCG の描画の基本的なプログラムが書かれている。
1. 画面に描く各四角形の4頂点の座標計算
2. 奥から手前の順になるようにソート(画家のアルゴリズム)
3. 描画
webGL やその他 3DCG などのライブラリは一切使用していません。
*/
//---以下 変更しない部分
/*
→ relative, z-index=0; に変更される
STOPやSPEEDなどのリンク
自動作成される
自動作成される
*/
var indexjs01 = new App();
App.prototype.onloadx = function( e ) {
this.onloadxCount++;
console.log( this.onloadxCount + " / " + this.onloadxCountMax );
//check.
if( this.onloadxCount == this.onloadxCountMax ) {
this.onloadx1(); //onloadx1は最後にonloadx2を呼んでいる
}
};
//ページ読み込み完了で開始
//---画像
indexjs01.images = {
"yuyake" : "20180801-indexJS/imgs/Sheet1.png",
"gaito" : "20180801-indexJS/imgs/Sheet2.png",
}
indexjs01.onloadxCount = 0;
indexjs01.onloadxCountMax = Object.keys( indexjs01.images ).length + 1;
for( var name in indexjs01.images ) {
var src = indexjs01.images[ name ];
indexjs01.images[ name ] = new Image();
indexjs01.images[ name ].onload = indexjs01.onloadx.bind( indexjs01 );
indexjs01.images[ name ].src = src;
}
addEventListener( "load", indexjs01.onloadx.bind( indexjs01 ) );
function App() { //Appクラスのコンストラクタ
this.name = "test";
}
App.prototype.onloadx1 = function() {
console.log( "onloadx1" );
//(※正直言うとcanvasのサイズについて多少混乱中…)
this.canvasEL = document.getElementById( "canvas01" );
//アニメの停止ボタン 親要素の上部に配置
this.swEL = document.createElement( "div" );
with( this.swEL.style ) {
border = "solid 0px black";
backgroundColor = "lightblue";
display = "inline-block";
right = "0px";
top = "0px";
boxSizing = "border-box";
zIndex = 2;
padding = ".5em 1em";
}
this.swEL.innerHTML = "";
this.swEL.innerHTML += "START|STOP / ";
this.swEL.innerHTML += 'SPEED';
this.canvasEL.parentNode.insertBefore( this.swEL, this.canvasEL );
var br = document.createElement( "br" );
this.canvasEL.parentNode.insertBefore( br, this.canvasEL );
//true にするとドットがシャープになる。falseはアンチエイリアスが入る(通常)。IEは非対応
if( true ) {
this.canvasEL.style.imageRendering = "pixelated";
this.canvasEL.style.imageRendering = "optimizeSpeed";
}
this.lowmode = false;
this.cc = this.canvasEL.getContext( "2d" );
this.onresizex();
//resize設定
addEventListener( "resize", ( function( e ) { this.onresizex( e ); } ).bind( this ), false );
//canvasが画面外に出たらCPUパワーを抑えるために停止する処理
addEventListener( "scroll", function( e ) {
var scrollEL = document.documentElement ? document.documentElement : document.body;
var canvasR = indexjs01.cc.canvas.getBoundingClientRect();
if( canvasR.top + canvasR.height / 2 < scrollEL.scrollTop //画面より上にある
|| canvasR.top + canvasR.height / 2 > scrollEL.scrollTop + window.innerHeight //画面より下にある
)
indexjs01.stop();
else
if( ! indexjs01.timerID ) indexjs01.start();
}, false );
this.onloadx2();
}//onloadx1
App.prototype.onresizex = function( e ) {
//リサイズされた親要素に合わせて、サイズ変更
this.canvasW = this.cc.canvas.width;
this.canvasH = this.cc.canvas.height;
this.canvasEL.style.width = this.canvasW + "px";
this.canvasEL.style.height = this.canvasH + "px";
this.canvasEL.setAttribute( "width", this.canvasW );
this.canvasEL.setAttribute( "height", this.canvasH );
//true にすると解像度を下げる。false は通常。
if( this.lowmode ) {
var pixelsize = 2; //ドットの大きさ2~
this.canvasEL.style.width = this.canvasEL.width + "px"; //実物画面大きさとして
this.canvasEL.style.height = this.canvasEL.height + "px";
this.canvasEL.width /= pixelsize; //解像度として
this.canvasEL.height /= pixelsize;
this.cc.scale( 1 / pixelsize, 1 / pixelsize );
}
if( e ) this.draw();
}//onresizex
//---以上 変更しない部分
//---以下 変更する部分 目的のソフトウェア
App.prototype.onloadx2 = function() {
console.log( "onloadx2" );
//like DirectX
///---allMaster
this.allMaster = {
type : "normal",
name : "noname",
tens : null,
mens : null,
fillStyle : "salmon",
strokeStyle : "black",
kaitenX : 0,
kaitenY : 0,
kaitenZ : 0,
kaitenStep : .05,
zm : 100,
pos : {
x : 0,
y : 0,
z : 200,
},
parent : null,
visibility : true,
};
///---cubeMaster
this.cubeMaster = objcopy( this.allMaster );
with( this.cubeMaster ) {
type = "cube";
tens = [
{ x : -1, y : +1, z : -1 }, //手前 左上点 0
{ x : +1, y : +1, z : -1 }, //手前 右上点 1
{ x : +1, y : -1, z : -1 }, //手前 右下点 2
{ x : -1, y : -1, z : -1 }, //手前 左下点 3
{ x : -1, y : +1, z : +1 }, //奥 左上点 4
{ x : +1, y : +1, z : +1 }, //奥 右上点 5
{ x : +1, y : -1, z : +1 }, //奥 右下点 6
{ x : -1, y : -1, z : +1 }, //奥 左下点 7
];
mens = [
[ 0, 1, 2, 3 ], //前面
[ 1, 5, 6, 2 ], //右面
[ 4, 0, 3, 7 ], //左面
[ 4, 5, 1, 0 ], //上面
[ 3, 2, 6, 7 ], //下面
[ 5, 4, 7, 6 ], //背面
];
fillStyle = "";
};
///---hanabiraMaster
this.hanabiraMaster = objcopy( this.allMaster );
with( this.hanabiraMaster ) {
type = "hanabira";
tens = [
{ x : 0, y : 4, z : 0 }, //花びらスリット部
{ x : 1, y : 7, z : 0 },
{ x : 3, y : 5, z : 0 },
{ x : 4, y : 1, z : 0 }, //花びら右端
{ x : 3, y : -3, z : 0 },
{ x : 0, y : -6, z : 0 }, //花びら根元部
{ x : -3, y : -3, z : 0 },
{ x : -4, y : 1, z : 0 }, //花びら左端
{ x : -3, y : 5, z : 0 },
{ x : -1, y : 7, z : 0 },
];
mens = [
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ], //上記頂点をこの順で描いて面とする
];
zm = .5;
strokeStyle = "";
}
///---areaMaster
this.areaMaster = objcopy( this.cubeMaster );
with( this.areaMaster ) {
zm = 1;
visibility = false; //エリアを示すワイヤフレームは最初見えない
pos.x = 30;
pos.z = 450;
fillStyle = "";
strokeStyle = "pink"; //ワイヤフレームの色(後で上書きされている)
//kaitenZ = 3.14 / 16; //左にやや傾ける
kaitenStep = .025;
}
//独自メンバを新規作成
this.areaMaster.hankei = 300; //花びらが配置される筒状の半径
this.areaMaster.takasa = this.canvasH / 1; //筒状の高さ
this.areaMaster.holeHankeiRate = .5; //ドーナツ状にくりぬく分の割合
this.areaMaster.holeHankei = null;
this.areaMaster.countOfHanabira = 100; //花びらの数(後で上書きされている)
this.areaMaster.hanabira = null;
//---areaの作成
this.areas = new Array();
switch( 0 ) {
case 0: //ノーマル
this.bgmode = 0; //夕焼け
this.timerMS = 50;
this.cam = new Object();
this.cam.s = 62; //焦点距離
this.cam.zm = 14.25; //引き伸ばし
//エリアは2個
for( var i = 0; i < 1; i++ ) {
this.areas[ i ] = objcopy( this.areaMaster );
with( this.areas[ i ] ) {
switch( i ) {
case 0:
countOfHanabira = 130;
pos.x = 140;
//kaitenX = 0.174;
strokeStyle = "white"; //ワイヤフレーム表示時のワイヤ色
break;
case 1:
//エリア2は花びらの数20%、全体の傾き20%
countOfHanabira = Math.round( this.areas[ 0 ].countOfHanabira * .2 );
kaitenZ = this.areas[ 0 ].kaitenZ - .2;
strokeStyle = "magenta";
break;
}
}
}
break;
}//switch
var areaChange = function() {
//エリアのサイズが変更されたら、頂点も変更する。
for( var j = 0; j < this.areas.length; j++ ) {
var area = this.areas[ j ];
area.holeHankei = area.hankei * area.holeHankeiRate;
area.tens = objcopy( this.areaMaster.tens ); //初期化
for( var i = 0; i < area.tens.length; i++ ) {
var ten = area.tens[ i ];
ten.x *= area.hankei;
ten.y *= area.takasa / 2;
ten.z *= area.hankei;
}
}
}.bind( this );
areaChange();
/*
onkeydown = function( e ) {
//イベントだが、this は indexjs01 になっている
//---キー入力
switch( e.which ) {
case 32: //space stop時コマ送り
e.preventDefault();
e.stopPropagation();
//check.
if( this.timerID ) break;
this.run();
return false;
default:
}
switch( String.fromCharCode( e.which + 32 ) ) {
case "q": this.cam.s++; break; //焦点距離増減
case "a": this.cam.s--; break;
case "o": //背景切り替え
this.bgmode += 1;
if( this.bgmode >= this.bgs.length ) this.bgmode = 0;
break;
case "z": this.lowmode = !this.lowmode; this.onresizex(); break; //背景切り替え
case "l": //設定値出力
for( var name in this.cam ) console.log( "this.cam:", name, this.cam[ name ] );
break;
default:
console.log( "ascii:", String.fromCharCode( e.which + 32 ), "e.which:", e.which );
}
for( var i = 0; i < this.areas.length; i++ ) {
var area = this.areas[ i ];
switch( String.fromCharCode( e.which + 32 ) ) {
case "p": area.visibility = ! area.visibility; break;//エリア枠の表示
case "w": area.kaitenX += area.kaitenStep; break; //エリア枠をX軸回転
case "s": area.kaitenX -= area.kaitenStep; break;
case "e": area.kaitenY += area.kaitenStep; break; //エリア枠をY軸回転
case "d": area.kaitenY -= area.kaitenStep; break;
case "r": area.kaitenZ += area.kaitenStep; break; //エリア枠をZ軸回転
case "f": area.kaitenZ -= area.kaitenStep; break;
case "t": area.hankei += 10; areaChange(); this.reset(); break; //エリア枠の幅を増
case "g": area.hankei -= 10; areaChange(); this.reset(); break;
case "y": area.takasa += 10; areaChange(); this.reset(); break; //エリア枠の高さを増減
case "h": area.takasa -= 10; areaChange(); this.reset(); break;
case "i": area.pos.z += 10; this.reset(); break; //ijkmの十字キーで、エリア枠を水平方向に移動
case "j": area.pos.x -= 10; this.reset(); break;
case "k": area.pos.x += 10; this.reset(); break;
case "m": area.pos.z -= 10; this.reset(); break;
case "l": //設定値出力
console.log( "areas[ " + i + " ].hankei:", area.hankei );
console.log( "areas[ " + i + " ].takasa:", area.takasa );
for( var name in area ) console.log( "areas[ " + i + " ]:", name, area[ name ] );
for( var name in area.pos ) console.log( "areas[ " + i + " ].pos:", name, area.pos[ name ] );
break;
default:
}//switch
}//for
this.draw();
}.bind( this );
*/
//---背景描画設定
this.bgs = new Array();
this.bgs[ 0 ] = function() { //透明(白)背景
this.cc.fillStyle = "blue";
this.cc.fillRect( 0,0, this.canvasW, this.canvasH );
}.bind( this );
this.onloadx3();
};
App.prototype.onloadx3 = function() {
this.models = new Array();
//---hanabiraの作成
for( var j = 0; j < this.areas.length; j++ ) {
var area = this.areas[ j ];
area.hanabiras = new Array();
for( var i = 0; i < area.countOfHanabira; i++ ) {
var hanabira = objcopy( this.hanabiraMaster );
// var baumkuchenW = area.hankei; //バームクーヘンの太さ
var ry = Math.round( Math.random() * area.takasa ) - area.takasa / 2;
//バームクーヘン状にちりばめる
// var theta = Math.random() * 6.28;
// var hankei2 = Math.random() * baumkuchenW;
// var rx = Math.cos( theta ) * hankei2
// var rz = Math.sin( theta ) * hankei2;
var rx = Math.round( Math.random() * area.hankei * 2 ) - area.hankei;
var rz = Math.round( Math.random() * area.hankei * 2 ) - area.hankei;
hanabira.pos = {
x : area.pos.x + rx,
y : area.pos.y + ry,
z : area.pos.z + rz,
};
hanabira.kaitenY = Math.random() * 6.28;
hanabira.kaitenX = Math.random() * 6.28;
hanabira.parent = area;
hanabira.fillStyle = j == 0 ? "rgb(255,240,240)" : "rgb(255,210,210)";
area.hanabiras.push( hanabira );
}
this.models.push( area );
this.models = this.models.concat( area.hanabiras );
}
this.draw();
}
App.prototype.start = function() {
this.timerID = setInterval( this.run.bind( this ), this.timerMS );
};
App.prototype.stop = function() {
if( this.timerID ) {
clearInterval( this.timerID );
this.timerID = null;
}
}
/*
this.hanabirasを
this.areas[ 0 ].hanabirasにしたところ。
花びら作成をエリア対応に。
エリアごとの花びら数を調整。
花びら描画をエリア対応に。
作成した花びらをmodelsへ追加するところエリア対応。
*/
App.prototype.reset = function() {
this.stop();
this.onloadx3();
this.start();
}
App.prototype.run = function() {
//それぞれの回転を進める
for( var j = 0; j < this.areas.length; j++ ) {
this.areas[ j ].kaitenY += this.areas[ j ].kaitenStep;
}
for( var i = 0; i < this.areas[ 0 ].hanabiras.length; i++ ) {
var hanabira = this.areas[ 0 ].hanabiras[ i ];
hanabira.kaitenY += .05;
hanabira.kaitenX += .05;
}
this.draw();
};
App.prototype.draw = function() {
//背景描画
this.bgs[ this.bgmode ]();
this.cc.save();
this.cc.translate( this.canvasW / 2, this.canvasH / 2 );
this.cc.scale( 1, -1 );
//都合により頂点計算部分を関数へ分離
var tenkeisan = function( model ) {
//check.
if( ! model.visibility ) return;
model.tensC = new Array();
//modelは、はなびらと、エリア枠も含まれる。(汎用的)
for( var i = 0; i < model.tens.length; i++ ) {
var ten = model.tens[ i ];
var x = ten.x;
var y = ten.y;
var z = ten.z;
x *= model.zm;
y *= model.zm;
z *= model.zm;
/* //自転
//kaitenY
var k = kaiten( x, z, model.kaitenY );
x = k.X;
z = k.Y;
//kaitenX
var k = kaiten( z, y, model.kaitenX );
z = k.X;
y = k.Y;
//kaitenZ
var k = kaiten( x, y, model.kaitenZ );
x = k.X;
y = k.Y;
*/
x += model.pos.x;
y += model.pos.y;
z += model.pos.z;
//エリアが回転すると、エリアを親とする花びらたちも回転する
if( model.parent != null ) {
var p = model.parent;
//kaitenY
var k = kaiten2( p.pos.x, p.pos.z, x, z, p.kaitenY );
x = k.X;
z = k.Y;
//kaitenX
var k = kaiten2( p.pos.z, p.pos.y, z, y, p.kaitenX );
z = k.X;
y = k.Y;
//kaitenZ
var k = kaiten2( p.pos.x, p.pos.y, x, y, p.kaitenZ );
x = k.X;
y = k.Y;
}
//3Dを2D化
var h = x * ( this.cam.s / z ) * this.cam.zm;
var v = y * ( this.cam.s / z ) * this.cam.zm;
var tenC = {
x : x,
y : y,
z : z,
h : h,
v : v,
};
model.tensC[ i ] = tenC;
}
}.bind( this );//function
//頂点を計算済みにする
for( var j = 0; j < this.models.length; j++ ) {
tenkeisan( this.models[ j ] );
}
forK:for( var k = 0; k < this.models.length; k++ ) {
var model = this.models[ k ];
//check.
if( ! model.visibility ) continue;
for( var j = 0; j < model.mens.length; j++ ) {
var men = model.mens[ j ];
var overZ = false;
var points = new Array();
this.cc.beginPath();
for( var i = 0; i < men.length; i++ ) {
var tenIdx = men[ i ];
var tenC = model.tensC[ tenIdx ];
//check. 視点の背後に触れた
if( tenC.z <= 0 ) {
overZ = true;
break;
}
var h = tenC.h;
var v = tenC.v;
if( i == 0 )
this.cc.moveTo( h, v );
else
this.cc.lineTo( h, v );
points.push( tenC );
}
this.cc.closePath();
//check. 視点の背後に触れたものは描かない。
if( overZ ) continue;
/*
//check. 線状に見える角度の花びらを正面向きに直す。視線と面の向きの内積で、線状に見えてるのかどうか判断
if( model.type == "hanabira" ) {
var housen = getHousen( points );
var sisen = toNorm( {
x : -points[ 1 ].x,
y : -points[ 1 ].y,
z : -points[ 1 ].z,
} );
var cosTheta = naiseki( housen, sisen );
var limit = .3; //線状に見えるとする幅
if( cosTheta > -limit && cosTheta < limit ) {
model.kaitenY = Math.random() * 6.28;
model.kaitenX = Math.random() * 6.28;
//このモデルを新しい角度で計算し直し
tenkeisan( model );
k--;
continue forK;
}
}
*/
//描く
if( model.fillStyle ) {
this.cc.fillStyle = model.fillStyle;
this.cc.fill();
}
if( model.strokeStyle ) {
this.cc.strokeStyle = model.strokeStyle;
this.cc.stroke();
}
}//for
}//for
this.cc.restore();
if( this.bgmode == 3 ) {
this.drawNightForeground();
}
};
function kaiten( x, y, theta2 ) { //数学関数
//0,0を原点として回転
var theta1 = Math.atan2( y, x );
var hankei = Math.sqrt( x * x + y * y );
var rx = Math.cos( theta1 + theta2 ) * hankei;
var ry = Math.sin( theta1 + theta2 ) * hankei;
return { X : rx, Y : ry };
}
function kaiten2( cx, cy, x, y, theta2 ) { //数学関数
//cx,cyを原点として回転
x -= cx;
y -= cy;
var res = kaiten( x, y, theta2 );
res.X += cx;
res.Y += cy;
return res;
}
function objcopy( obj ) { //オブジェクトを簡易コピーする
var res;
if( obj instanceof Array ) {
res = new Array();
for( var i = 0; i < obj.length; i++ ) {
if( typeof obj[ i ] == "object" ) {
res[ i ] = objcopy( obj[ i ] );
} else {
res[ i ] = obj[ i ];
}
}
} else if( obj instanceof Object ) {
res = new Object();
for( var name in obj ) {
if( typeof obj[ name ] == "object" ) {
res[ name ] = objcopy( obj[ name ] );
} else {
res[ name ] = obj[ name ];
}
}
} else {
res = null;
}
return res;
}
function getHousen( p ) {
/*
数学分野の汎用関数
法線を得る
*/
var x1 = p[ 0 ].x, y1 = p[ 0 ].y, z1 = p[ 0 ].z;
var x2 = p[ 1 ].x, y2 = p[ 1 ].y, z2 = p[ 1 ].z;
var x3 = p[ 2 ].x, y3 = p[ 2 ].y, z3 = p[ 2 ].z;
//法線
var res = new Object();
res.x = (y2-y1)*(z3-z2)-(z2-z1)*(y3-y2);
res.y = (z2-z1)*(x3-x2)-(x2-x1)*(z3-z2);
res.z = (x2-x1)*(y3-y2)-(y2-y1)*(x3-x2);
res = toNorm( res );
return res;
}
function toNorm( p ) {
/*
数学分野の汎用関数
原点から座標pまでの距離を1にした座標を返す(単位ベクトル化)
*/
var len = Math.sqrt( p.x * p.x + p.y * p.y + p.z * p.z );
var res = new Object();
res.x = p.x / len;
res.y = p.y / len;
res.z = p.z / len;
return res;
}
function naiseki( v1, v2 ) {
/*
数学分野の汎用関数
内積を得る
*/
var a = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
return a;
}