famibee/SKYNovel

View on GitHub
src/sn/LayerMng.ts

Summary

Maintainability
A
0 mins
Test Coverage
/* ***** BEGIN LICENSE BLOCK *****
    Copyright (c) 2018-2024 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, argChk_Boolean, argChk_Num, getExt, addStyle, argChk_Color, parseColor} from './CmnLib';
import {CmnTween} from './CmnTween';
import {IHTag, HArg} from './Grammar';
import {IVariable, IMain, HIPage, IGetFrm, IPropParser, IRecorder} from './CmnInterface';
import {Pages} from './Pages';
import {Layer} from './Layer';
import {GrpLayer} from './GrpLayer';
import {SpritesMng} from './SpritesMng';
import {TxtLayer} from './TxtLayer';
import {RubySpliter} from './RubySpliter';
import {TxtStage} from './TxtStage';
import {Config} from './Config';
import {ScriptIterator} from './ScriptIterator';
import {SysBase} from './SysBase';
import {FrameMng} from './FrameMng';
import {Button} from './Button';
import {SoundMng} from './SoundMng';
import {AnalyzeTagArg} from './AnalyzeTagArg';
import {DesignCast} from './DesignCast';
import {EventListenerCtn} from './EventListenerCtn';
import {disableEvent, enableEvent} from './ReadState';

import {Container, Application, Graphics, Texture, Filter, RenderTexture, Sprite, DisplayObject, autoDetectRenderer} from 'pixi.js';

export interface IMakeDesignCast { (idc    : DesignCast): void; };

export interface HPage {[ln: string]: Pages};

export class LayerMng implements IGetFrm, IRecorder {
    readonly    #stage    : Container;
                #fore    = new Container;
                #back    = new Container;

    readonly    #frmMng        : FrameMng;
    readonly    #bg_color    : number;

    readonly    #elc        = new EventListenerCtn;

    //MARK: コンストラクタ
    constructor(readonly cfg: Config, readonly hTag: IHTag, readonly appPixi: Application, readonly val: IVariable, readonly main: IMain, readonly scrItr: ScriptIterator, readonly sys: SysBase, readonly sndMng: SoundMng, readonly alzTagArg: AnalyzeTagArg, readonly prpPrs: IPropParser) {
        // レスポンシブや回転・全画面切り替え・DevTools 表示切り替えの対応
        const fncResizeLay = ()=> {
            sys.cvsResize();
            this.cvsResizeDesign();
            if (this.#modeLnSub) for (const ln of this.#aLayName) {
                this.#hPages[ln].fore.cvsResizeChildren();
            }
            else for (const ln of this.#aLayName) {
                this.#hPages[ln].fore.cvsResize();
            }

            this.#frmMng.cvsResize();
            this.#evtMng.cvsResize();
        };
        if (CmnLib.isMobile) {
            this.#elc.add(globalThis, 'orientationchange', fncResizeLay, {passive: true});
        }
        else {
            let tid: NodeJS.Timeout | undefined = undefined;
            this.#elc.add(globalThis, 'resize', ()=> {
                if (tid) return;
                tid = setTimeout(()=> {tid = undefined; fncResizeLay()}, 1000 /60 *10);    // clearTimeout()不要と判断
            }, {passive: true});
        }
        sys.cvsResize();

        TxtLayer.init(cfg, hTag, val, this, me=> this.#hPages[me.layname].fore === me, appPixi);
        GrpLayer.init(main, cfg, appPixi, sys, sndMng, val);
        FrameMng.init(cfg, sys, main);
        Button.init(cfg);

        this.#frmMng = new FrameMng(hTag, appPixi, val);
        sys.hFactoryCls.grp = ()=> new GrpLayer;
        sys.hFactoryCls.txt = ()=> new TxtLayer;

        //    システム
        hTag.loadplugin        = o=> this.#loadplugin(o);    // プラグインの読み込み
        //hTag.set_focus    // EventMng.tsで定義         // フォーカス移動
        hTag.snapshot        = o=> this.#snapshot(o);    // スナップショット

        //    レイヤ共通
        hTag.add_lay        = o=> this.#add_lay(o);        // レイヤを追加する
        hTag.clear_lay        = o=> this.#clear_lay(o);    // レイヤ設定の消去
        hTag.finish_trans    = ()=> CmnTween.finish_trans();// トランス強制終了
        hTag.lay            = o=> this.#lay(o);            // レイヤ設定
        hTag.trans            = o=> this.#trans(o);        // ページ裏表を交換
        hTag.wt                = o=> CmnTween.wt(o);        // トランス終了待ち

        hTag.quake            = o=> this.#quake(o);        // 画面を揺らす
        hTag.stop_quake        = hTag.finish_trans;        // 画面揺らし中断
        hTag.wq                = o=> hTag.wt(o);            // 画面揺らし終了待ち

        hTag.pause_tsy        = o=> CmnTween.pause_tsy(o);    // 一時停止
        hTag.resume_tsy        = o=> CmnTween.resume_tsy(o);    // 一時停止再開
        hTag.stop_tsy        = o=> CmnTween.stop_tsy(o);    // トゥイーン中断
        hTag.tsy            = o=> this.#tsy(o);            // トゥイーン開始
        hTag.wait_tsy        = o=> CmnTween.wait_tsy(o);    // トゥイーン終了待ち

        hTag.add_filter        = o=> this.#add_filter(o);    // フィルター追加
        hTag.clear_filter    = o=> this.#clear_filter(o);// フィルター全削除
        hTag.enable_filter    = o=> this.#enable_filter(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        = o=> this.#endlink(o);        // ハイパーリンクの終了
        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_ch            = o=> this.#rec_ch(o);        // 履歴書き込み
        hTag.rec_r            = o=> this.#rec_r(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=> SpritesMng.add_face(o);    // 差分名称の定義

        //    ムービーレイヤ
        hTag.wv                = o=> SpritesMng.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 = (hArg: HArg)=> {
            delete hArg.visible;
            hArg.id = 'break';
            hArg.pic= 'breakline';
            const sArg = encodeURIComponent(JSON.stringify(hArg));
            this.#cmdTxt('grp|'+ sArg);
        };
        if (cfg.existsBreakpage) this.breakPage = (hArg: HArg)=> {
            delete hArg.visible;
            hArg.id = 'break';
            hArg.pic= 'breakpage';
            const sArg = encodeURIComponent(JSON.stringify(hArg));
            this.#cmdTxt('grp|'+ sArg);
        };

        this.#bg_color = parseColor(String(cfg.oCfg.init.bg_color));
        const grp = new Graphics;
        grp
        .beginFill(this.#bg_color, 1)    // イベントを受け取るためにも塗る
        .lineStyle(0, this.#bg_color)
        .drawRect(0, 0, CmnLib.stageW, CmnLib.stageH)
        .endFill();
        this.#fore.addChild(grp.clone());
        this.#back.addChild(grp);
        this.#back.visible = false;
        this.#fore.name = 'page:A';    // 4tst
        this.#back.name = 'page:B';    // 4tst

        this.#stage = appPixi.stage;
        this.#stage.addChild(this.#back);
        this.#stage.addChild(this.#fore);
        this.#stage.addChild(this.#spTransBack);
        this.#stage.addChild(this.#spTransFore);
        this.#stage.name = 'stage';    // 4tst
/*
        console.group('new DispMng info');
        console.info(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.#oLastPage.text = this.#oLastPage.text?.replaceAll(`<\/span><span class='sn_ch'>`, '') ?? '')
                ? [...this.#aTxtLog, this.#oLastPage]
                : this.#aTxtLog
        ));
        val.defTmp('const.sn.last_page_text', ()=> this.currentTxtlayFore?.pageText ?? '');
        val.defTmp('const.sn.last_page_plain_text', ()=> this.currentTxtlayFore?.pagePlainText ?? '');
        if (CmnLib.isDbg) {
            DesignCast.init(appPixi, sys, scrItr, prpPrs, alzTagArg, cfg, this.#hPages);
            this.cvsResizeDesign = ()=> DesignCast.cvsResizeDesign();
            sys.addHook((type, o)=> {
                if (! this.#hProcDbgRes[type]?.(type, o)) return;
                delete this.#hProcDbgRes[type];
            });
        }
    }
    private    cvsResizeDesign() {}


    readonly    #hProcDbgRes
    : {[type: string]: (type: string, o: any)=> boolean}    = {
        attach        : _=> {DesignCast.leaveMode();    return false},
        continue    : _=> {DesignCast.leaveMode();    return false},
        disconnect    : _=> {DesignCast.leaveMode();    return false},
        _enterDesign: _=> {
            DesignCast.enterMode();
            for (const ln of this.#aLayName) {
                const lay = this.#hPages[ln].fore;
                lay.makeDesignCastChildren(gdc=> gdc.make());
                lay.makeDesignCast(gdc=> gdc.make());
            }

            this.#selectNode(this.#curTxtlay);    return false;
        //    this.selectNode('mes/ボタン');    return false;    // Test用
        //    this.selectNode(this.firstGrplay);    return false;
                // 制作中は普通画像レイヤをいじるのが主なので、これがいい
        },
        _replaceToken: (_, o)=> {DesignCast.replaceToken(o); return false},
        _selectNode    : (_, o)=> {this.#selectNode(o.node); return false},
    }
    #modeLn        = '';
    #modeLnSub    = '';
    #selectNode(node: string) {
        [this.#modeLn, this.#modeLnSub = ''] = node.split('/');
        const pages = this.#hPages[this.#modeLn];
        if (! pages) return;

        DesignCast.allHide();
        if (this.#modeLnSub) pages.fore.showDesignCastChildren();
        else pages.fore.showDesignCast();
    }

    getFrmDisabled = (id: string)=> this.#frmMng.getFrmDisabled(id);

    #grpCover : Graphics | undefined = undefined;
    cover(visible: boolean, bg_color = 0x0) {
        if (this.#grpCover) {
            this.#stage.removeChild(this.#grpCover);
            this.#grpCover.destroy();
            this.#grpCover = undefined;
        }
        if (visible) this.#stage.addChild(
            (this.#grpCover = new Graphics)
            .beginFill(bg_color)
            .lineStyle(0, bg_color)
            .drawRect(0, 0, CmnLib.stageW, CmnLib.stageH)
            .endFill()
        );
    }

    #evtMng    : IEvtMng;
    setEvtMng(evtMng: IEvtMng) {
        this.#evtMng = evtMng;
        this.#frmMng.setEvtMng(evtMng);
        SpritesMng.setEvtMng(evtMng);
        CmnTween.init(evtMng, this.appPixi);
    }

    before_destroy() {for (const p of Object.values(this.#hPages)) p.destroy()}
    destroy() {
        this.#elc.clear();
        GrpLayer.destroy();
        RubySpliter.destroy();
        TxtStage.destroy();
        TxtLayer.destroy();

        this.#frmMng.destroy();

        CmnTween.destroy();
        LayerMng.#msecChWait = 10;
    }


    // 既存の全文字レイヤの実際のバック不透明度、を再計算
    #foreachRedrawTxtLayBack(g_alpha: number) {
        for (const name of this.#getLayers()) {
            const pg = this.#hPages[name];
            if (! (pg.fore instanceof TxtLayer)) continue;
            pg.fore.chgBackAlpha(g_alpha);
            (pg.back as TxtLayer).chgBackAlpha(g_alpha);
        }
    }


    #cmdTxt = (cmd: string, tl = this.currentTxtlayForeNeedErr, _record = true)=> tl.tagCh('| 《'+ cmd +'》');
    goTxt = ()=> {};
    breakLine = (_hArg: HArg)=> {};
    breakPage = (_hArg: HArg)=> {};
    clearBreak() {
        if (! this.currentTxtlayFore) return;

        this.clearBreak = ()=> this.#cmdTxt('del|break');
        this.clearBreak();
    }

    clickTxtLay(): boolean {    // true: 文字出現中だったので、停止する
        if (! this.currentTxtlayFore) return false;

        return this.#getLayers().some(ln=> {
            const f = this.#hPages[ln].fore;
            return f instanceof TxtLayer && f.click();
        });
    }


//    //    システム
    //MARK: スナップショット
    #snapshot(hArg: HArg) {
        const fn0 = hArg.fn
        ? hArg.fn.slice(0, 10) === 'userdata:/'
            ? hArg.fn
            : `downloads:/${hArg.fn + getDateStr('-', '_', '', '_')}.png`
        : `downloads:/snapshot${getDateStr('-', '_', '', '_')}.png`;
        const url = this.cfg.searchPath(fn0);
        const width = argChk_Num(hArg, 'width', CmnLib.stageW);
        const height= argChk_Num(hArg, 'height', CmnLib.stageH);
        if (this.sys.isApp) {
            return this.#snapshot4app(hArg, url, width, height);
        }
        return this.#snapshot4web(hArg, url, width, height);
    }
    #snapshot4app(hArg: HArg, url: string, width: number, height: number): boolean {
        this.#frmMng.hideAllFrame();
        disableEvent();
        if (! ('layer' in hArg)) {
            this.sys.capturePage(url, width, height, ()=> {
                this.#frmMng.restoreAllFrame();
                enableEvent();
            });
            return true;
        }

        // 一時的に非表示にしてスナップショット
        const hBk: {[ln: string]: boolean} = {};
        for (const ln of this.#getLayers()) {
            const sp = this.#hPages[ln].fore.spLay;
            hBk[ln] = sp.visible;
            sp.visible = false;
        }
        for (const ln of this.#getLayers(hArg.layer)) this.#hPages[ln].fore.spLay.visible = true;

        this.sys.capturePage(url, width, height, ()=> {
            for (const [ln, v] of Object.entries(hBk)) {
                this.#hPages[ln].fore.spLay.visible = v;
            }
            this.#frmMng.restoreAllFrame();
            enableEvent();
        });
        return true;
    }
    #snapshot4web(hArg: HArg, url: string, width: number, height: number): boolean {
        disableEvent();
        const ext = getExt(url);
        const b_color = argChk_Color(hArg, 'b_color', this.#bg_color);
        const rnd = autoDetectRenderer({
            width,
            height,
            backgroundAlpha: (b_color > 0x1000000) && (ext === 'png') ?0 :1,
            antialias: argChk_Boolean(hArg, 'smoothing', false),
            preserveDrawingBuffer: true,
            backgroundColor: b_color & 0xFFFFFF,
            autoDensity: true,
        });
        const a = [];
        const pg = (hArg.page !== 'back') ?'fore' :'back';
        if (CmnTween.isTrans) a.push(new Promise<void>(re=> {    // [trans]中
            this.#back.visible = true;
            for (const lay of this.#aBackTransAfter) {
                rnd.render(lay, {clear: false});
            }
            this.#back.visible = false;
            this.#spTransBack.visible = true;

            const a = [...this.#fore.filters!];
            this.#fore.filters = [...a!, ...this.#spTransFore.filters!];
            this.#fore.visible = true;
            rnd.render(this.#fore, {clear: false});
            this.#fore.visible = false;
            this.#fore.filters = a;
            re();
        }));
        else for (const v of this.#getLayers(hArg.layer)) a.push(
            new Promise<void>(re=> this.#hPages[v][pg].snapshot(rnd, ()=>re()))
        );
        Promise.allSettled(a).then(async ()=> {
            const renTx = RenderTexture.create({width: rnd.width, height: rnd.height});    // はみ出し対策
            rnd.render(this.#stage, {renderTexture: renTx});
/*
            await this.sys.savePic(
                url,
                rnd.plugins.extract.base64(Sprite.from(renTx)),
            );
*/

            const imgUrl = rnd.view.toDataURL('image/jpeg')
            await this.sys.savePic(
                url,
                imgUrl
            );

/*
            await this.sys.savePic(
                fn,
                rnd.plugins.extract.base64(Sprite.from(renTx), 'image/jpeg'),
//                rnd.plugins.extract.base64(Sprite.from(renTx)),
            );
*/
            if (! CmnTween.isTrans) for (const v of this.#getLayers(hArg.layer)) this.#hPages[v][pg].snapshot_end();
            rnd.destroy(true);
            enableEvent();
        });

        return true;
    }

    //MARK: プラグインの読み込み
    #loadplugin(hArg: HArg) {
        const {fn} = hArg;
        if (! fn) throw 'fnは必須です';
        const join = argChk_Boolean(hArg, 'join', true);

        if (join) disableEvent();
        switch (getExt(fn)) {
            case 'css':        // 読み込んで<style>に追加
                (async ()=> {
                    const res = await fetch(fn);
                    if (! res.ok) throw new Error('Network response was not ok.');

                    addStyle(await res.text());
                    if (join) enableEvent();
                })();
                break;

            default:    throw 'サポートされない拡張子です'
        }

        return join;
    }


//    //    レイヤ共通
    //MARK: レイヤを追加する
    #add_lay(hArg: HArg) {
        const {layer, class: cls} = hArg;
        if (! layer) throw 'layerは必須です';
        if (layer.includes(',')) throw 'layer名に「,」は使えません';
        if (layer in this.#hPages) throw `layer【${layer}】はすでにあります`;
        if (! cls) throw 'clsは必須です';

        const ret = {isWait: false}
        this.#hPages[layer] = new Pages(layer, cls, this.#fore, this.#back, hArg, this.sys, this.val, ret);
        this.#aLayName.push(layer);
        switch (cls) {
        case 'txt':
            if (! this.#curTxtlay) {
                this.#chkTxtLay = ()=> {};
                this.#getTxtLayer = this.#$getTxtLayer;
                this.#current = this.#$current;
                this.hTag.current({layer});    // hPages更新後でないと呼べない
                this.goTxt = ()=> {
                    if (this.#evtMng.isSkipping) LayerMng.#msecChWait = 0;
                    else this.setNormalChWait();
                    for (const name of this.#getLayers()) {
                        const f = this.#hPages[name].fore;
                        if (f instanceof TxtLayer) this.#cmdTxt('gotxt|', f, false);
                    }
                }
            }

            this.val.setVal_Nochk(
                'save',
                'const.sn.layer.'+ (layer ?? this.#curTxtlay) +'.enabled',
                true,
            );
            break;

        case 'grp':
            if (this.#firstGrplay) break;
            this.#firstGrplay = layer;
            break;
        }

        this.scrItr.recodeDesign(hArg);    // hArg[':id_tag'] は new Pages 内で設定

        return ret.isWait;
    }
    #hPages        : HPage        = {};    // しおりLoad時再読込
    #aLayName    : string[]    = [];    // 最適化用
    #curTxtlay        = '';
    #firstGrplay    = '';

    #lay(hArg: HArg): boolean {
        // Trans
        const ln = this.#argChk_layer(hArg);
        const pg = this.#hPages[ln];
        const back = pg.back.spLay;
        const fore = pg.fore.spLay;
        if (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 (argChk_Num(hArg, 'index', 0)) {
                this.#back.setChildIndex(back, hArg.index);
                this.#fore.setChildIndex(fore, hArg.index);
                this.#rebuildLayerRankInfo();
            }
        }
        else if (hArg.dive) {
            const {dive} = hArg;
            let idx_dive = 0;
            if (ln === 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.spLay);
            const idx_fore_dive = this.#fore.getChildIndex(fore_dive.spLay);
            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();
        }

        hArg[':id_tag'] = pg.fore.name.slice(0, -7);
        this.scrItr.recodeDesign(hArg);    // 必ず[':id_tag'] を設定すること

        return pg.lay(hArg);
    }
    #rebuildLayerRankInfo() {this.#aLayName = this.#sortLayers()}

    //MARK: レイヤ設定の消去
    #clear_lay(hArg: HArg) {
        this.#foreachLayers(hArg, layer=> {
            //if (name === this.strTxtlay && hArg.page !== 'back') this.recText('', true);
                // 改ページ
            const pg = this.#hPages[this.#argChk_layer({layer})];
            if (hArg.page === 'both') {    // page=both で両面削除
                pg.fore.clearLay(hArg);
                pg.back.clearLay(hArg);
                return;
            }
            pg.getPage(hArg).clearLay(hArg);
        });

        return false;
    }

    readonly    #srcRuleTransFragment = `
precision mediump float;

varying vec2 vTextureCoord;
uniform sampler2D uSampler;

uniform sampler2D rule;
uniform float vague;
uniform float tick;

uniform vec4 inputPixel;
uniform highp vec4 outputFrame;
vec2 getUV(vec2 coord) {
    return coord * inputPixel.xy / outputFrame.zw;
}

void main(void) {
    vec4 fg = texture2D(uSampler, vTextureCoord);
    vec4 ru = texture2D(rule, getUV(vTextureCoord));

    float v = ru.r - tick;
    if (abs(v) < vague) {
        float f_a = fg.a *(0.5 +v /vague *0.5);

        gl_FragColor.rgb = fg.rgb *f_a;
        gl_FragColor.a = f_a;
    }
    else {
        gl_FragColor = (v >= 0.0)? fg : vec4(0);
    }
}`;
    #ufRuleTrans = {
        rule : Texture.EMPTY,
        vague : 0.0,
        tick : 0.0,
    };
    #fltRule = new Filter(undefined, this.#srcRuleTransFragment, this.#ufRuleTrans);

    #rtTransBack = RenderTexture.create({
        width: CmnLib.stageW,
        height: CmnLib.stageH,
    });
    #spTransBack = new Sprite(this.#rtTransBack);

    #rtTransFore = RenderTexture.create({
        width    : CmnLib.stageW,
        height    : CmnLib.stageH,
    });
    #spTransFore = new Sprite(this.#rtTransFore);

    #aBackTransAfter    : DisplayObject[] = [];

    //MARK: ページ裏表を交換
    #trans(hArg: HArg) {
        CmnTween.finish_trans();
        this.#evtMng.hideHint();

        const {layer} = hArg;
        this.#aBackTransAfter = [];
        const hTarget: {[ln: string]: boolean} = {};
        const aFore: Layer[] = [];
        for (const ln of this.#getLayers(layer)) {
            hTarget[ln] = true;
            aFore.push(this.#hPages[ln].fore);
        }
        const aBack: Layer[] = [];
        for (const ln of this.#getLayers()) {
            const lay = this.#hPages[ln][hTarget[ln] ?'back' :'fore'];
            this.#aBackTransAfter.push(lay.spLay);
            aBack.push(lay);
        }
        this.#rtTransBack.resize(CmnLib.stageW, CmnLib.stageH);
        this.appPixi.renderer.render(this.#back, {renderTexture: this.#rtTransBack});    // clear: true

        let fncRenderBack = ()=> {
            this.#back.visible = true;
            for (const spLay of this.#aBackTransAfter) {
                this.appPixi.renderer.render(spLay, {renderTexture: this.#rtTransBack, clear: false});
            }
            this.#back.visible = false;
        };
        if (! aBack.some(lay=> lay.containMovement)) {
            const oldFnc = fncRenderBack;    // 動きがないなら最初に一度
            fncRenderBack = ()=> {fncRenderBack = ()=> {}; oldFnc()};
        }

        this.#rtTransFore.resize(CmnLib.stageW, CmnLib.stageH);
        this.appPixi.renderer.render(this.#fore, {renderTexture: this.#rtTransFore});    // clear: true
        let fncRenderFore = ()=> {
            this.#fore.visible = true;
            this.appPixi.renderer.render(this.#fore, {renderTexture: this.#rtTransFore});
            this.#fore.visible = false;
        };
        if (! aFore.some(lay=> lay.containMovement)) {
            const oldFnc = fncRenderFore;    // 動きがないなら最初に一度
            fncRenderFore = ()=> {fncRenderFore = ()=> {}; oldFnc()};
        }
        const fncRender = ()=> {
            fncRenderBack();
            this.#spTransBack.visible = true;

            fncRenderFore();
            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 comp = ()=> {
            this.appPixi.ticker?.remove(fncRender);
                // transなしでもadd()してなくても走るが、構わないっぽい。
            [this.#fore, this.#back] = [this.#back, this.#fore];
            const aPrm: Promise<void>[] = [];
            for (const [ln, pg] of Object.entries(this.#hPages)) {
                if (hTarget[ln]) {pg.transPage(aPrm); continue}

                // transしないために交換する
                const {fore: {spLay: f}, back: {spLay: b}} = pg;
                const idx = this.#fore.getChildIndex(b);
                this.#fore.removeChild(b);
                this.#back.removeChild(f);
                this.#fore.addChildAt(f, idx);
                this.#back.addChildAt(b, idx);
            }
            Promise.allSettled(aPrm);

            this.#fore.visible = true;
            this.#back.visible = false;
            this.#spTransBack.visible = false;
            this.#spTransFore.visible = false;
        };
        const time = argChk_Num(hArg, 'time', 0);
//        hArg[':id'] = pg.fore.name.slice(0, -7);
//        this.scrItr.getDesignInfo(hArg);    // 必ず[':id'] を設定すること
        if (time === 0 || this.#evtMng.isSkipping) {comp(); return false}

        // クロスフェード
        const {glsl, rule, chain} = hArg;
        if (! glsl && ! rule) {
            this.#spTransFore.filters = [];
            CmnTween.tween(CmnTween.TW_INT_TRANS, hArg, this.#spTransFore, {alpha: 0}, ()=> {}, comp, ()=> {});
            this.appPixi.ticker.add(fncRender);
            return false;
        }

        // ルール画像、またはGLSL
        const flt = glsl
            ? new Filter(undefined, glsl, this.#ufRuleTrans)
            : this.#fltRule;
        flt.uniforms.vague = argChk_Num(hArg, 'vague', 0.04);
        flt.uniforms.tick = 0;
        this.#spTransFore.filters = [flt];
        if (glsl) {
            CmnTween.tween(CmnTween.TW_INT_TRANS, hArg, flt.uniforms, {tick: 1}, ()=> {}, comp, ()=> {});
            this.appPixi.ticker.add(fncRender);
            return false;
        }

        if (! rule) throw 'ruleが指定されていません';
        const tw = CmnTween.tweenA(CmnTween.TW_INT_TRANS, hArg, flt.uniforms, {tick: 1}, ()=> {}, comp, ()=> {});
        this.#sps.destroy();
        this.#sps = new SpritesMng(rule, undefined, sp=> {
            flt.uniforms.rule = sp.texture;
            sp.destroy();

            CmnTween.tweenB(chain, tw);
            this.appPixi.ticker.add(fncRender);
        });
        return false;
    }
    #sps    = new SpritesMng;

    #getLayers(layer = ''): string[] {return (layer)? layer.split(',') : this.#aLayName}
    #foreachLayers(hArg: HArg, fnc: (ln: string, $pg: Pages)=> void): ReadonlyArray<string> {
        const vct = this.#getLayers(hArg.layer);
        for (const ln of vct) {
            if (! ln) continue;

            const pg = this.#hPages[ln];
            if (! pg) throw '存在しないlayer【'+ ln +'】です';

            fnc(ln, pg);
        }

        return vct;
    }
    #sortLayers(layers = ''): string[] {
        return this.#getLayers(layers)
        .sort((a, b)=> {
            const ai = this.#fore.getChildIndex(this.#hPages[a].fore.spLay);
            const bi = this.#fore.getChildIndex(this.#hPages[b].fore.spLay);
            if (ai < bi) return -1;
            if (ai > bi) return 1;
            return 0;
        });
    }


    //MARK: 画面を揺らす
    #quake(hArg: HArg) {
        CmnTween.finish_trans();
        const time = argChk_Num(hArg, 'time', NaN);
        if (time === 0) return false;    // skip時でもエラーは出したげたい
        if (this.#evtMng.isSkipping) return false;

        const {layer} = hArg;
        const aDo: DisplayObject[] = [];
        for (const ln of this.#getLayers(layer)) {
            aDo.push(this.#hPages[ln].fore.spLay);
        }
        this.#rtTransFore.resize(CmnLib.stageW, CmnLib.stageH);
            // NOTE: スマホ回転対応が要るかも?
        const fncRender = ()=> {
            this.#fore.visible = true;
            for (const lay of aDo) this.appPixi.renderer.render(
                lay, {renderTexture: this.#rtTransFore, clear: false}
            );
            this.#fore.visible = false;
        };
        this.#spTransFore.visible = true;
        this.#spTransFore.alpha = 1;

        const h = uint(argChk_Num(hArg, 'hmax', 10));
        const v = uint(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 = [];

        CmnTween.tween(CmnTween.TW_INT_TRANS, hArg, this.#spTransFore, {x: 0, y: 0}, ()=> {fncH(); fncV()}, ()=> {
            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;
        }, ()=> {});
        this.appPixi.ticker.add(fncRender);

        return false;
    }


    //MARK: トゥイーン開始
    #tsy(hArg: HArg) {
        const {layer, render, name} = hArg;
        if (! layer) throw 'layerは必須です';

        const pg = this.#hPages[this.#argChk_layer(hArg)];
        const lay = pg.fore;

        let finishBlendLayer = ()=> {};
        if (render && ! this.#evtMng.isSkipping) {
            lay.renderStart();
            finishBlendLayer = ()=> lay.renderEnd();
        }

        const hTo = CmnTween.cnvTweenArg(hArg, lay);
        const arrive = argChk_Boolean(hArg, 'arrive', false);
        const backlay = argChk_Boolean(hArg, 'backlay', false);
        const spBack: any = pg.back.spLay;    // fore, back が変わる恐れで外へ
        CmnTween.tween(name ?? layer, hArg, lay, CmnTween.cnvTweenArg(hArg, lay), ()=> {}, finishBlendLayer, ()=> {
            if (arrive) Object.assign(lay, hTo);
            if (backlay) for (const nm of Object.keys(CmnTween.hMemberCnt)) spBack[nm] = (<any>lay)[nm];
        });
//        hArg[':id'] = pg.fore.name.slice(0, -7);
//        this.scrItr.getDesignInfo(hArg);    // 必ず[':id'] を設定すること

        if ('filter' in hArg) {
            lay.spLay.filters = [Layer.bldFilters(hArg)];
            lay.aFltHArg = [hArg];
        }

        return false;
    }


    //MARK: フィルター追加
    #add_filter(hArg: HArg) {
        CmnTween.finish_trans();

        this.#foreachLayers(hArg, name=> {
            const pg = this.#hPages[this.#argChk_layer({layer: name})];
            if (hArg.page === 'both') {    // page=both で両面に
                this.#add_filter2(pg.fore, hArg);
                this.#add_filter2(pg.back, hArg);
                return;
            }
            const l = pg.getPage(hArg);
            this.#add_filter2(l, hArg);
        });

        return false;
    }
    #add_filter2(l: Layer, hArg: HArg) {
        const s = l.spLay;
        s.filters ??= [];
        s.filters = [...s.filters, Layer.bldFilters(hArg)];
        l.aFltHArg.push(hArg);
    }

    //MARK: フィルター全削除
    #clear_filter(hArg: HArg) {
        this.#foreachLayers(hArg, layer=> {
            const pg = this.#hPages[this.#argChk_layer({layer})];
            if (hArg.page === 'both') {    // page=both で両面に
                const f = pg.fore;
                const b = pg.back;
                f.spLay.filters = null;
                b.spLay.filters = null;
                f.aFltHArg = [];
                b.aFltHArg = [];
                return;
            }
            const l = pg.getPage(hArg);
            l.spLay.filters = null;
            l.aFltHArg = [];
        });

        return false;
    }

    //MARK: フィルター個別切替
    #enable_filter(hArg: HArg) {
        this.#foreachLayers(hArg, layer=> {
            const pg = this.#hPages[this.#argChk_layer({layer})];
            if (hArg.page === 'both') {    // page=both で両面に
                this.#enable_filter2(pg.fore, hArg);
                this.#enable_filter2(pg.back, hArg);
                return;
            }
            const l = pg.getPage(hArg);
            this.#enable_filter2(l, hArg);
        });

        return false;
    }
    #enable_filter2(l: Layer, hArg: HArg) {
        const s = l.spLay;
        if (! s.filters) throw 'フィルターがありません';

        const i = uint(argChk_Num(hArg, 'index', 0));
        const len = s.filters.length;
        if (len <= i) throw `フィルターの個数(${len})を越えています`;

        l.aFltHArg[i].enabled =
        s.filters[i].enabled = argChk_Boolean(hArg, 'enabled', true);
    }


//    // 文字・文字レイヤ
    static        #msecChWait        = 10;
    static get    msecChWait() {return LayerMng.#msecChWait}
    static set    msecChWait(v) {LayerMng.#msecChWait = v}
    //MARK: 文字を追加する
    #ch(hArg: HArg) {
        const {text} = hArg;
        if (! text) throw 'textは必須です';

        const tl = this.#getTxtLayer(hArg);
        delete hArg.text;    // [graph]時、次行がルビ文法でトラブったので
        if (this.#evtMng.isSkipping) hArg.wait = 0;
        else if ('wait' in hArg) argChk_Num(hArg, 'wait', NaN);

        const sArg = encodeURIComponent(JSON.stringify(hArg));
        this.#cmdTxt('add|'+ sArg, tl);    // [ch style]用

        const record = argChk_Boolean(hArg, 'record', true);
        const doRecLog = this.val.doRecLog();
        if (! record) this.val.setVal_Nochk('save', 'sn.doRecLog', record);
        tl.tagCh(text.replaceAll('[r]', '\n'));
        this.val.setVal_Nochk('save', 'sn.doRecLog', doRecLog);

        this.#cmdTxt(`add_close|`, tl);    // [ch style]用

        return false;
    }

    #getTxtLayer = (_hArg: HArg): TxtLayer=> {this.#chkTxtLay(); throw 0};
    #$getTxtLayer(hArg: HArg): TxtLayer {
        const ln = this.#argChk_layer(hArg, this.#curTxtlay);
        const pg = this.#hPages[ln];
        const lay = pg.getPage(hArg);
        if (! (lay instanceof TxtLayer)) throw ln +'はTxtLayerではありません';

        return lay;
    }
    setNormalChWait(): void {LayerMng.#msecChWait = this.scrItr.normalWait}


    //MARK: 操作対象のメッセージレイヤの指定
    #current = (_hArg: HArg): boolean=> {this.#chkTxtLay(); throw 0};
    #$current(hArg: HArg) {
        const {layer} = hArg;
        if (! layer) throw '[current] layerは必須です';

        this.#pgTxtlay = this.#hPages[layer];
        if (! (this.#pgTxtlay.getPage(hArg) instanceof TxtLayer)) throw `${layer}はTxtLayerではありません`;

        this.recPagebreak();    // カレント変更前に現在の履歴を保存
        this.#curTxtlay = layer;
        this.val.setVal_Nochk('save', 'const.sn.mesLayer', layer);
        for (const ln of this.#getLayers()) {
            const pg = this.#hPages[ln];
            if (! (pg.fore instanceof TxtLayer)) continue;
            (pg.fore as TxtLayer).isCur =
            (pg.back as TxtLayer).isCur = (ln === layer);
        }

        return false;
    }
    get currentTxtlayForeNeedErr(): TxtLayer {
        this.#chkTxtLay();
        return this.currentTxtlayFore!;
    }
    get currentTxtlayFore(): TxtLayer | null {
        if (! this.#pgTxtlay) return null;

        return this.#pgTxtlay.fore as TxtLayer;
    }
    #pgTxtlay    : Pages;    // カレントテキストレイヤ
    #chkTxtLay    : ()=> void    = ()=> {throw '文字レイヤーがありません。文字表示や操作する前に、[add_lay layer=(レイヤ名) class=txt]で文字レイヤを追加して下さい'};

    #argChk_layer(hash: any, def = ''): string {
        const ln = hash.layer ?? def;
        if (ln.includes(',')) throw 'layer名に「,」は使えません';
        if (! (ln in this.#hPages)) throw '属性 layer【'+ ln +'】が不正です。レイヤーがありません';

        return hash.layer = ln;
    }


    #oLastPage    : HArg    = {text: ''};
    #aTxtLog    : {[name: string]: number | string}[]    = [];
    recText(txt: string) {
        this.#oLastPage.text = txt;
        this.val.setVal_Nochk('save', 'const.sn.sLog',
            String(this.val.getVal('const.sn.log.json'))    // これを起動したい
        );
    }
    recPagebreak() {
        if (! this.#oLastPage.text) return;

        this.#oLastPage.text = this.#oLastPage.text.replaceAll(`<\/span><span class='sn_ch'>`, '');
        if (this.#aTxtLog.push(this.#oLastPage as {[name: string]: any}) > this.cfg.oCfg.log.max_len) this.#aTxtLog = this.#aTxtLog.slice(-this.cfg.oCfg.log.max_len);
        this.#oLastPage = {text: ''};
    }


    //MARK: 文字消去
    #clear_text(hArg: HArg) {
        const tf = this.#getTxtLayer(hArg);
        if (hArg.layer === this.#curTxtlay && hArg.page === 'fore') this.recPagebreak();    // 改ページ、クリア前に
        tf.clearText();
        return false;
    }


    //MARK: ハイパーリンクの終了
    #endlink(hArg: HArg) {this.#cmdTxt('endlink|', this.#getTxtLayer(hArg)); return false}

    //MARK: ページ両面の文字消去
    #er(hArg: HArg) {
        if (argChk_Boolean(hArg, 'rec_page_break', true)) this.recPagebreak();    // 改ページ、クリア前に

        if (this.#pgTxtlay) {
            this.#pgTxtlay.fore.clearLay(hArg);
            this.#pgTxtlay.back.clearLay(hArg);
        }

        return false;
    }

    //MARK: インライン画像表示
    #graph(hArg: HArg) {
        if (! hArg.pic) throw '[graph] picは必須です';

        const sArg = encodeURIComponent(JSON.stringify(hArg));
        this.#cmdTxt('grp|'+ sArg, this.#getTxtLayer(hArg));
        return false;
    }

    //MARK: ハイパーリンク
    #link(hArg: HArg) {
        if (! hArg.fn && ! hArg.label && ! hArg.url) throw 'fnまたはlabelまたはurlは必須です';
        hArg.fn ??= this.scrItr.scriptFn;    // ここで指定する必要がある

        hArg.style ??= 'background-color: rgba(255,0,0,0.5);';
        hArg.style_hover ??= 'background-color: rgba(255,0,0,0.9);';
        hArg.style_clicked ??= hArg.style;
        const sArg = encodeURIComponent(JSON.stringify(hArg));
        this.#cmdTxt('link|'+ sArg, this.#getTxtLayer(hArg));
        return false;
    }

    //MARK: 改行
    #r(hArg: HArg) {hArg.text = '\n'; return this.#ch(hArg)}

    //MARK: 履歴改行
    #rec_r(hArg: HArg) {return this.#rec_ch({...hArg, text: '[r]'})};

    //MARK: 履歴書き込み
    #rec_ch(hArg: HArg) {
        this.#oLastPage = {...hArg, text: this.#oLastPage.text};    // text 以外を先に更新
        if (! hArg.text) return false;

        hArg.record = true;
        hArg.style ??= '';
        hArg.style += 'display: none;';    // gotxt内で削除し履歴に表示
        hArg.wait = 0;
        return this.#ch(hArg);    // この先は text, style, r_style 以外破棄されてしまうので注意
    };

    //MARK: 履歴リセット
    #reset_rec(hArg: HArg) {
        this.#aTxtLog = [];
        this.#oLastPage = {text: hArg.text ?? ''};
        this.val.setVal_Nochk('save', 'const.sn.sLog', 
            hArg.text ?`[{text:"${hArg.text}"}]` : '[]'
        );

        return false;
    }

    //MARK: 文字列と複数ルビの追加
    #ruby2(hArg: HArg) {
        const {t, r} = hArg;
        if (! t) throw '[ruby2] tは必須です';
        if (! r) throw '[ruby2] rは必須です';

        hArg.text = '|'+ encodeURIComponent(t) +'《'+ encodeURIComponent(r) +'》';
        delete hArg.t;
        delete hArg.r;
        return this.#ch(hArg);
    }


    //MARK: インラインスタイル設定
    #span(hArg: HArg) {
        const sArg = encodeURIComponent(JSON.stringify(hArg));
        this.#cmdTxt('span|'+ sArg, this.#getTxtLayer(hArg));
        return false;
    }

    //MARK: tcy縦中横を表示する
    #tcy(hArg: HArg) {
        if (! hArg.t) throw '[tcy] tは必須です';

        const sArg = encodeURIComponent(JSON.stringify(hArg));
        this.#cmdTxt('tcy|'+ sArg, this.#getTxtLayer(hArg));
        return false;
    }


    //MARK: レイヤのダンプ
    #dump_lay(hArg: HArg) {
        console.group('🥟 [dump_lay]');
        for (const ln of this.#getLayers(hArg.layer)) {
            const pg = this.#hPages[ln];
            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;
    }


    //MARK: イベント有無の切替
    #enable_event(hArg: HArg) {
        const layer = this.#argChk_layer(hArg, this.#curTxtlay);
        const v = argChk_Boolean(hArg, 'enabled', true);
        this.#getTxtLayer(hArg).enabled = v;
        this.val.setVal_Nochk('save', 'const.sn.layer.'+ layer +'.enabled', v);

        return false;
    }


    //MARK: ボタンを表示
    #button(hArg: HArg) {
        Pages.argChk_page(hArg, 'back');    // チェックしたいというよりデフォルトをbackに
        hArg.fn ??= this.scrItr.scriptFn;    // ここで指定する必要がある
            // fn省略時、画像ボタンはロード後という後のタイミングで scrItr.scriptFn を
            // 参照してしまうので
        this.#getTxtLayer(hArg).addButton(hArg);    // hArg[':id_tag'] も設定

        this.scrItr.recodeDesign(hArg);    // 必ず[':id_tag'] を設定すること

        return false;
    }


    record(): any {
        const o: any = {};
        for (const ln of this.#aLayName) {
            const pg = this.#hPages[ln];
            o[ln] = {
                cls: pg.cls,
                fore: pg.fore.record(),
                back: pg.back.record(),
            };
        }
        return o;
    }
    playback($hPages: HIPage, fncComp: ()=> void): void {
        // これを先に。save:const.sn.sLog がクリアされてしまう
        this.#aTxtLog = JSON.parse(String(this.val.getVal('save:const.sn.sLog')));
        this.#oLastPage = {text: ''};

        const aPrm: Promise<void>[] = [];
        const aSort: {layer: string, idx: number}[] = [];
        for (const [layer, {fore, fore: {idx}, back, cls}] of Object.entries($hPages)) {    // 引数で言及の無いレイヤはそのまま。特に削除しない
            aSort.push({layer, idx});

            const ps = this.#hPages[layer] ??= new Pages(layer, cls, this.#fore, this.#back, {}, this.sys, this.val, {isWait: false});
            ps.fore.playback(fore, aPrm);
            ps.back.playback(back, aPrm);
        }
        const len = this.#fore.children.length;
        Promise.allSettled(aPrm).then(()=> {
            // 若い順にsetChildIndex()
            for (const {layer, idx} of
                aSort.sort(({idx: a}, {idx: b})=> a === b ?0 :a < b ?-1 :1)) {
                const {fore, back} = this.#hPages[layer];
                if (! fore) return;
                const i = len > idx ?idx :len -1;
                this.#fore.setChildIndex(fore.spLay, i);
                this.#back.setChildIndex(back.spLay, i);
            }

            fncComp();
        })
        .catch(e=> console.error(`fn:LayerMng.ts playback e:%o`, e));
    }

}