skynovel
Version:
webgl novelgame framework
899 lines (802 loc) • 29.8 kB
text/typescript
/* ***** 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 {Layer} from './Layer';
import {uint, CmnLib, IEvtMng} from './CmnLib';
import {IVariable, IHTag, HArg, IPutCh, IMain} from './CmnInterface';
import {TxtStage, IInfTxLay} from './TxtStage';
import {Config} from './Config';
import {RubySpliter} from './RubySpliter';
import {GrpLayer} from './GrpLayer';
import {Button} from './Button';
import {Sprite, DisplayObject, Graphics, Container, Renderer} from 'pixi.js';
import {LayerMng} from './LayerMng';
export class TxtLayer extends Layer {
private static cfg : Config;
private static val : IVariable;
private static recText : (txt: string, pagebreak?: boolean)=> void;
static init(cfg: Config, hTag: IHTag, val: IVariable, recText: (txt: string)=> void): void {
TxtLayer.cfg = cfg;
TxtStage.init(cfg);
TxtLayer.val = val;
TxtLayer.recText = recText;
val.setDoRecProc(TxtLayer.chgDoRec);
hTag.autowc = o=> TxtLayer.autowc(o); // 文字を追加する
const o: any = {enabled: 'false', text: '', time: ''};
hTag.autowc(o);
hTag.ch_in_style = o=> TxtLayer.ch_in_style(o); // 文字出現演出
hTag.ch_out_style = o=> TxtLayer.ch_out_style(o); // 文字消去演出
// ギャラリーリロード用初期化
TxtStage.initChStyle();
const he = document.getElementsByTagName('head')[0];
const len = he.children.length;
for (let i=len -1; i>=0; --i) {
const v = he.children[i];
if (! (v instanceof HTMLStyleElement)) continue;
if (v.innerText.slice(0, 14) != TxtLayer.css_key4del) continue;
he.removeChild(v);
}
let font = '';
for (const o of cfg.matchPath('.+', Config.EXT_FONT)) {
for (const key in o) font += `
@font-face {
font-family: '${CmnLib.getFn(o[key])}';
src: url('${o[key]}');
}
`;
}
// 文字出現演出関係
font += `
.sn_tx {
pointer-events: none;
user-select: none;
-webkit-touch-callout: none;
box-sizing: border-box;
}
.sn_ch {
position: relative;
display: inline-block;
}
`; // 「sn_ch」と「sn_ch_in_〜」の中身が重複しているが、これは必須
TxtLayer.addStyle(font);
TxtLayer.ch_in_style({
name : 'default',
wait : 500,
alpha : 0,
x : '=0.3',
y : '=0',
scale_x : 1,
scale_y : 1,
rotate : 0,
join : true,
ease : 'ease-out',
});
TxtLayer.ch_out_style({
name : 'default',
wait : 0,
alpha : 0,
x : '=0',
y : '=0',
scale_x : 1,
scale_y : 1,
rotate : 0,
join : false,
ease : 'ease-out',
});
}
private static css_key4del = '/* SKYNovel */';
static addStyle(style: string) {
const gs = document.createElement('style');
gs.type = 'text/css';
gs.innerHTML = TxtLayer.css_key4del + style;
document.getElementsByTagName('head')[0].appendChild(gs);
}
// 文字出現演出
private static ch_in_style(hArg: HArg) {
const o = TxtStage.ch_in_style(hArg);
const x = (o.x.charAt(0) == '=') ?`${o.nx *100}%` :`${o.nx}px`;
const y = (o.y.charAt(0) == '=') ?`${o.ny *100}%` :`${o.ny}px`;
const name = hArg.name;
TxtLayer.addStyle(`
.sn_ch_in_${name} {
position: relative;
display: inline-block;
}
.go_ch_in_${name} {
opacity: ${o.alpha};
position: relative;
display: inline-block;
animation: sn_ch_in_${name} ${o.wait}ms ${o.ease} 0s both;
}
@keyframes sn_ch_in_${name} {
from {transform: rotate(${o.rotate}deg) scale(${o.scale_x}, ${o.scale_y}) translate(${x}, ${y});}
to {opacity: 1; transform: none;}
}
` );
return false;
}
// 文字消去演出
private static ch_out_style(hArg: HArg) {
const o = TxtStage.ch_out_style(hArg);
const x = (o.x.charAt(0) == '=') ?`${o.nx *100}%` :`${o.nx}px`;
const y = (o.y.charAt(0) == '=') ?`${o.ny *100}%` :`${o.ny}px`;
const name = hArg.name;
TxtLayer.addStyle(`
.go_ch_out_${name} {
position: relative;
display: inline-block;
animation: go_ch_out_${name} ${o.wait}ms ${o.ease} 0s both;
}
@keyframes go_ch_out_${name} {
to {
opacity: ${o.alpha};
transform: rotate(${o.rotate}deg) scale(${o.scale_x}, ${o.scale_y}) translate(${x}, ${y});
}
`);
return false;
}
private static main : IMain;
private static evtMng : IEvtMng;
static setEvtMng(main: IMain, evtMng: IEvtMng) {
TxtLayer.main = main;
TxtLayer.evtMng = evtMng;
TxtStage.setEvtMng(evtMng);
}
// 文字ごとのウェイト
private static doAutoWc = false;
private static hAutoWc : {[ch: string]: number} = Object.create(null);//{}
private static autowc(hArg: HArg) {
TxtLayer.doAutoWc = CmnLib.argChk_Boolean(hArg, 'enabled', TxtLayer.doAutoWc);
TxtLayer.val.setVal_Nochk('save', 'const.sn.autowc.enabled', TxtLayer.doAutoWc);
const ch = hArg.text;
if (('text' in hArg) != ('time' in hArg)) throw '[autowc] textとtimeは同時指定必須です';
TxtLayer.val.setVal_Nochk('save', 'const.sn.autowc.text', ch);
if (! ch) {
TxtLayer.val.setVal_Nochk('save', 'const.sn.autowc.time', '');
return false;
}
const len = ch.length;
if (TxtLayer.doAutoWc && len == 0) throw '[autowc] enabled == false かつ text == "" は許されません';
const a = String(hArg.time).split(',');
if (a.length != len) throw '[autowc] text文字数とtimeに記述された待ち時間(コンマ区切り)は同数にして下さい';
TxtLayer.hAutoWc = Object.create(null); //{} // 毎回クリアを仕様とする
a.forEach((v, i)=> TxtLayer.hAutoWc[ch[i]] = uint(v));
TxtLayer.val.setVal_Nochk('save', 'const.sn.autowc.time', hArg.time);
return false;
}
private infTL :IInfTxLay = {
fontsize : 24,
$width : 0, // レイヤサイズであり、背景色(画像)サイズ
$height : 0,
pad_left : 0, // paddingLeft(レイヤサイズの内側のスペーサー)
pad_right : 0, // paddingRight
pad_top : 0, // paddingTop
pad_bottom : 0, // paddingBottom
}
// バック
private b_color = 0x000000;
private b_alpha = 0;
private b_alpha_isfixed = false;
private b_do : DisplayObject | null = null;
private b_pic = ''; // 背景画像無し(=単色塗り)
// 文字表示
private cntInsidePadding= new Container;
private txs = new TxtStage(this.infTL, this.cntInsidePadding, this.cnt);
private rbSpl = new RubySpliter;
private cntBtn = new Container;
constructor() {
super();
const padding = 16 *CmnLib.retinaRate; // 初期padding
this.cnt.addChild(this.cntInsidePadding);
this.cntInsidePadding.name = 'cntInsidePadding';
this.cntInsidePadding.position.set(padding, padding);
this.rbSpl.init(this.putCh);
this.cnt.addChild(this.cntBtn); // ボタンはpaddingの影響を受けない
this.cntBtn.name = 'cntBtn';
this.lay({style: `width: ${CmnLib.stageW}px; height: ${CmnLib.stageH}px; font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', '游ゴシック Medium', meiryo, sans-serif; color: white; font-size: 24px; line-height: 1.5; padding: ${padding}px;`, in_style: 'default', out_style: 'default', back_clear: 'true'});
}
destroy() {
if (this.b_do) {this.cnt.removeChild(this.b_do).destroy(); this.b_do = null}
this.clearText();
this.txs.destroy();
}
set name(nm: string) {if (this.txs) this.txs.name = nm;}
get name() {return this.txs ?this.txs.name :''}
cvsResize() {this.txs.cvsResize();}
lay(hArg: HArg) {
super.lay(hArg);
Layer.setXY(this.cnt, hArg, this.cnt);
this.rbSpl.setting(hArg);
this.setFfs(hArg);
this.txs.lay(hArg);
if ('r_align' in hArg) this.r_align = hArg.r_align ?? '';
this.ruby_pd = CmnLib.isSafari
? this.txs.tategaki // Safariでは親文字幅 l は疑似値
? (v, l)=> `text-align: start; height: ${l}em; padding-top: ${v}; padding-bottom: ${v};`
: (v, l)=> `text-align: start; width: ${l}em; padding-left: ${v}; padding-right: ${v};`
: this.txs.tategaki
? v=> `text-align: justify; text-align-last: justify; padding-top: ${v}; padding-bottom: ${v};`
: v=> `text-align: justify; text-align-last: justify; padding-left: ${v}; padding-right: ${v};`;
if (CmnLib.isFirefox) this.mkStyle_r_align = this.mkStyle_r_align4ff;
if ('alpha' in hArg) this.cntBtn.children.forEach(e=> e.alpha = this.cnt.alpha);
this.set_ch_in(hArg);
this.set_ch_out(hArg);
return this.drawBack(hArg);
}
private set_ch_in(hArg: HArg) {
const ins = hArg.in_style;
if (! ins) return;
const cis = TxtStage.getChInStyle(ins);
if (! cis) throw `存在しないin_style【${ins}】です`;
this.ch_in_style = ins;
this.ch_in_join = cis.join;
}
private ch_in_style = '';
private ch_in_join = true;
private set_ch_out(hArg: HArg) {
const outs = hArg.out_style;
if (! outs) return;
const cos = TxtStage.getChOutStyle(outs);
if (! cos) throw `存在しないout_style【${outs}】です`;
this.ch_out_style = outs;
}
private ch_out_style = '';
private drawBack(hArg: HArg): boolean {
if ('back_clear' in hArg) {
if (CmnLib.argChk_Boolean(hArg, 'back_clear', false)) {
this.b_color = 0x000000;
this.b_alpha = 0;
this.b_alpha_isfixed = false;
this.b_pic = '';
}
return false;
}
this.b_alpha = CmnLib.argChk_Num(hArg, 'b_alpha', this.b_alpha);
this.b_alpha_isfixed = CmnLib.argChk_Boolean(hArg, 'b_alpha_isfixed', this.b_alpha_isfixed);
const alpha = (this.b_alpha_isfixed
? 1
: Number(TxtLayer.val.getVal('sys:TextLayer.Back.Alpha'))
) *this.b_alpha;
if (hArg.b_pic) {
if (this.b_pic != hArg.b_pic) {
this.b_pic = hArg.b_pic;
if (this.b_do) {
this.cnt.removeChild(this.b_do);
this.b_do.destroy();
}
return GrpLayer.csv2Sprites(this.b_pic, this.cnt, sp=> {
this.b_do = sp;
sp.name = 'back(pic)';
sp.visible = (alpha > 0);
sp.alpha = alpha;
//CmnLib.adjustRetinaSize(this.b_pic, sp);
this.txs.setSize(sp.width, sp.height);
// ちなみに左上表示位置は本レイヤと同じ
this.cnt.setChildIndex(sp, 0);
TxtLayer.main.resume();
});
}
}
else if ('b_color' in hArg) {
this.b_color = parseInt(hArg.b_color || '0');
if (this.b_do) {
this.cnt.removeChild(this.b_do);
this.b_do.destroy();
}
this.b_pic = ''; // 忘れずクリア
const grp = this.b_do = new Graphics;
grp.name = 'back(color)';
grp.beginFill(this.b_color);
grp.lineStyle(undefined);
grp.drawRect(0, 0, this.infTL.$width, this.infTL.$height);
grp.endFill();
this.cnt.addChildAt(grp, 0);
//cacheAsBitmap = true; // これを有効にするとスナップショットが撮れない??
}
if (this.b_do) {
this.b_do.visible = (alpha > 0);
// 透明の時は表示しない。こうしないと透明テキストレイヤ下のボタンが
// 押せなくなる(透明だが塗りがあるという扱いなので)
this.b_do.alpha = alpha;
}
return false;
}
chgBackAlpha(g_alpha: number): void {
const alpha = this.b_alpha_isfixed
? this.b_alpha
: g_alpha * this.b_alpha;
if (this.b_do instanceof Graphics) {
if (this.b_do) {
this.cnt.removeChild(this.b_do);
this.b_do.destroy();
}
const grp = this.b_do = new Graphics;
grp.name = 'back(color)';
grp.beginFill(this.b_color);
grp.lineStyle(undefined);
grp.drawRect(0, 0, this.infTL.$width, this.infTL.$height);
grp.endFill();
this.cnt.addChildAt(grp, 0);
//cacheAsBitmap = true; // これを有効にするとスナップショットが撮れない??
}
if (this.b_do) {
this.b_do.visible = (alpha > 0);
// 透明の時は表示しない。こうしないと透明テキストレイヤ下のボタンが
// 押せなくなる(透明だが塗りがあるという扱いなので)
this.b_do.alpha = alpha;
}
}
private setFfs(hArg: HArg) {
if ('noffs' in hArg) {
this.strNoFFS = hArg.noffs ?? '';
this.regNoFFS = new RegExp(`[ ${this.strNoFFS}]`);
}
if (! ('ffs' in hArg)) return;
this.ffs = hArg.ffs ?? '';
if (this.ffs == '') {
this.fncFFSStyle = ()=> '';
this.fncFFSSpan = ch=> ch;
}
else {
this.fncFFSStyle = ch=> this.regNoFFS.test(ch)
? ''
: ` font-feature-settings: ${this.ffs};`;
this.fncFFSSpan = ch=> this.regNoFFS.test(ch)
? ch
: `<span style='font-feature-settings: ${this.ffs};'>${ch}</span>`;
}
}
private ffs = '';
private fncFFSStyle = (_ch: string)=> '';
private fncFFSSpan = (ch: string)=> ch;
private strNoFFS = '';
private regNoFFS = new RegExp('[ ]');
// Safariが全体に「font-feature-settings」した後、特定文字の「font-feature-settings: initial;」を受け付けてくれないのでわざわざ一つずつ指定
static chgDoRec(doRec: boolean) {
TxtLayer.rec = doRec
? (tx: string)=> tx
: (tx: string)=> `<span class='offrec'>${tx}</span>`;
// 囲んだ領域は履歴で非表示
}
static rec = (tx: string)=> tx;
isCur = false;
private ruby_pd: (v: string, l: number)=> string = ()=> '';
private mkStyle_r_align(ch: string, rb: string, r_align: string): string {
if (! r_align) return '';
const len = ch.length *2;
if (len -rb.length < 0) return ` style='text-align: ${r_align};'`;
let st = '';
switch (r_align) {
case 'justify':
st = this.ruby_pd('0', len); break;
case '121':
st = this.ruby_pd(`calc(${(len -rb.length) /(rb.length *2)}em)`, len); break;
case 'even':
st = this.ruby_pd(`calc(${(len -rb.length) /(rb.length +1)}em)`, len); break;
case '1ruby':
st = this.ruby_pd('1em', len); break;
default:
st = `text-align: ${r_align};`;
}
return ` style='${st}'`;
};
private r_align = '';
private mkStyle_r_align4ff(ch: string, rb: string, r_align: string): string {
if (! r_align) return '';
const len = ch.length *2;
if (len -rb.length < 0) return ` style='text-align: ${r_align};'`;
let st = '';
switch (r_align) {
case 'left': st = `ruby-align: start;`; break;
case 'center': st = `ruby-align: center;`; break;
case 'right': // エレガントにサポートできていない
st = `ruby-align: start;`; break;
case 'justify': st = `ruby-align: space-between;`; break;
case '121': st = `ruby-align: space-around;`; break;
case 'even':
const ev = (len -rb.length) /(rb.length +1);
st = `ruby-align: space-between; `+
(this.txs.tategaki
? `padding-top: ${ev}em; padding-bottom: ${ev}em;`
: `padding-left: ${ev}em; padding-right: ${ev}em;`);
break;
case '1ruby': st = `ruby-align: space-between; `+
(this.txs.tategaki
? `padding-top: 1em; padding-bottom: 1em;`
: `padding-left: 1em; padding-right: 1em;`);
break;
default: st = `text-align: ${r_align};`;
}
return ` style='${st}'`;
}
tagCh(text: string): void {this.rbSpl.putTxt(text);}
private needGoTxt = false;
private putCh : IPutCh = (ch, ruby)=> {
if (TxtLayer.cfg.oCfg.debug.putCh) console.log(`🖊 文字表示 text:\`${ch}\` ruby:\`${ruby}\` name:\`${this.name}\``);
const a_ruby = ruby.split('|');
let add_htm = '';
const isSkip = TxtLayer.evtMng.isSkipKeyDown();
switch (a_ruby.length) {
case 1: // 字or春《はる》
this.needGoTxt = true;
if (ch == '\n') {
if (this.aSpan_bk) {
add_htm = this.aSpan_bk.slice(-1)[0];
this.autoCloseSpan();
this.aSpan.push(TxtLayer.rec('<br/>'));
this.aSpan.push(add_htm); // ここでaSpan末尾に追加しないと続かない
this.aSpan_bk = this.aSpan;
this.aSpan = [];
return; // breakではない
}
if (this.firstCh) { // 1文字目にルビが無い場合は不可視ルビで、行揃え
this.firstCh = false;
add_htm = '<ruby> <rt> </rt></ruby><br/>';
}
else {
add_htm = '<br/>';
}
break;
}
if (this.firstCh) { // 1文字目にルビが無い場合は見えないルビで、行揃え
this.firstCh = false;
if (ruby == '') ruby = ' ';
}
add_htm = this.tagCh_sub(ch, ruby, isSkip, this.r_align);
break;
case 2: // 《grp|{"id":"break","pic":"breakline"}》
switch (a_ruby[0]) {
// ルビ揃え指定と同時シリーズ
case 'start': // 初期値
case 'left': //(肩付き)先頭親文字から、ルビ間は密着
case 'center': //(中付き)センター合わせ、〃
case 'right': //(右/下揃え)末尾親文字から、〃
case 'justify': //(両端揃え)先頭から末尾親文字間に、ルビ間は均等にあける
case '121': //(1-2-1(JIS))ルビの前後を比率1、ルビ間を比率2であける
case 'even': //(均等アキ)ルビの前後、ルビ間も均等にあける
case '1ruby': //(1ルビ文字アキ)ルビの前後をルビ一文字空け、ルビ間は均等にあける
this.firstCh = false;
this.needGoTxt = true;
add_htm = this.tagCh_sub(ch, a_ruby[1], isSkip, a_ruby[0]);
break;
case 'gotxt':
{
this.autoCloseSpan();
if (this.isCur) TxtLayer.recText(
this.aSpan.join('')
.replace(/^<ruby> <rt> <\/rt><\/ruby>(<br\/>)+/, '')
// 前方の空行をtrim
.replace(/style='(anim\S+ \S+?;\s*)+/g, `style='`)
.replace(/( style=''| data-(add|arg|cmd)='.+?'|\n+|\t+)/g, '')
.replace(/class='sn_ch .+?'/g, `class='sn_ch'`)
// 不要情報削除
.replace(/class='offrec'/g, `style='display: none;'`)
// 囲んだ領域は履歴で非表示
.replace(/`/g, '\\`')
// JSON対策
);
if (! this.needGoTxt) return; // breakではない
this.txs.goTxt( // クリック待ち用ダミー空白を追加
[...this.aSpan, this.tagCh_sub(' ', '', false, '')]
);
this.needGoTxt = false;
this.cumDelay = 0;
return; // breakではない
}
case 'add': // 文字幅を持たない汎用的な命令(必ずadd_closeすること)
{
const o = JSON.parse(a_ruby[1]);
o.style = o.style ?? '';
this.beginSpan(o);
if (this.aSpan_bk) this.autoCloseSpan();
else {
if (isSkip) this.cumDelay = 0;
const wait = o.wait ?? -1;
const sn_ch = (wait == 0)
? ''
: ` sn_ch_in_${this.ch_in_style}`;
const ad = (wait < 0)
? ''
: ` animation-duration: ${wait}ms;`
this.aSpan.push(`<span class='sn_ch${sn_ch}' style='animation-delay: ${this.cumDelay}ms;${ad} ${o.style}' data-add='${JSON.stringify(o)}'>`); // "を"にしてはいけない
}
this.aSpan_bk = this.aSpan;
this.aSpan = [];
}
return; // breakではない
case 'add_close':
this.autoCloseSpan();
return; // breakではない
case 'grp': // 画像など 《grp|{"id":"break","pic":"breakline"}》
this.needGoTxt = true;
{
if (isSkip) this.cumDelay = 0;
const arg = (a_ruby[1] ?a_ruby[1].slice(0, -1) +',' :`{`) +`"delay": ${this.cumDelay}}`;
if (this.ch_in_join) this.cumDelay += (TxtLayer.doAutoWc) ?0 :LayerMng.msecChWait;
const o = JSON.parse(arg);
o.style = o.style ?? '';
if (! ('id' in o)) o.id = this.aSpan.length;
if (o.id == 'break') {this.txs.dispBreak(o.pic); return;}
// breakではない
add_htm = `<span data-cmd='grp' data-id='${o.id}' data-arg='${arg}'`;
const wait = o.wait ?? -1;
const sn_ch = (wait == 0)
? ''
: ` sn_ch_in_${this.ch_in_style}`;
const ad = (wait < 0)
? ''
: ` animation-duration: ${wait}ms;`
add_htm += ` class='sn_ch${sn_ch}' style='animation-delay: ${this.cumDelay}ms;${ad} ${o.style}' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'> </span>`;
if (this.firstCh) { // 1文字目にルビが無い場合は不可視ルビで、行揃え
this.firstCh = false;
add_htm = `<ruby>${add_htm}<rt> </rt></ruby>`;
}
if (this.aSpan.slice(-1)[0] == add_htm) return; // breakではない
}
break;
case 'del':
const id_del = a_ruby[1];
if (id_del != 'break') throw '文字レイヤdelコマンドは、現在id=breakのみサポートします';
TxtStage.delBreak();
return; // breakではない
case 'span':
this.autoCloseSpan();
this.needGoTxt = true;
{
// style, in_style
const o = JSON.parse(a_ruby[1]);
// o.style = o.style ?? '';
this.beginSpan(o);
if (! o.style) return; // breakではない
if (isSkip) this.cumDelay = 0;
const wait = o.wait ?? -1;
const sn_ch = (wait == 0)
? ''
: ` sn_ch_in_${this.ch_in_style}`;
const ad = (wait < 0)
? ''
: ` animation-duration: ${wait}ms;`
this.aSpan.push(`<span class='sn_ch${sn_ch}' style='animation-delay: ${this.cumDelay}ms;${ad} ${o.style}' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>`);
this.aSpan_bk = this.aSpan;
this.aSpan = [];
}
return; // breakではない
case 'link':
this.autoCloseSpan();
this.needGoTxt = true;
{
// b_color, b_alpha, fn, label
const o = JSON.parse(a_ruby[1]);
o.style = o.style ?? '';
this.beginSpan(o);
if (isSkip) this.cumDelay = 0;
const wait = o.wait ?? -1;
const sn_ch = (wait == 0)
? ''
: ` sn_ch_in_${this.ch_in_style}`;
const ad = (wait < 0)
? ''
: ` animation-duration: ${wait}ms;`
this.aSpan_link = ` data-cmd='link' data-arg='${a_ruby[1]}'`;
this.aSpan.push(`<span${this.aSpan_link} class='sn_ch${sn_ch}' style='animation-delay: ${this.cumDelay}ms;${ad} ${o.style}' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>`);
this.aSpan_bk = this.aSpan;
this.aSpan = [];
}
return; // breakではない
case 'endlink':
this.needGoTxt = true;
if (this.aSpan_bk) {
const len = this.aSpan.length;
for (let i=0; i<len; ++i) this.aSpan[i] = this.aSpan[i].replace(/ data-cmd='linkrsv'/, this.aSpan_link);
}
this.autoCloseSpan(); return; // breakではない
default: // ルビあり文字列
this.needGoTxt = true;
add_htm = this.tagCh_sub(ch, ruby, isSkip, this.r_align);
}
break;
case 3: // 《tcy|451|かし》
this.firstCh = false;
this.needGoTxt = true;
switch (a_ruby[0]) {
case 'tcy': // ルビ付き縦中横
{
if (TxtLayer.val.doRecLog()) this.page_text += ch
+(ruby ?`《${ruby}》` :'');
// text-combine-upright: all; 縦中横
// -webkit-text-combine: horizontal; 縦中横(Safari)
const tx = a_ruby[1];
const id_tcy = (tx.length > 1)
? (this.aSpan.length +1) // 0にならないよう +1
: '';
const rb = CmnLib.isSafari
? a_ruby[2].replace(/[A-Za-z0-9]/g, s=> String.fromCharCode(s.charCodeAt(0) + 65248))
// 英数字を全角に(Safariで縦中横ルビが半角文字だと、
// 選択矩形が横倒しになる不具合対策)
: a_ruby[2];
if (isSkip) this.cumDelay = 0;
const rs = this.mkStyle_r_align(tx, rb, this.r_align);
add_htm = rb
? (this.aSpan_bk
? (`<ruby><span data-tcy='${id_tcy}' style='
text-combine-upright: all;
-webkit-text-combine: horizontal;
${this.fncFFSStyle(tx)}
' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}' data-cmd='linkrsv'>${tx}</span>`
+`<rt${rs}>${rb}</rt></ruby>`)
: (`<span class='sn_ch sn_ch_in_${this.ch_in_style}' style='animation-delay: ${this.cumDelay}ms;'>`
+`<ruby><span data-tcy='${id_tcy}' style='
text-combine-upright: all;
-webkit-text-combine: horizontal;
${this.fncFFSStyle(tx)}
' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>${tx}</span>`
+`<rt${rs}>${rb}</rt></ruby>`
+`</span>`))
: (this.aSpan_bk
? (`<span data-tcy='${id_tcy}' style='
text-combine-upright: all;
-webkit-text-combine: horizontal;
${this.fncFFSStyle(tx)}
' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}' data-cmd='linkrsv'>${tx}</span>`)
: `<span data-tcy='${id_tcy}' style='
text-combine-upright: all;
-webkit-text-combine: horizontal;
animation-delay: ${this.cumDelay}ms;
height: 1em;
${this.fncFFSStyle(tx)}
' class='sn_ch sn_ch_in_${this.ch_in_style}' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>${tx}</span>`);
if (this.ch_in_join) this.cumDelay += (TxtLayer.doAutoWc)
? TxtLayer.hAutoWc[ch.charAt(0)] ?? 0
: LayerMng.msecChWait;
}
break;
default:
throw `異常な値です putCh(text: ${ch}, ruby: ${ruby})`;
}
break;
}
this.aSpan.push(TxtLayer.rec(add_htm));
}
private tagCh_sub(ch: string, ruby: string, isSkip: boolean, r_align: string): string {
if (ch == ' ') ch = ' ';
if (TxtLayer.val.doRecLog()) this.page_text += ch
+(ruby ?`《${ruby}》` :'');
let add_htm = '';
const rs = this.mkStyle_r_align(ch, ruby, r_align);
if (isSkip) this.cumDelay = 0;
add_htm = ruby
? (this.aSpan_bk
? `<ruby style='${this.fncFFSStyle(ch)}' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}' data-cmd='linkrsv'>${ch}<rt${rs}>${ruby}</rt></ruby>`
: (`<span class='sn_ch sn_ch_in_${this.ch_in_style}' style='animation-delay: ${this.cumDelay}ms;${this.fncFFSStyle(ch)}'>`
+`<ruby data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>${ch}<rt${rs}>${ruby}</rt></ruby>`
+`</span>`))
: (this.aSpan_bk
? this.fncFFSSpan(ch)
: `<span class='sn_ch sn_ch_in_${this.ch_in_style}' style='animation-delay: ${this.cumDelay}ms;${this.fncFFSStyle(ch)}' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>${ch}</span>`);
if (this.ch_in_join) this.cumDelay += TxtLayer.doAutoWc
? TxtLayer.hAutoWc[ch.charAt(0)] ?? 0
: LayerMng.msecChWait;
return add_htm;
}
private cumDelay = 0;
private firstCh = true;
private aSpan : string[] = [];
private aSpan_bk : any[] | null = null;
private aSpan_link = '';
private hSpanBk = {
ch_in_style : '',
ch_out_style: '',
r_align : '',
};
private beginSpan(o :any) {
this.hSpanBk.ch_in_style = this.ch_in_style;
this.set_ch_in(o);
this.hSpanBk.ch_out_style = this.ch_out_style;
this.set_ch_out(o);
this.hSpanBk.r_align = this.r_align;
if ('r_align' in o) this.r_align = o.r_align;
}
private autoCloseSpan() {
if (! this.aSpan_bk) return;
this.aSpan_bk.push(this.aSpan, '</span>')
this.aSpan = Array.prototype.concat.apply([], this.aSpan_bk);
this.aSpan_bk = null;
this.set_ch_in({in_style: this.hSpanBk.ch_in_style});
this.set_ch_out({out_style: this.hSpanBk.ch_out_style});
this.r_align = this.hSpanBk.r_align;
}
readonly click = ()=> {
if (! this.cnt.interactiveChildren || ! this.cnt.visible) return true;
return this.txs.skipChIn(); // true is stay
}
clearText(): void {
const txs = this.txs;
this.txs = this.txs.passBaton();
txs.destroy();
this.cumDelay = 0;
this.firstCh = true;
this.aSpan = [];
this.aSpan_bk = null;
this.page_text = '';
TxtLayer.recText('', true);
}
private page_text = '';
get pageText() {return this.page_text}
get enabled() {return this.cnt.interactiveChildren}
set enabled(e) {this.cnt.interactiveChildren = e}
addButton(hArg: HArg): boolean {
hArg.key = `btn=[${this.cntBtn.children.length}] `+ this.name;
const btn = new Button(TxtLayer.main, TxtLayer.evtMng, hArg);
btn.name = JSON.stringify(hArg);
this.cntBtn.addChild(btn);
return btn.isStop;
}
clearLay(hArg: HArg): void {
super.clearLay(hArg);
this.clearText();
for (const c of this.cntBtn.removeChildren()) c.removeAllListeners().destroy(); // removeAllListeners()はマウスオーバーイベントなど。クリックは別
}
readonly record = ()=> Object.assign(super.record(), {
enabled : this.enabled,
r_align : this.r_align,
// バック
b_do : (this.b_do == null)
? null
: (this.b_do instanceof Sprite ?'Sprite' :'Graphics'),
b_pic : this.b_pic,
b_color : this.b_color,
b_alpha : this.b_alpha,
b_alpha_isfixed : this.b_alpha_isfixed,
txs : this.txs.record(),
ffs : this.ffs,
strNoFFS: this.strNoFFS,
btns : this.cntBtn.children.map(btn=> btn.name),
});
playback(hLay: any, fncComp: undefined | {(): void} = undefined): boolean {
super.playback(hLay);
this.enabled = hLay.enabled;
this.r_align = hLay.r_align;
this.cvsResize();
// バック
this.b_alpha = hLay.b_alpha;
this.b_alpha_isfixed = hLay.b_alpha_isfixed;
let ret = this.drawBack(
(hLay.b_do)
? (hLay.b_do == 'Sprite' ?{b_pic: hLay.b_pic} :{b_color: hLay.b_color})
: {b_pic: ''}
);
this.setFfs(hLay);
this.txs.playback(hLay.txs);
// addButton(hArg: HArg): boolean
const aBtn: string[] = hLay.btns;
aBtn.forEach(v=> ret = ret || this.addButton(JSON.parse(v)));
if (fncComp != undefined) fncComp();
return ret;
}
snapshot(rnd: Renderer, re: ()=> void) {
rnd.render(this.cnt, undefined, false);
this.txs.snapshot(rnd, re);
}
snapshot_end() {this.txs.snapshot_end();}
dump(): string {
let aPixiObj: string[] = [];
const len = this.cnt.children.length;
for (let i=0; i<len; ++i) {
const e = this.cnt.children[i];
const cls = (e instanceof Sprite) ?"Sprite" :(
(e instanceof Graphics) ?"Graphics" :(
(e instanceof Container) ?"Container" :"?"
)
);
aPixiObj.push(`{"class":"${cls}", "name":"${e.name}", "alpha":${e.alpha || 1}, "x":${e.x}, "y":${e.y}, "visible":"${
e.visible}"}`);
}
return super.dump() +`, "enabled":"${this.enabled}", ${this.txs.dump()
}, "b_pic":"${this.b_pic}", "b_color":"${this.b_color
}", "b_alpha":${this.b_alpha}, "b_alpha_isfixed":"${this.b_alpha_isfixed
}", "b_width":${this.infTL.$width}, "b_height":${this.infTL.$height
}, "pixi_obj":[${aPixiObj.join(',')}]`;
}
}