UNPKG

skynovel

Version:
1,162 lines (992 loc) 37.4 kB
/* ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2018-2020 Famibee (famibee.blog38.fc2.com) This software is released under the MIT License. http://opensource.org/licenses/mit-license.php ** ***** END LICENSE BLOCK ***** */ import {CmnLib, getDateStr, uint, IEvtMng} from './CmnLib'; import {CmnTween, ITwInf} from './CmnTween'; import {IHTag, IVariable, IMain, HPage, HArg} from './CmnInterface'; import {Pages} from './Pages'; import {GrpLayer} from './GrpLayer'; import {TxtLayer} from './TxtLayer'; import {RubySpliter} from './RubySpliter'; import {Config} from './Config'; import {ScriptIterator} from './ScriptIterator'; import {SysBase} from './SysBase'; import {FrameMng} from './FrameMng'; import {Button} from './Button'; const Tween = require('@tweenjs/tween.js').default; import {Container, Application, Graphics, Texture, Filter, RenderTexture, Sprite, DisplayObject, autoDetectRenderer} from 'pixi.js'; import {EventListenerCtn} from './EventListenerCtn'; export class LayerMng { private stage : Container; private fore = new Container; private back = new Container; private frmMng : FrameMng; constructor(private readonly cfg: Config, private readonly hTag: IHTag, private readonly appPixi: Application, private readonly val: IVariable, private readonly main: IMain, private readonly scrItr: ScriptIterator, private readonly sys: SysBase) { // レスポンシブや回転の対応 const cvs = document.getElementById(CmnLib.SN_ID) as HTMLCanvasElement; const fncResizeLay = ()=> { if (! CmnLib.cvsResize(cvs)) return; this.aLayName.forEach(layer=> { const pg = this.hPages[layer]; pg.fore.cvsResize(); pg.back.cvsResize(); }); this.frmMng.cvsResize(); }; if (CmnLib.isMobile) { window.addEventListener('orientationchange', fncResizeLay, {passive: true}); } else { let tid: any = 0; window.addEventListener('resize', ()=> { if (tid) return; tid = setTimeout(()=> {tid = 0; fncResizeLay();}, 500); }, {passive: true}); } CmnLib.cvsResize(cvs); TxtLayer.init(cfg, hTag, val, (txt: string)=> this.recText(txt)); GrpLayer.init(main, cfg, sys); this.frmMng = new FrameMng(this.cfg, this.hTag, this.appPixi, this.val, main, this.sys, this.hTwInf); sys.hFactoryCls['grp'] = ()=> new GrpLayer; sys.hFactoryCls['txt'] = ()=> new TxtLayer; // システム hTag.snapshot = o=> this.snapshot(o); // スナップショット hTag.loadplugin = o=> this.loadplugin(o); // プラグインの読み込み hTag.set_focus = o=> this.set_focus(o); // フォーカス移動 // レイヤ共通 hTag.add_lay = o=> this.add_lay(o); // レイヤを追加する hTag.lay = o=> this.lay(o); // レイヤ設定 hTag.clear_lay = o=> this.clear_lay(o); // レイヤ設定の消去 hTag.trans = o=> this.trans(o); // ページ裏表を交換 hTag.wt = o=> this.wt(o); // トランス終了待ち hTag.finish_trans = ()=> this.finish_trans(); // トランス強制終了 hTag.quake = o=> this.quake(o); // 画面を揺らす hTag.wq = o=> hTag.wt(o); // 画面揺らし終了待ち hTag.stop_quake = o=> hTag.finish_trans(o); // 画面揺らし中断 hTag.tsy = o=> this.tsy(o); // トゥイーン開始 hTag.wait_tsy = o=> this.wait_tsy(o); // トゥイーン終了待ち hTag.stop_tsy = o=> this.stop_tsy(o); // トゥイーン中断 hTag.pause_tsy = o=> this.pause_tsy(o); // 一時停止 hTag.resume_tsy = o=> this.resume_tsy(o); // 一時停止再開 // 文字・文字レイヤ // hTag.auto_pager = o=> this.auto_pager(o); // 自動改ページの設定 //hTag.autowc // TxtLayer.ts で定義 // 文字ごとのウェイト hTag.ch = o=> this.ch(o); // 文字を追加する //hTag.ch_in_style // TxtLayer.ts で定義 // 文字出現文字出現演出 //hTag.ch_out_style // TxtLayer.ts で定義 // 文字消去文字出現演出 hTag.clear_text = o=> this.clear_text(o); // 文字消去 hTag.current = o=> this.current(o); // デフォルト文字レイヤ設定 hTag.endlink = ()=> this.endlink(); // ハイパーリンクの終了 hTag.er = o=> this.er(o); // ページ両面の文字消去 hTag.graph = o=> this.graph(o); // インライン画像表示 hTag.link = o=> this.link(o); // ハイパーリンク hTag.r = o=> this.r(o); // 改行 hTag.rec_r = ()=> this.rec_r(); // 履歴改行 hTag.rec_ch = o=> this.rec_ch(o); // 履歴書き込み hTag.reset_rec = o=> this.reset_rec(o); // 履歴リセット //hTag.ruby = o=> this.ruby(o); // 直前一文字のルビ(廃止 // タグでは無く、「|@《》」というスクリプト書き換えで良い hTag.ruby2 = o=> this.ruby2(o); // 文字列と複数ルビの追加 hTag.span = o=> this.span(o); // インラインスタイル設定 hTag.tcy = o=> this.tcy(o); // 縦中横を表示する // 画像・画像レイヤ hTag.add_face = o=> GrpLayer.add_face(o); // 差分画像の追加 // ムービーレイヤ hTag.wv = o=> GrpLayer.wv(o); // ムービー再生終了待ち // デバッグ・その他 hTag.dump_lay = o=> this.dump_lay(o); // レイヤのダンプ // イベント hTag.enable_event = o=> this.enable_event(o); // イベント有無の切替 // ラベル・ジャンプ hTag.button = o=> this.button(o); // ボタンを表示 if (cfg.existsBreakline) this.breakLine = ()=> this.cmdTxt('grp|{"id":"break","pic":"breakline"}'); if (cfg.existsBreakpage) this.breakPage = ()=> this.cmdTxt('grp|{"id":"break","pic":"breakpage"}'); const grp = new Graphics; grp.beginFill(cfg.oCfg.init.bg_color, 1); // イベントを受け取るためにも塗る grp.lineStyle(0, cfg.oCfg.init.bg_color); grp.drawRect(0, 0, CmnLib.stageW, CmnLib.stageH); grp.endFill(); this.fore.addChild(grp.clone()); this.back.addChild(grp); this.back.visible = false; this.stage = this.appPixi.stage; this.stage.addChild(this.back); this.stage.addChild(this.fore); this.stage.addChild(this.spTransBack); this.stage.addChild(this.spTransFore); this.appPixi.ticker.add(this.fncTicker); // TWEEN 更新 /* console.group('new DispMng info'); console.info(this.appPixi.renderer); console.info('utils.isMobile():'+ utils.isMobile.any); // https://github.com/kaimallea/isMobile console.info('utils.isWebGLSupported():'+ utils.isWebGLSupported()); console.info('w:%O: h:%O:', CmnLib.stageW, CmnLib.stageH); console.groupEnd(); */ const fncTxt_b_alpha = (_name: string, val: any)=> { this.foreachRedrawTxtLayBack(Number(val)) }; fncTxt_b_alpha('', val.getVal('sys:TextLayer.Back.Alpha', 1)); val.defValTrg('sys:TextLayer.Back.Alpha', fncTxt_b_alpha); const fncBtnFont = (_name: string, val: any)=> { Button.fontFamily = val; }; fncBtnFont('', val.getVal('tmp:sn.button.fontFamily', Button.fontFamily)); val.defValTrg('tmp:sn.button.fontFamily', fncBtnFont); val.defTmp('const.sn.log.json', ()=> JSON.stringify( [...this.aPageLog, this.oLastPage] )); val.defTmp('const.sn.last_page_text', ()=> { const tl = this.getCurrentTxtlayFore(); return tl ?tl.pageText :''; }); } private fncTicker = ()=> Tween.update(); private grpCover : Graphics | null = null; cover(visible: boolean, bg_color: number = 0x0) { if (this.grpCover) { this.stage.removeChild(this.grpCover); this.grpCover.destroy(); this.grpCover = null; } if (visible) { this.grpCover = new Graphics; this.grpCover.beginFill(bg_color); this.grpCover.lineStyle(0, bg_color); this.grpCover.drawRect(0, 0, CmnLib.stageW, CmnLib.stageH); this.grpCover.endFill(); this.stage.addChild(this.grpCover); } } private evtMng : IEvtMng; setEvtMng(evtMng: IEvtMng) { this.evtMng = evtMng; this.frmMng.setEvtMng(evtMng); GrpLayer.setEvtMng(evtMng); } before_destroy() {for (const pg in this.hPages) this.hPages[pg].destroy();} destroy() { GrpLayer.destroy(); RubySpliter.destroy(); this.frmMng.destroy(); Tween.removeAll(); this.appPixi.ticker.remove(this.fncTicker); LayerMng.$msecChWait = 10; } // 既存の全文字レイヤの実際のバック不透明度、を再計算 private foreachRedrawTxtLayBack(g_alpha: number): void { const vct = this.getLayers(); const len = vct.length; for (let i=0; i<len; ++i) { const name = vct[i]; const pg = this.hPages[name]; if (! (pg.fore instanceof TxtLayer)) continue; const pTxt = pg.fore as TxtLayer; pTxt.chgBackAlpha(g_alpha); (pg.back as TxtLayer).chgBackAlpha(g_alpha); } } private cmdTxt = (cmd: string, tl = this.getCurrentTxtlayForeNeedErr(), _record = true)=> tl.tagCh('| 《'+ cmd +'》'); goTxt = ()=> {}; breakLine = ()=> {}; breakPage = ()=> {}; clearBreak() { if (! this.getCurrentTxtlayFore()) return; this.clearBreak = ()=> this.cmdTxt('del|break'); this.clearBreak(); } clickTxtLay(): boolean { // true is stay const tl = this.getCurrentTxtlayFore(); if (! tl) return true; // イベントを受ける文字レイヤが一つでも存在すれば、クリック待ち解除(false)する const vct = this.getLayers(); const len = vct.length; for (let i=0; i<len; ++i) { const name = vct[i]; const pg = this.hPages[name]; if (! (pg.fore instanceof TxtLayer)) continue; const pTxt = pg.fore as TxtLayer; if (! pTxt.click()) return false; } return true; } // // システム // スナップショット private snapshot(hArg: HArg) { const fn = (hArg.fn) ? ((hArg.fn.substr(0, 10) == 'userdata:/') ? hArg.fn : ('desktop:/'+ hArg.fn+ getDateStr('-', '_', '', '_') +'.png')) : ('desktop:/snapshot'+ getDateStr('-', '_', '', '_') +'.png'); const ext = CmnLib.getExt(fn); const b_color = hArg.b_color ?? this.cfg.oCfg.init.bg_color; const renderer = autoDetectRenderer({ width: CmnLib.argChk_Num(hArg, 'width', CmnLib.stageW), height: CmnLib.argChk_Num(hArg, 'height', CmnLib.stageH), transparent: (b_color > 0x1000000) && (ext == 'png'), antialias: CmnLib.argChk_Boolean(hArg, 'smoothing', false), preserveDrawingBuffer: true, backgroundColor: uint(b_color) & 0xFFFFFF, autoDensity: true, }); const a = []; if (this.twInfTrans.tw) { // [trans]中 a.push(new Promise(re=> { this.back.visible = true; for (const lay of this.aBackTransAfter) { renderer.render(lay, undefined, false); } this.back.visible = false; this.spTransBack.visible = true; this.fore.filters = this.spTransFore.filters; this.fore.visible = true; renderer.render(this.fore, undefined, false); this.fore.visible = false; this.fore.filters = []; re(); })); } else { const pg = (hArg.page != 'back') ?'fore' :'back'; for (const v of this.getLayers(hArg.layer)) a.push(new Promise( re=> this.hPages[v][pg].snapshot(renderer, re) )); } Promise.all(a).then(()=> { this.sys.savePic( this.cfg.searchPath(fn), this.appPixi.renderer.extract.base64(this.stage) ); if (! this.twInfTrans.tw) { const pg = (hArg.page != 'back') ?'fore' :'back'; for (const v of this.getLayers(hArg.layer)) this.hPages[v][pg].snapshot_end(); } renderer.destroy(true); }); return false; } // プラグインの読み込み private loadplugin(hArg: HArg) { const fn = hArg.fn; if (! fn) throw 'fnは必須です'; const join = CmnLib.argChk_Boolean(hArg, 'join', true); switch (CmnLib.getExt(fn)) { case 'css': // 読み込んで<style>に追加 (async ()=> { const res = await fetch(fn); if (! res.ok) throw new Error('Network response was not ok.'); TxtLayer.addStyle(await res.text()); if (join) this.main.resume(); })(); break; default: throw 'サポートされない拡張子です' } return join; } protected set_focus(hArg: HArg) { // フォーカス移動 const to = hArg.to; if (! to) throw '[set_focus] toは必須です'; return false; // TODO: 未作成:フォーカス移動 /* if (to == 'null') { // stage.focus = stage; return false; } const vct:Vector.<InteractiveObject> = new Vector.<InteractiveObject>; trans.foreachLayers(hArg, function (name:String, pg:Pages):void { const tf:TxtLayer = pg.getPage(hArg) as TxtLayer; if (! tf) return; if (! tf._visible) return; if (! tf.enabled) return; const vct_tl:Vector.<InteractiveObject> = tf.getButton(); const len_tl:uint = vct_tl.length; for (var j:uint=0; j<len_tl; ++j) vct.push(vct_tl[j]); }); const len = vct.length; if (len == 0) return false; if (stage.focus == stage) { stage.focus = vct[0]; return false; } if (to == 'next' || to == 'prev') { for (var i:uint=0; i<len; ++i) { if (stage.focus != vct[i]) continue; stage.focus = vct[(i +(to == 'next' ?1 :len-1))% len]; break; } return false; } var numTo:Number = parseInt(to); if (isNaN(numTo)) return false; numTo = uint(numTo); if (numTo < 0 || numTo >= len) return false; stage.focus = vct[numTo]; return false; */ } // // レイヤ共通 // レイヤを追加する private add_lay(hArg: HArg) { const layer = hArg.layer; if (! layer) throw 'layerは必須です'; if (layer.includes(',')) throw 'layer名に「,」は使えません'; if (layer in this.hPages) throw `layer【${layer}】はすでにあります`; const cls = hArg.class; if (! cls) throw 'clsは必須です'; this.hPages[layer] = new Pages(layer, cls, this.fore, hArg, this.back, hArg, this.sys, this.val); this.aLayName.push(layer); switch (cls) { case 'txt': if (! this.curTxtlay) { this.fncChkTxtLay = ()=> {}; this.getTxtLayer = this.$getTxtLayer; this.current = this.$current; this.hTag.current({layer: layer}); // hPages更新後でないと呼べない this.goTxt = ()=> { if (this.val.getVal('sn.skip.enabled')) { LayerMng.$msecChWait = 0; } else { this.setNormalWaitTxtLayer(); } for (const name of this.getLayers()) { const pg = this.hPages[name]; if (! (pg.fore instanceof TxtLayer)) continue; this.cmdTxt('gotxt|', pg.fore as TxtLayer, false); } } } this.val.setVal_Nochk( 'save', 'const.sn.layer.'+ (layer ?? this.curTxtlay) + '.enabled', true); break; } /* fncLetAs(hArg); fncReCover(); */ return false; } private hPages : {[name: string]: Pages} = {}; // しおりLoad時再読込 private aLayName : string[] = []; // 最適化用 private curTxtlay = ''; private lay(hArg: HArg): boolean { // Trans const layer = this.argChk_layer(hArg); const pg = this.hPages[layer]; const back = pg.back.cnt; const fore = pg.fore.cnt; if (CmnLib.argChk_Boolean(hArg, 'float', false)) { this.back.setChildIndex(back, this.back.children.length -1); this.fore.setChildIndex(fore, this.fore.children.length -1); this.rebuildLayerRankInfo(); } else if (hArg.index) { if (CmnLib.argChk_Num(hArg, 'index', 0)) { this.back.setChildIndex(back, uint(hArg.index)); this.fore.setChildIndex(fore, uint(hArg.index)); this.rebuildLayerRankInfo(); } } else if (hArg.dive) { const dive = hArg.dive; let idx_dive = 0; if (layer == dive) throw '[lay] 属性 layerとdiveが同じ【'+ dive +'】です'; const pg_dive = this.hPages[dive]; if (! pg_dive) throw '[lay] 属性 dive【'+ dive +'】が不正です。レイヤーがありません'; const back_dive = pg_dive.back; const fore_dive = pg_dive.fore; const idx_back_dive = this.back.getChildIndex(back_dive.cnt); const idx_fore_dive = this.fore.getChildIndex(fore_dive.cnt); idx_dive = (idx_back_dive < idx_fore_dive) ?idx_back_dive :idx_fore_dive; if (idx_dive > this.back.getChildIndex(back)) --idx_dive; //自分が無くなって下がる分下げる this.fore.setChildIndex(fore, idx_dive); this.back.setChildIndex(back, idx_dive); this.rebuildLayerRankInfo(); } return pg.lay(hArg); } private rebuildLayerRankInfo() {this.aLayName = this.sortLayers();} // レイヤ設定の消去 private clear_lay(hArg: HArg) { this.foreachLayers(hArg, name=> { //if (name == this.strTxtlay && hArg.page != 'back') this.recText('', true); // 改ページ const pg = this.hPages[this.argChk_layer({layer: name})]; if (hArg.page == 'both') { // page=both で両面削除 pg.fore.clearLay(hArg); pg.back.clearLay(hArg); } else { pg.getPage(hArg).clearLay(hArg); } }); return false; } private readonly srcRuleTransFragment = ` precision mediump float; varying vec2 vTextureCoord; uniform sampler2D uSampler; uniform sampler2D rule; uniform float vague; uniform float tick; void main(void) { vec4 fg = texture2D(uSampler, vTextureCoord); vec4 ru = texture2D(rule, vTextureCoord); float v = ru.r - tick; if (abs(v) < vague) { float grd = 0.5 -v /vague *0.5; float f_a = fg.a *(1.0 -grd); gl_FragColor.rgb = fg.rgb *f_a; gl_FragColor.a = f_a; } else { gl_FragColor = (v >= 0.0)? fg : vec4(0); } }`; private ufRuleTrans = { rule : {type: 'sampler2D', value: Texture.EMPTY}, vague : {type: '1f', value: 0.0}, tick : {type: '1f', value: 0.0}, }; private fltRule = new Filter(undefined, this.srcRuleTransFragment, this.ufRuleTrans); private rtTransBack = RenderTexture.create({ width: CmnLib.stageW, height: CmnLib.stageH, }); private spTransBack = new Sprite(this.rtTransBack); private rtTransFore = RenderTexture.create({ width: CmnLib.stageW, height: CmnLib.stageH, }); private spTransFore = new Sprite(this.rtTransFore); private aBackTransAfter : DisplayObject[] = []; // ページ裏表を交換 private trans(hArg: HArg) { this.finish_trans(); const ease = CmnTween.ease(hArg.ease); this.aBackTransAfter = []; const hTarget: {[ley_nm: string]: boolean} = {}; for (const v of this.getLayers(hArg.layer)) hTarget[v] = true; for (const lay_nm of this.getLayers()) this.aBackTransAfter.push( this.hPages[lay_nm][hTarget[lay_nm] ?'back' :'fore'].cnt ); this.rtTransBack.resize(CmnLib.stageW, CmnLib.stageH); this.appPixi.renderer.render(this.back, this.rtTransBack); // clear this.rtTransFore.resize(CmnLib.stageW, CmnLib.stageH); this.appPixi.renderer.render(this.fore, this.rtTransFore); // clear const fncRender = ()=> { this.back.visible = true; for (const lay of this.aBackTransAfter) { this.appPixi.renderer.render(lay, this.rtTransBack, false); } this.back.visible = false; this.spTransBack.visible = true; this.fore.visible = true; this.appPixi.renderer.render(this.fore, this.rtTransFore); this.fore.visible = false; this.spTransFore.visible = true; }; // visibleはfncRender()に任せる。でないとちらつく //this.back.visible = false; //this.fore.visible = false; //this.sprRtAtTransBack.visible = true; // trans中専用back(Render Texture) //this.sprRtAtTransFore.visible = true; // trans中専用fore(Render Texture) this.spTransFore.alpha = 1; const closeTrans = ()=> { if (this.appPixi.ticker) this.appPixi.ticker.remove(fncRender); // transなしでもadd()してなくても走るが、構わないっぽい。 this.elcTrans.clear(); [this.fore, this.back] = [this.back, this.fore]; for (const lay_name in this.hPages) { const pg = this.hPages[lay_name]; if (hTarget[lay_name]) {pg.transPage(); continue;} // transしないために交換する const idx = this.fore.getChildIndex(pg.back.cnt); this.fore.removeChild(pg.back.cnt); this.back.removeChild(pg.fore.cnt); this.fore.addChildAt(pg.fore.cnt, idx); this.back.addChildAt(pg.back.cnt, idx); } this.fore.visible = true; this.back.visible = false; this.spTransBack.visible = false; this.spTransFore.visible = false; if (this.twInfTrans.resume) this.main.resume(); this.twInfTrans = {tw: null, resume: false}; }; const time = CmnLib.argChk_Num(hArg, 'time', 0); if (time == 0 || this.evtMng.isSkipKeyDown()) {closeTrans(); return false;} // クロスフェード this.twInfTrans = {tw: null, resume: false}; const is_glsl = 'glsl' in hArg; if ((! is_glsl) && ! ('rule' in hArg)) { this.spTransFore.filters = []; this.twInfTrans.tw = new Tween.Tween(this.spTransFore) .to({alpha: 0}, time) .delay(CmnLib.argChk_Num(hArg, 'delay', 0)) .easing(ease) .onComplete(closeTrans) .start(); this.appPixi.ticker.add(fncRender); return false; } // ルール画像、またはGLSL const flt = is_glsl ? new Filter(undefined, hArg.glsl, this.ufRuleTrans) : this.fltRule; flt.uniforms.vague = CmnLib.argChk_Num(hArg, 'vague', 0.04); flt.uniforms.tick = 0; this.twInfTrans.tw = new Tween.Tween(flt.uniforms) .to({tick: 1}, time) .delay(CmnLib.argChk_Num(hArg, 'delay', 0)) .easing(ease) .onComplete(closeTrans); this.spTransFore.filters = [flt]; if (is_glsl) { this.twInfTrans.tw!.start(); this.appPixi.ticker.add(fncRender); return false; } if (! hArg.rule) throw 'ruleが指定されていません'; GrpLayer.ldPic(hArg.rule, tx=> { flt.uniforms.rule = tx; if (this.twInfTrans.tw) this.twInfTrans.tw.start(); this.appPixi.ticker.add(fncRender); }); return false; } private twInfTrans : ITwInf = {tw: null, resume: false}; private getLayers(layer = ''): string[] { return (layer)? layer.split(',') : this.aLayName; } private foreachLayers(hArg: HArg, fnc: (name: string, $pg: Pages)=> void): ReadonlyArray<string> { const vct = this.getLayers(hArg['layer']); for (const name of vct) { if (! name) continue; const pg = this.hPages[name]; if (pg == null) throw '存在しないlayer【'+ name +'】です'; fnc(name, pg); } return vct; } private sortLayers(layers = ''): string[] { const a = this.getLayers(layers); a.sort((a, b)=> { const ai = this.fore.getChildIndex(this.hPages[a].fore.cnt); const bi = this.fore.getChildIndex(this.hPages[b].fore.cnt); if (ai < bi) return -1; if (ai > bi) return 1; return 0; }); return a; } private wt(hArg: HArg) { if (! this.twInfTrans.tw) return false; this.twInfTrans.resume = true; this.evtMng.waitCustomEvent(hArg, this.elcTrans, ()=> this.finish_trans()); return true; } private elcTrans = new EventListenerCtn; // レイヤのトランジションの停止 private finish_trans() { if (this.twInfTrans.tw) this.twInfTrans.tw.stop().end(); // stop()とend()は別 return false; } // 画面を揺らす private quake(hArg: HArg) { this.finish_trans(); if (this.val.getVal('tmp:sn.skip.enabled')) return false; if (this.evtMng.isSkipKeyDown()) return false; const aDo: DisplayObject[] = []; for (const lay_nm of this.getLayers(hArg.layer)) { aDo.push(this.hPages[lay_nm].fore.cnt); } this.rtTransFore.resize(CmnLib.stageW, CmnLib.stageH); // ========= スマホ回転対応が要るかも? const fncRender = ()=> { this.fore.visible = true; for (const lay of aDo) { this.appPixi.renderer.render(lay, this.rtTransFore, false); } this.fore.visible = false; }; this.spTransFore.visible = true; this.spTransFore.alpha = 1; const closeTrans = ()=> { if (this.appPixi.ticker) this.appPixi.ticker.remove(fncRender); // transなしでもadd()してなくても走るが、構わないっぽい。 this.fore.visible = true; this.spTransFore.visible = false; this.spTransFore.x = 0; // 必須、onUpdateのせいかtoの値にしてくれない this.spTransFore.y = 0; if (this.twInfTrans.resume) this.main.resume(); this.twInfTrans = {tw: null, resume: false}; }; const ease = CmnTween.ease(hArg.ease); const h = uint(CmnLib.argChk_Num(hArg, 'hmax', 10)); const v = uint(CmnLib.argChk_Num(hArg, 'vmax', 10)); const fncH = (h == 0) ? ()=> {} : ()=> this.spTransFore.x = Math.round(Math.random()* h*2) -h; const fncV = (v == 0) ? ()=> {} : ()=> this.spTransFore.y = Math.round(Math.random()* v*2) -v; this.spTransFore.filters = []; const repeat = CmnLib.argChk_Num(hArg, 'repeat', 1); this.twInfTrans = {tw: null, resume: false}; this.twInfTrans.tw = new Tween.Tween(this.spTransFore) .to({x: 0, y: 0}, CmnLib.argChk_Num(hArg, 'time', NaN)) .delay(CmnLib.argChk_Num(hArg, 'delay', 0)) .easing(ease) .onUpdate(()=> {fncH(); fncV();}) .repeat(repeat == 0 ?Infinity :(repeat -1)) // 一度リピート→計二回なので .yoyo(CmnLib.argChk_Boolean(hArg, 'yoyo', false)) .onComplete(closeTrans) .start(); this.appPixi.ticker.add(fncRender); return false; } // トゥイーン開始 private readonly hMemberCnt = { alpha :0, height :0, rotation :0, scale_x :0, scale_y :0, width :0, x :0, y :0, }; // rotationX〜Z、scaleZ、zは設定すると // 三次元方向の拡大縮小ルーチンが働き画像がぼやけるので // backlayで設定しない方針 private hTwInf : {[name: string]: ITwInf} = {}; private tsy(hArg: HArg) { if (! hArg.layer) throw 'layerは必須です'; const layer = this.argChk_layer(hArg); const foreLay: any = this.hPages[layer].fore; const ease = CmnTween.ease(hArg.ease); const hTo: any = {}; for (const nm in this.hMemberCnt) { if (! (nm in hArg)) continue; // {x:500} X位置を500に // {x:'=500'} 現在のX位置に+500加算した位置 // {x:'=-500'} 現在のX位置に-500加算した位置 // {x:'250,500'} +250から+500までの間でランダムな値をX位置に // {x:'=250,500'} +250から+500までの間でランダムな値を現在のX位置に加算 const v = String((hArg as any)[nm]); const a = ((v.charAt(0) == '=') ?v.slice(1) :v).split(','); const a0 = hTo[nm] = parseFloat(a[0]); if (a.length > 1) hTo[nm] += Math.round(Math.random() * (parseFloat(a[1]) -a0 +1)); if (v.charAt(0) == '=') hTo[nm] += parseFloat(foreLay[nm]); // 相対に } const repeat = CmnLib.argChk_Num(hArg, 'repeat', 1); const tw_nm = hArg.name ?? hArg.layer; const tw = new Tween.Tween(foreLay) .to(hTo, CmnLib.argChk_Num(hArg, 'time', NaN) * (Boolean(this.val.getVal('tmp:sn.skip.enabled')) ?0 :1)) .delay(CmnLib.argChk_Num(hArg, 'delay', 0)) .easing(ease) .repeat(repeat == 0 ?Infinity :(repeat -1)) // 一度リピート→計二回なので .yoyo(CmnLib.argChk_Boolean(hArg, 'yoyo', false)) .onComplete(()=> { const twInf = this.hTwInf[tw_nm]; if (! twInf) return; delete this.hTwInf[tw_nm]; this.evtMng.popLocalEvts(); // [wait_tsy]したのにキャンセルされなかった場合向け if (twInf.resume) this.main.resume(); if (twInf.onComplete) twInf.onComplete(); }); if ('chain' in hArg) { const twFrom = this.hTwInf[hArg.chain ?? '']; if (! twFrom || ! twFrom.tw) throw `${hArg.chain}は存在しない・または終了したトゥイーンです`; twFrom.onComplete = ()=> {}; twFrom.tw.chain(tw); } else tw.start(); const arrive = CmnLib.argChk_Boolean(hArg, 'arrive', false); const backlay = CmnLib.argChk_Boolean(hArg, 'backlay', false); this.hTwInf[tw_nm] = {tw: tw, resume: false, onComplete: ()=> { if (arrive) Object.assign(foreLay, hTo); if (backlay) { const backCnt: any = this.hPages[layer].back.cnt; for (const nm in this.hMemberCnt) backCnt[nm] = foreLay[nm]; } }} return false; } // トゥイーン終了待ち private wait_tsy(hArg: HArg) { const tw_nm = ('id' in hArg) ?`frm\n${hArg.id}` :(hArg.name ?? hArg.layer); if (! tw_nm) throw 'トゥイーンが指定されていません'; const twInf = this.hTwInf[tw_nm]; if (! twInf || ! twInf.tw) return false; twInf.resume = true; this.evtMng.stdWait( ()=> {if (twInf.tw) twInf.tw.stop().end()}, // stop()とend()は別 CmnLib.argChk_Boolean(hArg, 'canskip', true) ); return true; } // トゥイーン中断 private stop_tsy(hArg: HArg) { const tw_nm = ('id' in hArg) ?`frm\n${hArg.id}` :(hArg.name ?? hArg.layer); if (! tw_nm) throw 'トゥイーンが指定されていません'; const twInf = this.hTwInf[tw_nm]; if (! twInf || ! twInf.tw) return false; twInf.tw.stop().end(); // stop()とend()は別 return false; } // 一時停止 private pause_tsy(hArg: HArg) { const tw_nm = ('id' in hArg) ?`frm\n${hArg.id}` :(hArg.name ?? hArg.layer); if (! tw_nm) throw 'トゥイーンが指定されていません'; const twInf = this.hTwInf[tw_nm]; if (! twInf || ! twInf.tw) return false; twInf.tw.stop(); return false; } // 一時停止再開 private resume_tsy(hArg: HArg) { const tw_nm = ('id' in hArg) ?`frm\n${hArg.id}` :(hArg.name ?? hArg.layer); if (! tw_nm) throw 'トゥイーンが指定されていません'; const twInf = this.hTwInf[tw_nm]; if (! twInf || ! twInf.tw) return false; twInf.tw.start(); return false; } // // 文字・文字レイヤ private static $msecChWait = 10; static get msecChWait() {return LayerMng.$msecChWait;} static set msecChWait(v) {LayerMng.$msecChWait = v;} // 文字を追加する private ch(hArg: HArg) { if (! hArg.text) throw 'textは必須です'; let wait = CmnLib.argChk_Num(hArg, 'wait', -1); if (wait > 0 && this.val.getVal('tmp:sn.skip.enabled')) wait = 0; hArg.wait = wait; const tl = this.getTxtLayer(hArg) as TxtLayer; if (wait >= 0) this.cmdTxt('add|'+ JSON.stringify(hArg), tl); const record = CmnLib.argChk_Boolean(hArg, 'record', true); const doRecLog = this.val.doRecLog(); if (! record) this.val.setVal_Nochk('save', 'sn.doRecLog', record); tl.tagCh(hArg.text.replace(/\[r]/g, '\n')); if (! record) this.val.setVal_Nochk('save', 'sn.doRecLog', doRecLog); if (wait >= 0) this.cmdTxt(`add_close|`, tl); return false; } private getTxtLayer = (_hArg: HArg): TxtLayer=> {this.fncChkTxtLay(); throw 0}; private $getTxtLayer(hArg: HArg): TxtLayer { const layer = this.argChk_layer(hArg, this.curTxtlay); const pg = this.hPages[layer]; const lay = pg.getPage(hArg); if (! (lay instanceof TxtLayer)) throw layer +'はTxtLayerではありません'; const tf = lay as TxtLayer; return tf; } setNormalWaitTxtLayer(): void {LayerMng.$msecChWait = this.scrItr.normalWait} // 操作対象のメッセージレイヤの指定 private current = (_hArg: HArg): boolean=> {this.fncChkTxtLay(); throw 0}; private $current(hArg: HArg) { const layer = hArg.layer; if (! layer) throw '[current] layerは必須です'; this.pgTxtlay = this.hPages[layer]; if (! (this.pgTxtlay.getPage(hArg) instanceof TxtLayer)) throw `${layer}はTxtLayerではありません`; this.recText('', true); // カレント変更前に現在の履歴を保存 this.curTxtlay = layer; this.val.setVal_Nochk('save', 'const.sn.mesLayer', layer); const vct = this.getLayers(); const len = vct.length; for (let i=0; i<len; ++i) { const name = vct[i]; const pg = this.hPages[name]; if (! (pg.fore instanceof TxtLayer)) continue; (pg.fore as TxtLayer).isCur = (pg.back as TxtLayer).isCur = (name == layer); } return false; } getCurrentTxtlayForeNeedErr(): TxtLayer { this.fncChkTxtLay(); return this.getCurrentTxtlayFore()!; } getCurrentTxtlayFore(): TxtLayer | undefined { if (! this.pgTxtlay) return undefined; return this.pgTxtlay.fore as TxtLayer; } private pgTxtlay: Pages; // カレントテキストレイヤ private fncChkTxtLay : ()=> void = ()=> {throw '文字レイヤーがありません。文字表示や操作する前に、[add_lay layer=(レイヤ名) class=txt]で文字レイヤを追加して下さい'}; private argChk_layer(hash: any, def = ''): string { const v = hash.layer ?? def; if (v.includes(',')) throw 'layer名に「,」は使えません'; if (! (v in this.hPages)) throw '属性 layer【'+ v +'】が不正です。レイヤーがありません'; return hash.layer = v; } private oLastPage : HArg = {text: ''}; private aPageLog : {[name: string]: any}[] = []; recText(txt: string, pagebreak = false) { if (pagebreak) { if (this.oLastPage.text) { this.aPageLog.push(this.oLastPage); this.aPageLog = this.aPageLog.slice(-this.cfg.oCfg.log.max_len); } this.oLastPage = {text: ''}; return; } this.oLastPage.text = txt.replace(/\\`/, '`'); // 本文→HTML化の過程でつけられてしまうエスケープ文字を削除 this.val.setVal_Nochk('save', 'const.sn.sLog', String(this.val.getVal('const.sn.log.json')) ); } private clear_text(hArg: HArg) { const tf = this.getTxtLayer(hArg); if (hArg.layer == this.curTxtlay && hArg.page == 'fore') this.recText('', true); // 改ページ、クリア前に tf.clearText(); return false; } // ハイパーリンクの終了 private endlink() {this.cmdTxt('endlink|'); return false;} // ページ両面の文字消去 private er(hArg: HArg) { if (CmnLib.argChk_Boolean(hArg, 'rec_page_break', true)) this.recText('', true); // 改ページ、クリア前に if (this.pgTxtlay) { this.pgTxtlay.fore.clearLay(hArg); this.pgTxtlay.back.clearLay(hArg); } return false; } // インライン画像表示 private graph(hArg: HArg) { if (! ('pic' in hArg)) throw '[graph] picは必須です'; hArg.text = '| 《grp|'+ JSON.stringify(hArg) +'》'; return this.ch(hArg); } // ハイパーリンク private link(hArg: HArg) { if (! hArg.style) hArg.style = 'background-color: rgba(255,0,0,0.5);'; this.cmdTxt('link|'+ JSON.stringify(hArg)); return false; } // 改行 private r(hArg: HArg) {hArg.text = '\n'; return this.ch(hArg);} // 履歴改行 private rec_r() {this.recText('\n'); return false;}; // 履歴書き込み private rec_ch(hArg: HArg) { this.oLastPage = hArg; this.recText(hArg.text ?? ''); return false; }; // 履歴リセット private reset_rec(hArg: HArg) { this.val.setVal_Nochk('save', 'const.sn.sLog', hArg.text ?? ''); this.aPageLog = []; this.oLastPage = {text: hArg.text ?? ''}; return false; } // 文字列と複数ルビの追加 private ruby2(hArg: HArg) { const t = hArg.t; if (! t) throw '[ruby2] tは必須です'; const r = hArg.r; if (! r) throw '[ruby2] rは必須です'; hArg.text = '|'+ t +'《'+ r +'》'; return this.ch(hArg); } // インラインスタイル設定 private span(hArg: HArg) { this.cmdTxt('span|'+ JSON.stringify(hArg)); return false; } // tcy縦中横を表示する private tcy(hArg: HArg) { if (! hArg.t) throw '[tcy] tは必須です'; hArg.text = '| |《tcy|'+ hArg.t +'|'+ (hArg.r ?? '') +'》'; return this.ch(hArg); } // レイヤのダンプ private dump_lay(hArg: HArg) { console.group('🥟 [dump_lay]'); for (const name of this.getLayers(hArg.layer)) { const pg = this.hPages[name]; try { console.info(`%c${pg.fore.name.slice(0, -7)} %o`, `color:#${CmnLib.isDarkMode ?'49F' :'05A'};`, JSON.parse(`{"back":{${pg.back.dump()}}, "fore":{${pg.fore.dump()}}}`)); } catch (error) { console.error(`dump_lay err:%o`, error); console.error(` back:${pg.back.dump()}`); console.error(` fore:${pg.fore.dump()}`); } } console.groupEnd(); return false; } // イベント有無の切替 private enable_event(hArg: HArg) { const layer = this.argChk_layer(hArg, this.curTxtlay); const enb = this.getTxtLayer(hArg).enabled = CmnLib.argChk_Boolean(hArg, 'enabled', true); this.val.setVal_Nochk('save', 'const.sn.layer.'+ layer +'.enabled', enb); return false; } // ボタンを表示 private button(hArg: HArg) { Pages.argChk_page(hArg, 'back'); // チェックしたいというよりデフォルトをbackに hArg.clicksebuf = hArg.clicksebuf ?? 'SYS'; hArg.entersebuf = hArg.entersebuf ?? 'SYS'; hArg.leavesebuf = hArg.leavesebuf ?? 'SYS'; return this.getTxtLayer(hArg).addButton(hArg); } record(): any { const o: any = {}; this.aLayName.forEach(layer=> { const pg = this.hPages[layer]; o[layer] = { cls: pg.cls, fore: pg.fore.record(), back: pg.back.record(), } }); return o; } playback($hPages: HPage, fncComp: ()=> void): void { const aPromise: any[] = []; const aSort: {layer: string, idx: number}[] = []; for (const layer in $hPages) { // 引数で言及の無いレイヤはそのまま。特に削除しない const $pg = $hPages[layer]; aSort.push({layer: layer, idx: $pg.fore.idx}); const pg = this.hPages[layer] || new Pages(layer, $pg.cls, this.fore, {}, this.back, {}, this.sys, this.val); this.hPages[layer] = pg; aPromise.push(new Promise(re=> pg.fore.playback($pg.fore, re))); aPromise.push(new Promise(re=> pg.back.playback($pg.back, re))); } const len = this.fore.children.length; Promise.all(aPromise).then(()=> { aSort.sort(function(a, b) { // ソートし若い順にsetChildIndex() if (a.idx < b.idx) return -1; if (a.idx > b.idx) return 1; return 0; }); aSort.forEach(o=> { const pg = this.hPages[o.layer]; if (! pg) return; const idx = len > o.idx ?o.idx :len -1; this.fore.setChildIndex(pg.fore.cnt, idx); this.back.setChildIndex(pg.back.cnt, idx); }); fncComp(); }) .catch(e=> console.error(`fn:LayerMng.ts playback e:%o`, e)); this.aPageLog = JSON.parse(String(this.val.getVal('save:const.sn.sLog'))); this.oLastPage = {text: ''}; } }