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;
if (index === this.frames) index = 0;
}
}
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);
}
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 传递给动画控制器的初始化参数