| 1 |
///<reference path="all.ts"/> |
| 2 |
module jg { |
| 3 |
/** |
| 4 |
* 一行の情報 |
| 5 |
*/ |
| 6 |
export class TextLineInfo { |
| 7 |
/** 行の幅 */ |
| 8 |
width: number; |
| 9 |
/** 行の高さ */ |
| 10 |
height: number; |
| 11 |
/** 縦座標のオフセット値 */ |
| 12 |
offsetY: number; |
| 13 |
|
| 14 |
/** |
| 15 |
* コンストラクタ |
| 16 |
* @param offsetY 縦座標のオフセット値 |
| 17 |
*/ |
| 18 |
constructor(offsetY:number) { |
| 19 |
this.width = 0; |
| 20 |
this.height = 0; |
| 21 |
this.offsetY = offsetY; |
| 22 |
} |
| 23 |
} |
| 24 |
|
| 25 |
/** |
| 26 |
* スクリプトを解析するクラス。 |
| 27 |
* このクラスはサンプルであり、#pageコマンドによる改ページのみしかサポートしていない。 |
| 28 |
*/ |
| 29 |
export class MultilineScriptAnalyzer { |
| 30 |
/** 現在のモード */ |
| 31 |
mode: number; |
| 32 |
/** 対象のMultilineTextクラス */ |
| 33 |
owner: MultilineText; |
| 34 |
/** 対象の描画コンテキスト */ |
| 35 |
context: CanvasRenderingContext2D; |
| 36 |
/** ポジション */ |
| 37 |
pos: CommonOffset; |
| 38 |
/** バッファ */ |
| 39 |
buf: string; |
| 40 |
|
| 41 |
/** |
| 42 |
* 初期化 |
| 43 |
* @param owner 対象のMultilineTextクラス |
| 44 |
* @param context 描画コンテキスト |
| 45 |
* @param pos ポジション |
| 46 |
*/ |
| 47 |
init(owner:MultilineText, context:CanvasRenderingContext2D, pos:CommonOffset) { |
| 48 |
this.mode = 0; |
| 49 |
this.owner = owner; |
| 50 |
this.context = context; |
| 51 |
this.pos = pos; |
| 52 |
} |
| 53 |
|
| 54 |
/** |
| 55 |
* 次の文字を判定する |
| 56 |
*/ |
| 57 |
next(c:string):number { |
| 58 |
if (this.mode) { |
| 59 |
if (c == " ") { |
| 60 |
this.mode = 0; |
| 61 |
if (this.buf == "page") |
| 62 |
return -1; |
| 63 |
} else |
| 64 |
this.buf += c; |
| 65 |
|
| 66 |
return 1; |
| 67 |
} |
| 68 |
if (c == "#") { |
| 69 |
this.mode = 1; |
| 70 |
this.buf = ""; |
| 71 |
return 1; |
| 72 |
} |
| 73 |
return 0; |
| 74 |
} |
| 75 |
} |
| 76 |
|
| 77 |
/** |
| 78 |
* 複数行のテキストを扱うクラス |
| 79 |
*/ |
| 80 |
export class MultilineText extends E { |
| 81 |
/** 元スクリプト */ |
| 82 |
script: string; |
| 83 |
/** 裏画面バッファ */ |
| 84 |
buffer: HTMLCanvasElement; |
| 85 |
/** クリッピング用Line */ |
| 86 |
clip: Line; |
| 87 |
/** テキスト転送用Sprite */ |
| 88 |
sprite: Sprite; |
| 89 |
|
| 90 |
/** デフォルトのスタイル */ |
| 91 |
defaultStyle: any; |
| 92 |
/** デフォルトのフォント */ |
| 93 |
defaultFont: any; |
| 94 |
/** デフォルトの影 */ |
| 95 |
defaultBlur: number; |
| 96 |
/** デフォルトの影色 */ |
| 97 |
defaultShadowColor: any; |
| 98 |
/** デフォルトの影オフセットX */ |
| 99 |
defaultShadowOffsetX: number; |
| 100 |
/** デフォルトの影オフセットY */ |
| 101 |
defaultShadowOffsetY: number; |
| 102 |
/** テキストの影を無効にするかどうか */ |
| 103 |
disableShadow: boolean; |
| 104 |
|
| 105 |
/** 全行情報 */ |
| 106 |
lines: TextLineInfo[]; |
| 107 |
/** 現在アニメーション中のポジション */ |
| 108 |
animePos: CommonOffset; |
| 109 |
/** 現在アニメーション中の行 */ |
| 110 |
animeLine: number; |
| 111 |
/** テキストのアニメーションスピード。デフォルトは400 */ |
| 112 |
animeSpeed: number; |
| 113 |
|
| 114 |
/** アニメーション完了時に発火されるイベント */ |
| 115 |
animated: Trigger; |
| 116 |
/** スクリプト解析クラス */ |
| 117 |
scriptAnalyzer: MultilineScriptAnalyzer; |
| 118 |
/** バッファの背景 */ |
| 119 |
bufferBg: ImageData; |
| 120 |
/** 通常の行の高さ */ |
| 121 |
static LINE_HEIGHT_NORMAL: number = 1.2; |
| 122 |
/** 特殊なブラウザにおける余白 */ |
| 123 |
static BROWSER_BASELINE_MARGIN: number = 0; |
| 124 |
|
| 125 |
/** |
| 126 |
* コンストラクタ |
| 127 |
* @param size 表示サイズ |
| 128 |
* @param offset 場所 |
| 129 |
*/ |
| 130 |
constructor(size:CommonSize, offset?:CommonOffset) { |
| 131 |
super(); |
| 132 |
this.scriptAnalyzer = new MultilineScriptAnalyzer(); |
| 133 |
this.width = size.width; |
| 134 |
this.height = size.height; |
| 135 |
if (offset) |
| 136 |
this.moveTo(offset.x, offset.y); |
| 137 |
else |
| 138 |
this.moveTo(0, 0); |
| 139 |
|
| 140 |
//this.disableShadow = true; |
| 141 |
//this.defaultStyle = "#000"; |
| 142 |
this.defaultStyle = "#fff"; |
| 143 |
this.defaultFont = "18px sans-serif";//this.getDrawOption("font"); |
| 144 |
this.defaultBlur = 1; |
| 145 |
this.defaultShadowColor = "rgba(0,0,0,0.8)"; |
| 146 |
this.defaultShadowOffsetX = 1; |
| 147 |
this.defaultShadowOffsetY = 1; |
| 148 |
/* |
| 149 |
this.defaultBlur = 0.6; |
| 150 |
this.defaultShadowColor = "#000"; |
| 151 |
this.defaultShadowOffsetX = 0.3; |
| 152 |
this.defaultShadowOffsetY = 0.3; |
| 153 |
*/ |
| 154 |
|
| 155 |
//とりあえず全表示設定 |
| 156 |
this.clip = new Line({x:0, y:0}); |
| 157 |
this.clip.addLine(this.width, 0); |
| 158 |
this.clip.addLine(this.width, this.height); |
| 159 |
this.clip.addLine(0, this.height); |
| 160 |
this.clip.addLine(0, this.height); |
| 161 |
this.clip.addLine(0, this.height); |
| 162 |
this.clip.closePath = true; |
| 163 |
this.clip.setClip(true); |
| 164 |
|
| 165 |
this.entities = []; |
| 166 |
this.entities.push(this.clip); |
| 167 |
this.animeSpeed = 400; |
| 168 |
|
| 169 |
this.animated = new Trigger(); |
| 170 |
} |
| 171 |
|
| 172 |
/** |
| 173 |
* テキストをセットする |
| 174 |
* @param text 設定する文字列 |
| 175 |
* @param offset 読み込み開始位置。省略時は最初から |
| 176 |
*/ |
| 177 |
setText(text:string, offset?:number):number { |
| 178 |
//TODO: create plain script |
| 179 |
var plainScript = text; |
| 180 |
return this.setScript(plainScript, offset); |
| 181 |
} |
| 182 |
|
| 183 |
/** |
| 184 |
* スクリプトをセットする |
| 185 |
* @param script 設定するスクリプト |
| 186 |
* @param offset 読み込み開始位置。省略時は最初から |
| 187 |
*/ |
| 188 |
setScript(script:string, offset?:number):number { |
| 189 |
this.script = script.replace(/\r\n?/g, "\n"); |
| 190 |
this.updated(); |
| 191 |
return this.createBuffer(offset); |
| 192 |
} |
| 193 |
|
| 194 |
/** |
| 195 |
* 行の高さを取得する |
| 196 |
* @param c 対象の描画コンテキスト |
| 197 |
*/ |
| 198 |
getLineHeight(c:CanvasRenderingContext2D):number { |
| 199 |
var font = c.font; |
| 200 |
var firstPos = font.indexOf("px"); |
| 201 |
var lastPos = font.lastIndexOf(" ", firstPos); |
| 202 |
if (lastPos < 0) |
| 203 |
lastPos = 0; |
| 204 |
if (firstPos < 0) |
| 205 |
return 16; //バグっとる |
| 206 |
var fontSize = parseInt(font.substring(lastPos, firstPos)); |
| 207 |
//line-heightはどうもnormal(= 1.2)固定になるらしい |
| 208 |
//https://developer.mozilla.org/ja/docs/CSS/line-height (ほんとか?) |
| 209 |
var line_height = Math.round(fontSize * MultilineText.LINE_HEIGHT_NORMAL); |
| 210 |
return line_height; |
| 211 |
} |
| 212 |
|
| 213 |
/** |
| 214 |
* バッファを生成する |
| 215 |
* @param offset 読み込み開始位置。省略時は最初から |
| 216 |
*/ |
| 217 |
createBuffer(offset?:number):number { |
| 218 |
if (! this.buffer) |
| 219 |
this.buffer = window.createCanvas(this.width, this.height); |
| 220 |
if (offset === undefined) |
| 221 |
offset = 0; |
| 222 |
|
| 223 |
var script = this.script; |
| 224 |
var len = script.length; |
| 225 |
var pos = {x: 0, y: 0} |
| 226 |
var c = <CanvasRenderingContext2D>this.buffer.getContext("2d"); |
| 227 |
var s; |
| 228 |
var m = MultilineText.BROWSER_BASELINE_MARGIN; |
| 229 |
this.lines = []; |
| 230 |
|
| 231 |
if (this.bufferBg) |
| 232 |
c.putImageData(this.bufferBg, 0, 0); |
| 233 |
else |
| 234 |
c.clearRect(0, 0, this.width, this.height); |
| 235 |
|
| 236 |
c.fillStyle = this.defaultStyle; |
| 237 |
c.font = this.defaultFont; |
| 238 |
c.textBaseline = "top"; |
| 239 |
if (! this.disableShadow) { |
| 240 |
c.shadowBlur = this.defaultBlur; |
| 241 |
c.shadowColor = this.defaultShadowColor; |
| 242 |
c.shadowOffsetX = this.defaultShadowOffsetX; |
| 243 |
c.shadowOffsetY = this.defaultShadowOffsetY; |
| 244 |
} |
| 245 |
|
| 246 |
var lineHeight = this.getLineHeight(c); |
| 247 |
var lineInfo = new TextLineInfo(0); |
| 248 |
lineInfo.height = lineHeight; |
| 249 |
this.lines.push(lineInfo); |
| 250 |
|
| 251 |
var _newLine = ():boolean => { |
| 252 |
pos.x = 0; |
| 253 |
pos.y += lineInfo.height; //lineHeight |
| 254 |
if ((pos.y+lineInfo.height) > this.height) |
| 255 |
return false; |
| 256 |
lineInfo = new TextLineInfo(pos.y); |
| 257 |
lineInfo.height = lineHeight; |
| 258 |
this.lines.push(lineInfo); |
| 259 |
return true; |
| 260 |
} |
| 261 |
|
| 262 |
this.scriptAnalyzer.init(this, c, pos); |
| 263 |
while (offset < len) { |
| 264 |
s = script.substr(offset, 1); |
| 265 |
|
| 266 |
var script_ret = this.scriptAnalyzer.next(s); |
| 267 |
if (script_ret) { |
| 268 |
lineHeight = lineInfo.height; //スクリプト側でフォントサイズ変更した場合などの処置 |
| 269 |
if (script_ret < 0) { |
| 270 |
offset -= script_ret; |
| 271 |
break; |
| 272 |
} |
| 273 |
offset += script_ret; |
| 274 |
continue; |
| 275 |
} |
| 276 |
if (s == "\n") { |
| 277 |
offset++; |
| 278 |
if (! _newLine()) |
| 279 |
break; |
| 280 |
continue; |
| 281 |
} |
| 282 |
|
| 283 |
var metric = c.measureText(s); |
| 284 |
if ((pos.x+metric.width) > this.width) { |
| 285 |
if (! _newLine()) |
| 286 |
break; |
| 287 |
} |
| 288 |
//マニュアルシャドウの例。しかし全然綺麗に出ない。 |
| 289 |
//c.fillStyle = "#fff"; |
| 290 |
//c.fillText(s, pos.x+1, pos.y+m+1); |
| 291 |
//c.fillStyle = this.defaultStyle; |
| 292 |
c.fillText(s, pos.x, pos.y+m); |
| 293 |
pos.x += metric.width; |
| 294 |
lineInfo.width += metric.width; |
| 295 |
|
| 296 |
offset++; |
| 297 |
} |
| 298 |
|
| 299 |
this.sprite = new Sprite(this.buffer); |
| 300 |
this.sprite.moveTo(0, 0); |
| 301 |
if (this.entities.length == 1) |
| 302 |
this.entities.push(this.sprite); |
| 303 |
else |
| 304 |
this.entities[1] = this.sprite; |
| 305 |
|
| 306 |
return offset == len ? -1 : offset; |
| 307 |
} |
| 308 |
|
| 309 |
/** |
| 310 |
* バッファの再構築を行う |
| 311 |
*/ |
| 312 |
refresh() { |
| 313 |
delete this.buffer; |
| 314 |
this.createBuffer(); |
| 315 |
} |
| 316 |
|
| 317 |
/** |
| 318 |
* テキストのアニメーション表示を開始する |
| 319 |
* @param animeSpeed アニメーションのスピード。省略時は前回の設定から変更しない |
| 320 |
*/ |
| 321 |
startAnimation(animeSpeed?:number) { |
| 322 |
this.start(); |
| 323 |
this.animeLine = 0; |
| 324 |
this.animePos = {x: 0, y: this.lines[this.animeLine].height} |
| 325 |
if (animeSpeed !== undefined) |
| 326 |
this.animeSpeed = animeSpeed; |
| 327 |
this.hideAll(); |
| 328 |
this.clip.p[4].y = this.animePos.y; |
| 329 |
this.clip.p[5].y = this.animePos.y; |
| 330 |
} |
| 331 |
|
| 332 |
/** |
| 333 |
* Game.updateイベントに対するコールバック |
| 334 |
* @param t 経過時間 |
| 335 |
*/ |
| 336 |
update(t:number) { |
| 337 |
this.animePos.x += this.animeSpeed / 1000 * t; |
| 338 |
if (this.animePos.x >= this.lines[this.animeLine].width) { |
| 339 |
this.animePos.x = 0; |
| 340 |
this.animePos.y += this.lines[this.animeLine].height; |
| 341 |
this.animeLine++; |
| 342 |
|
| 343 |
if (this.animeLine < this.lines.length) { |
| 344 |
this.clip.p[2].y = this.lines[this.animeLine].offsetY; |
| 345 |
this.clip.p[3].y = this.clip.p[2].y; |
| 346 |
this.clip.p[4].y = this.animePos.y; |
| 347 |
this.clip.p[5].y = this.animePos.y; |
| 348 |
} |
| 349 |
} |
| 350 |
|
| 351 |
if (this.animeLine >= this.lines.length) { |
| 352 |
this.showAll(); |
| 353 |
} else { |
| 354 |
this.clip.p[3].x = this.animePos.x; |
| 355 |
this.clip.p[4].x = this.clip.p[3].x; |
| 356 |
} |
| 357 |
|
| 358 |
this.updated(); |
| 359 |
} |
| 360 |
|
| 361 |
/** |
| 362 |
* テキストをすべて非表示にする |
| 363 |
*/ |
| 364 |
hideAll() { |
| 365 |
this.clip.p[0] = {x:0, y:0}; |
| 366 |
this.clip.p[1] = {x:this.width, y:0}; |
| 367 |
this.clip.p[2] = {x:this.width, y:0}; |
| 368 |
this.clip.p[3] = {x:0, y:0}; |
| 369 |
this.clip.p[4] = {x:0, y:0}; |
| 370 |
this.clip.p[5] = {x:0, y:0}; |
| 371 |
} |
| 372 |
|
| 373 |
/** |
| 374 |
* テキストをすべて表示する |
| 375 |
*/ |
| 376 |
showAll() { |
| 377 |
this.clip.p[0] = {x:0, y:0}; |
| 378 |
this.clip.p[1] = {x: this.width, y: 0}; |
| 379 |
this.clip.p[2] = {x: this.width, y: this.height}; |
| 380 |
this.clip.p[3] = {x: 0, y: this.height}; |
| 381 |
this.clip.p[4] = {x: 0, y: this.height}; |
| 382 |
this.clip.p[5] = {x: 0, y: this.height}; |
| 383 |
this.stop(); |
| 384 |
this.animated.fire(); |
| 385 |
} |
| 386 |
} |
| 387 |
} |