149 lines
8.8 KiB
JavaScript
149 lines
8.8 KiB
JavaScript
// test/render/fakepixi.js — a minimal headless stand-in for PIXI 7 + pixi-spine + a 2D canvas document, enough to
|
|
// construct and update render/units.js views and the procedural textures of render/textures.js in Node.
|
|
// Records every canvas made (`canvases`) and every BaseTexture created / destroyed, so tests can assert what a
|
|
// view allocated. Install before the first render call; `restore()` puts the previous globals back.
|
|
|
|
const point = (x = 0, y = 0) => ({ x, y, set(a = 0, b = a) { this.x = a; this.y = b; } });
|
|
|
|
// every CanvasRenderingContext2D method is a no-op; gradients / measurements return plausible objects
|
|
function context2d(canvas) {
|
|
const store = {};
|
|
return new Proxy(store, {
|
|
get(t, k) {
|
|
if (k in t) return t[k];
|
|
if (k === 'canvas') return canvas;
|
|
if (k === 'measureText') return (s) => ({ width: String(s).length * 10, actualBoundingBoxAscent: 8, actualBoundingBoxDescent: 2 });
|
|
if (k === 'createLinearGradient' || k === 'createRadialGradient' || k === 'createConicGradient' || k === 'createPattern') return () => ({ addColorStop() {} });
|
|
if (k === 'getImageData' || k === 'createImageData') return (x, y, w, h) => { const W = w ?? x?.width ?? 1, H = h ?? x?.height ?? 1; return { width: W, height: H, data: new Uint8ClampedArray(W * H * 4) }; };
|
|
if (k === 'getTransform') return () => ({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });
|
|
if (typeof k === 'symbol') return undefined;
|
|
return () => {};
|
|
},
|
|
set(t, k, v) { t[k] = v; return true; },
|
|
});
|
|
}
|
|
|
|
export function installFakePixi() {
|
|
const canvases = [];
|
|
const baseTextures = [];
|
|
const prev = { PIXI: globalThis.PIXI, document: globalThis.document, had: { PIXI: 'PIXI' in globalThis, document: 'document' in globalThis } };
|
|
|
|
class DisplayObject {
|
|
constructor() {
|
|
this.children = []; this.parent = null; this.visible = true; this.renderable = true; this.alpha = 1; this.tint = 0xffffff;
|
|
this.position = point(); this.scale = point(1, 1); this.anchor = point(); this.pivot = point(); this.skew = point();
|
|
this.rotation = 0; this.zIndex = 0; this.destroyed = false; this.blendMode = 0; this.sortableChildren = false;
|
|
this._w = null; this._h = null;
|
|
}
|
|
get x() { return this.position.x; } set x(v) { this.position.x = v; }
|
|
get y() { return this.position.y; } set y(v) { this.position.y = v; }
|
|
get width() { return this._w ?? (this.texture ? this.texture.width * Math.abs(this.scale.x) : 0); }
|
|
set width(v) { this._w = v; }
|
|
get height() { return this._h ?? (this.texture ? this.texture.height * Math.abs(this.scale.y) : 0); }
|
|
set height(v) { this._h = v; }
|
|
addChild(...cs) { for (const c of cs) { if (c.parent && c.parent !== this) c.parent.removeChild(c); if (!this.children.includes(c)) this.children.push(c); c.parent = this; } return cs[0]; }
|
|
addChildAt(c) { return this.addChild(c); }
|
|
removeChild(...cs) { for (const c of cs) { const i = this.children.indexOf(c); if (i >= 0) this.children.splice(i, 1); c.parent = null; } return cs[0]; }
|
|
removeChildren() { const cs = this.children.slice(); for (const c of cs) this.removeChild(c); return cs; }
|
|
getLocalBounds() { return { x: -50, y: -100, width: 100, height: 100 }; }
|
|
getBounds() { return { x: 0, y: 0, width: 10, height: 10 }; }
|
|
toGlobal(p) { return { x: p.x, y: p.y }; }
|
|
on() { return this; } off() { return this; } once() { return this; }
|
|
destroy() { this.destroyed = true; this.parent?.removeChild(this); }
|
|
}
|
|
class Container extends DisplayObject {}
|
|
class BaseTexture {
|
|
constructor(resource = null) {
|
|
this.resource = resource; this.valid = true; this.destroyed = false; this.width = resource?.width || 1; this.height = resource?.height || 1;
|
|
this._glTextures = {}; baseTextures.push(this);
|
|
}
|
|
static from(src) { return new BaseTexture(src); }
|
|
update() {} setStyle() {} dispose() {} on() { return this; } once() { return this; } off() { return this; }
|
|
destroy() { this.destroyed = true; }
|
|
}
|
|
class Rectangle { constructor(x = 0, y = 0, width = 0, height = 0) { Object.assign(this, { x, y, width, height }); } }
|
|
class Texture {
|
|
constructor(base, frame) {
|
|
this.baseTexture = base || new BaseTexture();
|
|
this.frame = frame || new Rectangle(0, 0, this.baseTexture.width, this.baseTexture.height);
|
|
this.width = this.frame.width; this.height = this.frame.height; this.destroyed = false; this.orig = this.frame;
|
|
}
|
|
static from(src) { return new Texture(BaseTexture.from(src)); }
|
|
get valid() { return this.baseTexture.valid; }
|
|
update() {} on() { return this; } once() { return this; }
|
|
destroy(base) { this.destroyed = true; if (base) this.baseTexture.destroy(); }
|
|
}
|
|
Texture.EMPTY = new Texture(new BaseTexture({ width: 1, height: 1 }));
|
|
Texture.WHITE = new Texture(new BaseTexture({ width: 16, height: 16 }));
|
|
class Sprite extends Container { constructor(tex) { super(); this.texture = tex || Texture.EMPTY; } }
|
|
class TilingSprite extends Sprite {}
|
|
class Graphics extends Container {
|
|
constructor() { super(); this.geometry = { graphicsData: [] }; }
|
|
clear() { return this; } beginFill() { return this; } endFill() { return this; } lineStyle() { return this; } moveTo() { return this; } lineTo() { return this; }
|
|
drawRect() { return this; } drawCircle() { return this; } drawEllipse() { return this; } drawPolygon() { return this; } drawRoundedRect() { return this; } closePath() { return this; }
|
|
quadraticCurveTo() { return this; } bezierCurveTo() { return this; } arc() { return this; }
|
|
}
|
|
class Text extends Sprite {
|
|
constructor(text = '', style = {}) { super(); this.text = text; this.style = style; this.resolution = 1; this.rasters = 0; this.texture = new Texture(new BaseTexture({ width: 1, height: 1 })); }
|
|
updateText() { this.rasters++; }
|
|
}
|
|
class BitmapText extends Text {}
|
|
class Matrix { constructor() { this.a = 1; this.b = 0; this.c = 0; this.d = 1; this.tx = 0; this.ty = 0; } set() { return this; } translate() { return this; } scale() { return this; } identity() { return this; } }
|
|
class RenderTexture extends Texture { static create({ width = 1, height = 1 } = {}) { return new RenderTexture(new BaseTexture({ width, height })); } resize() {} }
|
|
const track = () => ({ timeScale: 1, trackTime: 0, animationEnd: 1, animationStart: 0, loop: false, mixDuration: 0, animation: { duration: 1, name: '' }, listener: null });
|
|
class Spine extends Container {
|
|
constructor(data) {
|
|
super();
|
|
this.spineData = { findAnimation: () => ({ duration: 1 }), ...data };
|
|
this.autoUpdate = true;
|
|
this.stateData = { defaultMix: 0, setMix() {} };
|
|
this.skeleton = { slots: [], setToSetupPose() {}, setSlotsToSetupPose() {}, updateWorldTransform() {}, findSlot: () => null };
|
|
const tracks = [];
|
|
this.state = {
|
|
tracks, timeScale: 1,
|
|
setAnimation: (i, name, loop) => { const e = { ...track(), loop, animation: { duration: 1, name } }; tracks[i] = e; return e; },
|
|
addAnimation: (i, name, loop) => { const e = { ...track(), loop, animation: { duration: 1, name } }; return e; },
|
|
setEmptyAnimation: () => track(), addEmptyAnimation: () => track(), clearTrack() {}, clearTracks() {}, addListener() {}, update() {}, apply() {},
|
|
};
|
|
}
|
|
update() {}
|
|
}
|
|
const modes = new Proxy({}, { get: () => 0 });
|
|
const P = {
|
|
Container, Sprite, TilingSprite, Graphics, Text, BitmapText, Texture, BaseTexture, Rectangle, Matrix, RenderTexture, DisplayObject,
|
|
ParticleContainer: Container,
|
|
BLEND_MODES: { NORMAL: 0, ADD: 1, MULTIPLY: 2, SCREEN: 3 }, MIPMAP_MODES: modes, SCALE_MODES: modes, WRAP_MODES: modes, ALPHA_MODES: modes,
|
|
BitmapFont: { from() {} },
|
|
utils: { TextureCache: {}, BaseTextureCache: {} },
|
|
spine: { Spine },
|
|
};
|
|
globalThis.PIXI = P;
|
|
globalThis.document = {
|
|
createElement(tag) {
|
|
const c = { tag, width: 300, height: 150, style: {} };
|
|
if (tag === 'canvas') { c.getContext = () => (c._ctx || (c._ctx = context2d(c))); canvases.push(c); }
|
|
return c;
|
|
},
|
|
fonts: undefined,
|
|
};
|
|
return {
|
|
P, canvases, baseTextures,
|
|
restore() {
|
|
if (prev.had.PIXI) globalThis.PIXI = prev.PIXI; else delete globalThis.PIXI;
|
|
if (prev.had.document) globalThis.document = prev.document; else delete globalThis.document;
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Layers + context for a UnitView (render/units.js ViewCtx). */
|
|
export function fakeViewCtx(P, extra = {}) {
|
|
const layers = {};
|
|
for (const k of ['ground', 'anim', 'overlay', 'shadow', 'groundFx', 'fxNormal', 'units', 'fxAdd', 'bars', 'text', 'screen']) layers[k] = new P.Container();
|
|
return {
|
|
P, layers, settings: { damageNumbers: true, quality: 'high' }, fx: null,
|
|
heightAt: () => 0, animRate: () => 1, timeScale: () => 1, lookupDef: () => null, crowded: () => false,
|
|
frameNo: () => 0, impostorInterval: () => 0, clipAllowed: () => true, surfaceLayer: () => null,
|
|
...extra,
|
|
};
|
|
}
|