feat: 将 HumanBreak 新内容移植入该样板

This commit is contained in:
AncTe committed 2026-02-28 22:56:59 +08:00
1 parent 6bf32174d8
commit bea2725d6a
128 files changed
+11984 -573

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
import { logger } from '@motajs/common';
export interface ICompiledProgram {
/** 着色器程序 */
readonly program: WebGLProgram;
/** 顶点着色器对象 */
readonly vertexShader: WebGLShader;
/** 片段着色器对象 */
readonly fragmentShader: WebGLShader;
}
/**
* 编译着色器
* @param gl WebGL2 上下文
* @param type 着色器类型
* @param source 着色器代码
*/
export function compileShader(
gl: WebGL2RenderingContext,
type: number,
source: string
): WebGLShader | null {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const info = gl.getShaderInfoLog(shader);
const typeStr = type === gl.VERTEX_SHADER ? 'vertex' : 'fragment';
logger.error(10, typeStr, info ?? '');
return null;
}
return shader;
}
/**
* 编译链接着色器程序
* @param gl WebGL2 上下文
* @param vs 顶点着色器对象
* @param fs 片段着色器对象
*/
export function compileProgram(
gl: WebGL2RenderingContext,
vs: WebGLShader,
fs: WebGLShader
) {
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const info = gl.getProgramInfoLog(program);
logger.error(9, info ?? '');
return null;
}
return program;
}
/**
* 使用指定着色器代码编译链接程序
* @param gl WebGL2 上下文
* @param vs 顶点着色器代码
* @param fs 片段着色器代码
*/
export function compileProgramWith(
gl: WebGL2RenderingContext,
vs: string,
fs: string
): ICompiledProgram | null {
const vsShader = compileShader(gl, gl.VERTEX_SHADER, vs);
const fsShader = compileShader(gl, gl.FRAGMENT_SHADER, fs);
if (!vsShader || !fsShader) return null;
const program = compileProgram(gl, vsShader, fsShader);
if (!program) return null;
return {
program,
vertexShader: vsShader,
fragmentShader: fsShader
};
}
+1
View File
@@ -1,2 +1,3 @@
export * from './glUtils';
export * from './keyCodes';
export * from './types';
+133
View File
@@ -0,0 +1,133 @@
import { isNil } from 'lodash-es';
import { IDirtyMark, IDirtyTracker } from './types';
/**
* 布尔类型的脏标记追踪器。当传入 `dirtySince` 的标记不属于当前的追踪器时,会返回 `true`
*/
export class PrivateBooleanDirtyTracker implements IDirtyTracker<boolean> {
/** 标记映射 */
private markMap: WeakMap<IDirtyMark, number> = new WeakMap();
/** 脏标记 */
private dirtyFlag: number = 0;
mark(): IDirtyMark {
const symbol = {};
this.markMap.set(symbol, this.dirtyFlag);
return symbol;
}
unmark(mark: IDirtyMark): void {
this.markMap.delete(mark);
}
dirtySince(mark: IDirtyMark): boolean {
const num = this.markMap.get(mark);
if (isNil(num)) return true;
return num < this.dirtyFlag;
}
hasMark(symbol: IDirtyMark): boolean {
return this.markMap.has(symbol);
}
/**
* 将数据标记为脏
*/
protected dirty() {
this.dirtyFlag++;
}
}
/**
* 列表的脏标记追踪器。当传入 `dirtySince` 的标记不属于当前的追踪器时,会返回空集合
*/
export class PrivateListDirtyTracker<T extends number>
implements IDirtyTracker<Set<T>>
{
/** 标记映射,键表示在索引,值表示其对应的标记数字 */
private readonly markMap: Map<T, number> = new Map();
/** 标记 symbol 映射,值表示这个 symbol 对应的标记数字 */
private readonly symbolMap: WeakMap<{}, number> = new WeakMap();
/** 脏标记数字 */
private dirtyFlag: number = 0;
constructor(protected length: number) {}
mark(): IDirtyMark {
const symbol = {};
this.symbolMap.set(symbol, this.dirtyFlag);
return symbol;
}
unmark(mark: IDirtyMark): void {
this.symbolMap.delete(mark);
}
dirtySince(mark: IDirtyMark): Set<T> {
const num = this.symbolMap.get(mark);
const res = new Set<T>();
if (isNil(num)) return res;
this.markMap.forEach((v, k) => {
if (v > num) res.add(k);
});
return res;
}
hasMark(symbol: IDirtyMark): boolean {
return this.symbolMap.has(symbol);
}
protected dirty(data: T): void {
if (data >= this.length) return;
this.dirtyFlag++;
this.markMap.set(data, this.dirtyFlag);
}
protected updateLength(length: number) {
this.length = length;
}
}
export class PrivateMapDirtyTracker<T extends string>
implements IDirtyTracker<Record<T, boolean>>
{
/** 标记映射,键表示名称,值表示其对应的标记数字 */
private readonly markMap: Map<T, number> = new Map();
/** 标记 symbol 映射,值表示这个 symbol 对应的标记数字 */
private readonly symbolMap: WeakMap<{}, number> = new WeakMap();
/** 脏标记数字 */
private dirtyFlag: number = 0;
constructor(protected length: number) {}
mark(): IDirtyMark {
const symbol = {};
this.symbolMap.set(symbol, this.dirtyFlag);
return symbol;
}
unmark(mark: IDirtyMark): void {
this.symbolMap.delete(mark);
}
dirtySince(mark: IDirtyMark): Record<T, boolean> {
const num = this.symbolMap.get(mark) ?? 0;
const obj: Partial<Record<T, boolean>> = {};
this.markMap.forEach((v, k) => {
if (v > num) obj[k] = true;
else obj[k] = false;
});
return obj as Record<T, boolean>;
}
hasMark(symbol: IDirtyMark): boolean {
return this.symbolMap.has(symbol);
}
protected dirty(data: T): void {
this.dirtyFlag++;
this.markMap.set(data, this.dirtyFlag);
}
}
+86
View File
@@ -0,0 +1,86 @@
import { logger } from './logger';
import { IHookable, IHookBase, IHookController, IHookObject } from './types';
export abstract class Hookable<
H extends IHookBase = IHookBase,
C extends IHookController<H> = IHookController<H>
> implements IHookable<H, C>
{
/** 加载完成的钩子列表 */
protected readonly loadedList: Set<IHookObject<H, C>> = new Set();
/** 钩子列表 */
private readonly hookList: Set<IHookObject<H, C>> = new Set();
/** 钩子对象到钩子存储对象的映射 */
private readonly hookMap: Map<Partial<H>, IHookObject<H, C>> = new Map();
/** 钩子控制器到钩子存储对象的映射 */
private readonly controllerMap: Map<C, IHookObject<H, C>> = new Map();
/**
* 创建钩子对象的控制器
* @param hook 钩子对象
*/
protected abstract createController(hook: Partial<H>): C;
addHook(hook: Partial<H>): C {
const controller = this.createController(hook);
const obj: IHookObject<H, C> = { hook, controller };
this.hookMap.set(hook, obj);
this.controllerMap.set(controller, obj);
this.hookList.add(obj);
return controller;
}
loadHook(hook: Partial<H>): void {
const obj = this.hookMap.get(hook);
if (!obj) {
logger.warn(85);
return;
}
hook.awake?.(obj.controller);
this.loadedList.add(obj);
}
removeHook(hook: Partial<H>): void {
const obj = this.hookMap.get(hook);
if (!obj) return;
obj.hook.destroy?.(obj.controller);
this.hookList.delete(obj);
this.loadedList.delete(obj);
this.hookMap.delete(hook);
this.controllerMap.delete(obj.controller);
}
removeHookByController(hook: C): void {
const obj = this.controllerMap.get(hook);
if (!obj) return;
obj.hook.destroy?.(obj.controller);
this.hookList.delete(obj);
this.loadedList.delete(obj);
this.controllerMap.delete(hook);
this.hookMap.delete(obj.hook);
}
forEachHook<T>(fn: (hook: Partial<H>, controller: C) => T): T[] {
const arr: T[] = [];
this.loadedList.forEach(v => {
arr.push(fn(v.hook, v.controller));
});
return arr;
}
}
export class HookController<H extends IHookBase> implements IHookController<H> {
constructor(
readonly hookable: IHookable<H, IHookController<H>>,
readonly hook: Partial<H>
) {}
load(): void {
this.hookable.loadHook(this.hook);
}
unload(): void {
this.hookable.removeHookByController(this);
}
}
+3
View File
@@ -1,2 +1,5 @@
export * from './dirtyTracker';
export * from './hook';
export * from './logger';
export * from './utils';
export * from './types';
+48
View File
@@ -26,6 +26,26 @@
"24": "Cannot decode source type of '$1', since there is no registered decoder for that type.",
"25": "Unknown audio type. Header: '$1'",
"26": "Uncaught error when fetching stream data from '$1'. Error info: $2.",
"27": "No autotile connection data, please ensure you have created autotile connection map.",
"28": "Cannot compile map render shader.",
"29": "Cannot get uniform location of map render shader.",
"30": "",
"31": "No asset data is specified when rending map.",
"32": "Every layer added to map renderer must share the same size. Different layer: $1.",
"33": "Map layer transfered to vertex generator must belong to the renderer that the generator use.",
"34": "The texture of moving block must be a part of built asset.",
"35": "Tile background transfered to map renderer does not exists.",
"36": "Tile background transfered to map renderer has no frame data.",
"37": "Frame of tile background transfered to map renderer must share the same size.",
"38": "Cached texture cannot be convert to asset. This is likely an internal bug, please contact us.",
"39": "Offset pool size exceeds WebGL2 limitation, ensure size type of your big image is less than $1.",
"40": "Material used by map block $1 is not found in built asset. Please ensure you have pack it into asset.",
"41": "You are trying to use a texture on moving block whose offset is not in the offset pool, please build it into asset after loading.",
"42": "The '$1' property of map-render element is required.",
"43": "Cannot bind face direction to main block $1, please call malloc in advance.",
"44": "Cannot bind face direction to main block $1, since main direction cannot be override.",
"45": "Cannot add $1 map renderer extension, since $1 already exists for the given state.",
"46": "Cannot execute close door action on $1,$2, since the given position is not empty.",
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency."
},
"warn": {
@@ -94,6 +114,34 @@
"63": "Uncaught promise error in waiting box component. Error reason: $1",
"64": "Text node type and block type mismatch: '$1' vs '$2'",
"65": "Cannot bind a weather controller twice.",
"66": "Texture identifier repeated: $1",
"67": "Cannot set alias '$1' for texture $2, since there is already an alias '$3' for it.",
"68": "Cannot set alias '$1' for texture $2, since '$1' is already an alias for $3.",
"69": "Clip totally exceeds texture's dimensions, no operation will be alppied.",
"70": "An TextureAnimater can only be created once.",
"71": "Cannot start animate when animater not created on texture.",
"72": "Frame count delivered to frame-based animater need to exactly divide texture's height.",
"73": "Consistent size is needed when using WebGL2 composer.",
"74": "Frame size not match texture's size, incomplete data will be skipped.",
"75": "Material count splitted by grid does not match alias count, some material may have no alias or some alias may not map to a texture. Splitted: $1, alias: $2.",
"76": "Cannot pipe texture store when asset builder has been started.",
"77": "Texture is clipped to an area of 0. Ensure that the texture contains your clip rect and clip rect's area is not zero.",
"78": "Adding tileset source must follow index order.",
"79": "Assets can only be built once.",
"80": "Parameter count of MapLayer.getMapData must be 0 or 4.",
"81": "Map data to get is partially (or totally) out of range. Overflowed area will be filled with zero.",
"82": "Big image offset size is larger than 64. Considier reduce big image offset bucket.",
"83": "It seems that you call '$1' too frequently. This will extremely affect game's performance, so considering integrate them into one 'updateArea' or 'updateBlockList' call.",
"84": "Cannot set alias '$1' for layer, since '$1' is already an alias for another layer.",
"85": "Hook to load does not belong to current hookable object.",
"86": "Cannot restore vertex data since delivered state does not belong to current vertex generator instance.",
"87": "Map texture asset cache not hit. This has no effect on rendering, but may extremely affect performance, please see the following link to find solution.",
"88": "No hero image is specified, please set image in advance.",
"89": "Cannot set hero image, since there is no image named '$1'.",
"90": "Cannot set hero image, since delivered image has incorrect format or size.",
"91": "Cannot add follower, since specified follower number $1 does not exist.",
"92": "Followers can only be added when the last follower is not moving.",
"93": "Followers can only be removed when the last follower is not moving.",
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
}
}
+11 -6
View File
@@ -72,8 +72,15 @@ export class Logger {
let paramNum = '';
while (++pointer < text.length) {
const char = text[pointer];
const next = text[pointer + 1];
if (char === '$' && text[pointer - 1] !== '\\') {
if (char === '\\' && next === '$') {
str += '$';
pointer++;
continue;
}
if (char === '$' && nums.has(next)) {
inParam = true;
continue;
}
@@ -82,7 +89,7 @@ export class Logger {
if (nums.has(char)) {
paramNum += char;
}
if (!nums.has(text[pointer + 1])) {
if (!nums.has(next)) {
inParam = false;
const num = Number(paramNum);
paramNum = '';
@@ -125,8 +132,7 @@ export class Logger {
hideTipText();
}
const n = Math.floor(code / 50) + 1;
const n2 = code % 50;
const url = `${location.origin}/_docs/logger/error/error${n}.html#error-code-${n2}`;
const url = `${location.origin}/_docs/logger/error/error${n}.html#error-code-${code}`;
console.error(`[ERROR Code ${code}] ${text} See ${url}`);
}
}
@@ -159,8 +165,7 @@ export class Logger {
hideTipText();
}
const n = Math.floor(code / 50) + 1;
const n2 = code % 50;
const url = `${location.origin}/_docs/logger/warn/warn${n}.html#warn-code-${n2}`;
const url = `${location.origin}/_docs/logger/warn/warn${n}.html#warn-code-${code}`;
console.warn(`[WARNING Code ${code}] ${text} See ${url}`);
}
}
+117
View File
@@ -0,0 +1,117 @@
//#region 脏标记
export interface IDirtyMarker<T> {
/**
* 标记为脏,即进行了一次更新
* @param data 传递给追踪器的数据
*/
dirty(data: T): void;
}
export interface IDirtyMark {}
export interface IDirtyTracker<T> {
/**
* 对状态进行标记
*/
mark(): IDirtyMark;
/**
* 取消指定标记符号
* @param mark 标记符号
*/
unmark(mark: IDirtyMark): void;
/**
* 从指定标记符号开始,数据是否发生了变动
* @param mark 标记符号
*/
dirtySince(mark: IDirtyMark): T;
/**
* 当前追踪器是否包含指定标记符号
* @param symbol 标记符号
*/
hasMark(symbol: IDirtyMark): boolean;
}
//#endregion
//#region 钩子
export interface IHookObject<
H extends IHookBase,
C extends IHookController<H>
> {
/** 钩子对象 */
readonly hook: Partial<H>;
/** 钩子控制器 */
readonly controller: C;
}
export interface IHookController<H extends IHookBase = IHookBase> {
/** 控制器的钩子对象 */
readonly hook: Partial<H>;
/**
* 加载此控制器对应的钩子对象
*/
load(): void;
/**
* 卸载此控制器对应的钩子对象,之后此钩子将不会再被触发
*/
unload(): void;
}
export interface IHookBase {
/**
* 加载此钩子对象
* @param controller 钩子控制器对象
*/
awake(controller: IHookController<this>): void;
/**
* 摧毁此钩子对象
* @param controller 钩子控制器对象
*/
destroy(controller: IHookController<this>): void;
}
export interface IHookable<
H extends IHookBase = IHookBase,
C extends IHookController<H> = IHookController<H>
> {
/**
* 添加钩子对象,返回控制该钩子对象的控制器
* @param hook 钩子对象
*/
addHook(hook: Partial<H>): C;
/**
* 加载指定的钩子对象
* @param hook 钩子对象
*/
loadHook(hook: Partial<H>): void;
/**
* 移除钩子对象,会调用钩子对象的 `destroy` 方法
* @param hook 钩子对象
*/
removeHook(hook: Partial<H>): void;
/**
* 传入钩子控制器,移除对应的钩子对象
* @param hook 钩子控制器
*/
removeHookByController(hook: C): void;
/**
* 遍历每个钩子,执行顺序不固定
* @param fn 对每个钩子执行的函数
* @returns 每个钩子的返回值组成的数组,顺序不固定
*/
forEachHook<T>(fn: (hook: Partial<H>, controller: C) => T): T[];
}
//#endregion
+6
View File
@@ -0,0 +1,6 @@
{
"name": "@motajs/render-assets",
"dependencies": {
"@motajs/client-base": "workspace:*"
}
}
+134
View File
@@ -0,0 +1,134 @@
import { ITexture, ITextureAnimater, ITextureRenderable } from './types';
/**
* 行动画控制器,将贴图按照从上到下的顺序依次组成帧动画,动画传入的参数代表帧数
*/
export class TextureRowAnimater implements ITextureAnimater<number> {
*once(texture: ITexture, frames: number): Generator<ITextureRenderable> {
if (frames <= 0) return;
const renderable = texture.render();
const { x: ox, y: oy } = renderable.rect;
const { width: w, height } = texture!;
const h = height / frames;
for (let i = 0; i < frames; i++) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
}
}
*cycled(texture: ITexture, frames: number): Generator<ITextureRenderable> {
if (frames <= 0) return;
const renderable = texture.render();
const { x: ox, y: oy } = renderable.rect;
const { width: w, height } = texture;
const h = height / frames;
let i = 0;
while (true) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
i++;
if (i === frames) i = 0;
}
}
}
/**
* 列动画控制器,将贴图按照从左到右的顺序依次组成帧动画,动画传入的参数代表帧数
*/
export class TextureColumnAnimater implements ITextureAnimater<number> {
*once(texture: ITexture, frames: number): Generator<ITextureRenderable> {
if (frames <= 0) return;
const renderable = texture.render();
const { x: ox, y: oy, w: width, h } = renderable.rect;
const w = width / frames;
for (let i = 0; i < frames; i++) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
}
}
*cycled(texture: ITexture, frames: number): Generator<ITextureRenderable> {
if (frames <= 0) return null;
const renderable = texture.render();
const { x: ox, y: oy } = renderable.rect;
const { width, height: h } = texture;
const w = width / frames;
let i = 0;
while (true) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
i++;
if (i === frames) i = 0;
}
}
}
export interface IScanAnimaterData {
/** 每帧的宽度 */
readonly width: number;
/** 每帧的高度 */
readonly height: number;
/** 总帧数 */
readonly frames: number;
}
/**
* 扫描动画控制器,会按照先从左到右,再从上到下的顺序依次输出,可以用于动画精灵图等
*/
export class TextureScanAnimater
implements ITextureAnimater<IScanAnimaterData>
{
*once(
texture: ITexture,
data: IScanAnimaterData
): Generator<ITextureRenderable, void> {
const w = texture.width;
const h = texture.height;
let frame = 0;
for (let y = 0; y < data.width; y++) {
for (let x = 0; x < data.height; x++) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: x * w, y: y * h, w, h })
};
yield renderable;
frame++;
if (frame === data.frames) break;
}
}
}
*cycled(
texture: ITexture,
data: IScanAnimaterData
): Generator<ITextureRenderable, void> {
const w = texture.width;
const h = texture.height;
let index = 0;
while (true) {
const x = index % data.width;
const y = Math.floor(index / data.height);
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: x * w, y: y * h, w, h })
};
yield renderable;
index++;
if (index === data.frames) index = 0;
}
}
}
+451
View File
@@ -0,0 +1,451 @@
import {
IOption,
IRectangle,
MaxRectsPacker,
Rectangle
} from 'maxrects-packer';
import { Texture } from './texture';
import {
IRect,
ITexture,
ITextureComposedData,
ITextureComposer,
SizedCanvasImageSource
} from './types';
import vert from './shader/pack.vert?raw';
import frag from './shader/pack.frag?raw';
import { logger } from '@motajs/common';
import { isNil } from 'lodash-es';
import { compileProgramWith } from '@motajs/client-base';
interface IndexMarkedComposedData {
/** 组合数据 */
readonly asset: ITextureComposedData;
/** 组合时最后一个用到的贴图的索引 */
readonly index: number;
}
export interface IGridComposerData {
/** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */
readonly width: number;
/** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */
readonly height: number;
}
export class TextureGridComposer
implements ITextureComposer<IGridComposerData>
{
/**
* 网格组合器,将等大小的贴图组合成图集,要求每个贴图的尺寸一致。
* 组合时按照先从左到右,再从上到下的顺序组合。
* @param maxWidth 图集最大宽度,也是输出纹理的宽度
* @param maxHeight 图集最大高度,也是输出纹理的高度
*/
constructor(
readonly maxWidth: number,
readonly maxHeight: number
) {}
private nextAsset(
tex: ITexture[],
start: number,
data: IGridComposerData,
rows: number,
cols: number,
index: number
): IndexMarkedComposedData {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = this.maxWidth;
canvas.height = this.maxHeight;
const count = Math.min(rows * cols, tex.length - start);
const map = new Map<ITexture, IRect>();
let x = 0;
let y = 0;
for (let i = 0; i < count; i++) {
const dx = x * data.width;
const dy = y * data.height;
const texture = tex[i + start];
const renderable = texture.render();
const { x: sx, y: sy, w: sw, h: sh } = renderable.rect;
ctx.drawImage(renderable.source, sx, sy, sw, sh, dx, dy, sw, sh);
map.set(texture, { x: dx, y: dy, w: sw, h: sh });
x++;
if (x === cols) {
y++;
x = 0;
}
}
const texture = new Texture(canvas);
const composed: ITextureComposedData = {
index,
texture,
assetMap: map
};
return { asset: composed, index: start + count };
}
*compose(
input: Iterable<ITexture>,
data: IGridComposerData
): Generator<ITextureComposedData, void> {
const arr = [...input];
const rows = Math.floor(this.maxWidth / data.width);
const cols = Math.floor(this.maxHeight / data.height);
let arrindex = 0;
let i = 0;
while (arrindex < arr.length) {
const res = this.nextAsset(arr, arrindex, data, rows, cols, i);
arrindex = res.index + 1;
i++;
yield res.asset;
}
}
}
export interface IMaxRectsComposerData extends IOption {
/** 贴图之间的间距 */
readonly padding: number;
}
interface MaxRectsRectangle extends IRectangle {
/** 这个矩形对应的贴图对象 */
readonly data: ITexture;
}
export class TextureMaxRectsComposer
implements ITextureComposer<IMaxRectsComposerData>
{
/**
* 使用 Max Rects 算法执行贴图整合,输入数据参考 {@link IMaxRectsComposerData},
* 输出的纹理的图像源将会是不同的画布,注意与 {@link TextureMaxRectsWebGL2Composer} 区分
* @param maxWidth 图集最大宽度,也是输出纹理的宽度
* @param maxHeight 图集最大高度,也是输出纹理的高度
*/
constructor(
public readonly maxWidth: number,
public readonly maxHeight: number
) {}
*compose(
input: Iterable<ITexture>,
data: IMaxRectsComposerData
): Generator<ITextureComposedData, void> {
const packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth,
this.maxHeight,
data.padding,
data
);
const arr = [...input];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.render().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
});
packer.addArray(rects);
let index = 0;
for (const bin of packer.bins) {
const map = new Map<ITexture, IRect>();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = this.maxWidth;
canvas.height = this.maxHeight;
ctx.imageSmoothingEnabled = false;
bin.rects.forEach(v => {
const rect: IRect = { x: v.x, y: v.y, w: v.width, h: v.height };
map.set(v.data, rect);
const renderable = v.data.render();
const { x, y, w, h } = renderable.rect;
const source = renderable.source;
ctx.drawImage(source, x, y, w, h, v.x, v.y, v.width, v.height);
});
const texture = new Texture(canvas);
const data: ITextureComposedData = {
index: index++,
texture,
assetMap: map
};
yield data;
}
}
}
interface RectProcessed {
/** 贴图位置映射 */
readonly texMap: Map<ITexture, Readonly<IRect>>;
/** 顶点数组 */
readonly attrib: Float32Array;
}
export class TextureMaxRectsWebGL2Composer
implements ITextureComposer<IMaxRectsComposerData>
{
/** 使用的画布 */
readonly canvas: HTMLCanvasElement;
/** 画布上下文 */
readonly gl: WebGL2RenderingContext;
/** WebGL2 程序 */
readonly program: WebGLProgram;
/** 顶点数组缓冲区 */
readonly vertBuffer: WebGLBuffer;
/** 纹理数组对象 */
readonly texArray: WebGLTexture;
/** 纹理数组对象的位置 */
readonly texArrayPos: WebGLUniformLocation;
/** `a_position` 的索引 */
readonly posPos: number;
/** `a_texCoord` 的索引 */
readonly texPos: number;
/** 本次处理的贴图宽度 */
private opWidth: number = 0;
/** 本次处理的贴图高度 */
private opHeight: number = 0;
/**
* 使用 Max Rects 算法执行贴图整合,使用 WebGL2 执行组合操作。
* 输入数据参考 {@link IMaxRectsComposerData},要求每个贴图的图像源尺寸一致,贴图大小可以不同。
* 注意,本组合器同时只能处理一个组合操作,上一个没执行完的时候再次调用 `compose` 会出现问题。
* 所有输出的内容中,贴图对象的图像源都是同一个画布,因此获取后要么直接使用,要么立刻调用 `toBitmap`,
* 否则在下一次调用 `next` 时,图像源将会被覆盖。
* @param maxWidth 图集最大宽度,也是输出纹理的宽度
* @param maxHeight 图集最大高度,也是输出纹理的高度
*/
constructor(
public readonly maxWidth: number,
public readonly maxHeight: number
) {
this.canvas = document.createElement('canvas');
this.canvas.width = maxWidth;
this.canvas.height = maxHeight;
this.gl = this.canvas.getContext('webgl2')!;
const { program } = compileProgramWith(this.gl, vert, frag)!;
this.program = program;
// 初始化画布数据
const texture = this.gl.createTexture();
this.texArray = texture;
const location = this.gl.getUniformLocation(program, 'u_textArray')!;
this.texArrayPos = location;
this.posPos = this.gl.getAttribLocation(program, 'a_position');
this.texPos = this.gl.getAttribLocation(program, 'a_texCoord');
this.vertBuffer = this.gl.createBuffer();
this.gl.useProgram(program);
}
/**
* 对贴图进行索引
* @param textures 贴图数组
*/
private mapTextures(
textures: ITexture[]
): Map<SizedCanvasImageSource, number> {
const map = new Map<SizedCanvasImageSource, number>();
const { width, height } = textures[0].source;
textures.forEach(v => {
const source = v.source;
if (map.has(source)) return;
if (source.width !== width || source.height !== height) {
logger.warn(73);
return;
}
map.set(source, map.size);
});
this.opWidth = width;
this.opHeight = height;
return map;
}
/**
* 传递贴图
* @param texMap 纹理映射
*/
private setTexture(texMap: Map<SizedCanvasImageSource, number>) {
const gl = this.gl;
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texArray);
gl.texStorage3D(
gl.TEXTURE_2D_ARRAY,
1,
gl.RGBA8,
this.opWidth,
this.opHeight,
texMap.size
);
texMap.forEach((index, source) => {
gl.texSubImage3D(
gl.TEXTURE_2D_ARRAY,
0,
0,
0,
index,
this.opWidth,
this.opHeight,
1,
gl.RGBA,
gl.UNSIGNED_BYTE,
source
);
});
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_MAG_FILTER,
gl.NEAREST
);
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_MIN_FILTER,
gl.NEAREST
);
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_WRAP_S,
gl.CLAMP_TO_EDGE
);
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_WRAP_T,
gl.CLAMP_TO_EDGE
);
}
/**
* 处理矩形数组,生成 WebGL2 顶点数据
* @param rects 要处理的矩形数组
* @param texMap 贴图到贴图数组索引的映射
*/
private processRects(
rects: MaxRectsRectangle[],
texMap: Map<SizedCanvasImageSource, number>
): RectProcessed {
const { width: ow, height: oh } = this.canvas;
const map = new Map<ITexture, IRect>();
const attrib = new Float32Array(rects.length * 5 * 6);
rects.forEach((v, i) => {
const rect: IRect = { x: v.x, y: v.y, w: v.width, h: v.height };
map.set(v.data, rect);
const renderable = v.data.render();
const { width: tw, height: th } = v.data.source;
const { x, y, w, h } = renderable.rect;
// 画到目标画布上的位置
const ol = (v.x / ow) * 2 - 1;
const ob = (v.y / oh) * 2 - 1;
const or = ((v.x + v.width) / ow) * 2 - 1;
const ot = ((v.y + v.height) / oh) * 2 - 1;
// 原始贴图位置
const tl = x / tw;
const tt = y / tw;
const tr = (x + w) / tw;
const tb = (y + h) / th;
// 贴图索引
const ti = texMap.get(v.data.source);
if (isNil(ti)) return;
// Benchmark https://www.measurethat.net/Benchmarks/Show/35246/2/different-method-to-write-a-typedarray
// prettier-ignore
const data = [
// x y u v i
ol, -ot, tl, tt, ti, // 左上角
ol, -ob, tl, tb, ti, // 左下角
or, -ot, tr, tt, ti, // 右上角
or, -ot, tr, tt, ti, // 右上角
ol, -ob, tl, tb, ti, // 左下角
or, -ob, tr, tb, ti // 右下角
];
attrib.set(data, i * 30);
});
const data: RectProcessed = {
texMap: map,
attrib
};
return data;
}
/**
* 执行渲染操作
* @param attrib 顶点数组
*/
private renderAtlas(attrib: Float32Array) {
const gl = this.gl;
gl.clearColor(0, 0, 0, 0);
gl.clearDepth(1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, attrib, gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(this.posPos, 2, gl.FLOAT, false, 5 * 4, 0);
gl.vertexAttribPointer(this.texPos, 3, gl.FLOAT, false, 5 * 4, 2 * 4);
gl.enableVertexAttribArray(this.posPos);
gl.enableVertexAttribArray(this.texPos);
gl.uniform1i(this.texArrayPos, 0);
gl.drawArrays(gl.TRIANGLES, 0, attrib.length / 5);
}
*compose(
input: Iterable<ITexture>,
data: IMaxRectsComposerData
): Generator<ITextureComposedData, void> {
this.opWidth = 0;
this.opHeight = 0;
const packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth,
this.maxHeight,
data.padding,
data
);
const arr = [...input];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.render().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
});
packer.addArray(rects);
const indexMap = this.mapTextures(arr);
this.setTexture(indexMap);
let index = 0;
for (const bin of packer.bins) {
const { texMap, attrib } = this.processRects(bin.rects, indexMap);
this.renderAtlas(attrib);
const texture = new Texture(this.canvas);
const data: ITextureComposedData = {
index: index++,
texture,
assetMap: texMap
};
yield data;
}
this.gl.disableVertexAttribArray(this.posPos);
this.gl.disableVertexAttribArray(this.texPos);
}
}
+7
View File
@@ -0,0 +1,7 @@
export * from './animater';
export * from './composer';
export * from './splitter';
export * from './store';
export * from './streamComposer';
export * from './texture';
export * from './types';
@@ -0,0 +1,12 @@
#version 300 es
precision highp float;
precision highp sampler2DArray;
in vec3 v_texCoord;
out vec4 outColor;
uniform sampler2DArray u_texArray;
void main() {
outColor = texture(u_texArray, v_texCoord);
}
@@ -0,0 +1,12 @@
#version 300 es
precision highp float;
in vec2 a_position;
in vec3 a_texCoord;
out vec3 v_texCoord;
void main() {
v_texCoord = a_texCoord;
gl_Position = vec4(a_position, 0.0, 1.0);
}
+68
View File
@@ -0,0 +1,68 @@
import { Texture } from './texture';
import { ITexture, ITextureSplitter, IRect } from './types';
/**
* 按行分割贴图,即分割成一行行的贴图,按从上到下的顺序输出
* 输入参数代表每一行的高度
*/
export class TextureRowSplitter implements ITextureSplitter<number> {
*split(texture: ITexture, data: number): Generator<ITexture> {
const lines = Math.ceil(texture.height / data);
const { x, y } = texture.render().rect;
for (let i = 0; i < lines; i++) {
const tex = new Texture(texture.source);
tex.clip(x, y + i * data, texture.width, data);
yield tex;
}
}
}
/**
* 按列分割贴图,即分割成一列列的贴图,按从左到右的顺序输出
* 输入参数代表每一列的宽度
*/
export class TextureColumnSplitter implements ITextureSplitter<number> {
*split(texture: ITexture, data: number): Generator<ITexture> {
const lines = Math.ceil(texture.width / data);
const { x, y } = texture.render().rect;
for (let i = 0; i < lines; i++) {
const tex = new Texture(texture.source);
tex.clip(x + i * data, y, data, texture.height);
yield tex;
}
}
}
/**
* 按照网格分割贴图,按照先从左到右,再从上到下的顺序输出
* 输入参数代表每一列的宽度和高度
*/
export class TextureGridSplitter implements ITextureSplitter<[number, number]> {
*split(texture: ITexture, data: [number, number]): Generator<ITexture> {
const [w, h] = data;
const rows = Math.ceil(texture.width / w);
const lines = Math.ceil(texture.height / h);
const { x, y } = texture.render().rect;
for (let ny = 0; ny < lines; ny++) {
for (let nx = 0; nx < rows; nx++) {
const tex = new Texture(texture.source);
tex.clip(x + nx * w, y + ny * h, w, h);
yield tex;
}
}
}
}
/**
* 根据图集信息分割贴图,按照传入的矩形数组的顺序输出
* 输入参数代表每个贴图对应到图集上的矩形位置
*/
export class TextureAssetSplitter implements ITextureSplitter<IRect[]> {
*split(texture: ITexture, data: IRect[]): Generator<ITexture> {
for (const { x, y, w, h } of data) {
const tex = new Texture(texture.source);
tex.clip(x, y, w, h);
yield tex;
}
}
}
+107
View File
@@ -0,0 +1,107 @@
import { isNil } from 'lodash-es';
import { ITexture, ITextureStore } from './types';
import { logger } from '@motajs/common';
export class TextureStore<T extends ITexture = ITexture>
implements ITextureStore<T>
{
private readonly texMap: Map<number, T> = new Map();
private readonly invMap: Map<T, number> = new Map();
private readonly aliasMap: Map<string, number> = new Map();
private readonly aliasInvMap: Map<number, string> = new Map();
[Symbol.iterator](): Iterator<[key: number, tex: T]> {
return this.texMap.entries();
}
entries(): Iterable<[key: number, tex: T]> {
return this.texMap.entries();
}
keys(): Iterable<number> {
return this.texMap.keys();
}
values(): Iterable<T> {
return this.texMap.values();
}
addTexture(identifier: number, texture: T): void {
if (this.texMap.has(identifier)) {
logger.warn(66, identifier.toString());
return;
}
this.texMap.set(identifier, texture);
this.invMap.set(texture, identifier);
}
private removeBy(id: number, tex: T, alias?: string) {
this.texMap.delete(id);
this.invMap.delete(tex);
if (alias) {
this.aliasMap.delete(alias);
this.aliasInvMap.delete(id);
}
}
removeTexture(identifier: number | string | T): void {
if (typeof identifier === 'string') {
const id = this.aliasMap.get(identifier);
if (isNil(id)) return;
const tex = this.texMap.get(id);
if (isNil(tex)) return;
this.removeBy(id, tex, identifier);
} else if (typeof identifier === 'number') {
const tex = this.texMap.get(identifier);
if (isNil(tex)) return;
const alias = this.aliasInvMap.get(identifier);
this.removeBy(identifier, tex, alias);
} else {
const id = this.invMap.get(identifier);
if (isNil(id)) return;
const alias = this.aliasInvMap.get(id);
this.removeBy(id, identifier, alias);
}
}
hasTexture(identifier: number): boolean {
return this.texMap.has(identifier);
}
getTexture(identifier: number): T | null {
return this.texMap.get(identifier) ?? null;
}
alias(identifier: number, alias: string): void {
const id = this.aliasMap.get(alias);
const al = this.aliasInvMap.get(identifier);
if (!isNil(al)) {
logger.warn(67, alias, identifier.toString(), al);
return;
}
if (!isNil(id)) {
logger.warn(68, alias, identifier.toString(), id.toString());
return;
}
this.aliasMap.set(alias, identifier);
this.aliasInvMap.set(identifier, alias);
}
fromAlias(alias: string): ITexture | null {
const id = this.aliasMap.get(alias);
if (isNil(id)) return null;
return this.texMap.get(id) ?? null;
}
idOf(texture: T): number | undefined {
return this.invMap.get(texture);
}
aliasOf(identifier: number): string | undefined {
return this.aliasInvMap.get(identifier);
}
identifierOf(alias: string): number | undefined {
return this.aliasMap.get(alias);
}
}
@@ -0,0 +1,201 @@
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 outputIndex: number = -1;
private nowTexture: ITexture;
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();
this.nowTexture = new Texture(this.nowCanvas);
}
private nextCanvas() {
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowMap = new Map();
this.nowIndex = 0;
this.nowTexture = new Texture(this.nowCanvas);
this.outputIndex++;
}
*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.render();
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 });
if (++index === max) {
const data: ITextureComposedData = {
index: this.outputIndex,
texture: this.nowTexture,
assetMap: this.nowMap
};
yield data;
this.nextCanvas();
}
}
const data: ITextureComposedData = {
index: this.outputIndex,
texture: this.nowTexture,
assetMap: this.nowMap
};
yield data;
}
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 outputIndex: number = -1;
private nowTexture!: ITexture;
private nowCanvas!: HTMLCanvasElement;
private nowCtx!: CanvasRenderingContext2D;
private nowMap!: Map<ITexture, Readonly<IRect>>;
private nowBin: number = 0;
/**
* 使用 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.nextCanvas();
}
private nextCanvas() {
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowCanvas.width = this.maxWidth;
this.nowCanvas.height = this.maxHeight;
this.nowMap = new Map();
this.outputIndex++;
this.nowTexture = new Texture(this.nowCanvas);
}
*add(textures: Iterable<ITexture>): Generator<ITextureComposedData, void> {
const arr = [...textures];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.render().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.nowBin) {
toYield.push(...bins.slice(this.nowBin));
}
for (let i = this.nowBin; i < bins.length; i++) {
const rects = bins[i].rects;
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.render();
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 = {
index: this.outputIndex,
texture: this.nowTexture,
assetMap: this.nowMap
};
yield data;
if (i < bins.length - 1) {
this.nextCanvas();
}
}
this.nowBin = bins.length - 1;
}
close(): void {
// We need to do nothing.
}
}
+193
View File
@@ -0,0 +1,193 @@
import { logger } from '@motajs/common';
import {
IRect,
ITexture,
ITextureComposedData,
ITextureRenderable,
ITextureSplitter,
SizedCanvasImageSource
} from './types';
import { clamp } from 'lodash-es';
export class Texture implements ITexture {
source: SizedCanvasImageSource;
width: number;
height: number;
isBitmap: boolean = false;
/** 裁剪矩形的左边框位置 */
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.cl = 0;
this.ct = 0;
this.cr = source.width;
this.cb = source.height;
}
/**
* 对纹理进行裁剪操作,不会改变图像源
* @param x 裁剪左上角横坐标
* @param y 裁剪左上角纵坐标
* @param w 裁剪宽度
* @param h 裁剪高度
*/
clip(x: number, y: number, w: number, h: number) {
const left = clamp(this.cl + x, this.cl, this.cr);
const top = clamp(this.ct + y, this.ct, this.cb);
const right = clamp(this.cl + x + w, this.cl, this.cr);
const bottom = clamp(this.ct + y + h, this.ct, this.cb);
if (left === right || top === bottom) {
logger.warn(69);
return;
}
const width = right - left;
const height = bottom - top;
if (width <= 0 || height <= 0) {
logger.warn(77);
return;
}
this.cl = left;
this.ct = top;
this.cr = right;
this.cb = bottom;
this.width = right - left;
this.height = bottom - top;
}
async toBitmap(): Promise<void> {
if (this.source instanceof ImageBitmap) return;
this.source = await createImageBitmap(
this.source,
this.cl,
this.ct,
this.width,
this.height
);
this.cl = 0;
this.ct = 0;
this.cr = this.width;
this.cb = this.height;
this.isBitmap = true;
}
split<U>(splitter: ITextureSplitter<U>, data: U): Generator<ITexture> {
return splitter.split(this, data);
}
render(): ITextureRenderable {
return {
source: this.source,
rect: { x: this.cl, y: this.ct, w: this.width, h: this.height }
};
}
clampRect(rect: Readonly<IRect>): Readonly<IRect> {
const l = clamp(rect.x, this.cl, this.cr);
const t = clamp(rect.y, this.ct, this.cb);
const r = clamp(rect.x + rect.w, this.cl, this.cr);
const b = clamp(rect.y + rect.h, this.ct, this.cb);
return { x: l, y: t, w: r - l, h: b - t };
}
clipped(rect: Readonly<IRect>): ITextureRenderable {
return {
source: this.source,
rect: this.clampRect(rect)
};
}
dispose(): void {
if (this.source instanceof ImageBitmap) {
this.source.close();
}
}
toAsset(asset: ITextureComposedData): boolean {
const rect = asset.assetMap.get(this);
if (!rect) return false;
if (this.isBitmap && this.source instanceof ImageBitmap) {
this.source.close();
}
this.isBitmap = false;
this.source = asset.texture.source;
this.cl = rect.x;
this.ct = rect.y;
this.width = rect.w;
this.height = rect.h;
this.cr = rect.x + rect.w;
this.cb = rect.y + rect.h;
return true;
}
/**
* 对贴图的动画执行偏移效果。动画效果可以不来自传入的贴图对象,
* 输出结果的图像源会是传入的贴图对象的图像源而非动画效果对应的图像源
* @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.render().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;
}
}
}
+218
View File
@@ -0,0 +1,218 @@
export type SizedCanvasImageSource = Exclude<
CanvasImageSource,
VideoFrame | SVGElement
>;
export type CanvasStyle = string | CanvasGradient | CanvasPattern;
export interface IRect {
x: number;
y: number;
w: number;
h: number;
}
export interface ITextureRenderable {
/** 可渲染贴图对象的图像源 */
readonly source: SizedCanvasImageSource;
/** 贴图裁剪区域 */
readonly rect: Readonly<IRect>;
}
export interface ITextureComposedData {
/** 这个纹理图集的贴图对象 */
readonly texture: ITexture;
/** 这个纹理图集的索引 */
readonly index: number;
/** 每个参与组合的贴图对应到图集对象的矩形范围 */
readonly assetMap: Map<ITexture, Readonly<IRect>>;
}
export interface ITextureComposer<T> {
/**
* 将一系列纹理组合成为一系列纹理图集
* @param input 输入纹理
* @param data 输入给组合器的参数
*/
compose(
input: Iterable<ITexture>,
data: T
): 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> {
/**
* 对一个贴图对象执行拆分操作
* @param texture 要拆分的贴图
* @param data 传给拆分器的参数
*/
split(texture: ITexture, data: T): Generator<ITexture, void>;
}
export interface ITextureAnimater<T> {
/**
* 开始动画序列
* @param texture 贴图对象
* @param data 动画初始化参数
*/
once(texture: ITexture, data: T): Generator<ITextureRenderable, void>;
/**
* 开始循环动画序列
* @param texture 贴图对象
* @param data 动画初始化参数
*/
cycled(texture: ITexture, data: T): Generator<ITextureRenderable, void>;
}
export interface ITexture {
/** 贴图的图像源 */
readonly source: SizedCanvasImageSource;
/** 贴图宽度 */
readonly width: number;
/** 贴图高度 */
readonly height: number;
/** 当前贴图是否是完整 bitmap 图像 */
readonly isBitmap: boolean;
/**
* 将此贴图转换为 bitmap 图像,图像源也会转变成 ImageBitmap
*/
toBitmap(): Promise<void>;
/**
* 使用指定贴图切分器切分贴图
* @param splitter 贴图切分器
* @param data 传递给切分器的参数
* @returns 切分出的贴图所组成的可迭代对象
*/
split<T>(splitter: ITextureSplitter<T>, data: T): Generator<ITexture>;
/**
* 获取整张图的可渲染对象
*/
render(): ITextureRenderable;
/**
* 限制矩形范围至当前贴图对象范围
* @param rect 矩形范围
*/
clampRect(rect: Readonly<IRect>): Readonly<IRect>;
/**
* 获取贴图经过指定矩形裁剪后的可渲染对象,并不是简单地对图像源裁剪,还会处理其他情况
* @param rect 裁剪矩形
*/
clipped(rect: Readonly<IRect>): ITextureRenderable;
/**
* 释放此贴图的资源,将不能再被使用
*/
dispose(): void;
/**
* 将贴图的图像源转换为指定图集的图像源,并将范围限定至图集中对应到此贴图的矩形范围。
* @param asset 图集信息
* @returns 是否转换成功,如果图集信息中不包含当前贴图,那么返回 `false`
*/
toAsset(asset: ITextureComposedData): boolean;
}
export const enum TextureOffsetDirection {
LeftToRight,
RightToLeft,
TopToBottom,
BottomToTop
}
export interface ITextureStore<T extends ITexture = ITexture> {
[Symbol.iterator](): Iterator<[key: number, tex: T]>;
/**
* 获取纹理对象键值对的可迭代对象
*/
entries(): Iterable<[key: number, tex: T]>;
/**
* 获取纹理对象的键的可迭代对象
*/
keys(): Iterable<number>;
/**
* 获取纹理对象的值的可迭代对象
*/
values(): Iterable<T>;
/**
* 添加一个贴图
* @param identifier 贴图 id
* @param texture 贴图对象
*/
addTexture(identifier: number, texture: T): void;
/**
* 移除一个贴图
* @param identifier 要移除的贴图对象 id 或 别名 或 贴图对象
*/
removeTexture(identifier: number | string | T): void;
/**
* 判断当前贴图存储对象是否包含指定贴图 id
* @param identifier 贴图对象 id
*/
hasTexture(identifier: number): boolean;
/**
* 根据贴图对象 id 获取贴图
* @param identifier 贴图对象 id
*/
getTexture(identifier: number): T | null;
/**
* 给贴图对象命名一个别名,如果贴图对象不存在也可以设置
* @param identifier 贴图对象 id
* @param alias 要命名的别名
*/
alias(identifier: number, alias: string): void;
/**
* 根据贴图对象别名获取贴图
* @param alias 贴图对象别名
*/
fromAlias(alias: string): ITexture | null;
/**
* 根据贴图对象别名获取贴图对象 id
* @param alias 贴图对象别名
*/
identifierOf(alias: string): number | undefined;
/**
* 根据贴图对象获取此贴图对象在此控制器中的 id,如果贴图不在此控制器,返回 `undefined`
* @param texture 贴图对象
*/
idOf(texture: ITexture): number | undefined;
/**
* 根据贴图对象获取此贴图对象在此控制器中的别名,如果不存在此 id,返回 `undefined`
* @param identifier 贴图 id
*/
aliasOf(identifier: number): string | undefined;
}
+3 -2
View File
@@ -1,6 +1,7 @@
{
"name": "@motajs/render-core",
"dependencies": {
"@motajs/common": "workspace:*"
"@motajs/common": "workspace:*",
"@motajs/render-assets": "workspace:*"
}
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ import { MotaOffscreenCanvas2D } from './canvas2d';
import { ERenderItemEvent, RenderItem, RenderItemPosition } from './item';
import { Transform } from './transform';
import { isWebGL2Supported } from './utils';
import { SizedCanvasImageSource } from './types';
import { SizedCanvasImageSource } from '@motajs/render-assets';
export interface IGL2ProgramPrefix {
readonly VERTEX: string;
@@ -1237,7 +1237,7 @@ export class GL2Program extends EventEmitter<ShaderProgramEvent> {
}
/**
* 设置渲染模式,目前可选 {@link Shader.DRAW_ARRAYS} 至 {@link Shader.DRAW_INSTANCED}
* 设置渲染模式,目前可选 {@link GL2.DRAW_ARRAYS} 至 {@link GL2.DRAW_INSTANCED}
*/
mode(mode: RenderMode) {
this.renderMode = mode;
-1
View File
@@ -8,5 +8,4 @@ export * from './render';
export * from './shader';
export * from './sprite';
export * from './transform';
export * from './types';
export * from './utils';
+8 -6
View File
@@ -225,7 +225,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
IRenderTickerSupport,
IRenderChildable,
IRenderVueSupport,
ITransformUpdatable,
ITransformUpdatable<Transform>,
IRenderEvent
{
/** 渲染的全局ticker */
@@ -301,7 +301,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
/** 该元素的变换矩阵 */
private _transform: Transform = new Transform();
set transform(value: Transform) {
this._transform.bind();
this._transform.unbind(this);
this._transform = value;
value.bind(this);
}
@@ -671,10 +671,12 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
}
}
updateTransform() {
updateTransform(transform: Transform) {
// 更新变换矩阵时,不需要更新自身的缓存,直接调用父元素的更新即可
this._parent?.update();
this.emit('transform', this, this._transform);
if (transform === this.transform) {
this._parent?.update();
this.emit('transform', this, this._transform);
}
}
//#endregion
@@ -796,7 +798,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
this._parent = void 0;
parent.requestSort();
parent.update();
this._transform.bind();
this._transform.unbind(this);
if (!success) return false;
this._root?.disconnect(this);
this._root = void 0;
+53 -25
View File
@@ -1,7 +1,11 @@
import { mat3, mat4, ReadonlyMat3, ReadonlyVec3, vec2, vec3 } from 'gl-matrix';
export interface ITransformUpdatable {
updateTransform?(): void;
export interface ITransformUpdatable<T> {
/**
* 当变换矩阵更新时触发
* @param transform 触发更新的变换矩阵
*/
updateTransform(transform: T): void;
}
export class Transform {
@@ -17,14 +21,26 @@ export class Transform {
private modified: boolean = false;
/** 绑定的可更新元素 */
bindedObject?: ITransformUpdatable;
bindedObject: Set<ITransformUpdatable<Transform>> = new Set();
/**
* 对这个变换实例添加绑定对象,当矩阵变换时,自动调用其 update 函数
* @param obj 要绑定的对象
*/
bind(obj?: ITransformUpdatable) {
this.bindedObject = obj;
bind(obj: ITransformUpdatable<Transform>) {
this.bindedObject.add(obj);
}
/**
* 取消对象的绑定
* @param obj 要取消绑定的对象
*/
unbind(obj: ITransformUpdatable<Transform>) {
this.bindedObject.delete(obj);
}
private emitUpdate() {
this.bindedObject.forEach(v => v.updateTransform(this));
}
/**
@@ -38,7 +54,7 @@ export class Transform {
this.scaleY = 1;
this.rad = 0;
this.modified = false;
this.bindedObject?.updateTransform?.();
this.emitUpdate();
}
/**
@@ -49,7 +65,7 @@ export class Transform {
this.scaleX *= x;
this.scaleY *= y;
this.modified = true;
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -61,7 +77,7 @@ export class Transform {
this.x += x;
this.y += y;
this.modified = true;
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -76,7 +92,7 @@ export class Transform {
this.rad -= n * Math.PI * 2;
}
this.modified = true;
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -88,7 +104,7 @@ export class Transform {
this.scaleX = x;
this.scaleY = y;
this.modified = true;
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -100,7 +116,7 @@ export class Transform {
this.x = x;
this.y = y;
this.modified = true;
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -111,7 +127,7 @@ export class Transform {
mat3.rotate(this.mat, this.mat, rad - this.rad);
this.rad = rad;
this.modified = true;
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -138,7 +154,7 @@ export class Transform {
mat3.fromValues(a, b, 0, c, d, 0, e, f, 1)
);
this.calAttributes();
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -161,7 +177,7 @@ export class Transform {
): this {
mat3.set(this.mat, a, b, 0, c, d, 0, e, f, 1);
this.calAttributes();
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -285,14 +301,26 @@ export class Transform3D {
mat: mat4 = mat4.create();
/** 绑定的可更新元素 */
bindedObject?: ITransformUpdatable;
bindedObject: Set<ITransformUpdatable<Transform3D>> = new Set();
/**
* 绑定可更新对象
* 对这个变换实例添加绑定对象,当矩阵变换时,自动调用其 update 函数
* @param obj 要绑定的对象
*/
bind(obj?: ITransformUpdatable) {
this.bindedObject = obj;
bind(obj: ITransformUpdatable<Transform3D>) {
this.bindedObject.add(obj);
}
/**
* 取消对象的绑定
* @param obj 要取消绑定的对象
*/
unbind(obj: ITransformUpdatable<Transform3D>) {
this.bindedObject.delete(obj);
}
private emitUpdate() {
this.bindedObject.forEach(v => v.updateTransform(this));
}
/**
@@ -300,7 +328,7 @@ export class Transform3D {
*/
reset(): this {
mat4.identity(this.mat);
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -312,7 +340,7 @@ export class Transform3D {
*/
scale(x: number, y: number, z: number): this {
mat4.scale(this.mat, this.mat, [x, y, z]);
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -324,7 +352,7 @@ export class Transform3D {
*/
translate(x: number, y: number, z: number): this {
mat4.translate(this.mat, this.mat, [x, y, z]);
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -335,7 +363,7 @@ export class Transform3D {
*/
rotate(rad: number, axis: vec3): this {
mat4.rotate(this.mat, this.mat, rad, axis);
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -371,7 +399,7 @@ export class Transform3D {
*/
lookAt(eye: vec3, center: vec3, up: vec3): this {
mat4.lookAt(this.mat, eye, center, up);
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -384,7 +412,7 @@ export class Transform3D {
*/
perspective(fovy: number, aspect: number, near: number, far: number): this {
mat4.perspective(this.mat, fovy, aspect, near, far);
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
@@ -406,7 +434,7 @@ export class Transform3D {
far: number
): this {
mat4.ortho(this.mat, left, right, bottom, top, near, far);
this.bindedObject?.updateTransform?.();
this.emitUpdate();
return this;
}
-6
View File
@@ -1,6 +0,0 @@
export type SizedCanvasImageSource = Exclude<
CanvasImageSource,
VideoFrame | SVGElement
>;
export type CanvasStyle = string | CanvasGradient | CanvasPattern;
+2 -2
View File
@@ -2,9 +2,9 @@ import {
Transform,
ERenderItemEvent,
RenderItem,
MotaOffscreenCanvas2D,
CanvasStyle
MotaOffscreenCanvas2D
} from '@motajs/render-core';
import { CanvasStyle } from '@motajs/render-assets';
import { logger } from '@motajs/common';
import { clamp, isEqual, isNil } from 'lodash-es';
+1 -2
View File
@@ -5,13 +5,12 @@ import {
Transform,
MotaOffscreenCanvas2D
} from '@motajs/render-core';
import { CanvasStyle } from '@motajs/render-assets';
import { Font } from '@motajs/render-style';
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
const SAFE_PAD = 1;
type CanvasStyle = string | CanvasGradient | CanvasPattern;
export interface ETextEvent extends ERenderItemEvent {
setText: [text: string, width: number, height: number];
}
+2 -2
View File
@@ -7,9 +7,9 @@ import {
ElementLocator,
ElementScale,
CustomContainerRenderFn,
CustomContainerPropagateFn,
CanvasStyle
CustomContainerPropagateFn
} from '@motajs/render-core';
import { CanvasStyle } from '@motajs/render-assets';
import {
BezierParams,
CircleParams,
+1
View File
@@ -1,3 +1,4 @@
export * from '@motajs/render-assets';
export * from '@motajs/render-core';
export * from '@motajs/render-elements';
export * from '@motajs/render-style';