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 { 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++) {
const renderable: ITextureRenderable = {
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;
}
@ -71,7 +71,7 @@ export class TextureRowAnimater extends FrameBasedAnimater<void> {
if (i === this.frames) i = 0;
const renderable: ITextureRenderable = {
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;
}
@ -91,7 +91,7 @@ export class TextureColumnAnimater extends FrameBasedAnimater<void> {
for (let i = 0; i < this.frames; i++) {
const renderable: ITextureRenderable = {
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;
}
@ -108,7 +108,7 @@ export class TextureColumnAnimater extends FrameBasedAnimater<void> {
if (i === this.frames) i = 0;
const renderable: ITextureRenderable = {
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;
}
@ -177,87 +177,33 @@ export class TextureScanAnimater
for (let y = 0; y < this.frameY; y++) {
for (let x = 0; x < this.frameX; x++) {
const rect: IRect = { x: x * w, y: y * h, w, h };
const data: ITextureRenderable = {
source: texture.source,
rect
rect: texture.clampRect({ x: x * w, y: y * h, w, h })
};
yield data;
}
}
}
cycled(): Generator<ITextureRenderable, void> | null {
throw new Error('Method not implemented.');
}
}
*cycled(): Generator<ITextureRenderable, void> | null {
const texture = this.texture;
if (!texture) return null;
export interface IAnimaterTranslatedInit<T> {
/** 以此矩形作为参考矩形 */
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;
const w = this.width;
const h = this.height;
let index = 0;
while (true) {
const next = ani.next();
if (next.done) break;
const renderable = next.value;
const { x, y, w, h } = renderable.rect;
const translated: IRect = { x: x - ox + nx, y: y - oy + ny, w, h };
const res: ITextureRenderable = {
source,
rect: translated
const x = index % this.frameX;
const y = Math.floor(index / this.frameX);
const data: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: x * w, y: y * h, w, h })
};
yield res;
}
}
yield data;
open(
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);
if (index === this.frames) index = 0;
}
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,
Rectangle
} from 'maxrects-packer';
import { IAnimaterTranslatedInit, TextureAnimaterTranslated } from './animater';
import { Texture } from './texture';
import {
IRect,
@ -26,12 +25,6 @@ interface IndexMarkedComposedData {
readonly index: number;
}
type TranslatedComposer<D, T> = ITextureComposer<
D,
void,
IAnimaterTranslatedInit<T>
>;
export interface IGridComposerData {
/** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */
readonly width: number;
@ -39,8 +32,8 @@ export interface IGridComposerData {
readonly height: number;
}
export class TextureGridComposer<T>
implements TranslatedComposer<IGridComposerData, T>
export class TextureGridComposer
implements ITextureComposer<IGridComposerData>
{
/**
*
@ -85,9 +78,7 @@ export class TextureGridComposer<T>
}
}
const texture = new Texture<void, IAnimaterTranslatedInit<T>>(canvas);
texture.animated(new TextureAnimaterTranslated(), void 0);
const texture = new Texture(canvas);
const composed: ITextureComposedData = {
texture,
assetMap: map
@ -99,7 +90,7 @@ export class TextureGridComposer<T>
*compose(
input: Iterable<ITexture>,
data: IGridComposerData
): Generator<ITextureComposedData> {
): Generator<ITextureComposedData, void> {
const arr = [...input];
const rows = Math.floor(this.maxWidth / data.width);
@ -125,8 +116,8 @@ interface MaxRectsRectangle extends IRectangle {
readonly data: ITexture;
}
export class TextureMaxRectsComposer<T>
implements TranslatedComposer<IMaxRectsComposerData, T>
export class TextureMaxRectsComposer
implements ITextureComposer<IMaxRectsComposerData>
{
/**
* 使 Max Rects {@link IMaxRectsComposerData}
@ -142,7 +133,7 @@ export class TextureMaxRectsComposer<T>
*compose(
input: Iterable<ITexture>,
data: IMaxRectsComposerData
): Generator<ITextureComposedData<void, IAnimaterTranslatedInit<T>>> {
): Generator<ITextureComposedData, void> {
const packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth,
this.maxHeight,
@ -173,10 +164,7 @@ export class TextureMaxRectsComposer<T>
const source = renderable.source;
ctx.drawImage(source, x, y, w, h, v.x, v.y, v.width, v.height);
});
const texture = new Texture<void, IAnimaterTranslatedInit<T>>(
canvas
);
texture.animated(new TextureAnimaterTranslated(), void 0);
const texture = new Texture(canvas);
const data: ITextureComposedData = {
texture,
assetMap: map
@ -193,8 +181,8 @@ interface RectProcessed {
readonly attrib: Float32Array;
}
export class TextureMaxRectsWebGL2Composer<T>
implements TranslatedComposer<IMaxRectsComposerData, T>
export class TextureMaxRectsWebGL2Composer
implements ITextureComposer<IMaxRectsComposerData>
{
/** 使用的画布 */
readonly canvas: HTMLCanvasElement;
@ -416,7 +404,7 @@ export class TextureMaxRectsWebGL2Composer<T>
*compose(
input: Iterable<ITexture>,
data: IMaxRectsComposerData
): Generator<ITextureComposedData<void, IAnimaterTranslatedInit<T>>> {
): Generator<ITextureComposedData, void> {
this.opWidth = 0;
this.opHeight = 0;
@ -441,10 +429,7 @@ export class TextureMaxRectsWebGL2Composer<T>
for (const bin of packer.bins) {
const { texMap, attrib } = this.processRects(bin.rects, indexMap);
this.renderAtlas(attrib);
const texture = new Texture<void, IAnimaterTranslatedInit<T>>(
this.canvas
);
texture.animated(new TextureAnimaterTranslated(), void 0);
const texture = new Texture(this.canvas);
const data: ITextureComposedData = {
texture,
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 {
IRect,
ITexture,
ITextureAnimater,
ITextureRenderable,
@ -13,15 +14,23 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
width: number;
height: number;
private cx: number;
private cy: number;
/** 裁剪矩形的左边框位置 */
private cl: number;
/** 裁剪矩形的上边框位置 */
private ct: number;
/** 裁剪矩形的右边框位置 */
private cr: number;
/** 裁剪矩形的下边框位置 */
private cb: number;
constructor(source: SizedCanvasImageSource) {
this.source = source;
this.width = source.width;
this.height = source.height;
this.cx = 0;
this.cy = 0;
this.cl = 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 right = Math.min(this.width, r);
const bottom = Math.min(this.height, b);
this.cx = left;
this.cy = top;
this.cl = left;
this.ct = top;
this.width = right - left;
this.height = bottom - top;
}
@ -63,19 +72,33 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
}
static(): ITextureRenderable {
const renderable: ITextureRenderable = {
return {
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;
return this.animater.open(data);
}
cycled(data: A): Generator<ITextureRenderable> | null {
cycled(data: A): Generator<ITextureRenderable, void> | null {
if (!this.animater) return null;
return this.animater.cycled(data);
}
@ -86,4 +109,66 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
}
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>;
}
export interface ITextureComposedData<T = unknown, A = unknown> {
export interface ITextureComposedData {
/** 这个纹理图集的贴图对象 */
readonly texture: ITexture<T, A>;
readonly texture: ITexture;
/** 每个参与组合的贴图对应到图集对象的矩形范围 */
readonly assetMap: Map<ITexture, Readonly<IRect>>;
}
export interface ITextureComposer<T, C, I> {
export interface ITextureComposer<T> {
/**
*
* @param input
@ -35,7 +35,24 @@ export interface ITextureComposer<T, C, I> {
compose(
input: Iterable<ITexture>,
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> {
@ -106,6 +123,18 @@ export interface ITexture<T = unknown, A = unknown> {
*/
static(): ITextureRenderable;
/**
*
* @param rect
*/
clampRect(rect: Readonly<IRect>): Readonly<IRect>;
/**
*
* @param rect
*/
clipped(rect: Readonly<IRect>): ITextureRenderable;
/**
*
* @param data