//- 関数リスト - // 1: App // 2: App.prototype.setCanvasSize // 3: App.prototype.exec // 4: App.prototype.ready // 5: App.prototype.onscrollx // 6: App.prototype.getBorder // 7: App.prototype.onresizex // 8: App.prototype.canvasApply // 9: App.prototype.canvasRefine_ // 10: App.prototype.resetMozaic // 11: App.prototype.onkeydownx // 12: App.prototype.onkeyupx // 13: App.prototype.ontouchx // 14: App.prototype.onmousedownx // 15: App.prototype.onclickx // 16: App.prototype.start // 17: App.prototype.stop // 18: App.prototype.resetTimer // 19: App.prototype.toBackgroundOf // 20: App.prototype.init // 21: App.prototype.run // 22: App.prototype.keySense // 23: App.prototype.keyType // 24: App.prototype.draw console.log( "SeamLess.js loading.." ); /* ※_save.txtは_save_SeamLess.txt このライブラリ: スクリプト同居を実現するライブラリです。 通常、日記ページの中で、作成したJavaScriptを公開する場合はそのJavaScript(HTMLファイル)へのリンクを貼ります。 しかし、もし日記ページの中で直にJavaScriptを実行できれば、新しいページを開くわずらわしさがなくなり、 意識せず、日記ページと一体的に利用できるでしょう。(シームレス) 複数のスクリプトが1つのページに同居できない原因は、 それぞれのスクリプトで、グローバル変数を自由に作成したり、キー入力やマウス入力などのイベントを自由に作成しており、 それらがスクリプト同士で競合する恐れがあるからです。 このスクリプトは、それぞれのスクリプトを1つのクラスのインスタンスにすることでこの原因を解消します。 グローバル変数はそれぞれインスタンスに内包されます。 イベントはそのスクリプトに対応するcanvasがWindow内に表示されているときだけ発生するように限定されます。 日記ページを作成しているときに、ほかのHTML要素と同様にcanvasを配置し、自由にスクリプトを作成し実行できます。 このスクリプトはそれを実現します。 このスクリプトの評価: 作者の公向けの自信 低 作者自身の使用期間(実績) 使用開始日:18/10/28(日) 13:04:26 期間確認日:(長い期間ここに日付がないときは「実績なし」と考えてください) 作者以外に使用を表明した人数 18/10/28(日) 13:07:27現在 0人 できないこと: 1. 普通に作成したスクリプトを自動的にインスタンス化するものではありません。 このライブラリを使用することを前提にしてからスクリプトを作る必要があります。 2. そのため、ごく自然なスクリプトではなく、このライブラリを使う不自然なスクリプトとなります。 スクリプトの一般的なサンプルとして視聴者に見せることができません。 (利用する人にとって、これらは大きなネックですが、その代わりスクリプト同居の機能が得られます) このスクリプトの導入手順: 1. このスクリプトをHTMLの最後(bodyタグの後など)に配置 最後にする理由は、コンストラクタでcanvas要素を参照しているのでHTMLドキュメントが必要になっているためです。 2. onload時などに、インスタンス作成のスクリプトを配置 その際、スクリプトのidを"sl_test"などと決めて以下のようにします。 app = new App( "sl_test" ); app.exec(); 3. canvasを設置 there is no canvas. 属性のidは、new App()で指定したidの後に _canvas を付けたものを指定してください。 以上の手順を踏んだ結果の模式図: 3. の手順のcanvasをここに。 または以下の方法でも可 3. の手順のcanvasをここに。 1. の手順のスクリプトをここに。 2. の手順のスクリプトをここに。 このhtmlをブラウザで表示すると、中央に円が描かれるサンプルが動作します。 なお、2,3の手順によるスクリプトはページ内に複数設置可能です。それが醍醐味です。 また、上記説明では、new App()やcanvasのid指定で sl_.. で始まるidを使っていますが、必ずしもそのようにする必要はありません。 (ただ、特別なライブラリを使用しているという「見分け」としてそうしたほうがいいかな) 以下のようなコードを用意しておくと便利かもしれません: //sl_test_canvasについて app = new App( "sl_test" ); app.init = function() { this.a = 0; }; app.run = function() { this.a++; this.draw(); }; app.draw = function() { var cc = this.cc; cc.clearRect( 0, 0, this.canvasW, this.canvasH ); cc.fillText( "a: " + this.a, 100, 100 ); }; addEventListener( "load", app.exec.bind( app ), false );//まだonloadしてない場合 動作確認: ・マウスでクリックすると、コンソールにメッセージを表示する。 test mousedown 241 59 ・キーを押すと、コンソールにメッセージを表示する。 test key typed: 65 ・タッチすると、コンソールにメッセージを表示する。 ... ・ページをスクロールして画面外になると、コンソールにメッセージを表示する。 test stop このとき、同様のキー、タッチ、マウスの入力はできない状態。 ・ページをスクロールして画面内になると、コンソールにメッセージを表示する。 test start このとき、同様のキー、タッチ、マウスの入力はできる状態。 以下逆引き式に: ・描く内容を変える app.draw = function() { ... }; ・アニメを実行する app.run = function() { ... }; ・画面をレトロゲーム風にする app.lowreso( 2 ); ・変数を使う app.init = function() { ... }; ・キー入力する(タイプ) サンプルを参考にして、app.keyType を新しく定義する。 ・キー入力する(センス) サンプルを参考にして、app.keySense を新しく定義する。 app.run()なの中で app.keySense() を呼ぶ。 ・タッチ入力する App.prototype.ontouchxが未開発の状態 onkeydownへリダイレクトする等。 ・マウス入力する App.prototype.onmousedownxが未開発の状態 onkeydownへリダイレクトする等。 その他: ・画像のプリロードを行う場合の記述例: app = new App( "sl_test" ); app.onloadCNT = 0; app.onloadMAX = 2; //画像1個とページ全体の合計2個と計算 app.onload = function() { this.onloadCNT ++; //check. if( this.onloadCNT == this.onloadMAX ) { this.exec(); //すべて読み込み完了で開始 } }; //画像1個 app.preload = function() { this.img = new Image(); this.img.onload = this.onload.bind( this ); this.img.src = "Sheet1.png"; } //ページ全体を1個と数える addEventListener( "load", app.onload.bind( app ), false ); ・プログラマが使う可能性の高いメンバ Appコンストラクタ内の同コメント以降のスクリプトを参照してください。 用語: キータイプ キーを一回だけ押したことを検知。タイプライターのように1回押すからタイプ。 キーセンス キーを押し続けていることを検知。センサーのように検知するからセンス。 タイマ setInterval()による定期実行のこと。 */ //---基本的にいじらないメソッド ここから var HereDocument = /[^]*\/\*\s([^]*)\*\/\}$/; //関数 1 / 24 function App( id, element ) { /* 第2引数 element のロジック elementが未指定のとき idはappのidだが、要素のidとしても使い、elementはその要素とする elementがcanvasのとき this.canvasEL = element elementがcanvasではないとき elementを親として、canvas要素を自動生成 */ // usage: var app = App( "appID" ); //appIDをidとするcanvas要素が用意済み // usage: var app = App( "appID" ); //appIDをidとするdiv要素等が用意済み // usage: var app = App( "appID", canvasEL ); // usage: var app = App( "appID", parentEL ); console.log( id, "new" ); //check. if( typeof apps === "undefined" ) apps = new Object(); apps[ id ] = this; //臨時にインスタンスを参照したい場合に利用 //プログラマが使う可能性の低いメンバ this.id = id; this.activity = false; this.pixelsize = 1; //変更の際はthis.lowreso()を使用。 this.isWidth100per = false; this.isKeepAspect = false; this.isPixelZoom = false; this.isSmoothZoom = false; //プログラマが使う可能性の高いメンバ //引数のelementについて if( typeof element === "undefined" ) element = document.getElementById( id ); if( element.tagName == "CANVAS" ) { this.canvasEL = element; } else { //canvas自動生成 var canvasEL = document.createElement( "canvas" ); canvasEL.setAttribute( "id", id + "_canvas" ); element.appendChild( canvasEL ); this.canvasEL = document.getElementById( id + "_canvas" ); } this.parentElement = this.canvasEL.parentNode; this.cc = this.canvasEL.getContext( "2d" ); //CANVASコンテキスト this.canvasWidthOrigin = this.canvasEL.width; this.canvasHeightOrigin = this.canvasEL.height; this.cc.save(); this.keytable = new Array(); //キーセンスされてるキー //イベントハンドラのthisをこのappにする this.onresizex = this.onresizex.bind( this ); this.onkeydownx = this.onkeydownx.bind( this ); //keySense, keyType this.onkeyupx = this.onkeyupx.bind( this ); this.ontouchx = this.ontouchx.bind( this ); //(開発途中) this.onmousedownx = this.onmousedownx.bind( this ); //(開発途中) this.onclickx = this.onclickx.bind( this ); //(開発途中) //イベントリスナの設定はthis.startに書かれている。 //これらハンドラは基本的にはプログラマがいじる必要はありません。 //プログラムは右端のコメントで示すメソッドをいじります。 this.setFps( 30 ); // this.onresizex(); };//function APP //関数 2 / 24 App.prototype.setCanvasSize = function( width, height ) { console.log( "-setCanvasSize" ); this.canvasWidthOrigin = width; this.canvasHeightOrigin = height; this.canvasEL.width = width; this.canvasEL.height = height; }; //関数 3 / 24 App.prototype.exec = function() { //ページ読み込みの最後に呼ばれる console.log( this.id, "exec" ); if( this.preload != null ) { this.preload(); } else if( this.init != null ) { this.init(); this.ready(); } }; //関数 4 / 24 App.prototype.ready = function() { console.log( "-ready" ); if( this.draw != null ) this.draw(); this.onscrollx(); //初期意図的実行 タイマ制御実行のため addEventListener( "scroll", this.onscrollx.bind( this ), false ); }; //関数 5 / 24 App.prototype.onscrollx = function( e ) { //"this" is app. //canvasが視聴者の目に入ったかどうか var rect = this.canvasEL.getBoundingClientRect(); var appCanvasCenterY = rect.top + rect.height / 2; var activityBak = this.activity; this.activity = appCanvasCenterY > 0 && appCanvasCenterY < window.innerHeight; if( activityBak != this.activity ) { if( this.activity ) { //innerWindowの中に入ったら this.start(); } else { //innerWindowの中から出たら this.stop(); } } }; //関数 6 / 24 App.prototype.getBorder = function( e ) { var res = new Object(); var s = getComputedStyle( this.canvasEL ); var f = function( s ) { return Number( s.replace( /px/, "" ) ) }; res.left = f( s.borderLeftWidth ); res.right = f( s.borderRightWidth ); res.top = f( s.borderTopWidth ); res.bottom = f( s.borderBottomWidth ); if( s.boxSizing != "border-box" ) { res.sumW = res.left + res.right; res.sumH = res.top + res.bottom; } else { res.sumW = 0; res.sumH = 0; } return res; }; //関数 7 / 24 App.prototype.onresizex = function( e ) { this.canvasApply(); this.draw(); }; //(apply: 適用) //関数 8 / 24 App.prototype.canvasApply = function( orderWidth, orderHeight ) { console.log( "-canvasApply" ); //check. var cr = this.canvasEL.getBoundingClientRect(); if( typeof orderWidth === "undefined" ) orderWidth = cr.width; var f = function( s ) { return Number( s.replace( /px/, "" ) ) }; var s = getComputedStyle( this.canvasEL ); var pr = this.parentElement.getBoundingClientRect(); var cr = this.canvasEL.getBoundingClientRect(); var ratioHW = f( s.height ) / f( s.width ); var heightBak = s.height; var bd = this.getBorder(); var toWidth = this.isWidth100per ? pr.width : orderWidth; var ratioWW = toWidth / f( s.width ); if( this.isScreenFit ) { var rate1 = cr.width / cr.height; var rate2 = pr.width / pr.height; var crWidth = cr.width; var crHeight = cr.height; if( rate1 > rate2 ) { //widthを100% this.canvasEL.style.width = pr.width + "px"; this.canvasEL.style.height = cr.height * ( pr.width / crWidth ) + "px"; } else { //heightを100% this.canvasEL.style.height = pr.height + "px"; this.canvasEL.style.width = cr.width * ( pr.height / crHeight ) + "px"; } var pr = this.parentElement.getBoundingClientRect(); var cr = this.canvasEL.getBoundingClientRect(); if( this.isPixelZoom ) this.pixelsize *= cr.width / crWidth; console.log( "px", this.pixelsize ); this.resetMozaic(); //センタリング this.canvasEL.style.left = ( pr.width - cr.width ) / 2 + "px"; return; } this.canvasEL.style.width = toWidth - bd.sumW + "px"; if( this.isKeepAspect ) this.canvasEL.style.height = toWidth * ratioHW - bd.sumH + "px"; else if( typeof orderHeight === "undefined" ) this.canvasEL.style.height = heightBak; else this.canvasEL.style.height = orderHeight - bd.sumH + "px"; if( this.isPixelZoom ) this.pixelsize *= ratioWW; this.resetMozaic(); }; //関数 9 / 24 App.prototype.canvasRefine_ = function( e ) { console.log( "-canvasRefine" ); if( this.isWidth100per ) { var f = function( s ) { return Number( s.replace( /px/, "" ) ) }; var s = getComputedStyle( this.canvasEL ); var pr = this.parentElement.getBoundingClientRect(); var ratioHW = f( s.height ) / f( s.width ); var ratioWW = pr.width / f( s.width ); var heightBak = s.height; var bd = this.getBorder(); this.canvasEL.style.width = pr.width - bd.sumW + "px"; if( this.isKeepAspect ) { this.canvasEL.style.height = pr.width * ratioHW - bd.sumH + "px"; } else { this.canvasEL.style.height = heightBak; } if( this.isPixelZoom ) { this.pixelsize *= ratioWW; } this.resetMozaic(); } }; //座標を維持しつつ画像を粗くする(画面をモザイク化) //関数 10 / 24 App.prototype.resetMozaic = function() { console.log( "-resetMozaic" ); //style.width,height、canvasEL.width,height、canvasW,Hの3種類のサイズを //調整してcanvasのモザイク化(レトロゲーム風表示)を実現する //true にするとドットがシャープになる。falseはアンチエイリアスが入る(通常)。IEは非対応 if( true ) { this.canvasEL.style.imageRendering = "pixelated"; this.canvasEL.style.imageRendering = "optimizeSpeed"; } //準備:ボーダー var bd = this.getBorder(); //準備:見た目のサイズ var cr = this.canvasEL.getBoundingClientRect(); var mitameW = cr.width - bd.sumW; var mitameH = cr.height - bd.sumH; //準備:モザイクサイズ var mozaicW = Math.round( mitameW / this.pixelsize ); var mozaicH = Math.round( mitameH / this.pixelsize ); //モザイク化実行 //1: canvasEL.width,height (canvasの表示上(モザイク)のサイズ) this.canvasEL.width = mozaicW; this.canvasEL.height = mozaicH; //2: style.width,height (画面上(物理)のサイズ) this.canvasEL.style.width = mitameW + bd.sumW + "px"; this.canvasEL.style.height = mitameH + bd.sumH + "px"; //3: canvasW,H (描画上(論理)のサイズ) var cw, ch; //cw is canvasW var sr; //sr is scale rate //canvasの表示上(モザイク)の1マスに詰める論理ピクセルの量 var rate = this.canvasWidthOrigin / mozaicW; if( this.isPixelZoom ) { //(ここの理解は困難) //モザイク cw = this.canvasWidthOrigin; ch = this.isKeepAspect ? this.canvasHeightOrigin : (this.canvasEL.height * rate); sr = 1 / rate; } else if( this.isSmoothZoom ) { //これはモザイクと関係のないおまけ機能 cw = mozaicW * rate; ch = mozaicH * rate; sr = 1 / rate; } else { cw = mitameW; ch = mitameH; sr = 1 / this.pixelsize; } this.canvasW = cw; this.canvasH = ch; this.cc.restore(); this.cc.scale( sr, sr ); //この時点でモザイク完了 }; //関数 11 / 24 App.prototype.onkeydownx = function( e ) { //this is app. //キーテーブルに登録 if( this.keytable.indexOf( e.which ) == -1 ) this.keytable.push( e.which ); if( document.getElementById( "keylock" ) ) if( document.getElementById( "keylock" ).checked ) e.preventDefault(); }; //関数 12 / 24 App.prototype.onkeyupx = function( e ) { //this is app. //キーテーブルから削除 this.keytable.splice( this.keytable.indexOf( e.which ), 1 ); //キータイプを処理 if( this.keyType( e.which ) ) { return true; } else { e.preventDefault(); e.stopPropagation(); return false; } }; //関数 13 / 24 App.prototype.ontouchx = function( e ) { //this is app. //未開発 for( var i = 0; i < e.changedTouches.length; i++ ) { var changedTouch = e.changedTouches[ i ]; console.log( changedTouch.target ); } }; //関数 14 / 24 App.prototype.onmousedownx = function( e ) { //this is app. var rect = e.target.getBoundingClientRect(); var elementX = Math.round( e.clientX - rect.left - e.target.clientLeft ); var elementY = Math.round( e.clientY - rect.top - e.target.clientTop ); console.log( this.id, e.type, elementX, elementY ); }; //関数 15 / 24 App.prototype.onclickx = function( e ) { //this is app. var rect = e.target.getBoundingClientRect(); var elementX = Math.round( e.clientX - rect.left - e.target.clientLeft ); var elementY = Math.round( e.clientY - rect.top - e.target.clientTop ); console.log( this.id, e.type, elementX, elementY ); }; //関数 16 / 24 App.prototype.start = function() { //onscrollxから呼ばれる console.log( this.id, "start" ); //キーテーブル初期化 this.keytable = new Array(); //タイマを開始 if( this.run != null && typeof this.timerID === "undefined" ) { this.timerID = setInterval( this.run.bind( this ), this.timerMS ); } //イベント有効化 addEventListener( "keydown", this.onkeydownx, false ); addEventListener( "keyup", this.onkeyupx, false ); addEventListener( "resize", this.onresizex, false ); this.canvasEL.addEventListener( "touchstart", this.ontouchx, false ); this.canvasEL.addEventListener( "mousedown", this.onmousedownx, false ); this.canvasEL.addEventListener( "click", this.onclickx, false ); }; //関数 17 / 24 App.prototype.stop = function() { //onscrollxから呼ばれる console.log( this.id, "stop" ); //イベント無効化 removeEventListener( "keydown", this.onkeydownx, false ); removeEventListener( "keyup", this.onkeyupx, false ); this.canvasEL.removeEventListener( "touchstart", this.ontouchx, false ); this.canvasEL.removeEventListener( "mousedown", this.onmousedownx, false ); this.canvasEL.removeEventListener( "click", this.onclickx, false ); //タイマを停止 if( this.run != null && typeof this.timerID !== "undefined" ) { clearInterval( this.timerID ); delete this.timerID; } }; //関数 18 / 24 App.prototype.resetTimer = function( fps ) { //check. if( ! this.run ) return; //check. if( typeof this.timerID !== "undefined" ) clearInterval( this.timerID ); this.setFps( fps ); this.timerID = setInterval( this.run.bind( this ), this.timerMS ); }; //関数 19 / 24 App.prototype.toBackgroundOf = function( p ) { console.log( "-toBackgroundOf" ); //check. if( ! p ) { alert( ( function() { /* Error: SeamLess.js At App.prototype.toBackgroundOf 引数pはHTML要素として存在しない 対策:pを検索するidやclass名が間違っているか、HTML要素の属性にその設定がない */} ).toString().match( HereDocument )[ 1 ] ); return; } this.parentElement = p; with( p.style ) { position = "relative"; zIndex = 0; } p.appendChild( this.canvasEL ); with( this.canvasEL.style ) { position = "absolute"; left = ( p.getBoundingClientRect().width - this.canvasEL.getBoundingClientRect().width ) / 2 + "px"; top = "0px"; boxSizing = "border-box"; zIndex = -1; } // this.onresizex(); } App.prototype.setFps = function( fps ) { this.fps = fps; this.timerMS = Math.floor( 1000 / this.fps ); } //---基本的にいじらないメソッド ここまで //---プログラマーが書き換えるメソッド ここから //※「書き換える」とは、メソッドをここでいじるのではなく、インスタンスの同関数名に新しい関数定義を行う //関数 20 / 24 App.prototype.init = function( e ) { //exec()から呼ばれる //サンプル }; /* //run()を定義すると、タイマ制御が開始される。そのためコメントアウトしている。 //関数 21 / 24 App.prototype.run = function() { //サンプル this.color = this.color == "rgb(255,128,128)" ? "black" : "rgb(255,128,128)"; this.draw(); }; */ //関数 22 / 24 App.prototype.keySense = function() { //keySenseはプログラマーがrun()などから適宜呼ぶ //サンプル //キーテーブルをすべて処理 for( var i = 0; i < this.keytable.length; i++ ) { var keynum = this.keytable[ i ]; switch( keynum ) { default: console.log( this.id + " key sensed: " + keynum ); } } }; //関数 23 / 24 App.prototype.keyType = function( keynum ) { //keyTypeはonkeyupから呼ばれている 不要な場合は下記に処理を書かないようにする。 //サンプル switch( keynum ) { default: console.log( this.id + " key typed: " + keynum ); return true; //ブラウザにイベントを渡す。F5更新など } return false; //ブラウザにイベントを渡さない。 }; //関数 24 / 24 App.prototype.draw = function() { var cc = this.cc; //サンプル cc.clearRect( 0, 0, this.canvasW, this.canvasH ); cc.strokeStyle = "black"; cc.strokeRect( 0, 0, this.canvasW, this.canvasH ); cc.beginPath(); cc.arc( this.canvasW / 2, this.canvasH / 2, 100, 0, 6.28, false ); cc.closePath(); cc.strokeStyle = this.color; cc.stroke(); cc.fillStyle = "black"; cc.fillText( this.keytable.join( "," ), 0, this.canvasH ); }; //---プログラマーが書き換えるメソッド ここまで