画像の先読み
実現のためのコードはいたってシンプルです。以下のようにします。
var imgObj = new Image(); imgObj.src = "sample1.jpg";
JavaScriptでImageオブジェクトのsrcプロパティに画像アドレスを設定すると、その時点でブラウザが画像をダウンロードするのを利用しています。
以下は引数に画像アドレス配列を渡すと、一括して先読みしてくれる関数です。
function preload(imgs){ for(var i = 0; i < imgs.length; i++){ var imgObj = new Image(); imgObj.src = imgs[i]; } } preload(["sample1.jpg", "sample2.jpg", "sample3.jpg"]);
・・・が、これだけでは先読みの終了どころか、本当に先読みしてるのかもわかりません^^;
テストのために、先読みの経過を表示するコードを追加してみます。
<html> <head> <script type="text/javascript"> function preload(imgs){ var objArray = []; for(var i = 0; i < imgs.length; i++){ var imgObj = new Image(); imgObj.src = imgs[i]; objArray.push(imgObj); } var viewStatus = function(){ var count = 0; var str = ""; for(var i = 0; i < objArray.length; i++){ if(objArray[i].complete) count++; } str += objArray.length + "件中" + count + "件完了"; if(count == objArray.length){ str += ":すべて完了しました。" } var viewElem = document.getElementById("view"); if(viewElem) viewElem.innerHTML = str; if(count < objArray.length){ setTimeout(viewStatus, 100); } }; viewStatus(); } function test(){ preload(["http://f.hatena.ne.jp/images/fotolife/s/susie-t/20070407/20070407220053.jpg", "http://f.hatena.ne.jp/images/fotolife/s/susie-t/20070407/20070407215356.jpg", "http://f.hatena.ne.jp/images/fotolife/s/susie-t/20070407/20070407215255.jpg"]); } </script> </head> <body> <div id="view"></div> <button onclick="test();">preload</button> </body> </html>
preloadボタンを押すと先読みを開始します。で、状態を表示するのですが。。。これぐらいだとすぐに終わってしまいますね^^;大量の画像や重い画像を指定するとよく分かるのですが。手っ取り早く、存在しないアドレスを入れれば途中で止まりますが、ずっとsetTimeoutで回り続けますのでご注意を。また、キャッシュがあるとすぐに終わりますので、再度テストする場合はキャッシュを削除してください。
Imageオブジェクトのcompleteプロパティにより、画像読み込みが完了(=true)したかがわかります。これを利用しています。Imageオブジェクトのonloadイベントを使う手もありますが、ここでは省略させていただきます。(実際には、負荷の面から、1秒ごとにチェックするくらいが妥当な気がしてます。上記コードはテスト用なので0.1秒に設定しています。)
関数名に「onload」を使うと・・・
常識かもしれないけど、初めて知ったのでメモ。
http://q.hatena.ne.jp/1179461074 で回答した際、グローバル宣言の関数ならwindow["関数名"]でアクセスできることを利用しました。
function test(str){ alert(str); } window["test"]("!!!");
ここで疑問が。「test」という名前を、「onload」にしたらどうなるのだろう・・・。
function onload(str){ alert(str); } window["onload"]("!!!");
通常、windowオブジェクトのonloadプロパティは、ページ読み込み直後に実行される関数を設定する場所です。
結果は・・・。
動作的に素直なのはFirefoxのほうでしょうか。単に、イベント発生時にwindow.onloadに設定されている関数を実行するということです。
これに対し、IE、Operaは、onloadという名前で関数が作成された場合、イベント発生時にwindow.onloadに設定されている関数を実行しない、という動作をします。
function onload(str){ alert(str); } window["onload"]("!!!"); window.onload=function(){alert("onload!");};
上記コードは、IE、Operaで「onload!」が表示されません。(Firefoxでは「!!!」、「onload!」の両方表示されます。)
つまり、グローバル宣言する変数名、関数名に、windowオブジェクトのメンバ名を使うべきでないということです。考えてみれば当たり前ですが。しかし、あんまり聞かない話のような。予約語でもないのでエラーにならないですし・・・。
ともかく、気をつけましょう^^;
【参考】windowオブジェクトのメンバを表示する ※IE、Firefoxで確認。Operaは動作せず。(windowオブジェクトのメンバが列挙可能でない) ※以下では表示されないメンバもあります(IEのalert等)。参考程度に。 <html> <head> </head> <body> <div id="test"></div> </body> </html> <script> (function (){ var str = "<table border>"; for(var i in window){ str += "<tr><th>" + i + "</th><td>" + window[i] + "</td></tr>"; } str += "</table>"; document.getElementById("test").innerHTML=str; })(); </script>
テロップを流すサンプル
question:1178848515の回答の修正です。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 //EN"> <html> <head> <script type="text/javascript"> /** * スタイル値取得関数 * elemID : 対象要素ID * IEStyleProp : IE用CSS属性名(aaaBbb形式) * CSSStyleProp : その他用CSS属性名(aaa-bbb形式) */ function getElementStyle(elemID, IEStyleProp, CSSStyleProp){ var elem; if(typeof(elemID) == "string"){ elem = document.getElementById(elemID); }else{ elem = elemID; } if(elem.currentStyle){ return elem.currentStyle[IEStyleProp]; }else if(window.getComputedStyle){ var compStyle = window.getComputedStyle(elem, ""); return compStyle.getPropertyValue(CSSStyleProp); } } /** * テロップ表示関数 * msgs : 表示メッセージ配列 * len : 移動距離(px) * time : 移動間隔(ms) * id : テロップ表示要素ID */ function terop(msgs, len, time, id){ var base = document.getElementById(id); var stylePosition = getElementStyle(base, "position", "position"); if(stylePosition == "" || stylePosition == "static"){ base.style.position = "relative"; } var styleWidth = getElementStyle(base, "width", "width"); if(styleWidth == "" || styleWidth == "auto"){ base.style.width = "100%"; } base.style.overflow = 'hidden'; var msg = document.createElement("span"); base.appendChild(msg); msg.style.position = "relative"; msg.style.left = base.offsetWidth + "px"; msg.style.whiteSpace = "nowrap"; var i = 0; msg.innerHTML = msgs[i]; var move = function(){ if(parseInt(msg.style.left) < -(msg.offsetWidth)){ msg.style.left = (base.offsetWidth + 1) + "px"; i++; if(i >= msgs.length) i = 0; msg.innerHTML = msgs[i]; } if(parseInt(msg.style.left) > base.offsetWidth){ msg.style.left = base.offsetWidth + "px"; }else{ msg.style.left = (parseInt(msg.style.left) - len) + "px"; } setTimeout(move, time); }; move(); } /** * ページ読み込み完了時実行関数 */ function init(){ terop([ "こんなかんじで", "つぎつぎに", "流れていきます。" ], 1, 20, "terop1"); terop([ "<span style='color:blue;'>" + "あああああああああああああああああああああ" + "あああああああああああああああああああああ" + "</span>", "<span style='color:red;'>" + "いいいいいいいいいいいいいいいいいいいいい" + "いいいいいいいいいいいいいいいいいいいいい" + "</span>", "<span style='font-weight:bold;'>" + "ううううううううううううううううううううう" + "ううううううううううううううううううううう" + "</span>" ], 1, 10, "terop2"); }; </script> <style type="text/css" media="screen"> .terop{ border:solid blue 2px; } </style> </head> <body onload="init();"> <div id="terop1" class="terop"></div> <div id="terop2" class="terop"></div> </body> </html>
2007/11/13 文字列の自動折り返しを禁止しました。(<br/>タグでの折り返しはできます)
2007/09/20 移動間隔とテロップ表示要素IDを引数で指定できるようにしました。
2009/03/11 テロップ表示要素がリサイズされて文字列が要素から外れた場合、表示要素の左端から文字列の表示を開始するようにしました。
theater.jsのサイト開設
theater.jsのサイトを開設しました。(まだ内容が不十分ですが)
http://susie-t.main.jp/
製作途中の自作ライブラリの解説を、ブログでするのは無理がある気がしたので^^;
今後はtheater.jsの解説を、上記サイトで行っていく予定です。
基本メニュー表示と結果取得
アドベンチャーゲームを作るという以上、ユーザに入力を促し、その結果によってシナリオを分岐させる機能は必須です。このとき、モーダル(modal)であること*1がより望ましいです。これを実現するのに最も簡単なのはJavaScriptのconfirm、およびpromptを利用することです。(参考:できるだけ単純にJavaScriptでアドベンチャーゲームを作る。 - Backstage of theater.js) しかし、デザイン・機能的に全く融通が利かないのがツライところです。*2
そこで、PrototypeWindowClassのように半透明の要素でブラウザウィンドウを覆い、その上にメニューを表示するようにしてみました。PrototypeWindowClassをそのまま使うこともできるのですが、高機能すぎて重い(^^;ので、自作しました。
これを使用して、Theaterクラスに以下のメソッドを作成しました。
alertメソッド
【使用例】 new Theater({ scenario: [ function(){ this.alert("あらーと1"); }, function(){ this.alert("あらーと2"); } ] });
シナリオ(関数配列)内の関数で「this.alert(表示文字列);」として使用します。指定文字列とOKボタンが表示されます。OKボタンを押すと閉じ、自動的に次の関数を実行します。なので、上記は「あらーと1」「あらーと2」が続けて表示されます。
confirmメソッド
【使用例】 new Theater({ scenario: [ function(){ this.confirm("かくにん"); }, function(result){ this.alert("けっか:" + result); } ] });
シナリオ(関数配列)内の関数で「this.confirm(表示文字列);」として使用します。指定文字列と「はい」と「いいえ」のボタンが表示されます。「はい」か「いいえ」のいずれかのボタンを押すと、閉じて次の関数を実行します。このとき、関数の引数に結果が設定されます。「はい」が押された場合はtrue、「いいえ」が押された場合はfalseです。上記例では表示しているだけですが、これによりシナリオを分岐させることもできます。
promptメソッド
【使用例】 new Theater({ scenario: [ function(){ this.prompt("なにかいれてね", "でふぉると"); }, function(result){ this.alert("けっか:" + result); } ] });
シナリオ(関数配列)内の関数で「this.confirm(表示文字列[, デフォルト値]);」として使用します。デフォルト値は省略可能です。指定文字列とテキストボックス、「入力」ボタンを表示します。「入力」ボタンを押すと、閉じて次の関数を実行します。このとき、関数の引数にテキストボックスに入力された値が設定されます。
上記3メソッドで表示されるメニューは、いずれも画像ボックス*3の中心に表示され、ドラッグ可能です。
これ以外のメニューを作成する方法については今後解説します。
とりあえず、例です。promptメソッドで名前入力を促し、未入力ならalertメソッドでメッセージを出して再入力を促します。入力ありならconfirmメソッドで入力を確認してもらい、「いいえ」が押されたら入力画面に戻ります。「はい」が押されたら次へ遷移します。
http://susie-t.seesaa.net/article/35350351.html
以下は上記例のブログ内で記述したコードです。(ファイルの読み込みはブログの共通設定で行っています。)
<button id='d20070306reset' class='reset'> はじめから</button> <div id="d20070306play" class='play'> <div id="d20070306frame" class='frame'></div> <div id="d20070306msg" class='msg'></div> </div> <script type="text/javascript"> var d20070306 = {} d20070306.imgRoot = 'http://susie-t.up.seesaa.net/image/' d20070306.img = { sample1: d20070306.imgRoot + 'sample1.gif', sample2: d20070306.imgRoot + 'sample2.gif', sample3: d20070306.imgRoot + 'sample3.gif' }; d20070306.scenario = [ function (){ this.work.name = ""; this.act({ img: d20070306.img.sample1, msg: '名前入力テスト' }); }, function (){ this.act({ img: d20070306.img.sample2, msg: 'お名前を入力してください' }); }, function (){ this.push([ function(){ this.prompt( "お名前を入力してください", this.work.name ); }, function (result){ this.work.name = result; if(result == ''){ this.back(); this.alert("お名前が入力されていません><;"); }else{ this.confirm("あなたのお名前:" + result + "<br/>よろしいですか?"); } } ]); this.play(); }, function (result){ if(result){ this.act({ img: d20070306.img.sample3, msg: "ようこそ" + this.work.name + "さん" }); }else{ this.back(); this.play(); } }, function (){ this.work.name = ""; this.act({ img: d20070306.img.sample1, msg: '名前入力テストおわり。' }); } ]; new Theater({ frameId: 'd20070306frame', msgId: 'd20070306msg', resetId: 'd20070306reset', playId: 'd20070306play', filter: ImageChanger.getFilter({no:10}), outEffect:'Fade', inEffect:'Appear', scenario: d20070306.scenario, img: d20070306.img }); </script>
・・・説明しなきゃいけないことが多いな><;
Theaterクラスインスタンス作成時にframeId等を指定しています。無指定時はデフォルトで'frame'等が割り振られるのですが、同ページに複数Theaterクラスの利用があると、競合してしまうので個別のIDを振る必要があります。なので、いちいち設定しています。また、競合を避けるためd20070306オブジェクトを作成・利用しています。・・・分かりづらくてすみません。
Theaterクラスインスタンス作成時のoutEffect、inEffectオプションは、IE以外にも画像切替効果をつけようとして追加したオプションです。値はscriptaculousのEffectに準じています・・・が、全部が動くわけではありません^^; とりあえずoutEffect(消滅時)に'Fade'、innEffect(出現時)に'Appear'は設定できます・・・。詳しくは今後解説します。
pushメソッドはサブシナリオを設定するメソッドです。引数はシナリオ(関数配列)です。サブシナリオが終了すると、親のシナリオへ復帰します。
backメソッドは、シナリオを「ひとつ戻す」メソッドです。ただし、これにより必ずしも「直前のシナリオ関数」が実行できるわけではありません。やっていることは、当該シナリオのインデックスを減算*4するだけです。このため、サブシナリオから復帰した直後の場合、backメソッドを使用してもサブシナリオへは戻れません。
ここでは、この特性をシナリオ作成に利用しています。サブシナリオ直後に判定し、結果によってbackし、再度サブシナリオを実行させています。
back以外にも、シナリオ操作メソッドとしてjump(指定インデックスへ飛ぶ)、move(現インデックスから指定数減算・加算する)も用意していますが、この二つの使用はあまりお勧めできません。シナリオ修正が異常にしにくくなるためです。たとえば、上記のback+サブシナリオの方法であれば、サブシナリオ部分の修正はbackメソッドに影響しません。サブシナリオを使用せずにmoveメソッドを使用すると、戻る先からbackメソッド間でシナリオ関数の追加・削除が発生すると、moveの引数を変更する必要があります。そして、これは非常に気づきにくいです。jumpメソッドについても同様の理由です。
pushやbackは、それだけでは「シナリオを設定する」だけで、実行はしません。直後に実行したい場合はplayメソッドを呼び出します。ただし、各種メニューを呼び出した場合は、閉じると自動的に次のシナリオ関数が呼び出されます。
・・・この辺りの仕様は改善の余地があるかもしれません。
以上。あ゛ー。分かりにくいですねぇ・・・TT
なにかご質問がありましたらコメントORメールください・・・。
ブログでアドベンチャーゲーム試作(ちょっと無理やり)
↑こんなタイトルですが、はてなはスクリプトタグが使えないので、seesaaブログに間借りしました^^;
http://susie-t.seesaa.net/category/2731315-1.html
こんな感じのブログもアリじゃないかなと思う次第。
ただ、やっぱり重いです・・・。それにこのままだと、複数日表示時に問題が。もうちょっと工夫する必要があります。やっぱり趣味の世界ですねぇ。。。
Menu表示機能も追加し、ほぼ完成しました。前Verをスクラップしてから結構速く仕上がったと思います。あのときはどうなることかと思いましたが。
seesaaで遊びながら、こちらで解説をしていきます。
Positionオブジェクトの枠線幅問題対策
※FirefoxのBODY枠線問題に対応しました。
【関連】 Positionオブジェクト(1) - Backstage of theater.js Positionオブジェクト(3) - Backstage of theater.js offsetTop/offsetLeft/offsetParentの闇 - Backstage of theater.js
prototype.jsのPosition.cumulativeOffset、Position.positionedOffset、Position.pageメソッドは、対象要素の上位offsetParentに枠線幅があると、正しく値を取得できない場合があります。これは、要素のoffsetTop/LeftプロパティがoffsetParent側の基準を枠線内側においている場合があり、このためメソッド処理中の各offsetTop/Left累積時に枠線幅分が加算されないのが理由です。
対策としては、offsetParent側の基準が枠線内側の場合、枠線幅を加算すればいいのですが、基準が枠線内側か外側かの条件は単純ではありません。特に、FireFox、NetscapeのTABLE要素内の要素に関しては嫌になるほど複雑です。
・・・なので、TABLE要素に関してはあきらめることにしました^^;
それ以外の場合ですが、現在分かっているのは
なのですが、今後の変更への対応、また他の環境にも対応させるために、以下のように判定することにします。
- 枠線幅を持つDIV要素A内に、DIV要素Bをひとつだけ作成。要素Aにはposition:absolute;を指定してoffsetParentの資格を与える。
- 要素BのoffsetLeftをチェック。0なら基準は枠線内側。1以上(=要素Aの枠線幅)なら基準は枠線外側。
上記チェックで基準枠線内側の場合のみ、枠線幅を加算するようにします。
以下、改修案です。ここではファイル名を「prototype_extend.js」とし、prototype.jsの後に読み込んで上書きすることを想定しています。
【prototype_extend.js】 Object.extend(Position, { positionedOffset: function(element) { var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; if(!this.isIncludeBorder){ var border = this.getBorder(element.offsetParent); valueT += border[0]; valueL += border[1]; if(element.offsetParent == document.body && this.isMinusBodyBorder){ valueT += border[0]; valueL += border[1]; } } element = element.offsetParent; if (element) { if(element.tagName=='BODY') break; var p = Element.getStyle(element, 'position'); if (p == 'relative' || p == 'absolute') break; } } while (element); return [valueL, valueT]; }, page: function(forElement) { var valueT = 0, valueL = 0; var element = forElement; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; if(!this.isIncludeBorder){ var border = this.getBorder(element.offsetParent); valueT += border[1]; valueL += border[0]; if(element.offsetParent == document.body && this.isMinusBodyBorder){ valueT += border[0]; valueL += border[1]; } } // Safari fix if (element.offsetParent==document.body) if (Element.getStyle(element,'position')=='absolute') break; } while (element = element.offsetParent); element = forElement; do { valueT -= element.scrollTop || 0; valueL -= element.scrollLeft || 0; } while (element = element.parentNode); return [valueL, valueT]; }, getBorder: function(element){ if(element == null){ return [0, 0]; } var top , left, style; style = Element.getStyle(element, 'border-top-style'); if(style != 'none'){ top = parseInt(Element.getStyle(element, 'border-top-width')); } style = Element.getStyle(element, 'border-left-style'); if(style != 'none'){ left = parseInt(Element.getStyle(element, 'border-left-width')); } top = (isNaN(top)) ? 0 : top; left = (isNaN(left)) ? 0 : left; return [left, top]; }, setIsIncludeBorder: function(){ var id = (new Date()).getTime(); new Insertion.Bottom(document.body, "<div id='parent_" + id + "'" + " style='border:solid blue 10px; padding:0px;" + " position:absolute; visibility:hidden;'>" + " <div id='child_" + id + "'></div></div>"); if($('child_' + id).offsetTop == 0){ this.isIncludeBorder = false; }else{ this.isIncludeBorder = true; } Element.remove('parent_' + id); var top = document.body.offsetTop; var margin = parseInt(Element.getStyle(document.body, "margin-top")); if(isNaN(margin)) margin = 0; if(top < margin){ this.isMinusBodyBorder = true; }else{ this.isMinusBodyBorder = false; } } }); Event.observe(window, 'load', Position.setIsIncludeBorder.bind(Position)); if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { Position.cumulativeOffset = function(element) { var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; if(!this.isIncludeBorder){ var border = this.getBorder(element.offsetParent); valueT += border[1]; valueL += border[0]; if(element.offsetParent == document.body && this.isMinusBodyBorder){ valueT += border[0]; valueL += border[1]; } } if (element.offsetParent == document.body) if (Element.getStyle(element, 'position') == 'absolute') break; element = element.offsetParent; } while (element); return [valueL, valueT]; } }else{ Position.cumulativeOffset = function(element) { var valueT = 0, valueL = 0; do { valueT += element.offsetTop || 0; valueL += element.offsetLeft || 0; if(!this.isIncludeBorder){ var border = this.getBorder(element.offsetParent); valueT += border[1]; valueL += border[0]; if(element.offsetParent == document.body && this.isMinusBodyBorder){ valueT += border[0]; valueL += border[1]; } } element = element.offsetParent; } while (element); return [valueL, valueT]; } }
以下、例です。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 //EN"> <html> <head> <title></title> <style> #parent{ left:200px; background-color:yellow; position:relative; width:200px; height:200px; } #mark{ background-color:red; position:relative; top:100px; left:100px; width:20px; height:20px; } </style> <script language="javascript" src="prototype.js" charset="utf-8"></script> <script language="javascript" src="prototype_extend.js" charset="utf-8"></script> <script> function test(){ var str = "Position.cumulativeOffset($('mark')) = " + Object.inspect(Position.cumulativeOffset($('mark'))) + "<br/>" + "$('mark').offsetLeft/Top = " + $('mark').offsetLeft + ", " + $('mark').offsetTop; Element.update('view', str); } function switchBorder(elm){ var style = (elm.checked) ? {'border':'solid blue 20px'} : {'border-style':'none'}; Element.setStyle('parent', style); } </script> </head> <body> <div id="parent"> <div id="mark"> MARK </div> </div> <button id="test" onclick="test();">TEST</button> <input type='checkbox' onclick='switchBorder(this);'/> <div id="view"></div> </body> </html>
TESTボタンを押すと、要素MARKのPosition.cumulativeOffsetの結果を表示します。チェックボックスにチェックを入れると、親要素(黄色)に枠線(青)が付きます。それから再度TESTボタンを押すと、IEやFirefox、Netscapeでは値が変わると思います。枠線は20pxを指定したので、+20です。prototype_extend.jsを読み込まないと、この値は変わりません。Operaでは、枠線があっても余計な加算はされません。
先に述べたように、TABLE要素内の要素に関しては対象外です。
不具合がありましたらご一報ください・・・。