skynovel
Version:
webgl novelgame framework
344 lines (301 loc) • 10.6 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 {CmnLib, int, IEvtMng} from './CmnLib';
import {HArg, IMain} from './CmnInterface';
import {Config} from './Config';
import {SysBase} from './SysBase';
import {Sprite, Container, Texture, BLEND_MODES, utils, Loader, LoaderResource, AnimatedSprite} from 'pixi.js';
import {EventListenerCtn} from './EventListenerCtn';
export interface IFncCompSpr { (sp: Sprite): void; };
interface Iface {
fn : string;
dx : number;
dy : number;
blendmode : number;
};
interface Ihface { [name: string]: Iface; };
interface IResAniSpr {
aTex : Texture[];
meta : {
animationSpeed? :number;
};
}
export class GrpLayer extends Layer {
private static readonly elc = new EventListenerCtn;
private static hFace : Ihface = {};
private static main : IMain;
private static cfg : Config;
private static sys : SysBase;
static init(main: IMain, cfg: Config, sys: SysBase): void {
GrpLayer.main = main;
GrpLayer.cfg = cfg;
GrpLayer.sys = sys;
if (GrpLayer.sys.crypto) GrpLayer.preThen = GrpLayer.preThen4Cripto;
}
private static evtMng : IEvtMng;
static setEvtMng(evtMng: IEvtMng) {GrpLayer.evtMng = evtMng;}
static destroy() {
GrpLayer.elc.clear();
GrpLayer.hFace = {};
GrpLayer.hFn2ResAniSpr = {};
}
private csvFn = '';
private sBkFn = '';
private sBkFace = '';
static hFn2ResAniSpr : {[name: string]: IResAniSpr} = {};
lay(hArg: HArg): boolean {
const fn = hArg.fn;
const face = hArg.face ?? '';
if (! fn) {
super.lay(hArg);
if (this.cnt.children.length > 0) this.setPos(hArg);
this.sBkFn = '';
this.csvFn = this.sBkFace = face;
return false;
}
const inFn = 'fn' in hArg;
const inFace = 'face' in hArg;
this.clearLay({filter: 'true'});
if (inFn) this.sBkFn = fn; // clearLay()後に置く事
if (inFace) this.sBkFace = face;
super.lay(hArg);
hArg.dx = 0;
hArg.dy = 0;
return GrpLayer.csv2Sprites(
this.csvFn = fn + (face ? ','+ face : ''),
this.cnt,
sp=> {
Layer.setXY(sp, hArg, this.cnt, true);
// if (hArg.page == 'fore') this.rsvEvent(sp); // ======
// [lay page=fore]のみswfアニメ終了イベント発生
},
GrpLayer.fncAllComp
);
}
private static fncDefAllComp = (isStop: boolean)=> {if (isStop) GrpLayer.main.resume()};
private static fncAllComp = GrpLayer.fncDefAllComp;
private static ldrHFn: {[name: string]: number} = {};
static csv2Sprites(csv: string, parent: Container, fncFirstComp: IFncCompSpr, fncAllComp: (isStop: boolean)=> void = ()=> {}): boolean {
const aComp : {fn: string, fnc: IFncCompSpr}[] = [];
let needLoad = false;
const ldr = new Loader();
csv.split(',').forEach((fn, i)=> {
if (! fn) throw 'face属性に空要素が含まれます';
// 差分絵を重ねる
const f = GrpLayer.hFace[fn] || {
fn: fn,
dx: 0,
dy: 0,
blendmode: BLEND_MODES.NORMAL
};
const fnc = (i == 0) ?fncFirstComp :(sp: Sprite)=> {
sp.x = f.dx;
sp.y = f.dy;
sp.blendMode = f.blendmode;
};
aComp.push({fn: f.fn, fnc: fnc});
if (f.fn in GrpLayer.hFn2ResAniSpr) return;
if (f.fn in utils.TextureCache) return;
if (f.fn in Loader.shared.resources) return;
if (f.fn in GrpLayer.ldrHFn) return;
GrpLayer.ldrHFn[f.fn] = 0;
needLoad = true;
const path = GrpLayer.cfg.searchPath(f.fn, Config.EXT_SPRITE);
const xt = this.sys.crypto
? {xhrType: (path.slice(-5) == '.json')
? LoaderResource.XHR_RESPONSE_TYPE.TEXT
: LoaderResource.XHR_RESPONSE_TYPE.BUFFER}
: {};
ldr.add(f.fn, path, xt);
});
const fncLoaded = (res: any)=> {
for (const v of aComp) {
const sp = GrpLayer.mkSprite(v.fn, res);
parent.addChild(sp);
v.fnc(sp);
}
fncAllComp(needLoad);
}
if (needLoad) {
ldr.pre((res: LoaderResource, next: Function)=> res.load(()=> {
this.sys.pre(res.extension, res.data)
.then(r=> GrpLayer.preThen(r, res, next))
.catch(e=> this.main.errScript(`Graphic ロード失敗です fn:${res.name} ${e}`, false));
}))
.load((_ldr: any, hRes: any)=> fncLoaded(hRes));
}
else fncLoaded(utils.TextureCache);
return needLoad;
}
private static preThen = (_r: any, _res: LoaderResource, next: Function)=> next();
private static preThen4Cripto(r: any, res: LoaderResource, next: Function): void {
res.data = r;
if (res.extension == 'bin') {
if (res.data instanceof HTMLImageElement) {
res.type = LoaderResource.TYPE.IMAGE;
URL.revokeObjectURL(res.data.src);
}
else if (res.data instanceof HTMLVideoElement) {
res.type = LoaderResource.TYPE.VIDEO;
URL.revokeObjectURL(res.data.src);
}
}
if (res.extension != 'json') {next(); return;}
const o = res.data = JSON.parse(r);
res.type = LoaderResource.TYPE.JSON;
if (! o.meta?.image) {next(); return;}
const fn = CmnLib.getFn(o.meta.image);
const url = GrpLayer.cfg.searchPath(fn, Config.EXT_SPRITE);
(new Loader())
.pre((res2: LoaderResource, next2: Function)=> res2.load(()=> {
this.sys.pre(res2.extension, res2.data)
.then(r=> {
res2.data = r;
if (res2.data instanceof HTMLImageElement) {
res2.type = LoaderResource.TYPE.IMAGE;
const mime = `image/${CmnLib.getExt(o.meta.image)}`;
o.meta.image = GrpLayer.im2Base64(res2.data, mime);
res2.data = o.meta.image;
}
/* else if (res2.data instanceof HTMLVideoElement) {
res2.type = LoaderResource.TYPE.VIDEO;
o.meta.image = res2.data.src;
}*/
next2();
})
.catch(e=> this.main.errScript(`Graphic ロード失敗です fn:${res2.name} ${e}`, false));
}))
.add(fn, url, {xhrType: LoaderResource.XHR_RESPONSE_TYPE.BUFFER})
.load(()=> next());
}
private static im2Base64(img: HTMLImageElement, mime: string) {
const cvs = document.createElement('canvas');
cvs.width = img.width;
cvs.height = img.height;
const ctx = cvs.getContext('2d');
ctx?.drawImage(img, 0, 0);
return cvs.toDataURL(mime);
}
private static mkSprite(fn: string, res: LoaderResource): Sprite {
//console.log(`fn:GrpLayer.ts line:153 fn:${fn} a:%O b:%O c:%O`, GrpLayer.hFn2ResAniSpr[fn], utils.TextureCache[fn], Loader.shared.resources[fn]);
if (fn in utils.TextureCache) return new Sprite(Texture.from(fn));
const ras = GrpLayer.hFn2ResAniSpr[fn];
if (ras) {
const asp = new AnimatedSprite(ras.aTex);
asp.animationSpeed = ras.meta['animationSpeed'] ?? 1.0;
asp.play();
return asp;
}
const r = (res as any)[fn];
if (! r) return new Sprite; // ロード中にリソース削除
switch (r.type) {
case LoaderResource.TYPE.JSON: // アニメスプライト
const aFK: string[] = r.spritesheet._frameKeys;
const a_base_name = /([^\d]+)\d+\.(\w+)/.exec(aFK[0]);
if (a_base_name) {
const is = a_base_name[1].length;
const ie = -a_base_name[2].length - 1;
aFK.sort((a, b)=>
(int(a.slice(is, ie)) > int(b.slice(is, ie))) ?1 :-1
);
}
const aTex: Texture[] = [];
for (const v of aFK) aTex.push(Texture.from(v));
GrpLayer.hFn2ResAniSpr[r.name] = {aTex: aTex, meta: r.data.meta};
return GrpLayer.mkSprite(fn, res);
case LoaderResource.TYPE.VIDEO:
const hve = r.data as HTMLVideoElement;
GrpLayer.fn2Video[fn] = hve;
// NOTE: hve.loop = true; [wv]でもループ時はスルーするように
return new Sprite(Texture.from(r.data));
default: return new Sprite(r.texture);
}
}
static fn2Video : {[name: string]: HTMLVideoElement} = {};
static wv(hArg: HArg) {
// 動画ファイル名指定でいいかなと。だって、「ループ」「それは再生しつつ」
// 同じファイル名の別の動画の再生は待ちたい、なんて状況は普通は無いだろうと
const fn = hArg.fn;
if (! fn) throw 'fnは必須です';
const hve = GrpLayer.fn2Video[fn];
if (! hve) return false;
if (hve.ended) {delete GrpLayer.fn2Video[fn]; return false;}
const fnc = ()=> {
hve.removeEventListener('ended', fnc);
delete GrpLayer.fn2Video[fn];
this.main.resume();
};
hve.addEventListener('ended', fnc, {once: true, passive: true});
GrpLayer.evtMng.stdWait(
()=> {hve.pause(); fnc();},
CmnLib.argChk_Boolean(hArg, 'canskip', true)
); // stdWait()したらreturn true;
return true;
}
static ldPic(fn: string, fnc: (tx: Texture)=> void): void {
const url = GrpLayer.cfg.searchPath(fn, Config.EXT_SPRITE);
const tx = utils.TextureCache[url];
if (tx) {fnc(tx); return;}
const tx2 = Texture.from(url);
GrpLayer.elc.add(tx2.baseTexture, 'loaded', ()=> fnc(tx2)); // ノイズ対策
}
setPos(hArg: HArg): void {
Layer.setXY(
(this.cnt.children.length == 0) ?this.cnt :this.cnt.children[0],
hArg,
this.cnt,
true
);
}
/*private rsvEvent(_$do: DisplayObject): void {
const ldr:Loader = $do as Loader;
if (ldr == null) return;
const mc:MovieClip = ldr.content as MovieClip;
if (mc == null) return;
GrpLayer.elc.add(mc, Event.EXIT_FRAME, rsvEvent_ExitFrame);
}*/
static add_face(hArg: HArg): boolean {
const name = hArg.name;
if (! name) throw 'nameは必須です';
if (name in GrpLayer.hFace) throw '一つのname('+ name +')に対して同じ画像を複数割り当てられません';
const fn = hArg.fn ?? name;
GrpLayer.hFace[name] = {
fn: fn,
dx: CmnLib.argChk_Num(hArg, 'dx', 0) * CmnLib.retinaRate,
dy: CmnLib.argChk_Num(hArg, 'dy', 0) * CmnLib.retinaRate,
blendmode: Layer.cnvBlendmode(hArg.blendmode || '')
};
return false;
}
static clearFace2Name(): void {GrpLayer.hFace = {};}
clearLay(hArg: HArg): void {
super.clearLay(hArg);
for (const c of this.cnt.removeChildren()) c.destroy();
this.sBkFn = '';
this.sBkFace= '';
this.csvFn = '';
}
readonly record = ()=> Object.assign(super.record(), {
sBkFn : this.sBkFn,
sBkFace : this.sBkFace,
});
playback(hLay: any, fncComp: undefined | {(): void} = undefined): boolean {
super.playback(hLay);
if (hLay.sBkFn == '' && hLay.sBkFace == '') {
this.sBkFn = hLay.sBkFn;
this.sBkFace= hLay.sBkFace;
if (fncComp != undefined) fncComp();
return false;
}
if (fncComp != undefined) GrpLayer.fncAllComp = ()=> {
GrpLayer.fncAllComp = GrpLayer.fncDefAllComp;
fncComp();
};
return this.lay({fn: hLay.sBkFn, face: hLay.sBkFace, left: hLay.x, top: hLay.y});
}
readonly dump = ()=> super.dump() +`, "pic":"${this.csvFn}"`;
}