feat: 流式组合器 & 部分接口调整

This commit is contained in:
unanmed 2025-10-21 21:56:38 +08:00
parent 146866d51f
commit 4ab87c613a
5 changed files with 340 additions and 116 deletions

View File

@ -1,5 +1,5 @@
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { IRect, ITexture, ITextureAnimater, ITextureRenderable } from './types'; import { ITexture, ITextureAnimater, ITextureRenderable } from './types';
/** /**
* *
@ -54,7 +54,7 @@ export class TextureRowAnimater extends FrameBasedAnimater<void> {
for (let i = 0; i < this.frames; i++) { for (let i = 0; i < this.frames; i++) {
const renderable: ITextureRenderable = { const renderable: ITextureRenderable = {
source: this.texture!.source, source: this.texture!.source,
rect: { x: ox, y: i * h + oy, w, h } rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h })
}; };
yield renderable; yield renderable;
} }
@ -71,7 +71,7 @@ export class TextureRowAnimater extends FrameBasedAnimater<void> {
if (i === this.frames) i = 0; if (i === this.frames) i = 0;
const renderable: ITextureRenderable = { const renderable: ITextureRenderable = {
source: this.texture!.source, source: this.texture!.source,
rect: { x: ox, y: i * h + oy, w, h } rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h })
}; };
yield renderable; yield renderable;
} }
@ -91,7 +91,7 @@ export class TextureColumnAnimater extends FrameBasedAnimater<void> {
for (let i = 0; i < this.frames; i++) { for (let i = 0; i < this.frames; i++) {
const renderable: ITextureRenderable = { const renderable: ITextureRenderable = {
source: this.texture!.source, source: this.texture!.source,
rect: { x: i * width + ox, y: oy, w, h } rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h })
}; };
yield renderable; yield renderable;
} }
@ -108,7 +108,7 @@ export class TextureColumnAnimater extends FrameBasedAnimater<void> {
if (i === this.frames) i = 0; if (i === this.frames) i = 0;
const renderable: ITextureRenderable = { const renderable: ITextureRenderable = {
source: this.texture!.source, source: this.texture!.source,
rect: { x: i * w + ox, y: oy, w, h } rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h })
}; };
yield renderable; yield renderable;
} }
@ -177,87 +177,33 @@ export class TextureScanAnimater
for (let y = 0; y < this.frameY; y++) { for (let y = 0; y < this.frameY; y++) {
for (let x = 0; x < this.frameX; x++) { for (let x = 0; x < this.frameX; x++) {
const rect: IRect = { x: x * w, y: y * h, w, h };
const data: ITextureRenderable = { const data: ITextureRenderable = {
source: texture.source, source: texture.source,
rect rect: texture.clampRect({ x: x * w, y: y * h, w, h })
}; };
yield data; yield data;
} }
} }
} }
cycled(): Generator<ITextureRenderable, void> | null { *cycled(): Generator<ITextureRenderable, void> | null {
throw new Error('Method not implemented.'); const texture = this.texture;
} if (!texture) return null;
}
export interface IAnimaterTranslatedInit<T> { const w = this.width;
/** 以此矩形作为参考矩形 */ const h = this.height;
readonly rect: Readonly<IRect>;
/** 传递给原先的动画控制器的参数 */
readonly data: T;
/** 原本所属的纹理 */
readonly texture: ITexture<unknown, T>;
}
type AdderImplements<T> = ITextureAnimater<void, IAnimaterTranslatedInit<T>>;
type AdderTexture<T> = ITexture<void, IAnimaterTranslatedInit<T>>;
/**
*
*
*/
export class TextureAnimaterTranslated<T> implements AdderImplements<T> {
texture: AdderTexture<T> | null = null;
create(texture: ITexture): void {
if (this.texture) {
logger.warn(70);
return;
}
this.texture = texture;
}
*output(
ani: Generator<ITextureRenderable>,
origin: Readonly<IRect>,
rect: Readonly<IRect>
) {
const { x: ox, y: oy } = origin;
const { x: nx, y: ny } = rect;
const source = this.texture!.source;
let index = 0;
while (true) { while (true) {
const next = ani.next(); const x = index % this.frameX;
if (next.done) break; const y = Math.floor(index / this.frameX);
const renderable = next.value; const data: ITextureRenderable = {
const { x, y, w, h } = renderable.rect; source: texture.source,
const translated: IRect = { x: x - ox + nx, y: y - oy + ny, w, h }; rect: texture.clampRect({ x: x * w, y: y * h, w, h })
const res: ITextureRenderable = {
source,
rect: translated
}; };
yield res; yield data;
}
}
open( if (index === this.frames) index = 0;
init: IAnimaterTranslatedInit<T>
): Generator<ITextureRenderable> | null {
const ani = init.texture.dynamic(init.data);
const origin = init.texture.static().rect;
if (!ani || !origin) return null;
return this.output(ani, origin, init.rect);
} }
cycled(
init: IAnimaterTranslatedInit<T>
): Generator<ITextureRenderable> | null {
const ani = init.texture.cycled(init.data);
const origin = init.texture.static().rect;
if (!ani || !origin) return null;
return this.output(ani, origin, init.rect);
} }
} }

View File

@ -4,7 +4,6 @@ import {
MaxRectsPacker, MaxRectsPacker,
Rectangle Rectangle
} from 'maxrects-packer'; } from 'maxrects-packer';
import { IAnimaterTranslatedInit, TextureAnimaterTranslated } from './animater';
import { Texture } from './texture'; import { Texture } from './texture';
import { import {
IRect, IRect,
@ -26,12 +25,6 @@ interface IndexMarkedComposedData {
readonly index: number; readonly index: number;
} }
type TranslatedComposer<D, T> = ITextureComposer<
D,
void,
IAnimaterTranslatedInit<T>
>;
export interface IGridComposerData { export interface IGridComposerData {
/** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */ /** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */
readonly width: number; readonly width: number;
@ -39,8 +32,8 @@ export interface IGridComposerData {
readonly height: number; readonly height: number;
} }
export class TextureGridComposer<T> export class TextureGridComposer
implements TranslatedComposer<IGridComposerData, T> implements ITextureComposer<IGridComposerData>
{ {
/** /**
* *
@ -85,9 +78,7 @@ export class TextureGridComposer<T>
} }
} }
const texture = new Texture<void, IAnimaterTranslatedInit<T>>(canvas); const texture = new Texture(canvas);
texture.animated(new TextureAnimaterTranslated(), void 0);
const composed: ITextureComposedData = { const composed: ITextureComposedData = {
texture, texture,
assetMap: map assetMap: map
@ -99,7 +90,7 @@ export class TextureGridComposer<T>
*compose( *compose(
input: Iterable<ITexture>, input: Iterable<ITexture>,
data: IGridComposerData data: IGridComposerData
): Generator<ITextureComposedData> { ): Generator<ITextureComposedData, void> {
const arr = [...input]; const arr = [...input];
const rows = Math.floor(this.maxWidth / data.width); const rows = Math.floor(this.maxWidth / data.width);
@ -125,8 +116,8 @@ interface MaxRectsRectangle extends IRectangle {
readonly data: ITexture; readonly data: ITexture;
} }
export class TextureMaxRectsComposer<T> export class TextureMaxRectsComposer
implements TranslatedComposer<IMaxRectsComposerData, T> implements ITextureComposer<IMaxRectsComposerData>
{ {
/** /**
* 使 Max Rects {@link IMaxRectsComposerData} * 使 Max Rects {@link IMaxRectsComposerData}
@ -142,7 +133,7 @@ export class TextureMaxRectsComposer<T>
*compose( *compose(
input: Iterable<ITexture>, input: Iterable<ITexture>,
data: IMaxRectsComposerData data: IMaxRectsComposerData
): Generator<ITextureComposedData<void, IAnimaterTranslatedInit<T>>> { ): Generator<ITextureComposedData, void> {
const packer = new MaxRectsPacker<MaxRectsRectangle>( const packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth, this.maxWidth,
this.maxHeight, this.maxHeight,
@ -173,10 +164,7 @@ export class TextureMaxRectsComposer<T>
const source = renderable.source; const source = renderable.source;
ctx.drawImage(source, x, y, w, h, v.x, v.y, v.width, v.height); ctx.drawImage(source, x, y, w, h, v.x, v.y, v.width, v.height);
}); });
const texture = new Texture<void, IAnimaterTranslatedInit<T>>( const texture = new Texture(canvas);
canvas
);
texture.animated(new TextureAnimaterTranslated(), void 0);
const data: ITextureComposedData = { const data: ITextureComposedData = {
texture, texture,
assetMap: map assetMap: map
@ -193,8 +181,8 @@ interface RectProcessed {
readonly attrib: Float32Array; readonly attrib: Float32Array;
} }
export class TextureMaxRectsWebGL2Composer<T> export class TextureMaxRectsWebGL2Composer
implements TranslatedComposer<IMaxRectsComposerData, T> implements ITextureComposer<IMaxRectsComposerData>
{ {
/** 使用的画布 */ /** 使用的画布 */
readonly canvas: HTMLCanvasElement; readonly canvas: HTMLCanvasElement;
@ -416,7 +404,7 @@ export class TextureMaxRectsWebGL2Composer<T>
*compose( *compose(
input: Iterable<ITexture>, input: Iterable<ITexture>,
data: IMaxRectsComposerData data: IMaxRectsComposerData
): Generator<ITextureComposedData<void, IAnimaterTranslatedInit<T>>> { ): Generator<ITextureComposedData, void> {
this.opWidth = 0; this.opWidth = 0;
this.opHeight = 0; this.opHeight = 0;
@ -441,10 +429,7 @@ export class TextureMaxRectsWebGL2Composer<T>
for (const bin of packer.bins) { for (const bin of packer.bins) {
const { texMap, attrib } = this.processRects(bin.rects, indexMap); const { texMap, attrib } = this.processRects(bin.rects, indexMap);
this.renderAtlas(attrib); this.renderAtlas(attrib);
const texture = new Texture<void, IAnimaterTranslatedInit<T>>( const texture = new Texture(this.canvas);
this.canvas
);
texture.animated(new TextureAnimaterTranslated(), void 0);
const data: ITextureComposedData = { const data: ITextureComposedData = {
texture, texture,
assetMap: texMap assetMap: texMap

View File

@ -0,0 +1,179 @@
import {
Bin,
IOption,
IRectangle,
MaxRectsPacker,
Rectangle
} from 'maxrects-packer';
import { Texture } from './texture';
import {
IRect,
ITexture,
ITextureComposedData,
ITextureStreamComposer
} from './types';
export class TextureGridStreamComposer implements ITextureStreamComposer<void> {
readonly rows: number;
readonly cols: number;
private nowIndex: number = 0;
private nowCanvas: HTMLCanvasElement;
private nowCtx: CanvasRenderingContext2D;
private nowMap: Map<ITexture, Readonly<IRect>>;
/**
*
*
* @param width
* @param height
* @param maxWidth
* @param maxHeight
*/
constructor(
readonly width: number,
readonly height: number,
readonly maxWidth: number,
readonly maxHeight: number
) {
this.rows = Math.floor(maxHeight / height);
this.cols = Math.floor(maxWidth / width);
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowMap = new Map();
}
private nextCanvas() {
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowMap = new Map();
}
*add(textures: Iterable<ITexture>): Generator<ITextureComposedData, void> {
let index = this.nowIndex;
const max = this.cols * this.rows;
for (const tex of textures) {
const nowRow = Math.floor(index / this.cols);
const nowCol = index % this.cols;
const { source, rect } = tex.static();
const { x: cx, y: cy, w: cw, h: ch } = rect;
const x = nowRow * this.width;
const y = nowCol * this.height;
this.nowCtx.drawImage(source, cx, cy, cw, ch, x, y, cw, ch);
this.nowMap.set(tex, { x, y, w: cw, h: ch });
index++;
if (index === max) {
const data: ITextureComposedData = {
texture: new Texture(this.nowCanvas),
assetMap: this.nowMap
};
yield data;
this.nextCanvas();
}
}
}
close(): void {
// We need to do nothing.
}
}
interface MaxRectsRectangle extends IRectangle {
/** 这个矩形对应的贴图对象 */
readonly data: ITexture;
}
export class TextureMaxRectsStreamComposer
implements ITextureStreamComposer<void>
{
/** Max Rects 打包器 */
readonly packer: MaxRectsPacker<MaxRectsRectangle>;
private nowCanvas: HTMLCanvasElement;
private nowCtx: CanvasRenderingContext2D;
private nowMap: Map<ITexture, Readonly<IRect>>;
private nowBins: number;
/**
* 使 Max Rects
* @param maxWidth
* @param maxHeight
* @param padding
* @param options
*/
constructor(
readonly maxWidth: number,
readonly maxHeight: number,
readonly padding: number,
options: IOption
) {
this.packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth,
this.maxHeight,
padding,
options
);
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowMap = new Map();
this.nowBins = 0;
}
private nextCanvas() {
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowMap = new Map();
}
*add(textures: Iterable<ITexture>): Generator<ITextureComposedData, void> {
const arr = [...textures];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.static().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
});
this.packer.addArray(rects);
const bins = this.packer.bins;
if (bins.length === 0) return;
const toYield: Bin<MaxRectsRectangle>[] = [bins[bins.length - 1]];
if (bins.length > this.nowBins) {
toYield.push(...bins.slice(this.nowBins));
}
for (const bin of toYield) {
bin.rects.forEach(v => {
if (this.nowMap.has(v.data)) return;
const target: IRect = {
x: v.x,
y: v.y,
w: v.width,
h: v.height
};
this.nowMap.set(v.data, target);
const { source, rect } = v.data.static();
const { x: cx, y: cy, w: cw, h: ch } = rect;
this.nowCtx.drawImage(source, cx, cy, cw, ch, v.x, v.y, cw, ch);
});
const data: ITextureComposedData = {
texture: new Texture(this.nowCanvas),
assetMap: this.nowMap
};
yield data;
this.nextCanvas();
}
this.nowBins = bins.length;
}
close(): void {
// We need to do nothing.
}
}

View File

@ -1,5 +1,6 @@
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { import {
IRect,
ITexture, ITexture,
ITextureAnimater, ITextureAnimater,
ITextureRenderable, ITextureRenderable,
@ -13,15 +14,23 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
width: number; width: number;
height: number; height: number;
private cx: number; /** 裁剪矩形的左边框位置 */
private cy: number; private cl: number;
/** 裁剪矩形的上边框位置 */
private ct: number;
/** 裁剪矩形的右边框位置 */
private cr: number;
/** 裁剪矩形的下边框位置 */
private cb: number;
constructor(source: SizedCanvasImageSource) { constructor(source: SizedCanvasImageSource) {
this.source = source; this.source = source;
this.width = source.width; this.width = source.width;
this.height = source.height; this.height = source.height;
this.cx = 0; this.cl = 0;
this.cy = 0; this.ct = 0;
this.cr = 0;
this.cb = 0;
} }
/** /**
@ -42,8 +51,8 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
const top = Math.max(0, y); const top = Math.max(0, y);
const right = Math.min(this.width, r); const right = Math.min(this.width, r);
const bottom = Math.min(this.height, b); const bottom = Math.min(this.height, b);
this.cx = left; this.cl = left;
this.cy = top; this.ct = top;
this.width = right - left; this.width = right - left;
this.height = bottom - top; this.height = bottom - top;
} }
@ -63,19 +72,33 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
} }
static(): ITextureRenderable { static(): ITextureRenderable {
const renderable: ITextureRenderable = { return {
source: this.source, source: this.source,
rect: { x: this.cx, y: this.cy, w: this.width, h: this.height } rect: { x: this.cl, y: this.ct, w: this.width, h: this.height }
}; };
return renderable;
} }
dynamic(data: A): Generator<ITextureRenderable> | null { clampRect(rect: Readonly<IRect>): Readonly<IRect> {
const l = Math.max(this.cl, this.cl + rect.x);
const t = Math.max(this.ct, this.ct + rect.y);
const r = Math.min(l + rect.w, this.cr);
const b = Math.min(t + rect.h, this.cb);
return { x: l, y: t, w: r - b, h: b - t };
}
clipped(rect: Readonly<IRect>): ITextureRenderable {
return {
source: this.source,
rect: this.clampRect(rect)
};
}
dynamic(data: A): Generator<ITextureRenderable, void> | null {
if (!this.animater) return null; if (!this.animater) return null;
return this.animater.open(data); return this.animater.open(data);
} }
cycled(data: A): Generator<ITextureRenderable> | null { cycled(data: A): Generator<ITextureRenderable, void> | null {
if (!this.animater) return null; if (!this.animater) return null;
return this.animater.cycled(data); return this.animater.cycled(data);
} }
@ -86,4 +109,66 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
} }
this.animater = null; this.animater = null;
} }
/**
*
*
* @param texture
* @param animate
* @param ox
* @param oy
*/
static *translated(
texture: ITexture,
animate: Generator<ITextureRenderable, void> | null,
ox: number,
oy: number
): Generator<ITextureRenderable, void> | null {
if (!animate) return null;
while (true) {
const next = animate.next();
if (next.done) break;
const renderable = next.value;
const { x, y, w, h } = renderable.rect;
const translated: IRect = { x: x + ox, y: y + oy, w, h };
const res: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect(translated)
};
yield res;
}
}
/**
* `fx` `fy`
* @param texture
* @param origin
* @param animate
* @param fx
* @param fy
*/
static *fixed(
texture: ITexture,
origin: ITexture,
animate: Generator<ITextureRenderable, void> | null,
fx: number,
fy: number
): Generator<ITextureRenderable, void> | null {
if (!animate) return null;
const { x: ox, y: oy } = origin.static().rect;
while (true) {
const next = animate.next();
if (next.done) break;
const renderable = next.value;
const { x, y, w, h } = renderable.rect;
const translated: IRect = { x: x - ox + fx, y: y - oy + fy, w, h };
const res: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect(translated)
};
yield res;
}
}
} }

View File

@ -19,14 +19,14 @@ export interface ITextureRenderable {
readonly rect: Readonly<IRect>; readonly rect: Readonly<IRect>;
} }
export interface ITextureComposedData<T = unknown, A = unknown> { export interface ITextureComposedData {
/** 这个纹理图集的贴图对象 */ /** 这个纹理图集的贴图对象 */
readonly texture: ITexture<T, A>; readonly texture: ITexture;
/** 每个参与组合的贴图对应到图集对象的矩形范围 */ /** 每个参与组合的贴图对应到图集对象的矩形范围 */
readonly assetMap: Map<ITexture, Readonly<IRect>>; readonly assetMap: Map<ITexture, Readonly<IRect>>;
} }
export interface ITextureComposer<T, C, I> { export interface ITextureComposer<T> {
/** /**
* *
* @param input * @param input
@ -35,7 +35,24 @@ export interface ITextureComposer<T, C, I> {
compose( compose(
input: Iterable<ITexture>, input: Iterable<ITexture>,
data: T data: T
): Generator<ITextureComposedData<C, I>, void>; ): Generator<ITextureComposedData, void>;
}
export interface ITextureStreamComposer<T> {
/**
*
* @param textures
* @param data
*/
add(
textures: Iterable<ITexture>,
data: T
): Generator<ITextureComposedData, void>;
/**
* 使
*/
close(): void;
} }
export interface ITextureSplitter<T> { export interface ITextureSplitter<T> {
@ -106,6 +123,18 @@ export interface ITexture<T = unknown, A = unknown> {
*/ */
static(): ITextureRenderable; static(): ITextureRenderable;
/**
*
* @param rect
*/
clampRect(rect: Readonly<IRect>): Readonly<IRect>;
/**
*
* @param rect
*/
clipped(rect: Readonly<IRect>): ITextureRenderable;
/** /**
* *
* @param data * @param data