/* ***** 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, argChk_Boolean, argChk_Num, getFn} 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: '${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 = 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 (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 = argChk_Num(hArg, 'b_alpha', this.b_alpha);
this.b_alpha_isfixed = 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
: `${ch}`;
}
}
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)=> `${tx}`;
// 囲んだ領域は履歴で非表示
}
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('
'));
this.aSpan.push(add_htm); // ここでaSpan末尾に追加しないと続かない
this.aSpan_bk = this.aSpan;
this.aSpan = [];
return; // breakではない
}
if (this.firstCh) { // 1文字目にルビが無い場合は不可視ルビで、行揃え
this.firstCh = false;
add_htm = '
';
}
else {
add_htm = '
';
}
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(/^