Compare commits

...

2 Commits

Author SHA1 Message Date
639a0d0f68 feat: 地图状态管理 2025-11-16 16:13:53 +08:00
0d9bef70b1 feat: WebGL2 地图渲染 2025-11-16 15:48:22 +08:00
49 changed files with 6008 additions and 691 deletions

View File

@ -1,6 +1,7 @@
{
"name": "@user/client-base",
"dependencies": {
"@motajs/render-asset": "workspace:*"
"@motajs/render-asset": "workspace:*",
"@motajs/client-base": "workspace:*"
}
}

View File

@ -3,7 +3,7 @@ import { IMaterialAsset } from './types';
export class MaterialAsset implements IMaterialAsset {
/** 标记列表 */
private readonly marks: Map<symbol, number> = new Map();
private readonly marks: WeakMap<symbol, number> = new WeakMap();
/** 脏标记,所有值小于此标记的都视为需要更新 */
private dirtyFlag: number = 0;
@ -27,4 +27,8 @@ export class MaterialAsset implements IMaterialAsset {
const value = this.marks.get(mark) ?? -1;
return value < this.dirtyFlag;
}
hasMark(symbol: symbol): boolean {
return this.marks.has(symbol);
}
}

View File

@ -1,13 +1,18 @@
import { IRect, ITexture, ITextureRenderable } from '@motajs/render-assets';
import {
IRect,
ITextureRenderable,
SizedCanvasImageSource
} from '@motajs/render-assets';
import {
AutotileConnection,
AutotileType,
BlockCls,
IAutotileConnection,
IAutotileProcessor,
IAutotileRenderable,
IMaterialFramedData,
IMaterialManager
} from './types';
import { logger } from '@motajs/common';
import { isNil } from 'lodash-es';
interface ConnectedAutotile {
readonly lt: Readonly<IRect>;
@ -16,19 +21,23 @@ interface ConnectedAutotile {
readonly lb: Readonly<IRect>;
}
export interface IAutotileData {
/** 图像源 */
readonly source: SizedCanvasImageSource;
/** 自动元件帧数 */
readonly frames: number;
}
/** 3x4 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
const connectionMap3x4 = new Map<number, [number, number, number, number]>();
/** 2x3 自动元件的连接映射,元组表示将对应大小的自动元件按照格子 1/4 大小切分后对应的索引位置 */
const connectionMap2x3 = new Map<number, [number, number, number, number]>();
/** 3x4 自动元件各方向连接矩形映射 */
/** 3x4 自动元件各方向连接矩形映射 */
const rectMap3x4 = new Map<number, ConnectedAutotile>();
/** 2x3 自动元件各方向连接的矩形映射 */
const rectMap2x3 = new Map<number, ConnectedAutotile>();
interface AutotileFrameList {
type: AutotileType;
rects: Readonly<IRect>[];
}
/** 不重复连接映射,用于平铺自动元件,一共 48 种 */
const distinctConnectionMap = new Map<number, number>();
export class AutotileProcessor implements IAutotileProcessor {
/** 自动元件父子关系映射,子元件 -> 父元件 */
@ -46,7 +55,7 @@ export class AutotileProcessor implements IAutotileProcessor {
return ensure;
}
setParent(autotile: number, parent: number): void {
setConnection(autotile: number, parent: number): void {
this.parentMap.set(autotile, parent);
const child = this.ensureChildSet(parent);
child.add(autotile);
@ -116,8 +125,14 @@ export class AutotileProcessor implements IAutotileProcessor {
index: number,
width: number
): IAutotileConnection {
let res: number = this.connectEdge(array.length, index, width);
const block = array[index];
if (block === 0) {
return {
connection: 0,
center: 0
};
}
let res: number = this.connectEdge(array.length, index, width);
const childList = this.childMap.get(block);
// 最高位表示左上,低位依次顺时针旋转
@ -132,7 +147,7 @@ export class AutotileProcessor implements IAutotileProcessor {
// Benchmark https://www.measurethat.net/Benchmarks/Show/35271/0/convert-boolean-to-number
if (!childList) {
if (!childList || childList.size === 0) {
// 不包含子元件,那么直接跟相同的连接
res |=
+(a0 === block) |
@ -161,157 +176,129 @@ export class AutotileProcessor implements IAutotileProcessor {
};
}
render(
updateConnectionFor(
connection: number,
center: number,
target: number,
direction: AutotileConnection
): number {
const childList = this.childMap.get(center);
if (!childList || !childList.has(target)) {
return connection & ~direction;
} else {
return connection | direction;
}
}
/**
*
* @param tile
*/
private checkAutotile(tile: IMaterialFramedData) {
if (tile.cls !== BlockCls.Autotile) return false;
const { texture, frames } = tile;
if (texture.width !== 96 * frames) return false;
if (texture.height === 128 || texture.height === 144) return true;
else return false;
}
render(autotile: number, connection: number): ITextureRenderable | null {
const tile = this.manager.getTile(autotile);
if (!tile) return null;
if (!this.checkAutotile(tile)) return null;
return this.renderWithoutCheck(tile, connection);
}
renderWith(
tile: IMaterialFramedData,
connection: number
): ITextureRenderable | null {
if (!this.checkAutotile(tile)) return null;
return this.renderWithoutCheck(tile, connection);
}
renderWithoutCheck(
tile: IMaterialFramedData,
connection: number
): ITextureRenderable | null {
const { texture } = tile;
const size = texture.height === 128 ? 32 : 48;
const index = distinctConnectionMap.get(connection);
if (isNil(index)) return null;
return {
source: texture.source,
rect: { x: 0, y: size * index, w: size, h: size }
};
}
*renderAnimated(
autotile: number,
connection: number
): Generator<IAutotileRenderable, void> | null {
const cls = this.manager.getBlockCls(autotile);
if (cls !== BlockCls.Autotile) return null;
const tile = this.manager.getTile(autotile)!;
return this.fromStaticRenderable(tile.static(), connection);
): Generator<ITextureRenderable, void> {
const tile = this.manager.getTile(autotile);
if (!tile) return;
yield* this.renderAnimatedWith(tile, connection);
}
/**
*
* @param renderable
*/
private getStaticRectList(
renderable: ITextureRenderable
): AutotileFrameList {
const { x, y, w, h } = renderable.rect;
const type = h === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
if (w === 96) {
return {
type,
rects: [renderable.rect]
};
} else {
return {
type,
rects: [
{ x: x + 0, y, w, h },
{ x: x + 96, y, w, h },
{ x: x + 192, y, w, h },
{ x: x + 288, y, w, h }
]
*renderAnimatedWith(
tile: IMaterialFramedData,
connection: number
): Generator<ITextureRenderable, void> {
if (!this.checkAutotile(tile)) return;
const { texture, frames } = tile;
const size = texture.height === 128 ? 32 : 48;
const index = distinctConnectionMap.get(connection);
if (isNil(index)) return;
for (let i = 0; i < frames; i++) {
yield {
source: texture.source,
rect: { x: i * size, y: size * index, w: size, h: size }
};
}
}
/**
*
* @param ox
* @param oy
* @param connection
* 48
* @param image
*/
private getConnectedRect(
ox: number,
oy: number,
connection: ConnectedAutotile
): ConnectedAutotile {
const { lt, rt, rb, lb } = connection;
return {
lt: { x: ox + lt.x, y: oy + lt.y, w: lt.w, h: lt.h },
rt: { x: ox + rt.x, y: oy + rt.y, w: rt.w, h: rt.h },
rb: { x: ox + rb.x, y: oy + rb.y, w: rb.w, h: rb.h },
lb: { x: ox + lb.x, y: oy + lb.y, w: lb.w, h: lb.h }
};
}
*fromStaticRenderable(
renderable: ITextureRenderable,
connection: number
): Generator<IAutotileRenderable, void> | null {
const { type, rects } = this.getStaticRectList(renderable);
static flatten(image: IAutotileData): SizedCanvasImageSource | null {
const { source, frames } = image;
if (source.width !== frames * 96) return null;
if (source.height !== 128 && source.height !== 144) return null;
const type =
source.height === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
const size = type === AutotileType.Big3x4 ? 32 : 48;
const width = frames * size;
const height = 48 * size;
// 画到画布上
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) return null;
const half = size / 2;
const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
const data = map.get(connection);
if (!data) {
logger.error(27);
return null;
}
if (rects.length === 1) {
const { x, y } = rects[0];
const connected = this.getConnectedRect(x, y, data);
if (!connected) return null;
const res: IAutotileRenderable = {
source: renderable.source,
lt: connected.lt,
rt: connected.rt,
rb: connected.rb,
lb: connected.lb
};
yield res;
} else {
for (const { x, y } of rects) {
const connected = this.getConnectedRect(x, y, data);
if (!connected) return null;
const res: IAutotileRenderable = {
source: renderable.source,
lt: connected.lt,
rt: connected.rt,
rb: connected.rb,
lb: connected.lb
};
yield res;
}
}
const used = new Set<number>();
// 遍历每个组合
distinctConnectionMap.forEach((index, conn) => {
if (used.has(conn)) return;
used.add(conn);
const { lt, rt, rb, lb } = map.get(conn)!;
const y = index * size;
for (let i = 0; i < frames; i++) {
const x = i * size;
// prettier-ignore
ctx.drawImage(source, lt.x + i * 96, lt.y, lt.w, lt.h, x, y, half, half);
// prettier-ignore
ctx.drawImage(source, rt.x + i * 96, rt.y, rt.w, rt.h, x + half, y, half, half);
// prettier-ignore
ctx.drawImage(source, rb.x + i * 96, rb.y, rb.w, rb.h, x + half, y + half, half, half);
// prettier-ignore
ctx.drawImage(source, lb.x + i * 96, lb.y, lb.w, lb.h, x, y + half, half, half);
}
});
fromAnimatedRenderable(
renderable: ITextureRenderable,
connection: number
): IAutotileRenderable | null {
const { x, y, h } = renderable.rect;
const type = h === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
const data = map.get(connection);
if (!data) {
logger.error(27);
return null;
}
const connected = this.getConnectedRect(x, y, data);
if (!connected) return null;
const res: IAutotileRenderable = {
source: renderable.source,
lt: connected.lt,
rt: connected.rt,
rb: connected.rb,
lb: connected.lb
};
return res;
}
*fromAnimatedGenerator(
texture: ITexture,
generator: Generator<ITextureRenderable> | null,
connection: number
): Generator<IAutotileRenderable, void> | null {
if (!generator) return null;
const h = texture.height;
const type = h === 128 ? AutotileType.Big3x4 : AutotileType.Small2x3;
const map = type === AutotileType.Big3x4 ? rectMap3x4 : rectMap2x3;
const data = map.get(connection);
if (!data) {
logger.error(27);
return null;
}
while (true) {
const value = generator.next();
if (value.done) break;
const renderable = value.value;
const { x, y } = renderable.rect;
const connected = this.getConnectedRect(x, y, data);
if (!connected) return null;
const res: IAutotileRenderable = {
source: renderable.source,
lt: connected.lt,
rt: connected.rt,
rb: connected.rb,
lb: connected.lb
};
yield res;
}
return canvas;
}
}
@ -444,4 +431,23 @@ export function createAutotile() {
lb: { x: lbx, y: lby, w: 24, h: 24 }
});
});
const usedRect: [number, number, number, number][] = [];
let flag = 0;
// 2x3 和 3x4 的自动元件连接方式一样,因此没必要映射两次
connectionMap2x3.forEach((conn, num) => {
const index = usedRect.findIndex(
used =>
used[0] === conn[0] &&
used[1] === conn[1] &&
used[2] === conn[2] &&
used[3] === conn[3]
);
if (index === -1) {
distinctConnectionMap.set(num, flag);
usedRect.push(conn.slice() as [number, number, number, number]);
flag++;
} else {
distinctConnectionMap.set(num, index);
}
});
}

View File

@ -31,6 +31,16 @@ function addTileset(set: Set<number>, map?: readonly (readonly number[])[]) {
});
}
function addAutotile(set: Set<number>, map?: readonly (readonly number[])[]) {
if (!map) return;
map.forEach(line => {
line.forEach(v => {
const id = core.maps.blocksInfo[v as keyof NumberToId];
if (id.cls === 'autotile') set.add(v);
});
});
}
/**
*
*/
@ -57,11 +67,11 @@ export function fallbackLoad() {
materials.addGrid(images.items, items);
// Row Animates
materials.addRowAnimate(images.animates, animates, 4, 32);
materials.addRowAnimate(images.enemys, enemys, 2, 32);
materials.addRowAnimate(images.npcs, npcs, 2, 32);
materials.addRowAnimate(images.enemy48, enemy48, 4, 48);
materials.addRowAnimate(images.npc48, npc48, 4, 48);
materials.addRowAnimate(images.animates, animates, 32);
materials.addRowAnimate(images.enemys, enemys, 32);
materials.addRowAnimate(images.npcs, npcs, 32);
materials.addRowAnimate(images.enemy48, enemy48, 48);
materials.addRowAnimate(images.npc48, npc48, 48);
// Autotile
for (const key of Object.keys(icons.autotile)) {
@ -91,10 +101,9 @@ export function fallbackLoad() {
materials.addImage(img, { index: i, alias: v });
});
materials.buildAssets();
// 地图上出现过的 tileset
const tilesetSet = new Set<number>();
const autotileSet = new Set<number>();
core.floorIds.forEach(v => {
const floor = core.floors[v];
addTileset(tilesetSet, floor.bgmap);
@ -102,7 +111,14 @@ export function fallbackLoad() {
addTileset(tilesetSet, floor.map);
addTileset(tilesetSet, floor.fgmap);
addTileset(tilesetSet, floor.fg2map);
addAutotile(autotileSet, floor.bgmap);
addAutotile(autotileSet, floor.bg2map);
addAutotile(autotileSet, floor.map);
addAutotile(autotileSet, floor.fgmap);
addAutotile(autotileSet, floor.fg2map);
});
materials.cacheTilesetList(tilesetSet);
materials.cacheTilesetList(tilesetSet.union(autotileSet));
materials.buildAssets();
}

View File

@ -1,7 +1,9 @@
import { loading } from '@user/data-base';
import { fallbackLoad } from './fallback';
import { createAutotile } from './autotile';
export function createMaterial() {
createAutotile();
loading.once('loaded', () => {
fallbackLoad();
});

View File

@ -6,7 +6,6 @@ import {
ITextureStore,
SizedCanvasImageSource,
Texture,
TextureColumnAnimater,
TextureGridSplitter,
TextureRowSplitter,
TextureStore
@ -18,15 +17,17 @@ import {
IIndexedIdentifier,
IMaterialAssetData,
BlockCls,
IBigImageData,
IBigImageReturn,
IAssetBuilder,
IMaterialAsset
IMaterialAsset,
IMaterialFramedData
} from './types';
import { logger } from '@motajs/common';
import { getClsByString } from './utils';
import { getClsByString, getTextureFrame } from './utils';
import { isNil } from 'lodash-es';
import { AssetBuilder } from './builder';
import { MaterialAsset } from './asset';
import { AutotileProcessor } from './autotile';
export class MaterialManager implements IMaterialManager {
readonly tileStore: ITextureStore = new TextureStore();
@ -35,18 +36,26 @@ export class MaterialManager implements IMaterialManager {
readonly assetStore: ITextureStore = new TextureStore();
readonly bigImageStore: ITextureStore = new TextureStore();
/** 自动元件图像源映射 */
readonly autotileSource: Map<number, SizedCanvasImageSource> = new Map();
/** 图集信息存储 */
readonly assetDataStore: Map<number, IMaterialAsset> = new Map();
/** 贴图到图集索引的映射 */
readonly assetMap: Map<ITexture, number> = new Map();
/** 大怪物数据 */
readonly bigImageData: Map<number, ITexture> = new Map();
readonly bigImageData: Map<number, IMaterialFramedData> = new Map();
/** tileset 中 `Math.floor(id / 10000) + 1` 映射到 tileset 对应索引的映射,用于处理图块超出 10000 的 tileset */
readonly tilesetOffsetMap: Map<number, number> = new Map();
/** 图集打包器 */
readonly assetBuilder: IAssetBuilder = new AssetBuilder();
/** 图块 id 到图块数字的映射 */
readonly idNumMap: Map<string, number> = new Map();
/** 图块数字到图块 id 的映射 */
readonly numIdMap: Map<number, string> = new Map();
/** 图块数字到图块类型的映射 */
readonly clsMap: Map<number, BlockCls> = new Map();
/** 网格切分器 */
@ -63,9 +72,6 @@ export class MaterialManager implements IMaterialManager {
/** 是否已经构建过素材 */
private built: boolean = false;
/** 标记列表 */
private readonly markList: symbol[] = [];
constructor() {
this.assetBuilder.pipe(this.assetStore);
}
@ -125,7 +131,6 @@ export class MaterialManager implements IMaterialManager {
addRowAnimate(
source: SizedCanvasImageSource,
map: ArrayLike<IBlockIdentifier>,
frames: number,
height: number
): Iterable<IMaterialData> {
return this.addMappedSource(
@ -133,18 +138,18 @@ export class MaterialManager implements IMaterialManager {
map,
this.tileStore,
this.rowSplitter,
height,
(tex: ITexture<number>) => {
tex.animated(new TextureColumnAnimater(), frames);
}
height
);
}
addAutotile(
source: SizedCanvasImageSource,
identifier: IBlockIdentifier
): IMaterialData {
const texture = new Texture(source);
): IMaterialData | null {
const frames = source.width === 96 ? 1 : 4;
const flattened = AutotileProcessor.flatten({ source, frames });
if (!flattened) return null;
const texture = new Texture(flattened);
this.tileStore.addTexture(identifier.num, texture);
this.tileStore.alias(identifier.num, identifier.id);
this.clsMap.set(identifier.num, BlockCls.Autotile);
@ -177,6 +182,7 @@ export class MaterialManager implements IMaterialManager {
logger.warn(78);
return null;
}
// 一个 tileset 可能不止 10000 个图块,需要计算偏移
const width = Math.floor(source.width / 32);
const height = Math.floor(source.height / 32);
const count = width * height;
@ -213,11 +219,26 @@ export class MaterialManager implements IMaterialManager {
return data;
}
getTile(identifier: number): ITexture | null {
getTile(identifier: number): IMaterialFramedData | null {
if (identifier < 10000) {
return this.tileStore.getTexture(identifier);
const texture = this.tileStore.getTexture(identifier);
if (!texture) return null;
const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown;
return {
texture,
cls,
offset: 32,
frames: getTextureFrame(cls, texture)
};
} else {
return this.cacheTileset(identifier);
const texture = this.cacheTileset(identifier);
if (!texture) return null;
return {
texture,
cls: BlockCls.Tileset,
offset: 32,
frames: 1
};
}
}
@ -229,11 +250,13 @@ export class MaterialManager implements IMaterialManager {
return this.imageStore.getTexture(identifier);
}
getTileByAlias(alias: string): ITexture | null {
getTileByAlias(alias: string): IMaterialFramedData | null {
if (/X\d{5,}/.test(alias)) {
return this.cacheTileset(parseInt(alias.slice(1)));
return this.getTile(parseInt(alias.slice(1)));
} else {
return this.tileStore.fromAlias(alias);
const identifier = this.tileStore.identifierOf(alias);
if (isNil(identifier)) return null;
return this.getTile(identifier);
}
}
@ -282,11 +305,33 @@ export class MaterialManager implements IMaterialManager {
} else {
// 如果有新图集,需要添加
const alias = `asset-${data.index}`;
const newAsset = new MaterialAsset(data);
newAsset.dirty();
this.assetStore.alias(data.index, alias);
this.assetDataStore.set(data.index, new MaterialAsset(data));
this.assetDataStore.set(data.index, newAsset);
}
}
/**
*
* @param composedData
* @param textures
*/
private cacheToAsset(
composedData: ITextureComposedData[],
textures: ITexture[]
) {
textures.forEach(tex => {
const assetData = composedData.find(v => v.assetMap.has(tex));
if (!assetData) {
logger.error(38);
return;
}
tex.toAsset(assetData);
});
composedData.forEach(v => this.checkAssetDirty(v));
}
cacheTileset(identifier: number): ITexture | null {
const newTexture = this.getTilesetOwnTexture(identifier);
if (!newTexture) return null;
@ -315,16 +360,59 @@ export class MaterialManager implements IMaterialManager {
this.numIdMap.set(v, `X${v}`);
});
const set = new Set(toAdd);
const data = this.assetBuilder.addTextureList(toAdd);
const res = [...data];
this.cacheToAsset(res, toAdd);
return toAdd;
}
/**
* `tileStore` `null`
* @param identifier
*/
private getFlattenedAutotile(
identifier: number
): SizedCanvasImageSource | null {
const cls = this.clsMap.get(identifier);
if (cls !== BlockCls.Autotile) return null;
if (this.tileStore.getTexture(identifier)) return null;
const source = this.autotileSource.get(identifier);
if (!source) return null;
const frames = source.width === 96 ? 1 : 4;
const flattened = AutotileProcessor.flatten({ source, frames });
if (!flattened) return null;
return flattened;
}
cacheAutotile(identifier: number): ITexture | null {
const flattened = this.getFlattenedAutotile(identifier);
if (!flattened) return null;
const tex = new Texture(flattened);
this.tileStore.addTexture(identifier, tex);
const data = this.assetBuilder.addTexture(tex);
tex.toAsset(data);
this.checkAssetDirty(data);
return tex;
}
cacheAutotileList(
identifierList: Iterable<number>
): Iterable<ITexture | null> {
const arr = [...identifierList];
const toAdd: ITexture[] = [];
arr.forEach(v => {
const flattened = this.getFlattenedAutotile(v);
if (!flattened) return;
const tex = new Texture(flattened);
this.tileStore.addTexture(v, tex);
toAdd.push(tex);
});
const data = this.assetBuilder.addTextureList(toAdd);
const res = [...data];
res.forEach(data => {
data.assetMap.keys().forEach(tex => {
if (set.has(tex)) tex.toAsset(data);
});
this.checkAssetDirty(data);
});
this.cacheToAsset(res, toAdd);
return toAdd;
}
@ -380,7 +468,7 @@ export class MaterialManager implements IMaterialManager {
if (isNil(cls)) return null;
const texture = this.getTextureOf(identifier, cls);
if (!texture) return null;
return texture.static();
return texture.render();
}
getRenderableByAlias(alias: string): ITextureRenderable | null {
@ -407,11 +495,22 @@ export class MaterialManager implements IMaterialManager {
return this.numIdMap.get(identifier);
}
setBigImage(identifier: number, image: ITexture): IBigImageData {
setBigImage(
identifier: number,
image: ITexture,
frames: number
): IBigImageReturn {
const bigImageId = this.bigImageId++;
this.bigImageStore.addTexture(bigImageId, image);
this.bigImageData.set(identifier, image);
const data: IBigImageData = {
const cls = this.clsMap.get(identifier) ?? BlockCls.Unknown;
const store: IMaterialFramedData = {
texture: image,
cls,
offset: image.width / 4,
frames
};
this.bigImageData.set(identifier, store);
const data: IBigImageReturn = {
identifier: bigImageId,
store: this.bigImageStore
};
@ -422,23 +521,27 @@ export class MaterialManager implements IMaterialManager {
return this.bigImageData.has(identifier);
}
getBigImage(identifier: number): ITexture | null {
getBigImage(identifier: number): IMaterialFramedData | null {
return this.bigImageData.get(identifier) ?? null;
}
getBigImageByAlias(alias: string): ITexture | null {
getBigImageByAlias(alias: string): IMaterialFramedData | null {
const identifier = this.idNumMap.get(alias);
if (isNil(identifier)) return null;
return this.bigImageData.get(identifier) ?? null;
}
getIfBigImage(identifier: number): ITexture | null {
getIfBigImage(identifier: number): IMaterialFramedData | null {
const bigImage = this.bigImageData.get(identifier) ?? null;
if (bigImage) return bigImage;
if (identifier < 10000) {
return this.tileStore.getTexture(identifier);
} else {
return this.cacheTileset(identifier);
else return this.getTile(identifier);
}
assetContainsTexture(texture: ITexture): boolean {
return this.assetMap.has(texture);
}
getTextureAsset(texture: ITexture): number | undefined {
return this.assetMap.get(texture);
}
}

View File

@ -1,5 +1,5 @@
import { IDirtyTracker, IDirtyMarker } from '@motajs/common';
import {
IRect,
ITexture,
ITextureComposedData,
ITextureRenderable,
@ -25,6 +25,17 @@ export const enum AutotileType {
Big3x4
}
export const enum AutotileConnection {
LeftUp = 0b1000_0000,
Up = 0b0100_0000,
RightUp = 0b0010_0000,
Right = 0b0001_0000,
RightDown = 0b0000_1000,
Down = 0b0000_0100,
LeftDown = 0b0000_0010,
Left = 0b0000_0001
}
export interface IMaterialData {
/** 此素材的贴图对象存入了哪个贴图存储对象 */
readonly store: ITextureStore;
@ -70,53 +81,27 @@ export interface IAutotileConnection {
readonly center: number;
}
export interface IAutotileRenderable {
/** 自动元件的图像源 */
readonly source: SizedCanvasImageSource;
/** 左上渲染的矩形范围 */
readonly lt: Readonly<IRect>;
/** 右上渲染的矩形范围 */
readonly rt: Readonly<IRect>;
/** 右下渲染的矩形范围 */
readonly rb: Readonly<IRect>;
/** 左下渲染的矩形范围 */
readonly lb: Readonly<IRect>;
}
export interface IBigImageData {
export interface IBigImageReturn {
/** 大怪物贴图在 store 中的标识符 */
readonly identifier: number;
/** 存储大怪物贴图的存储对象 */
readonly store: ITextureStore;
}
export interface IAssetDirtyMarker {
/**
*
*/
dirty(): void;
export interface IMaterialFramedData {
/** 贴图对象 */
readonly texture: ITexture;
/** 图块类型 */
readonly cls: BlockCls;
/** 贴图总帧数 */
readonly frames: number;
/** 每帧的横向偏移量 */
readonly offset: number;
}
export interface IAssetDirtyTracker {
/**
*
*/
mark(): symbol;
/**
*
* @param mark
*/
unmark(mark: symbol): void;
/**
*
* @param mark
*/
dirtySince(mark: symbol): boolean;
}
export interface IMaterialAsset extends IAssetDirtyTracker, IAssetDirtyMarker {
export interface IMaterialAsset
extends IDirtyTracker<boolean>,
IDirtyMarker<void> {
/** 图集的贴图数据 */
readonly data: ITextureComposedData;
}
@ -126,11 +111,12 @@ export interface IAutotileProcessor {
readonly manager: IMaterialManager;
/**
*
*
*
* @param autotile
* @param parent
* @param target
*/
setParent(autotile: number, parent: number): void;
setConnection(autotile: number, target: number): void;
/**
*
@ -145,52 +131,71 @@ export interface IAutotileProcessor {
): IAutotileConnection;
/**
*
*
* @param connection
* @param center
* @param target
* @param direction
* @returns
*/
updateConnectionFor(
connection: number,
center: number,
target: number,
direction: AutotileConnection
): number;
/**
*
* @param autotile
* @param connection
* @returns
*/
render(autotile: number, connection: number): ITextureRenderable | null;
/**
*
* @param tile
* @param connection
* @returns
*/
renderWith(
tile: IMaterialFramedData,
connection: number
): ITextureRenderable | null;
/**
*
* @param tile
* @param connection
* @returns
*/
renderWithoutCheck(
tile: IMaterialFramedData,
connection: number
): ITextureRenderable | null;
/**
*
* @param autotile
* @param connection
* @returns
*/
render(
renderAnimated(
autotile: number,
connection: number
): Generator<IAutotileRenderable, void> | null;
): Generator<ITextureRenderable, void>;
/**
* {@link ITexture.static}
* @param renderable
*
* @param autotile
* @param connection
* @returns
*/
fromStaticRenderable(
renderable: ITextureRenderable,
renderAnimatedWith(
tile: IMaterialFramedData,
connection: number
): Generator<IAutotileRenderable, void> | null;
/**
* {@link ITexture.dynamic} {@link ITexture.cycled}
*
* @param renderable
* @param connection
* @returns
*/
fromAnimatedRenderable(
renderable: ITextureRenderable,
connection: number
): IAutotileRenderable | null;
/**
* {@link ITexture.dynamic} {@link ITexture.cycled}
*
* @param texture
* @param generator
* @param connection
* @returns
*/
fromAnimatedGenerator(
texture: ITexture,
generator: Generator<ITextureRenderable> | null,
connection: number
): Generator<IAutotileRenderable, void> | null;
): Generator<ITextureRenderable, void>;
}
export interface IMaterialGetter {
@ -198,7 +203,7 @@ export interface IMaterialGetter {
*
* @param identifier
*/
getTile(identifier: number): ITexture | null;
getTile(identifier: number): IMaterialFramedData | null;
/**
*
@ -216,14 +221,14 @@ export interface IMaterialGetter {
* `bigImage`
* @param identifier
*/
getBigImage(identifier: number): ITexture | null;
getBigImage(identifier: number): IMaterialFramedData | null;
/**
* `bigImage` `bigImage`
* `null`
* @param identifier
*/
getIfBigImage(identifier: number): ITexture | null;
getIfBigImage(identifier: number): IMaterialFramedData | null;
/**
*
@ -249,7 +254,7 @@ export interface IMaterialAliasGetter {
* id
* @param alias id
*/
getTileByAlias(alias: string): ITexture | null;
getTileByAlias(alias: string): IMaterialFramedData | null;
/**
*
@ -279,7 +284,7 @@ export interface IMaterialAliasGetter {
* `bigImage`
* @param alias id
*/
getBigImageByAlias(alias: string): ITexture | null;
getBigImageByAlias(alias: string): IMaterialFramedData | null;
}
export interface IMaterialManager
@ -330,11 +335,12 @@ export interface IMaterialManager
*
* @param source
* @param identifier id
* @returns 西
*/
addAutotile(
source: SizedCanvasImageSource,
identifier: IBlockIdentifier
): IMaterialData;
): void;
/**
* tileset
@ -357,7 +363,7 @@ export interface IMaterialManager
): IMaterialData;
/**
* tileset
* tileset使 {@link cacheTilesetList}
* @param identifier tileset
*/
cacheTileset(identifier: number): ITexture | null;
@ -370,6 +376,20 @@ export interface IMaterialManager
identifierList: Iterable<number>
): Iterable<ITexture | null>;
/**
* 使 {@link cacheAutotileList}
* @param identifier
*/
cacheAutotile(identifier: number): ITexture | null;
/**
*
* @param identifierList
*/
cacheAutotileList(
identifierList: Iterable<number>
): Iterable<ITexture | null>;
/**
* 使
*/
@ -403,8 +423,25 @@ export interface IMaterialManager
* `bigImage`
* @param identifier
* @param image `bigImage`
* @param frames `bigImage`
*/
setBigImage(identifier: number, image: ITexture): IBigImageData;
setBigImage(
identifier: number,
image: ITexture,
frames: number
): IBigImageReturn;
/**
*
* @param texture
*/
assetContainsTexture(texture: ITexture): boolean;
/**
*
* @param texture
*/
getTextureAsset(texture: ITexture): number | undefined;
}
export interface IAssetBuilder {

View File

@ -1,3 +1,4 @@
import { ITexture } from '@motajs/render-assets';
import { BlockCls } from './types';
export function getClsByString(cls: Cls): BlockCls {
@ -24,3 +25,23 @@ export function getClsByString(cls: Cls): BlockCls {
return BlockCls.Unknown;
}
}
export function getTextureFrame(cls: BlockCls, texture: ITexture) {
switch (cls) {
case BlockCls.Animates:
case BlockCls.Enemy48:
case BlockCls.Npc48:
return 4;
case BlockCls.Autotile:
return texture.width === 384 ? 4 : 1;
case BlockCls.Enemys:
case BlockCls.Npcs:
return 2;
case BlockCls.Items:
case BlockCls.Terrains:
case BlockCls.Tileset:
return 1;
case BlockCls.Unknown:
return 0;
}
}

View File

@ -6,6 +6,8 @@ import { createViewport } from './viewport';
import { Icon, Winskin } from './misc';
import { Animate } from './animate';
import { createItemDetail } from './itemDetail';
import { logger } from '@motajs/common';
import { MapRender } from '../map/element';
export function createElements() {
createCache();
@ -66,6 +68,18 @@ export function createElements() {
return new Animate();
});
tagMap.register('icon', standardElementNoCache(Icon));
tagMap.register('map-render', (_0, _1, props) => {
if (!props) {
logger.error(42);
return new MapRender([]);
}
const { layerList } = props;
if (!layerList) {
logger.error(42);
return new MapRender([]);
}
return new MapRender(layerList);
});
}
export * from './animate';

View File

@ -11,6 +11,7 @@ import {
import { EAnimateEvent } from './animate';
import { EIconEvent, EWinskinEvent } from './misc';
import { IEnemyCollection } from '@motajs/types';
import { IRenderLayerData } from '../map/element';
export interface AnimateProps extends BaseProps {}
@ -60,6 +61,10 @@ export interface LayerProps extends BaseProps {
ex?: readonly ILayerRenderExtends[];
}
export interface MapRenderProps extends BaseProps {
layerList: IRenderLayerData;
}
declare module 'vue/jsx-runtime' {
namespace JSX {
export interface IntrinsicElements {

View File

@ -0,0 +1,31 @@
import { SizedCanvasImageSource } from '@motajs/render-assets';
import {
IMaterialGetter,
IMaterialManager,
materials
} from '@user/client-base';
import { IMapAssetData, IMapAssetManager } from './types';
import { PrivateListDirtyTracker } from '@motajs/common';
export class MapAssetManager implements IMapAssetManager {
materials: IMaterialManager = materials;
generateAsset(): IMapAssetData {
const data = new MapAssetData();
return data;
}
}
class MapAssetData
extends PrivateListDirtyTracker<number>
implements IMapAssetData
{
sourceList: ImageBitmap[] = [];
skipRef: Map<SizedCanvasImageSource, number> = new Map();
materials: IMaterialGetter = materials;
constructor() {
super(0);
}
}

View File

@ -0,0 +1,347 @@
import { clamp } from 'lodash-es';
import {
IBlockData,
IBlockIndex,
IBlockInfo,
IBlockSplitter,
IBlockSplitterConfig
} from './types';
export class BlockSplitter<T> implements IBlockSplitter<T> {
blockWidth: number = 0;
blockHeight: number = 0;
dataWidth: number = 0;
dataHeight: number = 0;
width: number = 0;
height: number = 0;
/** 分块映射 */
readonly blockMap: Map<number, IBlockData<T>> = new Map();
/** 数据宽度配置 */
private splitDataWidth: number = 0;
/** 数据高度配置 */
private splitDataHeight: number = 0;
/** 单个分块的宽度配置 */
private splitBlockWidth: number = 0;
/** 单个分块的高度配置 */
private splitBlockHeight: number = 0;
/**
*
* @param x
* @param y
*/
private checkLocRange(x: number, y: number) {
return x > 0 && y > 0 && x < this.width && y < this.height;
}
getBlockByLoc(x: number, y: number): IBlockData<T> | null {
if (!this.checkLocRange(x, y)) return null;
const index = y * this.width + x;
return this.blockMap.get(index) ?? null;
}
getBlockByIndex(index: number): IBlockData<T> | null {
return this.blockMap.get(index) ?? null;
}
setBlockByLoc(data: T, x: number, y: number): IBlockData<T> | null {
if (!this.checkLocRange(x, y)) return null;
const index = y * this.width + x;
const block = this.blockMap.get(index);
if (!block) return null;
block.data = data;
return block;
}
setBlockByIndex(data: T, index: number): IBlockData<T> | null {
const block = this.blockMap.get(index);
if (!block) return null;
block.data = data;
return block;
}
*iterateBlockByLoc(x: number, y: number): Generator<IBlockIndex, void> {
if (!this.checkLocRange(x, y)) return;
const index = y * this.width + x;
yield* this.iterateBlockByIndex(index);
}
*iterateBlockByIndex(index: number): Generator<IBlockIndex, void> {
const block = this.blockMap.get(index);
if (!block) return;
const startX = block.x * this.blockWidth;
const startY = block.y * this.blockHeight;
const endX = startX + block.width;
const endY = startY + block.height;
for (let ny = startY; ny < endY; ny++) {
for (let nx = startX; nx < endX; nx++) {
const index: IBlockIndex = {
x: nx,
y: ny,
dataX: nx * this.blockWidth,
dataY: ny * this.blockHeight,
index: ny * this.dataWidth + nx
};
yield index;
}
}
}
*iterateBlockByIndices(
indices: Iterable<number>
): Generator<IBlockIndex, void> {
for (const index of indices) {
yield* this.iterateBlockByIndex(index);
}
}
iterateBlocks(): Iterable<IBlockData<T>> {
return this.blockMap.values();
}
*iterateBlocksOfDataArea(
x: number,
y: number,
width: number,
height: number
): Generator<IBlockData<T>> {
const r = this.width - 1;
const b = this.height - 1;
const rx = x + width;
const by = y + height;
const left = clamp(Math.floor(x / this.blockWidth), 0, r);
const top = clamp(Math.floor(y / this.blockHeight), 0, b);
const right = clamp(Math.floor(rx / this.blockWidth), 0, r);
const bottom = clamp(Math.floor(by / this.blockHeight), 0, b);
for (let ny = left; ny <= right; ny++) {
for (let nx = top; nx <= bottom; nx++) {
const index = ny * this.width + nx;
const block = this.blockMap.get(index);
if (!block) continue;
yield block;
}
}
}
getIndexByLoc(x: number, y: number): number {
if (!this.checkLocRange(x, y)) return -1;
return y * this.width + x;
}
getLocByIndex(index: number): Loc | null {
if (index >= this.width * this.height) return null;
return {
x: index % this.width,
y: Math.floor(index / this.width)
};
}
getIndicesByLocList(list: Iterable<Loc>): Iterable<number> {
const res: number[] = [];
for (const { x, y } of list) {
res.push(this.getIndexByLoc(x, y));
}
return res;
}
getLocListByIndices(list: Iterable<number>): Iterable<Loc | null> {
const res: (Loc | null)[] = [];
for (const index of list) {
res.push(this.getLocByIndex(index));
}
return res;
}
getBlockByDataLoc(x: number, y: number): IBlockData<T> | null {
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) return null;
const index = y * this.width + x;
return this.blockMap.get(index) ?? null;
}
getBlockByDataIndex(index: number): IBlockData<T> | null {
const x = index % this.dataWidth;
const y = Math.floor(index / this.dataWidth);
return this.getBlockByDataLoc(x, y);
}
getIndicesByDataLocList(list: Iterable<Loc>): Set<number> {
const res = new Set<number>();
for (const { x, y } of list) {
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) continue;
res.add(bx + by * this.width);
}
return res;
}
getIndicesByDataIndices(list: Iterable<number>): Set<number> {
const res = new Set<number>();
for (const index of list) {
const x = index % this.dataWidth;
const y = Math.floor(index / this.dataWidth);
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) continue;
res.add(bx + by * this.width);
}
return res;
}
getBlocksByDataLocList(list: Iterable<Loc>): Set<IBlockData<T>> {
const res = new Set<IBlockData<T>>();
for (const { x, y } of list) {
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) continue;
const index = bx + by * this.width;
const data = this.blockMap.get(index);
if (data) res.add(data);
}
return res;
}
getBlocksByDataIndices(list: Iterable<number>): Set<IBlockData<T>> {
const res = new Set<IBlockData<T>>();
for (const index of list) {
const x = index % this.dataWidth;
const y = Math.floor(index / this.dataWidth);
const bx = Math.floor(x / this.blockWidth);
const by = Math.floor(y / this.blockHeight);
if (!this.checkLocRange(bx, by)) continue;
const blockIndex = bx + by * this.width;
const data = this.blockMap.get(blockIndex);
if (data) res.add(data);
}
return res;
}
configSplitter(config: IBlockSplitterConfig): void {
this.splitBlockWidth = config.blockWidth;
this.splitBlockHeight = config.blockHeight;
this.splitDataWidth = config.dataWidth;
this.splitBlockHeight = config.dataHeight;
}
private mapBlock(
x: number,
y: number,
realWidth: number,
width: number,
height: number,
fn: (block: IBlockInfo) => T
) {
const index = y * realWidth + x;
const block: IBlockInfo = {
index,
x,
y,
dataX: x * this.blockWidth,
dataY: y * this.blockHeight,
width,
height
};
const data = fn(block);
const blockData = new SplittedBlockData(this, block, data);
this.blockMap.set(index, blockData);
}
splitBlocks(mapFn: (block: IBlockInfo) => T): void {
this.blockMap.clear();
const restX = this.splitDataWidth % this.splitBlockWidth;
const restY = this.splitDataHeight % this.splitBlockHeight;
const width = Math.floor(this.splitDataWidth / this.splitBlockWidth);
const height = Math.floor(this.splitDataHeight / this.splitBlockHeight);
const hasXRest = restX > 0;
const hasYRest = restY > 0;
const realWidth = hasXRest ? width + 1 : width;
const bw = this.blockWidth;
const bh = this.blockHeight;
this.width = realWidth;
this.height = hasYRest ? height + 1 : height;
for (let ny = 0; ny < height; ny++) {
for (let nx = 0; nx < width; nx++) {
this.mapBlock(nx, ny, realWidth, bw, bh, mapFn);
}
}
if (hasXRest) {
for (let ny = 0; ny < height; ny++) {
this.mapBlock(width, ny, realWidth, restX, bh, mapFn);
}
}
if (hasYRest) {
for (let nx = 0; nx < width; nx++) {
this.mapBlock(nx, height, realWidth, bw, restY, mapFn);
}
}
if (hasXRest && hasYRest) {
this.mapBlock(width, height, realWidth, restX, restY, mapFn);
}
}
}
class SplittedBlockData<T> implements IBlockData<T> {
width: number;
height: number;
x: number;
y: number;
dataX: number;
dataY: number;
index: number;
data: T;
constructor(
readonly splitter: BlockSplitter<T>,
info: IBlockInfo,
data: T
) {
this.width = info.width;
this.height = info.height;
this.x = info.x;
this.y = info.y;
this.dataX = info.dataX;
this.dataY = info.dataY;
this.index = info.index;
this.data = data;
}
left(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x - 1, this.y);
}
right(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x + 1, this.y);
}
up(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x, this.y - 1);
}
down(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x, this.y + 1);
}
leftUp(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x - 1, this.y - 1);
}
leftDown(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x - 1, this.y + 1);
}
rightUp(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x + 1, this.y - 1);
}
rightDown(): IBlockData<T> | null {
return this.splitter.getBlockByLoc(this.x + 1, this.y + 1);
}
next(): IBlockData<T> | null {
return this.splitter.getBlockByIndex(this.index + 1);
}
}

View File

@ -0,0 +1,114 @@
import {
MotaOffscreenCanvas2D,
RenderItem,
Transform
} from '@motajs/render-core';
import { IMapLayer } from '@user/data-state';
import { IMapRenderer } from './types';
import { MapRenderer } from './renderer';
import { materials } from '@user/client-base';
import { ElementNamespace, ComponentInternalInstance } from 'vue';
export interface IRenderLayerData {
/** 图层对象 */
readonly layer: IMapLayer;
/** 图层纵深 */
readonly zIndex: number;
/** 图层别名 */
readonly alias?: string;
}
export class MapRender extends RenderItem {
/** 地图渲染器 */
readonly renderer: IMapRenderer;
/** 地图视角变换矩阵 */
readonly camera: Transform = new Transform();
/** 地图画布 */
readonly canvas: HTMLCanvasElement;
/** 画布上下文 */
readonly gl: WebGL2RenderingContext;
constructor(layerList: Iterable<IRenderLayerData>) {
super('static');
this.canvas = document.createElement('canvas');
const gl = this.canvas.getContext('webgl2')!;
this.gl = gl;
this.renderer = new MapRenderer(materials, this.gl, this.camera);
for (const layer of layerList) {
this.renderer.addLayer(layer.layer, layer.alias);
this.renderer.setZIndex(layer.layer, layer.zIndex);
}
}
/**
*
* @param layerList
*/
updateLayerList(layerList: Iterable<IRenderLayerData>) {
this.renderer
.getSortedLayer()
.forEach(v => this.renderer.removeLayer(v));
for (const layer of layerList) {
this.renderer.addLayer(layer.layer, layer.alias);
this.renderer.setZIndex(layer.layer, layer.zIndex);
}
}
private sizeGL(width: number, height: number) {
const ratio = this.highResolution ? devicePixelRatio : 1;
const scale = ratio * this.scale;
this.canvas.width = width * scale;
this.canvas.height = height * scale;
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
}
onResize(scale: number): void {
super.onResize(scale);
this.sizeGL(this.width, this.height);
}
size(width: number, height: number): void {
super.size(width, height);
this.sizeGL(width, height);
}
updateTransform(transform: Transform): void {
super.updateTransform(transform);
if (transform === this.camera) {
this.update();
}
}
protected render(canvas: MotaOffscreenCanvas2D): void {
console.time('map-element-render');
this.renderer.render(this.gl);
canvas.ctx.drawImage(
this.canvas,
0,
0,
this.canvas.width,
this.canvas.height
);
console.timeEnd('map-element-render');
}
patchProp(
key: string,
prevValue: any,
nextValue: any,
namespace?: ElementNamespace,
parentComponent?: ComponentInternalInstance | null
): void {
switch (key) {
case 'layerList': {
this.updateLayerList(nextValue);
break;
}
}
super.patchProp(key, prevValue, nextValue, namespace, parentComponent);
}
}

View File

@ -0,0 +1,3 @@
export * from './asset';
export * from './renderer';
export * from './types';

View File

@ -0,0 +1,229 @@
import { linear, TimingFn } from 'mutate-animate';
import { IMapRenderer, IMapVertexGenerator, IMovingBlock } from './types';
import { IMaterialFramedData, IMaterialManager } from '@user/client-base';
import { logger } from '@motajs/common';
import { IMapLayer } from '@user/data-state';
export interface IMovingRenderer {
/** 素材管理器 */
readonly manager: IMaterialManager;
/** 顶点数组生成器 */
readonly vertex: IMapVertexGenerator;
/**
*
*/
getTimestamp(): number;
/**
*
* @param block
*/
deleteMoving(block: IMovingBlock): void;
}
export class MovingBlock implements IMovingBlock {
readonly texture: IMaterialFramedData;
readonly tile: number;
readonly renderer: IMovingRenderer;
readonly index: number;
readonly layer: IMapLayer;
x: number = 0;
y: number = 0;
/** 当前动画开始的时刻 */
private startTime: number = 0;
/** 是否是直线动画 */
private line: boolean = false;
/** 是否是相对模式 */
private relative: boolean = false;
/** 目标横坐标 */
private targetX: number = 0;
/** 目标纵坐标 */
private targetY: number = 0;
/** 直线移动的横坐标增量 */
private dx: number = 0;
/** 直线移动的纵坐标增量 */
private dy: number = 0;
/** 动画时长 */
private time: number = 0;
/** 速率曲线 */
private timing: TimingFn = () => 0;
/** 移动轨迹曲线 */
private curve: TimingFn<2> = () => [0, 0];
/** 动画开始时横坐标 */
private startX: number = 0;
/** 动画开始时纵坐标 */
private startY: number = 0;
/** 当前动画是否已经结束 */
private end: boolean = false;
/** 兑现函数 */
private promiseFunc: () => void = () => {};
constructor(
renderer: IMovingRenderer & IMapRenderer,
index: number,
layer: IMapLayer,
block: number | IMaterialFramedData,
x: number,
y: number
) {
this.renderer = renderer;
this.index = index;
this.layer = layer;
this.x = x;
this.y = y;
if (typeof block === 'number') {
this.texture = renderer.manager.getTile(block)!;
this.tile = block;
} else {
if (!renderer.manager.assetContainsTexture(block.texture)) {
logger.error(34);
}
if (renderer.getOffsetIndex(block.offset) === -1) {
logger.error(41);
}
this.texture = block;
this.tile = -1;
}
}
lineTo(
x: number,
y: number,
time: number,
timing?: TimingFn
): Promise<this> {
this.startX = this.x;
this.startY = this.y;
this.targetX = x;
this.targetY = y;
this.dx = x - this.x;
this.dy = y - this.y;
this.time = time;
this.relative = false;
if (time === 0) {
this.x = x;
this.y = y;
this.end = true;
return Promise.resolve(this);
}
this.end = false;
this.timing = timing ?? linear();
this.line = true;
return new Promise(res => {
this.promiseFunc = () => res(this);
});
}
moveAs(curve: TimingFn<2>, time: number, timing?: TimingFn): Promise<this> {
this.time = time;
this.line = false;
this.relative = false;
this.startX = this.x;
this.startY = this.y;
if (time === 0) {
const [tx, ty] = curve(1);
this.x = tx;
this.y = ty;
this.end = true;
return Promise.resolve(this);
}
this.end = false;
this.timing = timing ?? linear();
this.curve = curve;
return new Promise(res => {
this.promiseFunc = () => res(this);
});
}
moveRelative(
curve: TimingFn<2>,
time: number,
timing?: TimingFn
): Promise<this> {
this.time = time;
this.line = false;
this.relative = false;
this.startX = this.x;
this.startY = this.y;
if (time === 0) {
const [tx, ty] = curve(1);
this.x = tx + this.startX;
this.y = ty + this.startY;
this.end = true;
return Promise.resolve(this);
}
this.end = false;
this.timing = timing ?? linear();
this.curve = curve;
return new Promise(res => {
this.promiseFunc = () => res(this);
});
}
stepMoving(timestamp: number): boolean {
if (this.end) return false;
const dt = timestamp - this.startTime;
if (this.line) {
if (dt > this.time) {
this.x = this.targetX;
this.y = this.targetY;
this.end = true;
this.promiseFunc();
return false;
} else {
const timeProgress = dt / this.time;
const progress = this.timing(timeProgress);
this.x = this.startX + progress * this.dx;
this.y = this.startY + progress * this.dy;
}
} else {
if (dt > this.time) {
const [tx, ty] = this.curve(1);
if (this.relative) {
this.x = tx + this.startX;
this.y = ty + this.startY;
} else {
this.x = tx;
this.y = ty;
}
this.end = true;
this.promiseFunc();
return false;
} else {
const timeProgress = dt / this.time;
const progress = this.timing(timeProgress);
const [tx, ty] = this.curve(progress);
if (this.relative) {
this.x = tx + this.startX;
this.y = ty + this.startY;
} else {
this.x = tx;
this.y = ty;
}
}
}
return true;
}
enableFrameAnimate(): void {
this.renderer.vertex.enableDynamicFrameAnimate(this);
}
disableFrameAnimate(): void {
this.renderer.vertex.disableDynamicFrameAnimate(this);
}
setAlpha(alpha: number): void {
this.renderer.vertex.setDynamicAlpha(this, alpha);
}
destroy(): void {
this.renderer.deleteMoving(this);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
#version 300 es
precision highp float;
in vec3 v_texCoord;
out vec4 outColor;
uniform sampler2DArray u_sampler;
void main() {
outColor = texture(u_sampler, v_texCoord);
}

View File

@ -0,0 +1,18 @@
#version 300 es
precision highp float;
in vec2 a_position;
in vec2 a_texCoord;
out vec3 v_texCoord;
uniform float u_nowFrame;
uniform mat3 u_transform;
void main() {
// 背景图永远是全图都画,因此变换矩阵应该作用于纹理坐标
vec3 texCoord = vec3(a_texCoord, 1.0);
vec3 transformed = u_transform * texCoord;
v_texCoord = vec3(transformed.xy, u_nowFrame);
gl_Position = vec4(a_position, 0.0, 1.0);
}

View File

@ -0,0 +1,14 @@
#version 300 es
precision highp float;
precision mediump uint;
in vec4 v_texCoord;
out vec4 outColor;
uniform sampler2DArray u_sampler;
void main() {
vec4 texColor = texture(u_sampler, v_texCoord.xyz);
outColor = vec4(texColor.rgb, texColor.a * v_texCoord.a);
}

View File

@ -0,0 +1,26 @@
#version 300 es
precision highp float;
precision mediump uint;
in vec3 a_position;
in vec3 a_texCoord;
// x: 最大帧数y: 偏移池索引,实例化绘制传入
in ivec2 a_offsetData;
// 不透明度,用于前景层虚化
in float a_alpha;
// x,y,z: 纹理坐标w: 不透明度
out vec4 v_texCoord;
uniform vec2 u_offsetPool[$1];
uniform uint u_nowFrame;
uniform mat3 u_transform;
void main() {
// 偏移量
uint offset = mod(u_nowFrame, a_offsetData.x);
float fOffset = float(offset);
// 贴图坐标
v_texCoord = vec4(a_texCoord.xy + u_offsetPool[a_offsetData.y] * fOffset, a_texCoord.z, a_alpha);
gl_Position = vec4(u_transform * a_position, 1.0);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,105 @@
import { Transform } from '@motajs/render-core';
import {
IBlockData,
IMapRenderArea,
IMapRenderData,
IMapRenderer,
IMapVertexBlock,
IMapVertexGenerator,
IMapViewportController
} from './types';
import { clamp } from 'lodash-es';
export class MapViewport implements IMapViewportController {
transform: Transform = new Transform();
/** 顶点生成器 */
readonly vertex: IMapVertexGenerator;
constructor(readonly renderer: IMapRenderer) {
this.vertex = renderer.vertex;
}
getRenderArea(): IMapRenderData {
const { cellWidth, cellHeight, renderWidth, renderHeight } =
this.renderer;
const { blockWidth, blockHeight, width, height } = this.vertex.block;
// 减一是因为第一个像素是 0所以最后一个像素就是宽度减一
const r = renderWidth - 1;
const b = renderHeight - 1;
// 其实只需要算左上角和右下角就行了
const [left, top] = this.transform.transformed(0, 0);
const [right, bottom] = this.transform.transformed(r, b);
const cl = left / cellWidth;
const ct = top / cellHeight;
const cr = right / cellWidth;
const cb = bottom / cellHeight;
const blockLeft = clamp(Math.floor(cl / blockWidth), 0, width - 1);
const blockRight = clamp(Math.floor(cr / blockWidth), 0, width - 1);
const blockTop = clamp(Math.floor(ct / blockHeight), 0, height - 1);
const blockBottom = clamp(Math.floor(cb / blockHeight), 0, height - 1);
const renderArea: IMapRenderArea[] = [];
const updateArea: IMapRenderArea[] = [];
const blockList: IBlockData<IMapVertexBlock>[] = [];
// 使用这种方式的话,索引在换行之前都是连续的,方便整合
for (let ny = blockTop; ny <= blockBottom; ny++) {
const first = this.vertex.block.getBlockByLoc(blockLeft, ny)!;
const last = this.vertex.block.getBlockByLoc(blockRight, ny)!;
if (first.data.dirty) {
blockList.push(first);
}
if (last.data.dirty) {
blockList.push(last);
}
renderArea.push({
startIndex: first.data.startIndex,
endIndex: last.data.endIndex,
count: last.data.endIndex - first.data.startIndex
});
for (let nx = blockLeft + 1; nx < blockRight; nx++) {
const block = this.vertex.block.getBlockByLoc(nx, ny)!;
if (block.data.dirty) {
blockList.push(block);
}
}
}
if (blockList.length > 0) {
if (blockList.length === 1) {
const block = blockList[0];
updateArea.push(block.data);
} else {
let continuousStart: IBlockData<IMapVertexBlock> = blockList[0];
let continuousLast: IBlockData<IMapVertexBlock> = blockList[0];
for (let i = 1; i < blockList.length; i++) {
const block = blockList[i];
if (block.index === continuousLast.index + 1) {
// 连续则合并
continuousLast = block;
} else {
const start = continuousStart.data.startIndex;
const end = continuousLast.data.endIndex;
updateArea.push({
startIndex: start,
endIndex: end,
count: end - start
});
continuousStart = block;
continuousLast = block;
}
}
}
}
return {
render: renderArea,
dirty: updateArea,
blockList
};
}
bindTransform(transform: Transform): void {
this.transform = transform;
}
}

View File

@ -19,6 +19,20 @@ export const MAP_HEIGHT = CELL_SIZE * MAP_BLOCK_HEIGHT;
export const HALF_MAP_WIDTH = MAP_WIDTH / 2;
/** 地图高度的一半 */
export const HALF_MAP_HEIGHT = MAP_HEIGHT / 2;
/**
*
*
*/
export const DYNAMIC_RESERVE = 16;
/**
*
*
*/
export const MOVING_TOLERANCE = 60;
/** 每个格子的默认宽度,现阶段用处不大 */
export const CELL_WIDTH = 32;
/** 每个格子的默认高度,现阶段用处不大 */
export const CELL_HEIGHT = 32;
//#region 状态栏

View File

@ -56,6 +56,7 @@ import {
LayerGroup
} from '../elements';
import { isNil } from 'lodash-es';
import { materials } from '@user/client-base';
const MainScene = defineComponent(() => {
//#region 基本定义
@ -267,8 +268,25 @@ const MainScene = defineComponent(() => {
core.doRegisteredAction('onmove', bx, by, ev.offsetX, ev.offsetY);
};
const testRender = (canvas: MotaOffscreenCanvas2D) => {
const tileset = materials.getAsset(0);
if (!tileset) return;
canvas.ctx.drawImage(
tileset.data.texture.source,
0,
0,
canvas.width,
canvas.height
);
};
return () => (
<container id="main-scene" width={MAIN_WIDTH} height={MAIN_HEIGHT}>
<sprite
render={testRender}
loc={[180, 0, 480, 480]}
zIndex={1000}
/>
<LeftStatusBar
loc={[0, 0, STATUS_BAR_WIDTH, STATUS_BAR_HEIGHT]}
status={leftStatus}

View File

@ -0,0 +1,10 @@
import { LayerState } from './layerState';
import { ICoreState, ILayerState } from './types';
export class CoreState implements ICoreState {
readonly layer: ILayerState;
constructor() {
this.layer = new LayerState();
}
}

View File

@ -0,0 +1,22 @@
import { CoreState } from './core';
export function createCoreState() {
const width = core._WIDTH_;
const height = core._HEIGHT_;
const bg = state.layer.addLayer(width, height);
const bg2 = state.layer.addLayer(width, height);
const event = state.layer.addLayer(width, height);
const fg = state.layer.addLayer(width, height);
const fg2 = state.layer.addLayer(width, height);
state.layer.setLayerAlias(bg, 'bg');
state.layer.setLayerAlias(bg2, 'bg2');
state.layer.setLayerAlias(event, 'event');
state.layer.setLayerAlias(fg, 'fg');
state.layer.setLayerAlias(fg2, 'fg2');
}
export const state = new CoreState();
export * from './core';
export * from './layerState';
export * from './types';

View File

@ -0,0 +1,60 @@
import { logger } from '@motajs/common';
import { IMapLayer, MapLayer } from '../map';
import { ILayerState } from './types';
export class LayerState implements ILayerState {
readonly layerList: WeakSet<IMapLayer> = new WeakSet();
/** 图层到图层别名映射 */
readonly layerAliasMap: WeakMap<IMapLayer, string> = new WeakMap();
/** 图层别名到图层的映射 */
readonly aliasLayerMap: WeakMap<symbol, IMapLayer> = new WeakMap();
addLayer(width: number, height: number): IMapLayer {
const array = new Uint32Array(width * height);
const layer = new MapLayer(array, width, height);
this.layerList.add(layer);
return layer;
}
removeLayer(layer: IMapLayer): void {
this.layerList.delete(layer);
const alias = this.layerAliasMap.get(layer);
if (alias) {
const symbol = Symbol.for(alias);
this.aliasLayerMap.delete(symbol);
this.layerAliasMap.delete(layer);
}
}
setLayerAlias(layer: IMapLayer, alias: string): void {
const symbol = Symbol.for(alias);
if (this.aliasLayerMap.has(symbol)) {
logger.warn(84, alias);
return;
}
this.layerAliasMap.set(layer, alias);
this.aliasLayerMap.set(symbol, layer);
}
getLayerByAlias(alias: string): IMapLayer | null {
const symbol = Symbol.for(alias);
return this.aliasLayerMap.get(symbol) ?? null;
}
getLayerAlias(layer: IMapLayer): string | undefined {
return this.layerAliasMap.get(layer);
}
resizeLayer(
layer: IMapLayer,
width: number,
height: number,
keepBlock?: boolean
): void {
if (keepBlock) {
layer.resize(width, height);
} else {
layer.resize2(width, height);
}
}
}

View File

@ -0,0 +1,57 @@
import { IMapLayer } from '../map';
export interface ILayerState {
/** 地图列表 */
readonly layerList: WeakSet<IMapLayer>;
/**
*
* @param width
* @param height
*/
addLayer(width: number, height: number): IMapLayer;
/**
*
* @param layer
*/
removeLayer(layer: IMapLayer): void;
/**
*
* @param layer
* @param alias
*/
setLayerAlias(layer: IMapLayer, alias: string): void;
/**
*
* @param alias
*/
getLayerByAlias(alias: string): IMapLayer | null;
/**
*
* @param layer
*/
getLayerAlias(layer: IMapLayer): string | undefined;
/**
*
* @param layer
* @param width
* @param height
* @param keepBlock
*/
resizeLayer(
layer: IMapLayer,
width: number,
height: number,
keepBlock?: boolean
): void;
}
export interface ICoreState {
/** 地图状态 */
readonly layer: ILayerState;
}

View File

@ -1,9 +1,17 @@
import { loading } from '@user/data-base';
import { createMechanism } from './mechanism';
import { createCoreState } from './core';
export function create() {
createMechanism();
loading.once('loaded', () => {
// 加载后初始化全局状态
createCoreState();
});
}
export * from './core';
export * from './enemy';
export * from './map';
export * from './mechanism';
export * from './state';

View File

@ -0,0 +1,2 @@
export * from './mapLayer';
export * from './types';

View File

@ -0,0 +1,260 @@
import { isNil } from 'lodash-es';
import {
IMapLayer,
IMapLayerData,
IMapLayerExtends,
IMapLayerExtendsController
} from './types';
import { logger } from '@motajs/common';
interface IExtendsData {
readonly ex: IMapLayerExtends;
readonly controller: IMapLayerExtendsController;
}
export class MapLayer implements IMapLayer {
width: number;
height: number;
empty: boolean = true;
/** 已经加载完毕的图层拓展 */
private loadedExtends: Set<IExtendsData> = new Set();
/** 添加的图层拓展 */
private addedExtends: Map<string, IExtendsData> = new Map();
/** 地图图块数组 */
private mapArray: Uint32Array;
/** 地图数据引用 */
private mapData: IMapLayerData;
constructor(array: Uint32Array, width: number, height: number) {
this.width = width;
this.height = height;
const area = width * height;
this.mapArray = new Uint32Array(area);
// 超出的裁剪,不足的补零
this.mapArray.set(array);
this.mapData = {
expired: false,
array: this.mapArray
};
}
resize(width: number, height: number): void {
if (this.width === width && this.height === height) {
this.loadedExtends.forEach(v => {
v.ex.onResize?.(v.controller, width, height);
});
return;
}
this.mapData.expired = true;
const before = this.mapArray;
const beforeWidth = this.width;
const beforeHeight = this.height;
const beforeArea = beforeWidth * beforeHeight;
this.width = width;
this.height = height;
const area = width * height;
const newArray = new Uint32Array(area);
this.mapArray = newArray;
// 将原来的地图数组赋值给现在的
if (beforeArea > area) {
// 如果地图变小了,那么直接设置,不需要补零
for (let ny = 0; ny < height; ny++) {
const begin = ny * beforeWidth;
newArray.set(before.subarray(begin, begin + width), ny * width);
}
} else {
// 如果地图变大了,那么需要补零。因为新数组本来就是用 0 填充的,实际上只要赋值就可以了
for (let ny = 0; ny < beforeHeight; ny++) {
const begin = ny * beforeWidth;
newArray.set(
before.subarray(begin, begin + beforeWidth),
ny * width
);
}
}
this.mapData = {
expired: false,
array: this.mapArray
};
this.loadedExtends.forEach(v => {
v.ex.onResize?.(v.controller, width, height);
});
}
resize2(width: number, height: number): void {
if (this.width === width && this.height === height) {
this.mapArray.fill(0);
this.loadedExtends.forEach(v => {
v.ex.onResize?.(v.controller, width, height);
});
return;
}
this.mapData.expired = true;
this.width = width;
this.height = height;
this.mapArray = new Uint32Array(width * height);
this.mapData = {
expired: false,
array: this.mapArray
};
this.loadedExtends.forEach(v => {
v.ex.onResize?.(v.controller, width, height);
});
this.empty = true;
}
setBlock(block: number, x: number, y: number): void {
const index = y * this.width + x;
this.mapArray[index] = block;
this.loadedExtends.forEach(v => {
v.ex.onUpdateBlock?.(v.controller, block, x, y);
});
if (block !== 0) {
this.empty = false;
}
}
getBlock(x: number, y: number): number {
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
// 不在地图内,返回 -1
return -1;
}
return this.mapArray[y * this.width + x];
}
putMapData(array: Uint32Array, x: number, y: number, width: number): void {
if (array.length % width !== 0) {
logger.warn(8);
}
const height = Math.ceil(array.length / width);
if (width === this.width && height === this.height) {
this.mapArray.set(array);
return;
}
const w = this.width;
const r = x + width;
const b = y + height;
if (x < 0 || y < 0 || r > w || b > this.height) {
logger.warn(9);
}
const nl = Math.max(x, 0);
const nt = Math.max(y, 0);
const nr = Math.min(r, w);
const nb = Math.min(b, this.height);
const nw = nr - nl;
const nh = nb - nt;
let empty = true;
for (let ny = 0; ny < nh; ny++) {
const start = ny * nw;
const offset = (ny + nt) * w + nl;
const sub = array.subarray(start, start + nw);
if (empty && sub.some(v => v !== 0)) {
// 空地图判断
empty = false;
}
this.mapArray.set(array.subarray(start, start + nw), offset);
}
this.loadedExtends.forEach(v => {
v.ex.onUpdateArea?.(v.controller, x, y, width, height);
});
this.empty &&= empty;
}
getMapData(): Uint32Array;
getMapData(
x: number,
y: number,
width: number,
height: number
): Uint32Array;
getMapData(
x?: number,
y?: number,
width?: number,
height?: number
): Uint32Array {
if (isNil(x)) {
return new Uint32Array(this.mapArray);
}
if (isNil(y) || isNil(width) || isNil(height)) {
logger.warn(80);
return new Uint32Array();
}
const w = this.width;
const h = this.height;
const r = x + width;
const b = y + height;
if (x < 0 || y < 0 || r > w || b > h) {
logger.warn(81);
}
const res = new Uint32Array(width * height);
const arr = this.mapArray;
const nr = Math.min(r, w);
const nb = Math.min(b, h);
for (let nx = x; nx < nr; nx++) {
for (let ny = y; ny < nb; ny++) {
const origin = ny * w + nx;
const target = (ny - y) * width + (nx - x);
res[target] = arr[origin];
}
}
return res;
}
/**
*
*/
getMapRef(): IMapLayerData {
return this.mapData;
}
loadExtends(ex: IMapLayerExtends): boolean {
if (!this.addedExtends.has(ex.id)) return false;
ex.awake?.();
const data = this.addedExtends.get(ex.id)!;
this.loadedExtends.add(data);
return true;
}
addExtends(ex: IMapLayerExtends): IMapLayerExtendsController {
const controller = new MapLayerExtendsController(this, ex);
this.addedExtends.set(ex.id, {
ex,
controller
});
return controller;
}
removeExtends(ex: IMapLayerExtends | string): void {
const id = typeof ex === 'string' ? ex : ex.id;
const data = this.addedExtends.get(id);
if (!data) return;
data.ex.destroy?.();
this.addedExtends.delete(id);
this.loadedExtends.delete(data);
}
}
class MapLayerExtendsController implements IMapLayerExtendsController {
loaded: boolean = false;
constructor(
readonly layer: MapLayer,
readonly ex: IMapLayerExtends
) {}
load(): void {
this.loaded = this.layer.loadExtends(this.ex);
}
getMapData(): Readonly<IMapLayerData> {
return this.layer.getMapRef();
}
unload(): void {
this.layer.removeExtends(this.ex);
this.loaded = false;
}
}

View File

@ -0,0 +1,167 @@
export interface IMapLayerData {
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
expired: boolean;
/** 地图图块数组,是对内部存储的直接引用 */
array: Uint32Array;
}
export interface IMapLayerHooks {
/**
*
* @param dependencies
*/
awake(): void;
/**
*
*/
destroy(): void;
/**
*
* @param controller
* @param width
* @param height
*/
onResize(
controller: IMapLayerExtendsController,
width: number,
height: number
): void;
/**
*
* @param controller
* @param x
* @param y
* @param width
* @param height
*/
onUpdateArea(
controller: IMapLayerExtendsController,
x: number,
y: number,
width: number,
height: number
): void;
/**
*
* @param controller
* @param block
* @param x
* @param y
*/
onUpdateBlock(
controller: IMapLayerExtendsController,
block: number,
x: number,
y: number
): void;
}
export interface IMapLayerExtends extends Partial<IMapLayerHooks> {
/** 这个拓展对象的标识符 */
readonly id: string;
}
export interface IMapLayerExtendsController {
/** 当前图层拓展是否已经被加载 */
readonly loaded: boolean;
/** 拓展所属的图层对象 */
readonly layer: IMapLayer;
/**
*
*/
load(): void;
/**
*
*/
getMapData(): Readonly<IMapLayerData>;
/**
*
*/
unload(): void;
}
export interface IMapLayer {
/** 地图宽度 */
readonly width: number;
/** 地图高度 */
readonly height: number;
/** 地图是否全部空白,此值具有保守性,即如果其为 `true`,则地图一定空白,但是如果其为 `false`,那么地图也有可能空白 */
readonly empty: boolean;
/**
*
* @param width
* @param height
*/
resize(width: number, height: number): void;
/**
*
* @param width
* @param height
*/
resize2(width: number, height: number): void;
/**
*
* @param block
* @param x
* @param y
*/
setBlock(block: number, x: number, y: number): void;
/**
*
* @param x
* @param y
* @returns 0 -1
*/
getBlock(x: number, y: number): number;
/**
*
* @param array
* @param x
* @param y
* @param width
*/
putMapData(array: Uint32Array, x: number, y: number, width: number): void;
/**
*
*/
getMapData(): Uint32Array;
/**
*
* @param x
* @param y
* @param width
* @param height
*/
getMapData(
x: number,
y: number,
width: number,
height: number
): Uint32Array;
/**
* 使
* @param ex
* @returns
*/
addExtends(ex: IMapLayerExtends): IMapLayerExtendsController;
/**
*
* @param ex
*/
removeExtends(ex: IMapLayerExtends | string): void;
}

View File

@ -1,5 +1,14 @@
import { logger } from '@motajs/common';
export interface ICompiledProgram {
/** 着色器程序 */
readonly program: WebGLProgram;
/** 顶点着色器对象 */
readonly vertexShader: WebGLShader;
/** 片段着色器对象 */
readonly fragmentShader: WebGLShader;
}
/**
*
* @param gl WebGL2
@ -61,11 +70,17 @@ export function compileProgramWith(
gl: WebGL2RenderingContext,
vs: string,
fs: string
): WebGLProgram | null {
): 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 compileProgram(gl, vsShader, fsShader);
return {
program,
vertexShader: vsShader,
fragmentShader: fsShader
};
}

View File

@ -1,2 +1,3 @@
export * from './glUtils';
export * from './keyCodes';
export * from './types';

View File

@ -0,0 +1,133 @@
import { isNil } from 'lodash-es';
import { IDirtyTracker } from './types';
/**
* `dirtySince` `true`
*/
export class PrivateBooleanDirtyTracker implements IDirtyTracker<boolean> {
/** 标记映射 */
private markMap: WeakMap<symbol, number> = new WeakMap();
/** 脏标记 */
private dirtyFlag: number = 0;
mark(): symbol {
const symbol = Symbol();
this.markMap.set(symbol, this.dirtyFlag);
return symbol;
}
unmark(mark: symbol): void {
this.markMap.delete(mark);
}
dirtySince(mark: symbol): boolean {
const num = this.markMap.get(mark);
if (isNil(num)) return true;
return num < this.dirtyFlag;
}
hasMark(symbol: symbol): 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<symbol, number> = new WeakMap();
/** 脏标记数字 */
private dirtyFlag: number = 0;
constructor(protected length: number) {}
mark(): symbol {
const symbol = Symbol();
this.symbolMap.set(symbol, this.dirtyFlag);
return symbol;
}
unmark(mark: symbol): void {
this.symbolMap.delete(mark);
}
dirtySince(mark: symbol): 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: symbol): 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<symbol, number> = new WeakMap();
/** 脏标记数字 */
private dirtyFlag: number = 0;
constructor(protected length: number) {}
mark(): symbol {
const symbol = Symbol();
this.symbolMap.set(symbol, this.dirtyFlag);
return symbol;
}
unmark(mark: symbol): void {
this.symbolMap.delete(mark);
}
dirtySince(mark: symbol): 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: symbol): boolean {
return this.symbolMap.has(symbol);
}
protected dirty(data: T): void {
this.dirtyFlag++;
this.markMap.set(data, this.dirtyFlag);
}
}

View File

@ -1,2 +1,4 @@
export * from './dirtyTracker';
export * from './logger';
export * from './utils';
export * from './types';

View File

@ -27,6 +27,21 @@
"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 layerList property of map-render element is required.",
"1101": "Shadow extension needs 'floor-hero' extension as dependency.",
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency.",
"1301": "Portal extension need 'floor-binder' extension as dependency.",
@ -112,6 +127,11 @@
"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 'updateBlock' 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.",
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
}

View File

@ -0,0 +1,32 @@
export interface IDirtyMarker<T> {
/**
*
* @param data
*/
dirty(data: T): void;
}
export interface IDirtyTracker<T> {
/**
*
*/
mark(): symbol;
/**
*
* @param mark
*/
unmark(mark: symbol): void;
/**
*
* @param mark
*/
dirtySince(mark: symbol): T;
/**
*
* @param symbol
*/
hasMark(symbol: symbol): boolean;
}

View File

@ -1,123 +1,82 @@
import { logger } from '@motajs/common';
import { ITexture, ITextureAnimater, ITextureRenderable } from './types';
/**
*
*
*/
export abstract class FrameBasedAnimater<T>
implements ITextureAnimater<number, T>
{
texture: ITexture<number, T> | null = null;
/** 动画的总帧数 */
protected frames: number = 0;
create(texture: ITexture, data: number): void {
if (this.texture) {
logger.warn(70);
return;
}
this.texture = texture;
this.frames = data;
}
/**
*
*/
protected check(): boolean {
if (!this.texture || this.frames === 0) {
logger.warn(71);
return false;
}
if (this.texture.height % this.frames !== 0) {
logger.warn(72);
return false;
}
return true;
}
abstract open(init: T): Generator<ITextureRenderable> | null;
abstract cycled(init: T): Generator<ITextureRenderable> | null;
}
/**
*
*/
export class TextureRowAnimater extends FrameBasedAnimater<void> {
*open(): Generator<ITextureRenderable> | null {
if (!this.check()) return null;
const renderable = this.texture!.static();
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 } = this.texture!;
const h = height / this.frames;
for (let i = 0; i < this.frames; i++) {
const { width: w, height } = texture!;
const h = height / frames;
for (let i = 0; i < frames; i++) {
const renderable: ITextureRenderable = {
source: this.texture!.source,
rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h })
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
}
}
*cycled(): Generator<ITextureRenderable> | null {
if (!this.check()) return null;
const renderable = this.texture!.static();
*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 } = this.texture!;
const h = height / this.frames;
const { width: w, height } = texture;
const h = height / frames;
let i = 0;
while (true) {
const renderable: ITextureRenderable = {
source: this.texture!.source,
rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h })
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
i++;
if (i === this.frames) i = 0;
if (i === frames) i = 0;
}
}
}
/**
*
*
*/
export class TextureColumnAnimater extends FrameBasedAnimater<void> {
*open(): Generator<ITextureRenderable> | null {
if (!this.check()) return null;
const renderable = this.texture!.static();
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 } = renderable.rect;
const { width, height: h } = this.texture!;
const w = width / this.frames;
for (let i = 0; i < this.frames; i++) {
const { width, height: h } = texture!;
const w = width / frames;
for (let i = 0; i < frames; i++) {
const renderable: ITextureRenderable = {
source: this.texture!.source,
rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h })
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
}
}
*cycled(): Generator<ITextureRenderable> | null {
if (!this.check()) return null;
const renderable = this.texture!.static();
*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 } = this.texture!;
const w = width / this.frames;
const { width, height: h } = texture;
const w = width / frames;
let i = 0;
while (true) {
const renderable: ITextureRenderable = {
source: this.texture!.source,
rect: this.texture!.clampRect({ x: i * w + ox, y: oy, w, h })
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
i++;
if (i === this.frames) i = 0;
if (i === frames) i = 0;
}
}
}
export interface IScanAnimaterCreate {
export interface IScanAnimaterData {
/** 每帧的宽度 */
readonly width: number;
/** 每帧的高度 */
@ -130,82 +89,47 @@ export interface IScanAnimaterCreate {
*
*/
export class TextureScanAnimater
implements ITextureAnimater<IScanAnimaterCreate, void>
implements ITextureAnimater<IScanAnimaterData>
{
texture: ITexture<IScanAnimaterCreate, void> | null = null;
*once(
texture: ITexture,
data: IScanAnimaterData
): Generator<ITextureRenderable, void> {
const w = texture.width;
const h = texture.height;
private width: number = 0;
private height: number = 0;
private frames: number = 0;
private frameX: number = 0;
private frameY: number = 0;
create(texture: ITexture, data: IScanAnimaterCreate): void {
if (this.texture) {
logger.warn(70);
return;
}
this.texture = texture;
this.width = data.width;
this.height = data.height;
this.frames = data.frames;
// 如果尺寸不匹配
if (
texture.width % data.width !== 0 ||
texture.height % data.height !== 0
) {
logger.warn(74);
}
const frameX = Math.floor(texture.width / data.width);
const frameY = Math.floor(texture.height / data.height);
const possibleFrames = frameX * frameY;
// 如果传入的帧数超出了可能的帧数上限
if (this.frames > possibleFrames) {
this.frames = possibleFrames;
}
}
*open(): Generator<ITextureRenderable, void> | null {
const texture = this.texture;
if (!texture) return null;
const w = this.width;
const h = this.height;
for (let y = 0; y < this.frameY; y++) {
for (let x = 0; x < this.frameX; x++) {
const data: ITextureRenderable = {
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 data;
yield renderable;
frame++;
if (frame === data.frames) break;
}
}
}
*cycled(): Generator<ITextureRenderable, void> | null {
const texture = this.texture;
if (!texture) return null;
const w = this.width;
const h = this.height;
*cycled(
texture: ITexture,
data: IScanAnimaterData
): Generator<ITextureRenderable, void> {
const w = texture.width;
const h = texture.height;
let index = 0;
while (true) {
const x = index % this.frameX;
const y = Math.floor(index / this.frameX);
const data: ITextureRenderable = {
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 data;
yield renderable;
index++;
if (index === this.frames) index = 0;
if (index === data.frames) index = 0;
}
}
}

View File

@ -16,7 +16,7 @@ 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 'packages/client-base/src/glUtils';
import { compileProgramWith } from '@motajs/client-base';
interface IndexMarkedComposedData {
/** 组合数据 */
@ -68,7 +68,7 @@ export class TextureGridComposer
const dx = x * data.width;
const dy = y * data.height;
const texture = tex[i + start];
const renderable = texture.static();
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 });
@ -146,7 +146,7 @@ export class TextureMaxRectsComposer
);
const arr = [...input];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.static().rect;
const rect = v.render().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
@ -164,7 +164,7 @@ export class TextureMaxRectsComposer
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.static();
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);
@ -229,7 +229,7 @@ export class TextureMaxRectsWebGL2Composer
this.canvas.width = maxWidth;
this.canvas.height = maxHeight;
this.gl = this.canvas.getContext('webgl2')!;
const program = compileProgramWith(this.gl, vert, frag)!;
const { program } = compileProgramWith(this.gl, vert, frag)!;
this.program = program;
// 初始化画布数据
@ -342,7 +342,7 @@ export class TextureMaxRectsWebGL2Composer
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.static();
const renderable = v.data.render();
const { width: tw, height: th } = v.data.source;
const { x, y, w, h } = renderable.rect;
// 画到目标画布上的位置
@ -422,7 +422,7 @@ export class TextureMaxRectsWebGL2Composer
);
const arr = [...input];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.static().rect;
const rect = v.render().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;

View File

@ -1,19 +1,20 @@
import { isNil } from 'lodash-es';
import { Texture } from './texture';
import { ITexture, ITextureStore, SizedCanvasImageSource } from './types';
import { ITexture, ITextureStore } from './types';
import { logger } from '@motajs/common';
export class TextureStore implements ITextureStore {
private readonly texMap: Map<number, ITexture> = new Map();
private readonly invMap: Map<ITexture, number> = new Map();
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: ITexture]> {
[Symbol.iterator](): Iterator<[key: number, tex: T]> {
return this.texMap.entries();
}
entries(): Iterable<[key: number, tex: ITexture]> {
entries(): Iterable<[key: number, tex: T]> {
return this.texMap.entries();
}
@ -21,15 +22,11 @@ export class TextureStore implements ITextureStore {
return this.texMap.keys();
}
values(): Iterable<ITexture> {
values(): Iterable<T> {
return this.texMap.values();
}
createTexture(source: SizedCanvasImageSource): ITexture {
return new Texture(source);
}
addTexture(identifier: number, texture: ITexture): void {
addTexture(identifier: number, texture: T): void {
if (this.texMap.has(identifier)) {
logger.warn(66, identifier.toString());
return;
@ -38,7 +35,7 @@ export class TextureStore implements ITextureStore {
this.invMap.set(texture, identifier);
}
private removeBy(id: number, tex: ITexture, alias?: string) {
private removeBy(id: number, tex: T, alias?: string) {
this.texMap.delete(id);
this.invMap.delete(tex);
if (alias) {
@ -47,7 +44,7 @@ export class TextureStore implements ITextureStore {
}
}
removeTexture(identifier: number | string | ITexture): void {
removeTexture(identifier: number | string | T): void {
if (typeof identifier === 'string') {
const id = this.aliasMap.get(identifier);
if (isNil(id)) return;
@ -67,7 +64,7 @@ export class TextureStore implements ITextureStore {
}
}
getTexture(identifier: number): ITexture | null {
getTexture(identifier: number): T | null {
return this.texMap.get(identifier) ?? null;
}
@ -92,7 +89,7 @@ export class TextureStore implements ITextureStore {
return this.texMap.get(id) ?? null;
}
idOf(texture: ITexture): number | undefined {
idOf(texture: T): number | undefined {
return this.invMap.get(texture);
}

View File

@ -18,7 +18,7 @@ export class TextureGridStreamComposer implements ITextureStreamComposer<void> {
readonly cols: number;
private nowIndex: number = 0;
private outputIndex: number = 0;
private outputIndex: number = -1;
private nowTexture: ITexture;
private nowCanvas: HTMLCanvasElement;
@ -65,7 +65,7 @@ export class TextureGridStreamComposer implements ITextureStreamComposer<void> {
const nowRow = Math.floor(index / this.cols);
const nowCol = index % this.cols;
const { source, rect } = tex.static();
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;
@ -107,7 +107,7 @@ export class TextureMaxRectsStreamComposer
/** Max Rects 打包器 */
readonly packer: MaxRectsPacker<MaxRectsRectangle>;
private outputIndex: number = 0;
private outputIndex: number = -1;
private nowTexture!: ITexture;
private nowCanvas!: HTMLCanvasElement;
@ -151,7 +151,7 @@ export class TextureMaxRectsStreamComposer
*add(textures: Iterable<ITexture>): Generator<ITextureComposedData, void> {
const arr = [...textures];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.static().rect;
const rect = v.render().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
@ -176,7 +176,7 @@ export class TextureMaxRectsStreamComposer
h: v.height
};
this.nowMap.set(v.data, target);
const { source, rect } = v.data.static();
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);
});

View File

@ -2,16 +2,14 @@ import { logger } from '@motajs/common';
import {
IRect,
ITexture,
ITextureAnimater,
ITextureComposedData,
ITextureRenderable,
ITextureSplitter,
SizedCanvasImageSource
} from './types';
export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
export class Texture implements ITexture {
source: SizedCanvasImageSource;
animater: ITextureAnimater<T, A> | null = null;
width: number;
height: number;
isBitmap: boolean = false;
@ -85,12 +83,7 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
return splitter.split(this, data);
}
animated(animater: ITextureAnimater<T, A>, data: T): void {
this.animater = animater;
animater.create(this, data);
}
static(): ITextureRenderable {
render(): ITextureRenderable {
return {
source: this.source,
rect: { x: this.cl, y: this.ct, w: this.width, h: this.height }
@ -112,21 +105,10 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
};
}
dynamic(data: A): Generator<ITextureRenderable, void> | null {
if (!this.animater) return null;
return this.animater.open(data);
}
cycled(data: A): Generator<ITextureRenderable, void> | null {
if (!this.animater) return null;
return this.animater.cycled(data);
}
dispose(): void {
if (this.source instanceof ImageBitmap) {
this.source.close();
}
this.animater = null;
}
toAsset(asset: ITextureComposedData): boolean {
@ -192,7 +174,7 @@ export class Texture<T = unknown, A = unknown> implements ITexture<T, A> {
fy: number
): Generator<ITextureRenderable, void> | null {
if (!animate) return null;
const { x: ox, y: oy } = origin.static().rect;
const { x: ox, y: oy } = origin.render().rect;
while (true) {
const next = animate.next();

View File

@ -66,40 +66,29 @@ export interface ITextureSplitter<T> {
split(texture: ITexture, data: T): Generator<ITexture, void>;
}
export interface ITextureAnimater<T, I> {
/** 此动画控制器所控制的贴图 */
readonly texture: ITexture<T, I> | null;
/**
*
* @param texture
* @param data
*/
create(texture: ITexture, data: T): void;
export interface ITextureAnimater<T> {
/**
*
* @param init
* @param texture
* @param data
*/
open(init: I): Generator<ITextureRenderable, void> | null;
once(texture: ITexture, data: T): Generator<ITextureRenderable, void>;
/**
*
* @param init
* @param texture
* @param data
*/
cycled(init: I): Generator<ITextureRenderable, void> | null;
cycled(texture: ITexture, data: T): Generator<ITextureRenderable, void>;
}
export interface ITexture<T = unknown, A = unknown> {
export interface ITexture {
/** 贴图的图像源 */
readonly source: SizedCanvasImageSource;
/** 此贴图使用的动画控制器 */
readonly animater: ITextureAnimater<T, A> | null;
/** 贴图宽度 */
readonly width: number;
/** 贴图高度 */
readonly height: number;
/** 当前贴图是否是完整 bitmap 图像 */
readonly isBitmap: boolean;
@ -116,17 +105,10 @@ export interface ITexture<T = unknown, A = unknown> {
*/
split<T>(splitter: ITextureSplitter<T>, data: T): Generator<ITexture>;
/**
* 使
* @param animater
* @param data
*/
animated(animater: ITextureAnimater<T, A>, data: T): void;
/**
*
*/
static(): ITextureRenderable;
render(): ITextureRenderable;
/**
*
@ -135,23 +117,11 @@ export interface ITexture<T = unknown, A = unknown> {
clampRect(rect: Readonly<IRect>): Readonly<IRect>;
/**
*
*
* @param rect
*/
clipped(rect: Readonly<IRect>): ITextureRenderable;
/**
*
* @param data
*/
dynamic(data: A): Generator<ITextureRenderable, void> | null;
/**
*
* @param data
*/
cycled(data: A): Generator<ITextureRenderable, void> | null;
/**
* 使
*/
@ -165,13 +135,20 @@ export interface ITexture<T = unknown, A = unknown> {
toAsset(asset: ITextureComposedData): boolean;
}
export interface ITextureStore {
[Symbol.iterator](): Iterator<[key: number, tex: ITexture]>;
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: ITexture]>;
entries(): Iterable<[key: number, tex: T]>;
/**
*
@ -181,35 +158,29 @@ export interface ITextureStore {
/**
*
*/
values(): Iterable<ITexture>;
/**
*
* @param source 使
*/
createTexture(source: SizedCanvasImageSource): ITexture;
values(): Iterable<T>;
/**
*
* @param identifier id
* @param texture
*/
addTexture(identifier: number, texture: ITexture): void;
addTexture(identifier: number, texture: T): void;
/**
*
* @param identifier id
*/
removeTexture(identifier: number | string | ITexture): void;
removeTexture(identifier: number | string | T): void;
/**
* id
* @param identifier id
*/
getTexture(identifier: number): ITexture | null;
getTexture(identifier: number): T | null;
/**
*
*
* @param identifier id
* @param alias
*/

View File

@ -225,7 +225,7 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
IRenderTickerSupport,
IRenderChildable,
IRenderVueSupport,
ITransformUpdatable,
ITransformUpdatable<Transform>,
IRenderEvent
{
/** 渲染的全局ticker */
@ -671,11 +671,13 @@ export abstract class RenderItem<E extends ERenderItemEvent = ERenderItemEvent>
}
}
updateTransform() {
updateTransform(transform: Transform) {
// 更新变换矩阵时,不需要更新自身的缓存,直接调用父元素的更新即可
if (transform === this.transform) {
this._parent?.update();
this.emit('transform', this, this._transform);
}
}
//#endregion

View File

@ -1,7 +1,7 @@
import { mat3, mat4, ReadonlyMat3, ReadonlyVec3, vec2, vec3 } from 'gl-matrix';
export interface ITransformUpdatable {
updateTransform?(): void;
export interface ITransformUpdatable<T> {
updateTransform?(transform: T): void;
}
export class Transform {
@ -17,13 +17,13 @@ export class Transform {
private modified: boolean = false;
/** 绑定的可更新元素 */
bindedObject?: ITransformUpdatable;
bindedObject?: ITransformUpdatable<Transform>;
/**
* update
* @param obj
*/
bind(obj?: ITransformUpdatable) {
bind(obj?: ITransformUpdatable<Transform>) {
this.bindedObject = obj;
}
@ -38,7 +38,7 @@ export class Transform {
this.scaleY = 1;
this.rad = 0;
this.modified = false;
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
}
/**
@ -49,7 +49,7 @@ export class Transform {
this.scaleX *= x;
this.scaleY *= y;
this.modified = true;
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
return this;
}
@ -61,7 +61,7 @@ export class Transform {
this.x += x;
this.y += y;
this.modified = true;
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
return this;
}
@ -76,7 +76,7 @@ export class Transform {
this.rad -= n * Math.PI * 2;
}
this.modified = true;
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
return this;
}
@ -88,7 +88,7 @@ export class Transform {
this.scaleX = x;
this.scaleY = y;
this.modified = true;
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
return this;
}
@ -100,7 +100,7 @@ export class Transform {
this.x = x;
this.y = y;
this.modified = true;
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
return this;
}
@ -111,7 +111,7 @@ export class Transform {
mat3.rotate(this.mat, this.mat, rad - this.rad);
this.rad = rad;
this.modified = true;
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
return this;
}
@ -138,7 +138,7 @@ export class Transform {
mat3.fromValues(a, b, 0, c, d, 0, e, f, 1)
);
this.calAttributes();
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
return this;
}
@ -161,7 +161,7 @@ export class Transform {
): this {
mat3.set(this.mat, a, b, 0, c, d, 0, e, f, 1);
this.calAttributes();
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
return this;
}
@ -285,13 +285,13 @@ export class Transform3D {
mat: mat4 = mat4.create();
/** 绑定的可更新元素 */
bindedObject?: ITransformUpdatable;
bindedObject?: ITransformUpdatable<Transform3D>;
/**
*
* @param obj
*/
bind(obj?: ITransformUpdatable) {
bind(obj?: ITransformUpdatable<Transform3D>) {
this.bindedObject = obj;
}
@ -300,7 +300,7 @@ export class Transform3D {
*/
reset(): this {
mat4.identity(this.mat);
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
return this;
}
@ -312,7 +312,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.bindedObject?.updateTransform?.(this);
return this;
}
@ -324,7 +324,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.bindedObject?.updateTransform?.(this);
return this;
}
@ -335,7 +335,7 @@ export class Transform3D {
*/
rotate(rad: number, axis: vec3): this {
mat4.rotate(this.mat, this.mat, rad, axis);
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
return this;
}
@ -371,7 +371,7 @@ export class Transform3D {
*/
lookAt(eye: vec3, center: vec3, up: vec3): this {
mat4.lookAt(this.mat, eye, center, up);
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
return this;
}
@ -384,7 +384,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.bindedObject?.updateTransform?.(this);
return this;
}
@ -406,7 +406,7 @@ export class Transform3D {
far: number
): this {
mat4.ortho(this.mat, left, right, bottom, top, near, far);
this.bindedObject?.updateTransform?.();
this.bindedObject?.updateTransform?.(this);
return this;
}

View File

@ -25,9 +25,16 @@ import { formatSize } from './utils.js';
'.vue',
'.less',
'.css',
'.html'
'.html',
'.vert',
'.frag'
];
const mapExt = (ext: string) => {
if (ext === '.vert' || ext === '.frag') return '.shader';
else return ext;
};
const check = async (dir: string) => {
if (ignoreDir.some(v => dir.includes(v))) return;
const d = await fs.readdir(dir);
@ -38,7 +45,7 @@ import { formatSize } from './utils.js';
if (exts.some(v => one.endsWith(v))) {
const file = await fs.readFile(resolve(dir, one), 'utf-8');
const lines = file.split('\n').length;
const ext = extname(one);
const ext = mapExt(extname(one));
list[ext] ??= [0, 0, 0];
list[ext][0]++;
list[ext][1] += lines;
@ -59,7 +66,7 @@ import { formatSize } from './utils.js';
});
for (const [ext, [file, lines, size]] of sorted) {
console.log(
`${ext.slice(1).padEnd(7, ' ')}files: ${file
`${ext.slice(1).padEnd(9, ' ')}files: ${file
.toString()
.padEnd(6, ' ')}lines: ${lines
.toString()

View File

@ -8,7 +8,7 @@ import * as glob from 'glob';
const custom = [
'container', 'image', 'sprite', 'shader', 'text', 'comment', 'custom',
'layer', 'layer-group', 'animate', 'damage', 'graphics', 'icon', 'winskin',
'container-custom'
'container-custom', 'map-render'
];
const aliases = glob.sync('packages/*/src').map((srcPath) => {