refactor: 将 render-core, render-elements, render-assets, render-style 合并

This commit is contained in:
AncTe committed 2026-03-08 16:50:44 +08:00
1 parent 93fb788bef
commit 744a21e042
105 files changed
+149 -192

No files matched your search

+134
View File
@@ -0,0 +1,134 @@
import { ITexture, ITextureAnimater, ITextureRenderable } from './types';
/**
* 行动画控制器,将贴图按照从上到下的顺序依次组成帧动画,动画传入的参数代表帧数
*/
export class TextureRowAnimater implements ITextureAnimater<number> {
*once(texture: ITexture, frames: number): Generator<ITextureRenderable> {
if (frames <= 0) return;
const renderable = texture.render();
const { x: ox, y: oy } = renderable.rect;
const { width: w, height } = texture!;
const h = height / frames;
for (let i = 0; i < frames; i++) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
}
}
*cycled(texture: ITexture, frames: number): Generator<ITextureRenderable> {
if (frames <= 0) return;
const renderable = texture.render();
const { x: ox, y: oy } = renderable.rect;
const { width: w, height } = texture;
const h = height / frames;
let i = 0;
while (true) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
i++;
if (i === frames) i = 0;
}
}
}
/**
* 列动画控制器,将贴图按照从左到右的顺序依次组成帧动画,动画传入的参数代表帧数
*/
export class TextureColumnAnimater implements ITextureAnimater<number> {
*once(texture: ITexture, frames: number): Generator<ITextureRenderable> {
if (frames <= 0) return;
const renderable = texture.render();
const { x: ox, y: oy, w: width, h } = renderable.rect;
const w = width / frames;
for (let i = 0; i < frames; i++) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
}
}
*cycled(texture: ITexture, frames: number): Generator<ITextureRenderable> {
if (frames <= 0) return null;
const renderable = texture.render();
const { x: ox, y: oy } = renderable.rect;
const { width, height: h } = texture;
const w = width / frames;
let i = 0;
while (true) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: i * w + ox, y: oy, w, h })
};
yield renderable;
i++;
if (i === frames) i = 0;
}
}
}
export interface IScanAnimaterData {
/** 每帧的宽度 */
readonly width: number;
/** 每帧的高度 */
readonly height: number;
/** 总帧数 */
readonly frames: number;
}
/**
* 扫描动画控制器,会按照先从左到右,再从上到下的顺序依次输出,可以用于动画精灵图等
*/
export class TextureScanAnimater
implements ITextureAnimater<IScanAnimaterData>
{
*once(
texture: ITexture,
data: IScanAnimaterData
): Generator<ITextureRenderable, void> {
const w = texture.width;
const h = texture.height;
let frame = 0;
for (let y = 0; y < data.width; y++) {
for (let x = 0; x < data.height; x++) {
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: x * w, y: y * h, w, h })
};
yield renderable;
frame++;
if (frame === data.frames) break;
}
}
}
*cycled(
texture: ITexture,
data: IScanAnimaterData
): Generator<ITextureRenderable, void> {
const w = texture.width;
const h = texture.height;
let index = 0;
while (true) {
const x = index % data.width;
const y = Math.floor(index / data.height);
const renderable: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect({ x: x * w, y: y * h, w, h })
};
yield renderable;
index++;
if (index === data.frames) index = 0;
}
}
}
+451
View File
@@ -0,0 +1,451 @@
import {
IOption,
IRectangle,
MaxRectsPacker,
Rectangle
} from 'maxrects-packer';
import { Texture } from './texture';
import {
IRect,
ITexture,
ITextureComposedData,
ITextureComposer
} from './types';
import vert from './shader/pack.vert?raw';
import frag from './shader/pack.frag?raw';
import { logger } from '@motajs/common';
import { isNil } from 'lodash-es';
import { compileProgramWith } from '@motajs/client-base';
import { SizedCanvasImageSource } from '../types';
interface IndexMarkedComposedData {
/** 组合数据 */
readonly asset: ITextureComposedData;
/** 组合时最后一个用到的贴图的索引 */
readonly index: number;
}
export interface IGridComposerData {
/** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */
readonly width: number;
/** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */
readonly height: number;
}
export class TextureGridComposer
implements ITextureComposer<IGridComposerData>
{
/**
* 网格组合器,将等大小的贴图组合成图集,要求每个贴图的尺寸一致。
* 组合时按照先从左到右,再从上到下的顺序组合。
* @param maxWidth 图集最大宽度,也是输出纹理的宽度
* @param maxHeight 图集最大高度,也是输出纹理的高度
*/
constructor(
readonly maxWidth: number,
readonly maxHeight: number
) {}
private nextAsset(
tex: ITexture[],
start: number,
data: IGridComposerData,
rows: number,
cols: number,
index: number
): IndexMarkedComposedData {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = this.maxWidth;
canvas.height = this.maxHeight;
const count = Math.min(rows * cols, tex.length - start);
const map = new Map<ITexture, IRect>();
let x = 0;
let y = 0;
for (let i = 0; i < count; i++) {
const dx = x * data.width;
const dy = y * data.height;
const texture = tex[i + start];
const renderable = texture.render();
const { x: sx, y: sy, w: sw, h: sh } = renderable.rect;
ctx.drawImage(renderable.source, sx, sy, sw, sh, dx, dy, sw, sh);
map.set(texture, { x: dx, y: dy, w: sw, h: sh });
x++;
if (x === cols) {
y++;
x = 0;
}
}
const texture = new Texture(canvas);
const composed: ITextureComposedData = {
index,
texture,
assetMap: map
};
return { asset: composed, index: start + count };
}
*compose(
input: Iterable<ITexture>,
data: IGridComposerData
): Generator<ITextureComposedData, void> {
const arr = [...input];
const rows = Math.floor(this.maxWidth / data.width);
const cols = Math.floor(this.maxHeight / data.height);
let arrindex = 0;
let i = 0;
while (arrindex < arr.length) {
const res = this.nextAsset(arr, arrindex, data, rows, cols, i);
arrindex = res.index + 1;
i++;
yield res.asset;
}
}
}
export interface IMaxRectsComposerData extends IOption {
/** 贴图之间的间距 */
readonly padding: number;
}
interface MaxRectsRectangle extends IRectangle {
/** 这个矩形对应的贴图对象 */
readonly data: ITexture;
}
export class TextureMaxRectsComposer
implements ITextureComposer<IMaxRectsComposerData>
{
/**
* 使用 Max Rects 算法执行贴图整合,输入数据参考 {@link IMaxRectsComposerData},
* 输出的纹理的图像源将会是不同的画布,注意与 {@link TextureMaxRectsWebGL2Composer} 区分
* @param maxWidth 图集最大宽度,也是输出纹理的宽度
* @param maxHeight 图集最大高度,也是输出纹理的高度
*/
constructor(
public readonly maxWidth: number,
public readonly maxHeight: number
) {}
*compose(
input: Iterable<ITexture>,
data: IMaxRectsComposerData
): Generator<ITextureComposedData, void> {
const packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth,
this.maxHeight,
data.padding,
data
);
const arr = [...input];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.render().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
});
packer.addArray(rects);
let index = 0;
for (const bin of packer.bins) {
const map = new Map<ITexture, IRect>();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = this.maxWidth;
canvas.height = this.maxHeight;
ctx.imageSmoothingEnabled = false;
bin.rects.forEach(v => {
const rect: IRect = { x: v.x, y: v.y, w: v.width, h: v.height };
map.set(v.data, rect);
const renderable = v.data.render();
const { x, y, w, h } = renderable.rect;
const source = renderable.source;
ctx.drawImage(source, x, y, w, h, v.x, v.y, v.width, v.height);
});
const texture = new Texture(canvas);
const data: ITextureComposedData = {
index: index++,
texture,
assetMap: map
};
yield data;
}
}
}
interface RectProcessed {
/** 贴图位置映射 */
readonly texMap: Map<ITexture, Readonly<IRect>>;
/** 顶点数组 */
readonly attrib: Float32Array;
}
export class TextureMaxRectsWebGL2Composer
implements ITextureComposer<IMaxRectsComposerData>
{
/** 使用的画布 */
readonly canvas: HTMLCanvasElement;
/** 画布上下文 */
readonly gl: WebGL2RenderingContext;
/** WebGL2 程序 */
readonly program: WebGLProgram;
/** 顶点数组缓冲区 */
readonly vertBuffer: WebGLBuffer;
/** 纹理数组对象 */
readonly texArray: WebGLTexture;
/** 纹理数组对象的位置 */
readonly texArrayPos: WebGLUniformLocation;
/** `a_position` 的索引 */
readonly posPos: number;
/** `a_texCoord` 的索引 */
readonly texPos: number;
/** 本次处理的贴图宽度 */
private opWidth: number = 0;
/** 本次处理的贴图高度 */
private opHeight: number = 0;
/**
* 使用 Max Rects 算法执行贴图整合,使用 WebGL2 执行组合操作。
* 输入数据参考 {@link IMaxRectsComposerData},要求每个贴图的图像源尺寸一致,贴图大小可以不同。
* 注意,本组合器同时只能处理一个组合操作,上一个没执行完的时候再次调用 `compose` 会出现问题。
* 所有输出的内容中,贴图对象的图像源都是同一个画布,因此获取后要么直接使用,要么立刻调用 `toBitmap`,
* 否则在下一次调用 `next` 时,图像源将会被覆盖。
* @param maxWidth 图集最大宽度,也是输出纹理的宽度
* @param maxHeight 图集最大高度,也是输出纹理的高度
*/
constructor(
public readonly maxWidth: number,
public readonly maxHeight: number
) {
this.canvas = document.createElement('canvas');
this.canvas.width = maxWidth;
this.canvas.height = maxHeight;
this.gl = this.canvas.getContext('webgl2')!;
const { program } = compileProgramWith(this.gl, vert, frag)!;
this.program = program;
// 初始化画布数据
const texture = this.gl.createTexture();
this.texArray = texture;
const location = this.gl.getUniformLocation(program, 'u_textArray')!;
this.texArrayPos = location;
this.posPos = this.gl.getAttribLocation(program, 'a_position');
this.texPos = this.gl.getAttribLocation(program, 'a_texCoord');
this.vertBuffer = this.gl.createBuffer();
this.gl.useProgram(program);
}
/**
* 对贴图进行索引
* @param textures 贴图数组
*/
private mapTextures(
textures: ITexture[]
): Map<SizedCanvasImageSource, number> {
const map = new Map<SizedCanvasImageSource, number>();
const { width, height } = textures[0].source;
textures.forEach(v => {
const source = v.source;
if (map.has(source)) return;
if (source.width !== width || source.height !== height) {
logger.warn(73);
return;
}
map.set(source, map.size);
});
this.opWidth = width;
this.opHeight = height;
return map;
}
/**
* 传递贴图
* @param texMap 纹理映射
*/
private setTexture(texMap: Map<SizedCanvasImageSource, number>) {
const gl = this.gl;
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texArray);
gl.texStorage3D(
gl.TEXTURE_2D_ARRAY,
1,
gl.RGBA8,
this.opWidth,
this.opHeight,
texMap.size
);
texMap.forEach((index, source) => {
gl.texSubImage3D(
gl.TEXTURE_2D_ARRAY,
0,
0,
0,
index,
this.opWidth,
this.opHeight,
1,
gl.RGBA,
gl.UNSIGNED_BYTE,
source
);
});
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_MAG_FILTER,
gl.NEAREST
);
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_MIN_FILTER,
gl.NEAREST
);
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_WRAP_S,
gl.CLAMP_TO_EDGE
);
gl.texParameteri(
gl.TEXTURE_2D_ARRAY,
gl.TEXTURE_WRAP_T,
gl.CLAMP_TO_EDGE
);
}
/**
* 处理矩形数组,生成 WebGL2 顶点数据
* @param rects 要处理的矩形数组
* @param texMap 贴图到贴图数组索引的映射
*/
private processRects(
rects: MaxRectsRectangle[],
texMap: Map<SizedCanvasImageSource, number>
): RectProcessed {
const { width: ow, height: oh } = this.canvas;
const map = new Map<ITexture, IRect>();
const attrib = new Float32Array(rects.length * 5 * 6);
rects.forEach((v, i) => {
const rect: IRect = { x: v.x, y: v.y, w: v.width, h: v.height };
map.set(v.data, rect);
const renderable = v.data.render();
const { width: tw, height: th } = v.data.source;
const { x, y, w, h } = renderable.rect;
// 画到目标画布上的位置
const ol = (v.x / ow) * 2 - 1;
const ob = (v.y / oh) * 2 - 1;
const or = ((v.x + v.width) / ow) * 2 - 1;
const ot = ((v.y + v.height) / oh) * 2 - 1;
// 原始贴图位置
const tl = x / tw;
const tt = y / tw;
const tr = (x + w) / tw;
const tb = (y + h) / th;
// 贴图索引
const ti = texMap.get(v.data.source);
if (isNil(ti)) return;
// Benchmark https://www.measurethat.net/Benchmarks/Show/35246/2/different-method-to-write-a-typedarray
// prettier-ignore
const data = [
// x y u v i
ol, -ot, tl, tt, ti, // 左上角
ol, -ob, tl, tb, ti, // 左下角
or, -ot, tr, tt, ti, // 右上角
or, -ot, tr, tt, ti, // 右上角
ol, -ob, tl, tb, ti, // 左下角
or, -ob, tr, tb, ti // 右下角
];
attrib.set(data, i * 30);
});
const data: RectProcessed = {
texMap: map,
attrib
};
return data;
}
/**
* 执行渲染操作
* @param attrib 顶点数组
*/
private renderAtlas(attrib: Float32Array) {
const gl = this.gl;
gl.clearColor(0, 0, 0, 0);
gl.clearDepth(1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
gl.bufferData(gl.ARRAY_BUFFER, attrib, gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(this.posPos, 2, gl.FLOAT, false, 5 * 4, 0);
gl.vertexAttribPointer(this.texPos, 3, gl.FLOAT, false, 5 * 4, 2 * 4);
gl.enableVertexAttribArray(this.posPos);
gl.enableVertexAttribArray(this.texPos);
gl.uniform1i(this.texArrayPos, 0);
gl.drawArrays(gl.TRIANGLES, 0, attrib.length / 5);
}
*compose(
input: Iterable<ITexture>,
data: IMaxRectsComposerData
): Generator<ITextureComposedData, void> {
this.opWidth = 0;
this.opHeight = 0;
const packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth,
this.maxHeight,
data.padding,
data
);
const arr = [...input];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.render().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
});
packer.addArray(rects);
const indexMap = this.mapTextures(arr);
this.setTexture(indexMap);
let index = 0;
for (const bin of packer.bins) {
const { texMap, attrib } = this.processRects(bin.rects, indexMap);
this.renderAtlas(attrib);
const texture = new Texture(this.canvas);
const data: ITextureComposedData = {
index: index++,
texture,
assetMap: texMap
};
yield data;
}
this.gl.disableVertexAttribArray(this.posPos);
this.gl.disableVertexAttribArray(this.texPos);
}
}
+7
View File
@@ -0,0 +1,7 @@
export * from './animater';
export * from './composer';
export * from './splitter';
export * from './store';
export * from './streamComposer';
export * from './texture';
export * from './types';
@@ -0,0 +1,12 @@
#version 300 es
precision highp float;
precision highp sampler2DArray;
in vec3 v_texCoord;
out vec4 outColor;
uniform sampler2DArray u_texArray;
void main() {
outColor = texture(u_texArray, v_texCoord);
}
@@ -0,0 +1,12 @@
#version 300 es
precision highp float;
in vec2 a_position;
in vec3 a_texCoord;
out vec3 v_texCoord;
void main() {
v_texCoord = a_texCoord;
gl_Position = vec4(a_position, 0.0, 1.0);
}
+68
View File
@@ -0,0 +1,68 @@
import { Texture } from './texture';
import { ITexture, ITextureSplitter, IRect } from './types';
/**
* 按行分割贴图,即分割成一行行的贴图,按从上到下的顺序输出
* 输入参数代表每一行的高度
*/
export class TextureRowSplitter implements ITextureSplitter<number> {
*split(texture: ITexture, data: number): Generator<ITexture> {
const lines = Math.ceil(texture.height / data);
const { x, y } = texture.render().rect;
for (let i = 0; i < lines; i++) {
const tex = new Texture(texture.source);
tex.clip(x, y + i * data, texture.width, data);
yield tex;
}
}
}
/**
* 按列分割贴图,即分割成一列列的贴图,按从左到右的顺序输出
* 输入参数代表每一列的宽度
*/
export class TextureColumnSplitter implements ITextureSplitter<number> {
*split(texture: ITexture, data: number): Generator<ITexture> {
const lines = Math.ceil(texture.width / data);
const { x, y } = texture.render().rect;
for (let i = 0; i < lines; i++) {
const tex = new Texture(texture.source);
tex.clip(x + i * data, y, data, texture.height);
yield tex;
}
}
}
/**
* 按照网格分割贴图,按照先从左到右,再从上到下的顺序输出
* 输入参数代表每一列的宽度和高度
*/
export class TextureGridSplitter implements ITextureSplitter<[number, number]> {
*split(texture: ITexture, data: [number, number]): Generator<ITexture> {
const [w, h] = data;
const rows = Math.ceil(texture.width / w);
const lines = Math.ceil(texture.height / h);
const { x, y } = texture.render().rect;
for (let ny = 0; ny < lines; ny++) {
for (let nx = 0; nx < rows; nx++) {
const tex = new Texture(texture.source);
tex.clip(x + nx * w, y + ny * h, w, h);
yield tex;
}
}
}
}
/**
* 根据图集信息分割贴图,按照传入的矩形数组的顺序输出
* 输入参数代表每个贴图对应到图集上的矩形位置
*/
export class TextureAssetSplitter implements ITextureSplitter<IRect[]> {
*split(texture: ITexture, data: IRect[]): Generator<ITexture> {
for (const { x, y, w, h } of data) {
const tex = new Texture(texture.source);
tex.clip(x, y, w, h);
yield tex;
}
}
}
+107
View File
@@ -0,0 +1,107 @@
import { isNil } from 'lodash-es';
import { ITexture, ITextureStore } from './types';
import { logger } from '@motajs/common';
export class TextureStore<T extends ITexture = ITexture>
implements ITextureStore<T>
{
private readonly texMap: Map<number, T> = new Map();
private readonly invMap: Map<T, number> = new Map();
private readonly aliasMap: Map<string, number> = new Map();
private readonly aliasInvMap: Map<number, string> = new Map();
[Symbol.iterator](): Iterator<[key: number, tex: T]> {
return this.texMap.entries();
}
entries(): Iterable<[key: number, tex: T]> {
return this.texMap.entries();
}
keys(): Iterable<number> {
return this.texMap.keys();
}
values(): Iterable<T> {
return this.texMap.values();
}
addTexture(identifier: number, texture: T): void {
if (this.texMap.has(identifier)) {
logger.warn(66, identifier.toString());
return;
}
this.texMap.set(identifier, texture);
this.invMap.set(texture, identifier);
}
private removeBy(id: number, tex: T, alias?: string) {
this.texMap.delete(id);
this.invMap.delete(tex);
if (alias) {
this.aliasMap.delete(alias);
this.aliasInvMap.delete(id);
}
}
removeTexture(identifier: number | string | T): void {
if (typeof identifier === 'string') {
const id = this.aliasMap.get(identifier);
if (isNil(id)) return;
const tex = this.texMap.get(id);
if (isNil(tex)) return;
this.removeBy(id, tex, identifier);
} else if (typeof identifier === 'number') {
const tex = this.texMap.get(identifier);
if (isNil(tex)) return;
const alias = this.aliasInvMap.get(identifier);
this.removeBy(identifier, tex, alias);
} else {
const id = this.invMap.get(identifier);
if (isNil(id)) return;
const alias = this.aliasInvMap.get(id);
this.removeBy(id, identifier, alias);
}
}
hasTexture(identifier: number): boolean {
return this.texMap.has(identifier);
}
getTexture(identifier: number): T | null {
return this.texMap.get(identifier) ?? null;
}
alias(identifier: number, alias: string): void {
const id = this.aliasMap.get(alias);
const al = this.aliasInvMap.get(identifier);
if (!isNil(al)) {
logger.warn(67, alias, identifier.toString(), al);
return;
}
if (!isNil(id)) {
logger.warn(68, alias, identifier.toString(), id.toString());
return;
}
this.aliasMap.set(alias, identifier);
this.aliasInvMap.set(identifier, alias);
}
fromAlias(alias: string): ITexture | null {
const id = this.aliasMap.get(alias);
if (isNil(id)) return null;
return this.texMap.get(id) ?? null;
}
idOf(texture: T): number | undefined {
return this.invMap.get(texture);
}
aliasOf(identifier: number): string | undefined {
return this.aliasInvMap.get(identifier);
}
identifierOf(alias: string): number | undefined {
return this.aliasMap.get(alias);
}
}
@@ -0,0 +1,201 @@
import {
Bin,
IOption,
IRectangle,
MaxRectsPacker,
Rectangle
} from 'maxrects-packer';
import { Texture } from './texture';
import {
IRect,
ITexture,
ITextureComposedData,
ITextureStreamComposer
} from './types';
export class TextureGridStreamComposer implements ITextureStreamComposer<void> {
readonly rows: number;
readonly cols: number;
private nowIndex: number = 0;
private outputIndex: number = -1;
private nowTexture: ITexture;
private nowCanvas: HTMLCanvasElement;
private nowCtx: CanvasRenderingContext2D;
private nowMap: Map<ITexture, Readonly<IRect>>;
/**
* 网格流式贴图组合器,将等大小的贴图组合成图集,要求每个贴图的尺寸一致。
* 组合时按照先从左到右,再从上到下的顺序组合。
* @param width 单个贴图的宽度
* @param height 单个贴图的高度
* @param maxWidth 图集的最大宽度,也是输出贴图对象的宽度
* @param maxHeight 图集的最大高度,也是输出贴图对象的高度
*/
constructor(
readonly width: number,
readonly height: number,
readonly maxWidth: number,
readonly maxHeight: number
) {
this.rows = Math.floor(maxHeight / height);
this.cols = Math.floor(maxWidth / width);
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowMap = new Map();
this.nowTexture = new Texture(this.nowCanvas);
}
private nextCanvas() {
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowMap = new Map();
this.nowIndex = 0;
this.nowTexture = new Texture(this.nowCanvas);
this.outputIndex++;
}
*add(textures: Iterable<ITexture>): Generator<ITextureComposedData, void> {
let index = this.nowIndex;
const max = this.cols * this.rows;
for (const tex of textures) {
const nowRow = Math.floor(index / this.cols);
const nowCol = index % this.cols;
const { source, rect } = tex.render();
const { x: cx, y: cy, w: cw, h: ch } = rect;
const x = nowRow * this.width;
const y = nowCol * this.height;
this.nowCtx.drawImage(source, cx, cy, cw, ch, x, y, cw, ch);
this.nowMap.set(tex, { x, y, w: cw, h: ch });
if (++index === max) {
const data: ITextureComposedData = {
index: this.outputIndex,
texture: this.nowTexture,
assetMap: this.nowMap
};
yield data;
this.nextCanvas();
}
}
const data: ITextureComposedData = {
index: this.outputIndex,
texture: this.nowTexture,
assetMap: this.nowMap
};
yield data;
}
close(): void {
// We need to do nothing.
}
}
interface MaxRectsRectangle extends IRectangle {
/** 这个矩形对应的贴图对象 */
readonly data: ITexture;
}
export class TextureMaxRectsStreamComposer
implements ITextureStreamComposer<void>
{
/** Max Rects 打包器 */
readonly packer: MaxRectsPacker<MaxRectsRectangle>;
private outputIndex: number = -1;
private nowTexture!: ITexture;
private nowCanvas!: HTMLCanvasElement;
private nowCtx!: CanvasRenderingContext2D;
private nowMap!: Map<ITexture, Readonly<IRect>>;
private nowBin: number = 0;
/**
* 使用 Max Rects 算法执行贴图整合。输出的纹理的图像源将会是不同的画布。
* @param maxWidth 图集最大宽度,也是输出贴图对象的宽度
* @param maxHeight 图集最大高度,也是输出贴图对象的高度
* @param padding 每个贴图之间的间距
* @param options 传递给打包器对象的参数
*/
constructor(
readonly maxWidth: number,
readonly maxHeight: number,
readonly padding: number,
options?: IOption
) {
this.packer = new MaxRectsPacker<MaxRectsRectangle>(
this.maxWidth,
this.maxHeight,
padding,
options
);
this.nextCanvas();
}
private nextCanvas() {
this.nowCanvas = document.createElement('canvas');
this.nowCtx = this.nowCanvas.getContext('2d')!;
this.nowCanvas.width = this.maxWidth;
this.nowCanvas.height = this.maxHeight;
this.nowMap = new Map();
this.outputIndex++;
this.nowTexture = new Texture(this.nowCanvas);
}
*add(textures: Iterable<ITexture>): Generator<ITextureComposedData, void> {
const arr = [...textures];
const rects = arr.map<MaxRectsRectangle>(v => {
const rect = v.render().rect;
const toPack = new Rectangle(rect.w, rect.h);
toPack.data = v;
return toPack;
});
this.packer.addArray(rects);
const bins = this.packer.bins;
if (bins.length === 0) return;
const toYield: Bin<MaxRectsRectangle>[] = [bins[bins.length - 1]];
if (bins.length > this.nowBin) {
toYield.push(...bins.slice(this.nowBin));
}
for (let i = this.nowBin; i < bins.length; i++) {
const rects = bins[i].rects;
rects.forEach(v => {
if (this.nowMap.has(v.data)) return;
const target: IRect = {
x: v.x,
y: v.y,
w: v.width,
h: v.height
};
this.nowMap.set(v.data, target);
const { source, rect } = v.data.render();
const { x: cx, y: cy, w: cw, h: ch } = rect;
this.nowCtx.drawImage(source, cx, cy, cw, ch, v.x, v.y, cw, ch);
});
const data: ITextureComposedData = {
index: this.outputIndex,
texture: this.nowTexture,
assetMap: this.nowMap
};
yield data;
if (i < bins.length - 1) {
this.nextCanvas();
}
}
this.nowBin = bins.length - 1;
}
close(): void {
// We need to do nothing.
}
}
+193
View File
@@ -0,0 +1,193 @@
import { logger } from '@motajs/common';
import {
IRect,
ITexture,
ITextureComposedData,
ITextureRenderable,
ITextureSplitter
} from './types';
import { clamp } from 'lodash-es';
import { SizedCanvasImageSource } from '../types';
export class Texture implements ITexture {
source: SizedCanvasImageSource;
width: number;
height: number;
isBitmap: boolean = false;
/** 裁剪矩形的左边框位置 */
private cl: number;
/** 裁剪矩形的上边框位置 */
private ct: number;
/** 裁剪矩形的右边框位置 */
private cr: number;
/** 裁剪矩形的下边框位置 */
private cb: number;
constructor(source: SizedCanvasImageSource) {
this.source = source;
this.width = source.width;
this.height = source.height;
this.cl = 0;
this.ct = 0;
this.cr = source.width;
this.cb = source.height;
}
/**
* 对纹理进行裁剪操作,不会改变图像源
* @param x 裁剪左上角横坐标
* @param y 裁剪左上角纵坐标
* @param w 裁剪宽度
* @param h 裁剪高度
*/
clip(x: number, y: number, w: number, h: number) {
const left = clamp(this.cl + x, this.cl, this.cr);
const top = clamp(this.ct + y, this.ct, this.cb);
const right = clamp(this.cl + x + w, this.cl, this.cr);
const bottom = clamp(this.ct + y + h, this.ct, this.cb);
if (left === right || top === bottom) {
logger.warn(69);
return;
}
const width = right - left;
const height = bottom - top;
if (width <= 0 || height <= 0) {
logger.warn(77);
return;
}
this.cl = left;
this.ct = top;
this.cr = right;
this.cb = bottom;
this.width = right - left;
this.height = bottom - top;
}
async toBitmap(): Promise<void> {
if (this.source instanceof ImageBitmap) return;
this.source = await createImageBitmap(
this.source,
this.cl,
this.ct,
this.width,
this.height
);
this.cl = 0;
this.ct = 0;
this.cr = this.width;
this.cb = this.height;
this.isBitmap = true;
}
split<U>(splitter: ITextureSplitter<U>, data: U): Generator<ITexture> {
return splitter.split(this, data);
}
render(): ITextureRenderable {
return {
source: this.source,
rect: { x: this.cl, y: this.ct, w: this.width, h: this.height }
};
}
clampRect(rect: Readonly<IRect>): Readonly<IRect> {
const l = clamp(rect.x, this.cl, this.cr);
const t = clamp(rect.y, this.ct, this.cb);
const r = clamp(rect.x + rect.w, this.cl, this.cr);
const b = clamp(rect.y + rect.h, this.ct, this.cb);
return { x: l, y: t, w: r - l, h: b - t };
}
clipped(rect: Readonly<IRect>): ITextureRenderable {
return {
source: this.source,
rect: this.clampRect(rect)
};
}
dispose(): void {
if (this.source instanceof ImageBitmap) {
this.source.close();
}
}
toAsset(asset: ITextureComposedData): boolean {
const rect = asset.assetMap.get(this);
if (!rect) return false;
if (this.isBitmap && this.source instanceof ImageBitmap) {
this.source.close();
}
this.isBitmap = false;
this.source = asset.texture.source;
this.cl = rect.x;
this.ct = rect.y;
this.width = rect.w;
this.height = rect.h;
this.cr = rect.x + rect.w;
this.cb = rect.y + rect.h;
return true;
}
/**
* 对贴图的动画执行偏移效果。动画效果可以不来自传入的贴图对象,
* 输出结果的图像源会是传入的贴图对象的图像源而非动画效果对应的图像源
* @param texture 贴图对象
* @param animate 动画效果
* @param ox 偏移横坐标
* @param oy 偏移纵坐标
*/
static *translated(
texture: ITexture,
animate: Generator<ITextureRenderable, void> | null,
ox: number,
oy: number
): Generator<ITextureRenderable, void> | null {
if (!animate) return null;
while (true) {
const next = animate.next();
if (next.done) break;
const renderable = next.value;
const { x, y, w, h } = renderable.rect;
const translated: IRect = { x: x + ox, y: y + oy, w, h };
const res: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect(translated)
};
yield res;
}
}
/**
* 对贴图的动画重定位。最终输出的动画会以传入的 `fx` `fy` 为左上角坐标计算
* @param texture 贴图对象
* @param origin 动画效果来自的贴图对象
* @param animate 动画效果
* @param fx 左上角横坐标
* @param fy 左上角纵坐标
*/
static *fixed(
texture: ITexture,
origin: ITexture,
animate: Generator<ITextureRenderable, void> | null,
fx: number,
fy: number
): Generator<ITextureRenderable, void> | null {
if (!animate) return null;
const { x: ox, y: oy } = origin.render().rect;
while (true) {
const next = animate.next();
if (next.done) break;
const renderable = next.value;
const { x, y, w, h } = renderable.rect;
const translated: IRect = { x: x - ox + fx, y: y - oy + fy, w, h };
const res: ITextureRenderable = {
source: texture.source,
rect: texture.clampRect(translated)
};
yield res;
}
}
}
+213
View File
@@ -0,0 +1,213 @@
import { SizedCanvasImageSource } from '../types';
export interface IRect {
x: number;
y: number;
w: number;
h: number;
}
export interface ITextureRenderable {
/** 可渲染贴图对象的图像源 */
readonly source: SizedCanvasImageSource;
/** 贴图裁剪区域 */
readonly rect: Readonly<IRect>;
}
export interface ITextureComposedData {
/** 这个纹理图集的贴图对象 */
readonly texture: ITexture;
/** 这个纹理图集的索引 */
readonly index: number;
/** 每个参与组合的贴图对应到图集对象的矩形范围 */
readonly assetMap: Map<ITexture, Readonly<IRect>>;
}
export interface ITextureComposer<T> {
/**
* 将一系列纹理组合成为一系列纹理图集
* @param input 输入纹理
* @param data 输入给组合器的参数
*/
compose(
input: Iterable<ITexture>,
data: T
): Generator<ITextureComposedData, void>;
}
export interface ITextureStreamComposer<T> {
/**
* 将一系列纹理添加到当前流式组合器中,并将这部分纹理的组合结果输出
* @param textures 输入纹理
* @param data 输入给组合器的参数
*/
add(
textures: Iterable<ITexture>,
data: T
): Generator<ITextureComposedData, void>;
/**
* 结束此组合器的使用,释放相关资源
*/
close(): void;
}
export interface ITextureSplitter<T> {
/**
* 对一个贴图对象执行拆分操作
* @param texture 要拆分的贴图
* @param data 传给拆分器的参数
*/
split(texture: ITexture, data: T): Generator<ITexture, void>;
}
export interface ITextureAnimater<T> {
/**
* 开始动画序列
* @param texture 贴图对象
* @param data 动画初始化参数
*/
once(texture: ITexture, data: T): Generator<ITextureRenderable, void>;
/**
* 开始循环动画序列
* @param texture 贴图对象
* @param data 动画初始化参数
*/
cycled(texture: ITexture, data: T): Generator<ITextureRenderable, void>;
}
export interface ITexture {
/** 贴图的图像源 */
readonly source: SizedCanvasImageSource;
/** 贴图宽度 */
readonly width: number;
/** 贴图高度 */
readonly height: number;
/** 当前贴图是否是完整 bitmap 图像 */
readonly isBitmap: boolean;
/**
* 将此贴图转换为 bitmap 图像,图像源也会转变成 ImageBitmap
*/
toBitmap(): Promise<void>;
/**
* 使用指定贴图切分器切分贴图
* @param splitter 贴图切分器
* @param data 传递给切分器的参数
* @returns 切分出的贴图所组成的可迭代对象
*/
split<T>(splitter: ITextureSplitter<T>, data: T): Generator<ITexture>;
/**
* 获取整张图的可渲染对象
*/
render(): ITextureRenderable;
/**
* 限制矩形范围至当前贴图对象范围
* @param rect 矩形范围
*/
clampRect(rect: Readonly<IRect>): Readonly<IRect>;
/**
* 获取贴图经过指定矩形裁剪后的可渲染对象,并不是简单地对图像源裁剪,还会处理其他情况
* @param rect 裁剪矩形
*/
clipped(rect: Readonly<IRect>): ITextureRenderable;
/**
* 释放此贴图的资源,将不能再被使用
*/
dispose(): void;
/**
* 将贴图的图像源转换为指定图集的图像源,并将范围限定至图集中对应到此贴图的矩形范围。
* @param asset 图集信息
* @returns 是否转换成功,如果图集信息中不包含当前贴图,那么返回 `false`
*/
toAsset(asset: ITextureComposedData): boolean;
}
export const enum TextureOffsetDirection {
LeftToRight,
RightToLeft,
TopToBottom,
BottomToTop
}
export interface ITextureStore<T extends ITexture = ITexture> {
[Symbol.iterator](): Iterator<[key: number, tex: T]>;
/**
* 获取纹理对象键值对的可迭代对象
*/
entries(): Iterable<[key: number, tex: T]>;
/**
* 获取纹理对象的键的可迭代对象
*/
keys(): Iterable<number>;
/**
* 获取纹理对象的值的可迭代对象
*/
values(): Iterable<T>;
/**
* 添加一个贴图
* @param identifier 贴图 id
* @param texture 贴图对象
*/
addTexture(identifier: number, texture: T): void;
/**
* 移除一个贴图
* @param identifier 要移除的贴图对象 id 或 别名 或 贴图对象
*/
removeTexture(identifier: number | string | T): void;
/**
* 判断当前贴图存储对象是否包含指定贴图 id
* @param identifier 贴图对象 id
*/
hasTexture(identifier: number): boolean;
/**
* 根据贴图对象 id 获取贴图
* @param identifier 贴图对象 id
*/
getTexture(identifier: number): T | null;
/**
* 给贴图对象命名一个别名,如果贴图对象不存在也可以设置
* @param identifier 贴图对象 id
* @param alias 要命名的别名
*/
alias(identifier: number, alias: string): void;
/**
* 根据贴图对象别名获取贴图
* @param alias 贴图对象别名
*/
fromAlias(alias: string): ITexture | null;
/**
* 根据贴图对象别名获取贴图对象 id
* @param alias 贴图对象别名
*/
identifierOf(alias: string): number | undefined;
/**
* 根据贴图对象获取此贴图对象在此控制器中的 id,如果贴图不在此控制器,返回 `undefined`
* @param texture 贴图对象
*/
idOf(texture: ITexture): number | undefined;
/**
* 根据贴图对象获取此贴图对象在此控制器中的别名,如果不存在此 id,返回 `undefined`
* @param identifier 贴图 id
*/
aliasOf(identifier: number): string | undefined;
}
+135
View File
@@ -0,0 +1,135 @@
type AdapterFunction<T> = (item: T, ...params: any[]) => Promise<any>;
type SyncAdapterFunction<T> = (item: T, ...params: any[]) => any;
type GlobalAdapterFunction = (...params: any[]) => Promise<any>;
/**
* 渲染适配器,用作渲染层与数据层沟通的桥梁,用于在数据层等待渲染层执行,常用与动画等。
* 例如移动图块,移动图块操作先由数据层执行,删除原有图块,然后通知渲染层进行移动动画,
* 而移动动画是耗时的,因此需要通过本类进行适配,实现等待移动动画执行完毕,然后进行后续操作
*/
export class RenderAdapter<T> {
static adapters: Map<string, RenderAdapter<any>> = new Map();
/** 所有元素的集合 */
items: Set<T> = new Set();
/** 适配器的id */
id: string;
private execute: Map<string, AdapterFunction<T>> = new Map();
private syncExecutes: Map<string, SyncAdapterFunction<T>> = new Map();
private globalExecutes: Map<string, GlobalAdapterFunction> = new Map();
constructor(id: string) {
this.id = id;
RenderAdapter.adapters.set(id, this);
}
/**
* 添加一个元素
*/
add(item: T) {
this.items.add(item);
}
/**
* 移除一个元素
*/
remove(item: T) {
this.items.delete(item);
}
/**
* 定义一个类似于静态函数的函数,只会调用一次,不会对每个元素执行
* @param id 函数名称
* @param fn 执行的函数
*/
receiveGlobal(id: string, fn: GlobalAdapterFunction) {
this.globalExecutes.set(id, fn);
}
/**
* 设置执行函数
* @param fn 对于每个元素执行的函数
*/
receive(id: string, fn: AdapterFunction<T>): void {
this.execute.set(id, fn);
}
/**
* 设置同步执行函数
* @param fn 对于每个元素执行的函数
*/
receiveSync(id: string, fn: SyncAdapterFunction<T>): void {
this.syncExecutes.set(id, fn);
}
/**
* 对所有元素执行函数,当所有元素都运行完毕后兑现,类似于Promise.all
* @returns 包含每个元素运行结果的数组
*/
all<R = any>(fn: string, ...params: any[]): Promise<R[]> {
const execute = this.execute.get(fn);
if (!execute) {
return Promise.reject();
} else {
return Promise.all(
[...this.items].map(v => execute!(v, ...params))
);
}
}
/**
* 对所有元素执行函数,当任意一个元素运行完毕后兑现,类似于Promise.any
* @returns 最先运行完毕的元素的结果
*/
any<R = any>(fn: string, ...params: any[]): Promise<R> {
const execute = this.execute.get(fn);
if (!execute) {
return Promise.reject();
} else {
return Promise.any(
[...this.items].map(v => execute!(v, ...params))
);
}
}
/**
* 对所有元素执行同步函数
* @returns 包含每个元素运行结果的数组
*/
sync<R = any>(fn: string, ...params: any[]): R[] {
const execute = this.syncExecutes.get(fn);
if (!execute) {
return [];
} else {
return [...this.items].map(v => execute!(v, ...params));
}
}
/**
* 调用一个全局函数
*/
global<R = any>(id: string, ...params: any[]): Promise<R> {
const execute = this.globalExecutes.get(id);
if (!execute) {
return Promise.reject();
} else {
return execute(...params);
}
}
/**
* 销毁这个adapter
*/
destroy() {
RenderAdapter.adapters.delete(this.id);
}
/**
* 获取适配器
*/
static get<T>(id: string): RenderAdapter<T> | undefined {
return this.adapters.get(id);
}
}
+119
View File
@@ -0,0 +1,119 @@
import { EventEmitter } from 'eventemitter3';
interface OffscreenCanvasEvent {
/** 当被动触发resize时(例如窗口大小变化)时触发,使用size函数并不会触发 */
resize: [];
}
export class MotaOffscreenCanvas2D extends EventEmitter<OffscreenCanvasEvent> {
canvas: HTMLCanvasElement;
ctx: CanvasRenderingContext2D;
width: number;
height: number;
/** 是否是高清画布 */
highResolution: boolean = true;
/** 是否启用抗锯齿 */
antiAliasing: boolean = true;
scale: number = 1;
/** 更新标识符,如果发生变化则说明画布被动清空 */
symbol: number = 0;
/**
* 创建一个新的离屏画布\
* **注意**:如果你在自定义渲染元素中使用,请避免使用此构造函数,而应该使用 `RenderItem.requireCanvas`,
* 之后如果不使用,再使用 `RenderItem.deleteCanvas` 删除。
* @param alpha 是否启用透明度通道
* @param canvas 指定画布,不指定时会自动创建一个新画布
*/
constructor(alpha: boolean = true, canvas?: HTMLCanvasElement) {
super();
this.canvas = canvas ?? document.createElement('canvas');
this.ctx = this.canvas.getContext('2d', { alpha })!;
this.width = this.canvas.width / devicePixelRatio;
this.height = this.canvas.height / devicePixelRatio;
}
/**
* 设置画布的缩放比
* @param scale 缩放比
*/
setScale(scale: number) {
// if (scale === this.scale) {
// this.clear();
// return;
// }
this.scale = scale;
let ratio = this.highResolution ? devicePixelRatio : 1;
ratio *= this.scale;
this.canvas.width = this.width * ratio;
this.canvas.height = this.height * ratio;
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.scale(ratio, ratio);
this.ctx.imageSmoothingEnabled = this.antiAliasing;
}
/**
* 设置画布的大小
*/
size(width: number, height: number) {
const w = Math.max(width, 1);
const h = Math.max(height, 1);
let ratio = this.highResolution ? devicePixelRatio : 1;
ratio *= this.scale;
this.canvas.width = w * ratio;
this.canvas.height = h * ratio;
this.width = w;
this.height = h;
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.scale(ratio, ratio);
this.ctx.imageSmoothingEnabled = this.antiAliasing;
}
/**
* 设置当前画布是否为高清画布
*/
setHD(hd: boolean) {
this.highResolution = hd;
this.size(this.width, this.height);
}
/**
* 设置当前画布的抗锯齿设置
*/
setAntiAliasing(anti: boolean) {
this.antiAliasing = anti;
this.ctx.imageSmoothingEnabled = anti;
}
/**
* 清空画布
*/
clear() {
this.ctx.save();
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.restore();
}
/**
* 复制一个离屏Canvas2D对象,一般用于缓存等操作
* @param canvas 被复制的MotaOffscreenCanvas2D对象
*/
static clone(canvas: MotaOffscreenCanvas2D): MotaOffscreenCanvas2D {
const newCanvas = new MotaOffscreenCanvas2D();
newCanvas.setHD(canvas.highResolution);
newCanvas.size(canvas.width, canvas.height);
newCanvas.ctx.drawImage(
canvas.canvas,
0,
0,
canvas.width,
canvas.height
);
return newCanvas;
}
}
+232
View File
@@ -0,0 +1,232 @@
import { MotaOffscreenCanvas2D } from './canvas2d';
import { ActionType, EventProgress, ActionEventMap } from './event';
import {
ERenderItemEvent,
IRenderChildable,
RenderItem,
RenderItemPosition
} from './item';
import { Transform } from './transform';
export interface EContainerEvent extends ERenderItemEvent {}
export class Container<E extends EContainerEvent = EContainerEvent>
extends RenderItem<E | EContainerEvent>
implements IRenderChildable
{
sortedChildren: RenderItem[] = [];
private needSort: boolean = false;
/**
* 创建一个容器,容器中可以包含其他渲染对象
* @param type 渲染模式,absolute表示绝对位置,static表示跟随摄像机移动
* @param cache 是否启用缓存机制
*/
constructor(
type: RenderItemPosition = 'static',
cache: boolean = true,
fall: boolean = false
) {
super(type, cache, fall);
this.type = type;
}
protected render(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void {
this.sortedChildren.forEach(v => {
if (v.hidden) return;
v.renderContent(canvas, transform);
});
}
onResize(scale: number): void {
this.children.forEach(v => v.onResize(scale));
super.onResize(scale);
}
requestSort() {
if (!this.needSort) {
this.needSort = true;
this.requestBeforeFrame(() => {
this.needSort = false;
this.sortChildren();
});
}
}
/**
* 添加子元素到这个容器上,然后在下一个tick执行更新
* @param children 要添加的子元素
*/
appendChild(...children: RenderItem<any>[]) {
children.forEach(v => {
v.appendTo(this);
});
this.requestSort();
this.update(this);
}
removeChild(...child: RenderItem<any>[]): void {
let changed = false;
child.forEach(v => {
if (v.parent !== this) return;
const success = v.remove();
if (success) {
changed = true;
}
});
if (changed) this.requestSort();
this.update(this);
}
appendTo(parent: RenderItem): void {
super.appendTo(parent);
if (this.root) {
const root = this.root;
this.forEachChild(ele => {
ele.setRoot(root);
});
}
}
/**
* 遍历这个元素中的每个子元素,并执行传入的函数
* @param fn 对每个元素执行的函数
*/
forEachChild(fn: (ele: RenderItem) => void) {
const stack: RenderItem[] = [this];
while (stack.length > 0) {
const ele = stack.pop()!;
stack.push(...ele.children);
fn(ele);
}
}
private sortChildren() {
this.sortedChildren = [...this.children]
.filter(v => !v.isComment)
.sort((a, b) => a.zIndex - b.zIndex);
this.update();
}
protected propagateEvent<T extends ActionType>(
type: T,
progress: EventProgress,
event: ActionEventMap[T]
): void {
const len = this.sortedChildren.length;
if (progress === EventProgress.Capture) {
let success = false;
for (let i = len - 1; i >= 0; i--) {
const child = this.sortedChildren[i];
if (child.hidden) continue;
if (child.captureEvent(type, event)) {
success = true;
break;
}
}
// 如果没有子元素能够触发,那么自身触发冒泡
if (!success) {
this.bubbleEvent(type, event);
}
} else {
this.parent?.bubbleEvent(type, event);
}
}
destroy(): void {
super.destroy();
this.children.forEach(v => {
v.destroy();
});
}
}
export type CustomContainerRenderFn = (
canvas: MotaOffscreenCanvas2D,
children: RenderItem[],
transform: Transform
) => void;
export type CustomContainerPropagateOrigin = <T extends ActionType>(
type: T,
progress: EventProgress,
event: ActionEventMap[T]
) => void;
export type CustomContainerPropagateFn = <T extends ActionType>(
type: T,
progress: EventProgress,
event: ActionEventMap[T],
container: ContainerCustom,
origin: CustomContainerPropagateOrigin
) => void;
export class ContainerCustom extends Container {
private renderFn?: CustomContainerRenderFn;
private propagateFn?: CustomContainerPropagateFn;
protected render(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void {
if (!this.renderFn) {
super.render(canvas, transform);
} else {
this.renderFn(canvas, this.sortedChildren, transform);
}
}
protected propagateEvent<T extends ActionType>(
type: T,
progress: EventProgress,
event: ActionEventMap[T]
): void {
if (this.propagateFn) {
this.propagateFn(type, progress, event, this, () => {
super.propagateEvent(type, progress, event);
});
} else {
super.propagateEvent(type, progress, event);
}
}
/**
* 设置这个自定义容器的渲染函数
* @param render 渲染函数
*/
setRenderFn(render?: CustomContainerRenderFn) {
this.renderFn = render;
}
/**
* 设置这个自定义容器的事件传递函数
* @param propagate 事件传递函数
*/
setPropagateFn(propagate: CustomContainerPropagateFn) {
this.propagateFn = propagate;
}
protected handleProps(
key: string,
prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'render': {
if (!this.assertType(nextValue, 'function', key)) return false;
this.setRenderFn(nextValue);
return true;
}
case 'propagate': {
if (!this.assertType(nextValue, 'function', key)) return false;
this.setPropagateFn(nextValue);
return true;
}
}
return super.handleProps(key, prevValue, nextValue);
}
}
+154
View File
@@ -0,0 +1,154 @@
import type { RenderItem } from './item';
export const enum MouseType {
/** 没有按键按下 */
None = 0,
/** 左键 */
Left = 1 << 0,
/** 中键,即按下滚轮 */
Middle = 1 << 1,
/** 右键 */
Right = 1 << 2,
/** 侧键后退 */
Back = 1 << 3,
/** 侧键前进 */
Forward = 1 << 4
}
export const enum WheelType {
None,
/** 以像素为单位 */
Pixel,
/** 以行为单位,每行长度视浏览器设置而定,约为 1rem */
Line,
/** 以页为单位,一般为一个屏幕高度 */
Page
}
export const enum ActionType {
/** 点击事件,即按下与抬起都在该元素上时触发 */
Click,
/** 鼠标或手指按下事件 */
Down,
/** 鼠标或手指移动事件 */
Move,
/** 鼠标或手指抬起事件 */
Up,
/** 鼠标或手指移动入该元素时触发的事件 */
Enter,
/** 鼠标或手指移出该元素时触发的事件 */
Leave,
/** 鼠标在该元素上滚轮时触发的事件 */
Wheel
}
export const enum EventProgress {
/** 捕获阶段 */
Capture,
/** 冒泡阶段 */
Bubble
}
export interface IActionEventBase {
/** 当前事件是监听的哪个元素 */
target: RenderItem;
/** 是触摸操作还是鼠标操作 */
touch: boolean;
/**
* 触发的按键种类,会出现在点击、按下、抬起三个事件中,而其他的如移动等该值只会是 {@link MouseType.None},
* 电脑端可以有左键、中键、右键等,手机只会触发左键,每一项的值参考 {@link MouseType}
*/
type: MouseType;
/**
* 当前按下了哪些按键。该值是一个数字,可以通过位运算判断是否按下了某个按键。
* 例如通过 `buttons & MouseType.Left` 来判断是否按下了左键。
* 注意在鼠标抬起或鼠标点击事件中,并不会包含触发的那个按键
*/
buttons: number;
/** 触发时是否按下了 alt 键 */
altKey: boolean;
/** 触发时是否按下了 shift 键 */
shiftKey: boolean;
/** 触发时是否按下了 ctrl 键 */
ctrlKey: boolean;
/** 触发时是否按下了 Windows(Windows) / Command(Mac) 键 */
metaKey: boolean;
/** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */
identifier: number;
}
export interface IActionEvent extends IActionEventBase {
/** 相对于触发元素左上角的横坐标 */
offsetX: number;
/** 相对于触发元素左上角的纵坐标 */
offsetY: number;
/** 相对于整个画布左上角的横坐标 */
absoluteX: number;
/** 相对于整个画布左上角的纵坐标 */
absoluteY: number;
/**
* 调用后将停止事件的继续传播。
* 在捕获阶段,将会阻止捕获的进一步进行,在冒泡阶段,将会阻止冒泡的进一步进行。
* 如果当前元素有很多监听器,该方法并不会阻止其他监听器的执行。
*/
stopPropagation(): void;
}
export interface IWheelEvent extends IActionEvent {
/** 滚轮事件的鼠标横向滚动量 */
wheelX: number;
/** 滚轮事件的鼠标纵向滚动量 */
wheelY: number;
/** 滚轮事件的鼠标垂直屏幕的滚动量 */
wheelZ: number;
/** 滚轮事件的滚轮类型,表示了对应值的单位 */
wheelType: WheelType;
}
export interface ERenderItemActionEvent {
/** 当这个元素被点击时的捕获阶段触发 */
clickCapture: [ev: Readonly<IActionEvent>];
/** 当这个元素被点击时的冒泡阶段触发 */
click: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上按下的捕获阶段触发 */
downCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上按下的冒泡阶段触发 */
down: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上移动的捕获阶段触发 */
moveCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上移动的冒泡阶段触发 */
move: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上抬起的捕获阶段触发 */
upCapture: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指在该元素上抬起的冒泡阶段触发 */
up: [ev: Readonly<IActionEvent>];
/** 当鼠标或手指进入该元素时触发 */
enter: [ev: Readonly<IActionEventBase>];
/** 当鼠标或手指离开该元素时触发 */
leave: [ev: Readonly<IActionEventBase>];
/** 当鼠标滚轮时的捕获阶段触发 */
wheelCapture: [ev: Readonly<IWheelEvent>];
/** 当鼠标滚轮时的冒泡阶段触发 */
wheel: [ev: Readonly<IWheelEvent>];
}
export interface ActionEventMap {
[ActionType.Click]: IActionEvent;
[ActionType.Down]: IActionEvent;
[ActionType.Enter]: IActionEvent;
[ActionType.Leave]: IActionEvent;
[ActionType.Move]: IActionEvent;
[ActionType.Up]: IActionEvent;
[ActionType.Wheel]: IWheelEvent;
}
export const eventNameMap: Record<ActionType, keyof ERenderItemActionEvent> = {
[ActionType.Click]: 'click',
[ActionType.Down]: 'down',
[ActionType.Move]: 'move',
[ActionType.Up]: 'up',
[ActionType.Enter]: 'enter',
[ActionType.Leave]: 'leave',
[ActionType.Wheel]: 'wheel'
};
File diff suppressed because it is too large. Load diff
+11
View File
@@ -0,0 +1,11 @@
export * from './adapter';
export * from './canvas2d';
export * from './container';
export * from './event';
export * from './gl2';
export * from './item';
export * from './render';
export * from './shader';
export * from './sprite';
export * from './transform';
export * from './utils';
File diff suppressed because it is too large. Load diff
+664
View File
@@ -0,0 +1,664 @@
import { logger } from '@motajs/common';
import { MotaOffscreenCanvas2D } from './canvas2d';
import { Container } from './container';
import {
ActionType,
IActionEvent,
IActionEventBase,
IWheelEvent,
MouseType,
WheelType
} from './event';
import { IRenderTreeRoot, RenderItem } from './item';
import { Transform } from './transform';
interface TouchInfo {
/** 这次触摸在渲染系统的标识符 */
identifier: number;
/** 浏览器的 clientX,用于判断这个触点有没有移动 */
clientX: number;
/** 浏览器的 clientY,用于判断这个触点有没有移动 */
clientY: number;
/** 是否覆盖在了当前元素上 */
hovered: boolean;
}
interface MouseInfo {
/** 这个鼠标按键的标识符 */
identifier: number;
}
export interface MotaRendererConfig {
/** 要挂载到哪个画布上,可以填 css 选择器或画布元素本身 */
canvas: string | HTMLCanvasElement;
/** 画布的宽度,所有渲染操作会自行适配缩放 */
width: number;
/** 画布的高度,所有渲染操作会自行适配缩放 */
height: number;
/** 是否启用不透明度通道,默认启用 */
alpha?: boolean;
}
export class MotaRenderer extends Container implements IRenderTreeRoot {
static list: Map<string, MotaRenderer> = new Map();
/** 所有连接到此根元素的渲染元素的 id 到元素自身的映射 */
protected idMap: Map<string, RenderItem> = new Map();
/** 最后一次按下的鼠标按键,用于处理鼠标移动 */
private lastMouse: MouseType = MouseType.None;
/** 每个触点的信息 */
private touchInfo: Map<number, TouchInfo> = new Map();
/** 触点列表 */
private touchList: Map<number, Touch> = new Map();
/** 每个鼠标按键的信息 */
private mouseInfo: Map<MouseType, MouseInfo> = new Map();
/** 操作的标识符 */
private actionIdentifier: number = 0;
/** 用于终止 document 上的监听 */
private abort?: AbortController;
/** 根据捕获行为判断光标样式 */
private targetCursor: string = 'auto';
/** 当前鼠标覆盖的元素 */
private hoveredElement: Set<RenderItem> = new Set();
/** 本次交互前鼠标覆盖的元素 */
private beforeHovered: Set<RenderItem> = new Set();
target!: MotaOffscreenCanvas2D;
readonly isRoot = true;
constructor(config: MotaRendererConfig) {
super('static', false);
const canvas = this.getMountCanvas(config.canvas);
if (!canvas) {
logger.error(19);
return;
}
this.target = new MotaOffscreenCanvas2D(config.alpha ?? true, canvas);
this.size(config.width, config.height);
this.target.setAntiAliasing(false);
this.setAnchor(0.5, 0.5);
MotaRenderer.list.set(canvas.id, this);
const update = () => {
this.requestRenderFrame(() => {
this.refresh();
update();
});
};
update();
this.listen();
this.setScale(1);
}
/**
* 设置这个渲染器的缩放比
* @param scale 缩放比
*/
setScale(scale: number) {
this.onResize(scale);
}
onResize(scale: number): void {
this.target.setScale(scale);
const width = this.target.width * scale;
const height = this.target.height * scale;
this.target.canvas.style.width = `${width}px`;
this.target.canvas.style.height = `${height}px`;
super.onResize(scale);
}
private getMountCanvas(canvas: string | HTMLCanvasElement) {
if (typeof canvas === 'string') {
return document.querySelector(canvas) as HTMLCanvasElement;
} else {
return canvas;
}
}
size(width: number, height: number): void {
super.size(width, height);
this.target.size(width, height);
this.transform.setTranslate(width / 2, height / 2);
}
private listen() {
// 画布监听
const canvas = this.target.canvas;
canvas.addEventListener('mousedown', ev => {
const mouse = this.getMouseType(ev);
this.lastMouse = mouse;
this.captureEvent(
ActionType.Down,
this.createMouseAction(ev, ActionType.Down, mouse)
);
});
canvas.addEventListener('mouseup', ev => {
const event = this.createMouseAction(ev, ActionType.Up);
this.captureEvent(ActionType.Up, event);
this.captureEvent(ActionType.Click, event);
});
canvas.addEventListener('mousemove', ev => {
const event = this.createMouseAction(
ev,
ActionType.Move,
this.lastMouse
);
this.targetCursor = 'auto';
const temp = this.beforeHovered;
temp.clear();
this.beforeHovered = this.hoveredElement;
this.hoveredElement = temp;
this.captureEvent(ActionType.Move, event);
if (this.targetCursor !== this.target.canvas.style.cursor) {
this.target.canvas.style.cursor = this.targetCursor;
}
this.checkMouseEnterLeave(
ev,
event,
this.beforeHovered,
this.hoveredElement
);
});
canvas.addEventListener('mouseleave', ev => {
const id = this.getMouseIdentifier(
ActionType.Leave,
this.getMouseType(ev)
);
this.hoveredElement.forEach(v => {
v.emit('leave', this.createMouseActionBase(ev, id, v));
});
this.hoveredElement.clear();
this.beforeHovered.clear();
});
document.addEventListener('touchstart', ev => {
this.createTouchAction(ev, ActionType.Down).forEach(v => {
this.captureEvent(ActionType.Down, v);
});
});
document.addEventListener('touchend', ev => {
this.createTouchAction(ev, ActionType.Up).forEach(v => {
this.captureEvent(ActionType.Up, v);
this.captureEvent(ActionType.Click, v);
});
[...ev.touches].forEach(v => {
this.touchInfo.delete(v.identifier);
});
});
document.addEventListener('touchcancel', ev => {
this.createTouchAction(ev, ActionType.Up).forEach(v => {
this.captureEvent(ActionType.Up, v);
});
[...ev.touches].forEach(v => {
this.touchInfo.delete(v.identifier);
});
});
document.addEventListener('touchmove', ev => {
this.createTouchAction(ev, ActionType.Move).forEach(v => {
const list = this.touchInfo.values();
if (!list.some(vv => v.identifier === vv.identifier)) {
return;
}
const temp = this.beforeHovered;
temp.clear();
this.beforeHovered = this.hoveredElement;
this.hoveredElement = temp;
this.captureEvent(ActionType.Move, v);
this.checkTouchEnterLeave(
ev,
v,
this.beforeHovered,
this.hoveredElement
);
});
});
canvas.addEventListener('wheel', ev => {
this.captureEvent(
ActionType.Wheel,
this.createWheelAction(ev, ActionType.Wheel)
);
});
// 文档监听
const abort = new AbortController();
const signal = abort.signal;
this.abort = abort;
const clear = (ev: MouseEvent) => {
const mouse = this.getMouseButtons(ev);
for (const button of this.mouseInfo.keys()) {
if (!(mouse & button)) {
this.mouseInfo.delete(button);
}
}
};
document.addEventListener('click', clear, { signal });
document.addEventListener('mouseenter', clear, { signal });
document.addEventListener('mouseleave', clear, { signal });
}
private isTouchInCanvas(clientX: number, clientY: number) {
const rect = this.target.canvas.getBoundingClientRect();
const { left, right, top, bottom } = rect;
const x = clientX;
const y = clientY;
return x >= left && x <= right && y >= top && y <= bottom;
}
private getMouseType(ev: MouseEvent): MouseType {
switch (ev.button) {
case 0:
return MouseType.Left;
case 1:
return MouseType.Middle;
case 2:
return MouseType.Right;
case 3:
return MouseType.Back;
case 4:
return MouseType.Forward;
}
return MouseType.None;
}
private getActiveMouseIdentifier(mouse: MouseType) {
if (this.lastMouse === MouseType.None) {
return -1;
} else {
const info = this.mouseInfo.get(mouse);
if (!info) return -1;
else return info.identifier;
}
}
private getMouseIdentifier(type: ActionType, mouse: MouseType): number {
switch (type) {
case ActionType.Down: {
const id = this.actionIdentifier++;
this.mouseInfo.set(mouse, { identifier: id });
return id;
}
case ActionType.Move:
case ActionType.Enter:
case ActionType.Leave:
case ActionType.Wheel: {
return this.getActiveMouseIdentifier(mouse);
}
case ActionType.Up:
case ActionType.Click: {
const id = this.getActiveMouseIdentifier(mouse);
this.mouseInfo.delete(mouse);
return id;
}
}
}
private getMouseButtons(event: MouseEvent): number {
if (event.buttons === 0) return MouseType.None;
let buttons = 0;
if (event.buttons & 0b1) buttons |= MouseType.Left;
if (event.buttons & 0b10) buttons |= MouseType.Right;
if (event.buttons & 0b100) buttons |= MouseType.Middle;
if (event.buttons & 0b1000) buttons |= MouseType.Back;
if (event.buttons & 0b10000) buttons |= MouseType.Forward;
return buttons;
}
private createMouseActionBase(
event: MouseEvent,
id: number,
target: RenderItem = this,
mouse: MouseType = this.getMouseType(event)
): IActionEventBase {
return {
identifier: id,
target: target,
touch: false,
type: mouse,
buttons: this.getMouseButtons(event),
altKey: event.altKey,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
metaKey: event.metaKey
};
}
private createTouchActionBase(
event: TouchEvent,
id: number,
target: RenderItem
): IActionEventBase {
return {
identifier: id,
target: target,
touch: false,
type: MouseType.Left,
buttons: MouseType.Left,
altKey: event.altKey,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
metaKey: event.metaKey
};
}
private createMouseAction(
event: MouseEvent,
type: ActionType,
mouse: MouseType = this.getMouseType(event)
): IActionEvent {
const id = this.getMouseIdentifier(type, mouse);
const x = event.offsetX / this.scale;
const y = event.offsetY / this.scale;
return {
target: this,
identifier: id,
touch: false,
offsetX: x,
offsetY: y,
absoluteX: x,
absoluteY: y,
type: mouse,
buttons: this.getMouseButtons(event),
altKey: event.altKey,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
metaKey: event.metaKey,
stopPropagation: () => {
this.propagationStoped.set(type, true);
}
};
}
private createWheelAction(
event: WheelEvent,
type: ActionType,
mouse: MouseType = this.getMouseType(event)
): IWheelEvent {
const ev = this.createMouseAction(event, type, mouse) as IWheelEvent;
ev.wheelX = event.deltaX;
ev.wheelY = event.deltaY;
ev.wheelZ = event.deltaZ;
switch (event.deltaMode) {
case 0x00:
ev.wheelType = WheelType.Pixel;
break;
case 0x01:
ev.wheelType = WheelType.Line;
break;
case 0x02:
ev.wheelType = WheelType.Page;
break;
default:
ev.wheelType = WheelType.None;
break;
}
return ev;
}
private getTouchIdentifier(touch: Touch, type: ActionType) {
if (type === ActionType.Down) {
const id = this.actionIdentifier++;
this.touchInfo.set(touch.identifier, {
identifier: id,
clientX: touch.clientX,
clientY: touch.clientY,
hovered: this.isTouchInCanvas(touch.clientX, touch.clientY)
});
return id;
}
const info = this.touchInfo.get(touch.identifier);
if (!info) return -1;
return info.identifier;
}
private createTouch(
touch: Touch,
type: ActionType,
event: TouchEvent,
rect: DOMRect
): IActionEvent {
const x = (touch.clientX - rect.left) / this.scale;
const y = (touch.clientY - rect.top) / this.scale;
return {
target: this,
identifier: this.getTouchIdentifier(touch, type),
touch: true,
offsetX: x,
offsetY: y,
absoluteX: x,
absoluteY: y,
type: MouseType.Left,
buttons: MouseType.Left,
altKey: event.altKey,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
metaKey: event.metaKey,
stopPropagation: () => {
this.propagationStoped.set(type, true);
}
};
}
private createTouchAction(
event: TouchEvent,
type: ActionType
): IActionEvent[] {
const list: IActionEvent[] = [];
const rect = this.target.canvas.getBoundingClientRect();
if (type === ActionType.Up) {
// 抬起是一个需要特殊处理的东西,因为 touches 不会包含这个内容,所以需要特殊处理
const touches = Array.from(event.touches).map(v => v.identifier);
for (const [id, touch] of this.touchList) {
if (!touches.includes(id)) {
// 如果不包含,才需要触发
if (this.isTouchInCanvas(touch.clientX, touch.clientY)) {
const ev = this.createTouch(touch, type, event, rect);
list.push(ev);
}
}
}
} else {
Array.from(event.touches).forEach(v => {
const ev = this.createTouch(v, type, event, rect);
if (type === ActionType.Move) {
const touch = this.touchInfo.get(v.identifier);
if (!touch) return;
const moveX = touch.clientX - v.clientX;
const moveY = touch.clientY - v.clientY;
if (moveX !== 0 || moveY !== 0) {
list.push(ev);
}
} else if (type === ActionType.Down) {
this.touchList.set(v.identifier, v);
if (this.isTouchInCanvas(v.clientX, v.clientY)) {
list.push(ev);
}
}
});
}
return list;
}
private checkMouseEnterLeave(
event: MouseEvent,
ev: IActionEvent,
before: Set<RenderItem>,
now: Set<RenderItem>
) {
// 先 leave,再 enter
before.forEach(v => {
if (!now.has(v)) {
v.emit(
'leave',
this.createMouseActionBase(event, ev.identifier, v)
);
}
});
now.forEach(v => {
if (!before.has(v)) {
v.emit(
'enter',
this.createMouseActionBase(event, ev.identifier, v)
);
}
});
}
private checkTouchEnterLeave(
event: TouchEvent,
ev: IActionEvent,
before: Set<RenderItem>,
now: Set<RenderItem>
) {
// 先 leave,再 enter
before.forEach(v => {
if (!now.has(v)) {
v.emit(
'leave',
this.createTouchActionBase(event, ev.identifier, v)
);
}
});
now.forEach(v => {
if (!before.has(v)) {
v.emit(
'enter',
this.createTouchActionBase(event, ev.identifier, v)
);
}
});
}
update(_item: RenderItem = this) {
this.cacheDirty = true;
}
protected refresh(): void {
if (!this.cacheDirty) return;
this.target.clear();
this.renderContent(this.target, Transform.identity);
}
/**
* 根据渲染元素的id获取一个渲染元素
* @param id 要获取的渲染元素id
* @returns
*/
getElementById(id: string): RenderItem | null {
if (id.length === 0) return null;
const item = this.idMap.get(id);
if (item) return item;
else {
const item = this.searchElement(this, id);
if (item) {
this.idMap.set(id, item);
}
return item;
}
}
private searchElement(ele: RenderItem, id: string): RenderItem | null {
for (const child of ele.children) {
if (child.id === id) return child;
else {
const ele = this.searchElement(child, id);
if (ele) return ele;
}
}
return null;
}
connect(item: RenderItem): void {
if (item.id.length === 0) return;
const existed = this.idMap.get(item.id);
if (existed) {
if (existed === item) return;
else logger.warn(23, item.id);
} else {
this.idMap.set(item.id, item);
}
}
disconnect(item: RenderItem): void {
this.idMap.delete(item.id);
}
modifyId(item: RenderItem, previous: string, current: string): void {
this.idMap.delete(previous);
if (current.length !== 0) {
if (this.idMap.has(item.id)) {
logger.warn(23, item.id);
} else {
this.idMap.set(item.id, item);
}
}
}
getCanvas(): HTMLCanvasElement {
return this.target.canvas;
}
hoverElement(element: RenderItem): void {
if (element.cursor !== 'inherit') {
this.targetCursor = element.cursor;
}
this.hoveredElement.add(element);
}
destroy() {
super.destroy();
MotaRenderer.list.delete(this.id);
this.abort?.abort();
}
private toTagString(item: RenderItem, space: number, deep: number): string {
if (item.isComment) return '';
const name = item.constructor.name;
if (item.children.size === 0) {
return `${' '.repeat(deep * space)}<${name} ${
item.id ? `id="${item.id}" ` : ''
}uid="${item.uid}"${item.hidden ? ' hidden' : ''} />\n`;
} else {
return (
`${' '.repeat(deep * space)}<${name} ${
item.id ? `${item.id} ` : ''
}uid="${item.uid}" ${item.hidden ? 'hidden' : ''}>\n` +
`${[...item.children]
.filter(v => !v.isComment)
.map(v => this.toTagString(v, space, deep + 1))
.join('')}` +
`${' '.repeat(deep * space)}</${name}>\n`
);
}
}
/**
* 调试功能,将渲染树输出为 XML 标签形式,只包含渲染元素类名,以及元素 id 等基础属性,不包含属性值等
* @param space 缩进空格数
*/
toTagTree(space: number = 4) {
if (!import.meta.env.DEV) return '';
return this.toTagString(this, space, 0);
}
static get(id: string) {
return this.list.get(id);
}
}
window.addEventListener('resize', () => {
MotaRenderer.list.forEach(v =>
v.requestAfterFrame(() => v.refreshAllChildren())
);
});
// @ts-expect-error debug
window.logTagTree = () => {
console.log(MotaRenderer.get('render-main')?.toTagTree());
};
+101
View File
@@ -0,0 +1,101 @@
import { MotaOffscreenCanvas2D } from './canvas2d';
import { EGL2Event, GL2, GL2Program, IGL2ProgramPrefix } from './gl2';
import { RenderItemPosition } from './item';
const SHADER_PREFIX: IGL2ProgramPrefix = {
VERTEX: /* glsl */ `#version 300 es
precision highp float;
in vec4 a_position;
in vec2 a_texCoord;
out vec2 v_texCoord;
`,
FRAGMENT: /* glsl */ `#version 300 es
precision highp float;
in vec2 v_texCoord;
uniform sampler2D u_sampler;
`
};
const DEFAULT_VS = /* glsl */ `
void main() {
v_texCoord = a_texCoord;
gl_Position = a_position;
}
`;
const DEFAULT_FS = /* glsl */ `
out vec4 color;
void main() {
color = texture(u_sampler, v_texCoord);
}
`;
export interface EShaderEvent extends EGL2Event {}
export class Shader<E extends EShaderEvent = EShaderEvent> extends GL2<
EShaderEvent | E
> {
constructor(type: RenderItemPosition = 'static') {
super(type);
}
protected drawScene(
canvas: MotaOffscreenCanvas2D,
gl: WebGL2RenderingContext
): void {
const program = this.program;
if (!program) return;
program.texTexture('u_sampler', canvas.canvas);
this.draw(gl, program);
}
}
export class ShaderProgram extends GL2Program {
protected readonly prefix: IGL2ProgramPrefix = SHADER_PREFIX;
constructor(gl2: GL2, vs?: string, fs?: string) {
super(gl2, vs, fs);
if (!vs) this.vs(DEFAULT_VS);
if (!fs) this.fs(DEFAULT_FS);
}
ready(): boolean {
this.useIndices('defaultIndices');
return true;
}
protected override compile() {
const success = super.compile();
if (!success) return false;
const shader = this.element;
const gl = shader.gl;
if (!gl) return false;
const tex = this.defineAttribArray('a_texCoord');
const position = this.defineAttribArray('a_position');
const sampler = this.defineTexture('u_sampler', 0);
const indices = this.defineIndices('defaultIndices');
if (!tex || !position || !sampler || !indices) {
return true;
}
position.buffer(
new Float32Array([1, -1, -1, -1, 1, 1, -1, 1]),
gl.STATIC_DRAW
);
position.pointer(2, gl.FLOAT, false, 0, 0);
position.enable();
tex.buffer(new Float32Array([1, 1, 0, 1, 1, 0, 0, 0]), gl.STATIC_DRAW);
tex.pointer(2, gl.FLOAT, false, 0, 0);
tex.enable();
indices.buffer(new Uint16Array([0, 1, 2, 2, 3, 1]), gl.STATIC_DRAW);
this.useIndices(indices);
this.mode(this.element.DRAW_ELEMENTS);
this.paramElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
return true;
}
}
+59
View File
@@ -0,0 +1,59 @@
import {
ERenderItemEvent,
RenderFunction,
RenderItem,
RenderItemPosition
} from './item';
import { MotaOffscreenCanvas2D } from './canvas2d';
import { Transform } from './transform';
export interface ESpriteEvent extends ERenderItemEvent {}
export class Sprite<
E extends ESpriteEvent = ESpriteEvent
> extends RenderItem<E> {
renderFn: RenderFunction;
/**
* 创建一个精灵,可以自由在上面渲染内容
* @param type 渲染模式,absolute表示绝对位置,不会跟随自身的Transform改变
* @param cache 是否启用缓存机制
*/
constructor(
type: RenderItemPosition = 'static',
cache: boolean = true,
fall: boolean = false
) {
super(type, cache, fall);
this.type = type;
this.renderFn = () => {};
}
protected render(
canvas: MotaOffscreenCanvas2D,
transform: Transform
): void {
canvas.ctx.save();
this.renderFn(canvas, transform);
canvas.ctx.restore();
}
setRenderFn(fn: RenderFunction) {
this.renderFn = fn;
this.update(this);
}
protected handleProps(
key: string,
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'render':
if (!this.assertType(nextValue, 'function', key)) return false;
this.setRenderFn(nextValue);
return true;
}
return false;
}
}
+488
View File
@@ -0,0 +1,488 @@
import { mat3, mat4, ReadonlyMat3, ReadonlyVec3, vec2, vec3 } from 'gl-matrix';
export interface ITransformUpdatable<T> {
/**
* 当变换矩阵更新时触发
* @param transform 触发更新的变换矩阵
*/
updateTransform(transform: T): void;
}
export class Transform {
mat: mat3 = mat3.create();
x: number = 0;
y: number = 0;
scaleX: number = 1;
scaleY: number = 1;
rad: number = 0;
/** 有没有对这个Transform进行过修改,用于优化常规表现 */
private modified: boolean = false;
/** 绑定的可更新元素 */
bindedObject: Set<ITransformUpdatable<Transform>> = new Set();
/**
* 对这个变换实例添加绑定对象,当矩阵变换时,自动调用其 update 函数
* @param obj 要绑定的对象
*/
bind(obj: ITransformUpdatable<Transform>) {
this.bindedObject.add(obj);
}
/**
* 取消对象的绑定
* @param obj 要取消绑定的对象
*/
unbind(obj: ITransformUpdatable<Transform>) {
this.bindedObject.delete(obj);
}
private emitUpdate() {
this.bindedObject.forEach(v => v.updateTransform(this));
}
/**
* 重设所有参数
*/
reset() {
mat3.identity(this.mat);
this.x = 0;
this.y = 0;
this.scaleX = 1;
this.scaleY = 1;
this.rad = 0;
this.modified = false;
this.emitUpdate();
}
/**
* 修改缩放,叠加关系
*/
scale(x: number, y: number = x): this {
mat3.scale(this.mat, this.mat, [x, y]);
this.scaleX *= x;
this.scaleY *= y;
this.modified = true;
this.emitUpdate();
return this;
}
/**
* 移动,叠加关系
*/
translate(x: number, y: number): this {
mat3.translate(this.mat, this.mat, [x, y]);
this.x += x;
this.y += y;
this.modified = true;
this.emitUpdate();
return this;
}
/**
* 旋转,叠加关系
*/
rotate(rad: number): this {
mat3.rotate(this.mat, this.mat, rad);
this.rad += rad;
if (this.rad >= Math.PI * 2) {
const n = Math.floor(this.rad / Math.PI / 2);
this.rad -= n * Math.PI * 2;
}
this.modified = true;
this.emitUpdate();
return this;
}
/**
* 设置缩放,非叠加关系
*/
setScale(x: number, y: number = x): this {
mat3.scale(this.mat, this.mat, [x / this.scaleX, y / this.scaleY]);
this.scaleX = x;
this.scaleY = y;
this.modified = true;
this.emitUpdate();
return this;
}
/**
* 设置位置,非叠加关系
*/
setTranslate(x: number, y: number): this {
mat3.translate(this.mat, this.mat, [x - this.x, y - this.y]);
this.x = x;
this.y = y;
this.modified = true;
this.emitUpdate();
return this;
}
/**
* 设置旋转,非叠加关系
*/
setRotate(rad: number): this {
mat3.rotate(this.mat, this.mat, rad - this.rad);
this.rad = rad;
this.modified = true;
this.emitUpdate();
return this;
}
/**
* 设置变换矩阵,叠加模式
* @param a 水平缩放
* @param b 垂直倾斜
* @param c 水平倾斜
* @param d 垂直缩放
* @param e 水平移动
* @param f 垂直移动
*/
transform(
a: number,
b: number,
c: number,
d: number,
e: number,
f: number
): this {
mat3.multiply(
this.mat,
this.mat,
mat3.fromValues(a, b, 0, c, d, 0, e, f, 1)
);
this.calAttributes();
this.emitUpdate();
return this;
}
/**
* 设置变换矩阵,非叠加模式
* @param a 水平缩放
* @param b 垂直倾斜
* @param c 水平倾斜
* @param d 垂直缩放
* @param e 水平移动
* @param f 垂直移动
*/
setTransform(
a: number,
b: number,
c: number,
d: number,
e: number,
f: number
): this {
mat3.set(this.mat, a, b, 0, c, d, 0, e, f, 1);
this.calAttributes();
this.emitUpdate();
return this;
}
/**
* 重新计算 translation scaling rotation
*/
calAttributes() {
const [x, y] = getTranslation(this.mat);
const [scaleX, scaleY] = getScaling(this.mat);
const rad = getRotation(this.mat);
this.x = x;
this.y = y;
this.scaleX = scaleX;
this.scaleY = scaleY;
this.rad = rad;
if (x === 0 && y === 0 && scaleX === 1 && scaleY === 1 && rad === 0) {
this.modified = false;
} else {
this.modified = true;
}
}
/**
* 与一个矩阵相乘,返回其计算结果(不改变原矩阵)
* @param transform 变换矩阵
*/
multiply(transform: Transform): Transform {
if (this.modified) {
const result = new Transform();
mat3.multiply(result.mat, this.mat, transform.mat);
result.modified = true;
return result;
} else {
return transform.clone();
}
}
/**
* 复制这个变换矩阵
*/
clone() {
const transform = new Transform();
transform.mat = mat3.clone(this.mat);
return transform;
}
/**
* 根据变换矩阵的信息,将一个点转换为计算后的位置
* @param x 横坐标
* @param y 纵坐标
*/
transformed(x: number, y: number): vec3 {
if (!this.modified) return [x, y, 1];
return multiplyVec3(this.mat, [x, y, 1]);
}
/**
* 根据变换矩阵的信息,将一个计算后的位置逆转换为原位置
* @param x 横坐标
* @param y 纵坐标
*/
untransformed(x: number, y: number): vec3 {
if (!this.modified) return [x, y, 1];
const invert = mat3.create();
mat3.invert(invert, this.mat);
return multiplyVec3(invert, [x, y, 1]);
}
/**
* 根据变换矩阵的信息,将一个点转换为计算后的位置
* @param transform 变换矩阵
* @param x 横坐标
* @param y 纵坐标
*/
static transformed(transform: Transform, x: number, y: number) {
return multiplyVec3(transform.mat, [x, y, 1]);
}
/**
* 根据变换矩阵的信息,将一个计算后的位置逆转换为原位置
* @param transform 变换矩阵
* @param x 横坐标
* @param y 纵坐标
*/
static untransformed(transform: Transform, x: number, y: number) {
const invert = mat3.create();
mat3.invert(invert, transform.mat);
return multiplyVec3(invert, [x, y, 1]);
}
/** 单位矩阵 */
static get identity() {
return new Transform();
}
}
function multiplyVec3(mat: ReadonlyMat3, vec: ReadonlyVec3): vec3 {
const v0 = vec[0];
const v1 = vec[1];
const v2 = vec[2];
return [
mat[0] * v0 + mat[3] * v1 + mat[6] * v2,
mat[1] * v0 + mat[4] * v1 + mat[7] * v2,
mat[2] * v0 + mat[5] * v1 + mat[8] * v2
];
}
function getTranslation(mat: ReadonlyMat3): vec2 {
return [mat[6], mat[7]];
}
function getScaling(mat: ReadonlyMat3): vec2 {
return [Math.hypot(mat[0], mat[3]), Math.hypot(mat[1], mat[4])];
}
function getRotation(mat: ReadonlyMat3): number {
return Math.atan2(mat[3], mat[0]);
}
export class Transform3D {
mat: mat4 = mat4.create();
/** 绑定的可更新元素 */
bindedObject: Set<ITransformUpdatable<Transform3D>> = new Set();
/**
* 对这个变换实例添加绑定对象,当矩阵变换时,自动调用其 update 函数
* @param obj 要绑定的对象
*/
bind(obj: ITransformUpdatable<Transform3D>) {
this.bindedObject.add(obj);
}
/**
* 取消对象的绑定
* @param obj 要取消绑定的对象
*/
unbind(obj: ITransformUpdatable<Transform3D>) {
this.bindedObject.delete(obj);
}
private emitUpdate() {
this.bindedObject.forEach(v => v.updateTransform(this));
}
/**
* 重置为单位矩阵
*/
reset(): this {
mat4.identity(this.mat);
this.emitUpdate();
return this;
}
/**
* 应用缩放变换
* @param x X轴缩放
* @param y Y轴缩放
* @param z Z轴缩放
*/
scale(x: number, y: number, z: number): this {
mat4.scale(this.mat, this.mat, [x, y, z]);
this.emitUpdate();
return this;
}
/**
* 应用平移变换
* @param x X轴平移
* @param y Y轴平移
* @param z Z轴平移
*/
translate(x: number, y: number, z: number): this {
mat4.translate(this.mat, this.mat, [x, y, z]);
this.emitUpdate();
return this;
}
/**
* 应用旋转变换
* @param rad 旋转角度(弧度)
* @param axis 旋转轴
*/
rotate(rad: number, axis: vec3): this {
mat4.rotate(this.mat, this.mat, rad, axis);
this.emitUpdate();
return this;
}
/**
* 应用绕X轴旋转
* @param rad 旋转角度(弧度)
*/
rotateX(rad: number): this {
return this.rotate(rad, [1, 0, 0]);
}
/**
* 应用绕Y轴旋转
* @param rad 旋转角度(弧度)
*/
rotateY(rad: number): this {
return this.rotate(rad, [0, 1, 0]);
}
/**
* 应用绕Z轴旋转
* @param rad 旋转角度(弧度)
*/
rotateZ(rad: number): this {
return this.rotate(rad, [0, 0, 1]);
}
/**
* 设置视图矩阵
* @param eye 摄像机位置
* @param center 目标位置
* @param up 上方向量
*/
lookAt(eye: vec3, center: vec3, up: vec3): this {
mat4.lookAt(this.mat, eye, center, up);
this.emitUpdate();
return this;
}
/**
* 设置透视投影矩阵
* @param fovy 垂直视野角度(弧度)
* @param aspect 宽高比
* @param near 近平面
* @param far 远平面
*/
perspective(fovy: number, aspect: number, near: number, far: number): this {
mat4.perspective(this.mat, fovy, aspect, near, far);
this.emitUpdate();
return this;
}
/**
* 设置正交投影矩阵
* @param left 左平面
* @param right 右平面
* @param bottom 底平面
* @param top 顶平面
* @param near 近平面
* @param far 远平面
*/
ortho(
left: number,
right: number,
bottom: number,
top: number,
near: number,
far: number
): this {
mat4.ortho(this.mat, left, right, bottom, top, near, far);
this.emitUpdate();
return this;
}
/**
* 与另一个变换矩阵相乘
* @param transform 另一个变换矩阵
*/
multiply(transform: Transform3D): Transform3D {
const result = new Transform3D();
mat4.multiply(result.mat, this.mat, transform.mat);
return result;
}
/**
* 克隆当前变换矩阵
*/
clone(): Transform3D {
const clone = new Transform3D();
mat4.copy(clone.mat, this.mat);
return clone;
}
/**
* 将点应用变换
* @param point 要变换的点
*/
transformed(point: vec3): vec3 {
const result = vec3.create();
vec3.transformMat4(result, point, this.mat);
return result;
}
/**
* 将点应用逆变换
* @param point 要逆变换的点
*/
untransformed(point: vec3): vec3 {
const inverse = mat4.create();
mat4.invert(inverse, this.mat);
const result = vec3.create();
vec3.transformMat4(result, point, inverse);
return result;
}
/**
* 静态方法:单位矩阵
*/
static get identity(): Transform3D {
return new Transform3D();
}
}
+80
View File
@@ -0,0 +1,80 @@
import { TimingFn } from 'mutate-animate';
import { JSX } from 'vue/jsx-runtime';
import { DefineComponent, DefineSetupFnComponent } from 'vue';
import { MotaOffscreenCanvas2D } from './canvas2d';
import { Transform } from './transform';
export type Props<
T extends
| keyof JSX.IntrinsicElements
| DefineSetupFnComponent<any>
| DefineComponent
> = T extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[T]
: T extends DefineSetupFnComponent<any>
? InstanceType<T>['$props'] & InstanceType<T>['$emits']
: T extends DefineComponent
? InstanceType<T>['$props'] & InstanceType<T>['$emits']
: unknown;
export type ElementLocator = [
x?: number,
y?: number,
width?: number,
height?: number,
anchorX?: number,
anchorY?: number
];
export type ElementAnchor = [x: number, y: number];
export type ElementScale = [x: number, y: number];
const { gl, gl2 } = checkSupport();
function checkSupport() {
const canvas = document.createElement('canvas');
const canvas2 = document.createElement('canvas');
const gl = canvas.getContext('webgl');
const gl2 = canvas2.getContext('webgl2');
return { gl: !!gl, gl2: !!gl2 };
}
export function isWebGLSupported() {
return gl;
}
export function isWebGL2Supported() {
return gl2;
}
/**
* 将两个缓动函数做加法
*/
export function addTiming(timing1: TimingFn, timing2: TimingFn): TimingFn {
return (p: number) => timing1(p) + timing2(p);
}
/**
* 将两个缓动函数做乘法
*/
export function multiplyTiming(timing1: TimingFn, timing2: TimingFn): TimingFn {
return (p: number) => timing1(p) * timing2(p);
}
/**
* 判断两个集合是否相等
*/
export function isSetEqual<T>(set1: Set<T>, set2: Set<T>) {
if (set1 === set2) return true;
else return set1.size === set2.size && set1.isSubsetOf(set2);
}
export function transformCanvas(
canvas: MotaOffscreenCanvas2D,
transform: Transform
) {
const { ctx } = canvas;
const mat = transform.mat;
const [a, b, , c, d, , e, f] = mat;
ctx.transform(a, b, c, d, e, f);
}
File diff suppressed because it is too large. Load diff
+2
View File
@@ -0,0 +1,2 @@
export * from './graphics';
export * from './misc';
+226
View File
@@ -0,0 +1,226 @@
import {
ERenderItemEvent,
RenderItem,
RenderItemPosition,
Transform,
MotaOffscreenCanvas2D
} from '../core';
import { CanvasStyle } from '../types';
import { Font } from '../style';
/** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */
const SAFE_PAD = 1;
export interface ETextEvent extends ERenderItemEvent {
setText: [text: string, width: number, height: number];
}
export class Text extends RenderItem<ETextEvent> {
text: string;
fillStyle?: CanvasStyle = '#fff';
strokeStyle?: CanvasStyle;
font: Font = new Font();
strokeWidth: number = 1;
private length: number = 0;
private descent: number = 0;
private static measureCanvas = new MotaOffscreenCanvas2D();
constructor(text: string = '', type: RenderItemPosition = 'static') {
super(type, false);
this.text = text;
if (text.length > 0) {
this.requestBeforeFrame(() => {
this.calBox();
this.emit('setText', text, this.width, this.height);
});
}
}
protected render(
canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {
const ctx = canvas.ctx;
const stroke = this.strokeWidth;
ctx.textBaseline = 'bottom';
ctx.fillStyle = this.fillStyle ?? 'transparent';
ctx.strokeStyle = this.strokeStyle ?? 'transparent';
ctx.font = this.font.string();
ctx.lineWidth = this.strokeWidth;
ctx.lineJoin = 'round';
if (this.strokeStyle) {
ctx.strokeText(this.text, stroke, this.descent + stroke + SAFE_PAD);
}
if (this.fillStyle) {
ctx.fillText(this.text, stroke, this.descent + stroke + SAFE_PAD);
}
}
/**
* 获取文字的长度
*/
measure() {
const ctx = Text.measureCanvas.ctx;
ctx.textBaseline = 'bottom';
ctx.font = this.font.string();
const res = ctx.measureText(this.text);
return res;
}
/**
* 设置显示文字
* @param text 显示的文字
*/
setText(text: string) {
this.text = text;
this.calBox();
this.update(this);
this.emit('setText', text, this.width, this.height);
}
/**
* 设置使用的字体
* @param font 字体
*/
setFont(font: Font) {
this.font = font;
this.calBox();
this.update(this);
}
/**
* 设置字体样式
* @param fill 填充样式
* @param stroke 描边样式
*/
setStyle(fill?: CanvasStyle, stroke?: CanvasStyle) {
this.fillStyle = fill;
this.strokeStyle = stroke;
this.update();
}
/**
* 设置描边宽度
* @param width 宽度
*/
setStrokeWidth(width: number) {
const before = this.strokeWidth;
this.strokeWidth = width;
const dw = width - before;
this.size(this.width + dw * 2, this.height + dw * 2);
this.update();
}
/**
* 计算字体所占空间,从而确定这个元素的大小
*/
calBox() {
const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } =
this.measure();
this.length = width;
this.descent = actualBoundingBoxAscent;
const height = actualBoundingBoxAscent + actualBoundingBoxDescent;
const stroke = this.strokeWidth * 2;
this.size(width + stroke, height + stroke + SAFE_PAD * 2);
}
protected handleProps(
key: string,
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'text':
if (!this.assertType(nextValue, 'string', key)) return false;
this.setText(nextValue);
return true;
case 'fillStyle':
this.setStyle(nextValue, this.strokeStyle);
return true;
case 'strokeStyle':
this.setStyle(this.fillStyle, nextValue);
return true;
case 'font':
if (!this.assertType(nextValue, Font, key)) return false;
this.setFont(nextValue);
return true;
case 'strokeWidth':
this.setStrokeWidth(nextValue);
return true;
}
return false;
}
}
export interface EImageEvent extends ERenderItemEvent {}
export class Image extends RenderItem<EImageEvent> {
image: CanvasImageSource;
constructor(image: CanvasImageSource, type: RenderItemPosition = 'static') {
super(type, false);
this.image = image;
if (image instanceof VideoFrame || image instanceof SVGElement) {
this.size(200, 200);
} else {
this.size(image.width, image.height);
}
}
protected render(
canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {
const ctx = canvas.ctx;
ctx.drawImage(this.image, 0, 0, this.width, this.height);
}
/**
* 设置图片资源
* @param image 图片资源
*/
setImage(image: CanvasImageSource) {
this.image = image;
this.update();
}
protected handleProps(
key: string,
_prevValue: any,
nextValue: any
): boolean {
switch (key) {
case 'image':
this.setImage(nextValue);
return true;
}
return false;
}
}
export class Comment extends RenderItem {
readonly isComment: boolean = true;
constructor(public text: string = '') {
super('static', false, false);
this.hide();
}
getBoundingRect(): DOMRectReadOnly {
return new DOMRectReadOnly(0, 0, 0, 0);
}
protected render(
_canvas: MotaOffscreenCanvas2D,
_transform: Transform
): void {}
protected handleProps(): boolean {
return false;
}
}
+5 -5
View File
@@ -1,5 +1,5 @@
export * from '@motajs/render-assets';
export * from '@motajs/render-core';
export * from '@motajs/render-elements';
export * from '@motajs/render-style';
export * from '@motajs/render-vue';
export * from './assets';
export * from './core';
export * from './elements';
export * from './style';
export * from './types';
+193
View File
@@ -0,0 +1,193 @@
import { logger } from '@motajs/common';
export interface IFontConfig {
/** 字体类型 */
readonly family: string;
/** 字体大小的值 */
readonly size: number;
/** 字体大小单位,推荐使用 px */
readonly sizeUnit: string;
/** 字体粗细,范围 0-1000 */
readonly weight: number;
/** 是否斜体 */
readonly italic: boolean;
}
export const enum FontWeight {
Light = 300,
Normal = 400,
Bold = 700
}
type _FontStretch =
| 'ultra-condensed'
| 'extra-condensed'
| 'condensed'
| 'semi-condensed'
| 'normal'
| 'semi-expanded'
| 'expanded'
| 'extra-expanded'
| 'ultra-expanded';
type _FontVariant = 'normal' | 'small-caps';
export class Font implements IFontConfig {
static defaultFamily: string = 'Verdana';
static defaultSize: number = 16;
static defaultSizeUnit: string = 'px';
static defaultWeight: number = 400;
static defaultItalic: boolean = false;
private readonly fallbacks: Font[] = [];
private fontString: string = '';
constructor(
public readonly family: string = Font.defaultFamily,
public readonly size: number = Font.defaultSize,
public readonly sizeUnit: string = Font.defaultSizeUnit,
public readonly weight: number = Font.defaultWeight,
public readonly italic: boolean = Font.defaultItalic
) {
this.fontString = this.getFont();
}
/**
* 添加后备字体,若当前字体不可用,那么会使用后备字体,后备字体也可以添加后备字体,但是请避免递归添加
* @param fallback 后备字体
*/
addFallback(...fallback: Font[]) {
this.fallbacks.push(...fallback);
this.fontString = this.getFont();
}
private build() {
return `${this.italic ? 'italic ' : ''} ${this.weight} ${this.size}${
this.sizeUnit
} ${this.family}`;
}
private getFallbackFont(used: Set<Font>) {
let font = this.build();
this.fallbacks.forEach(v => {
if (used.has(v)) {
logger.warn(62, this.build());
return;
}
used.add(v);
font += `, ${v.getFallbackFont(used)}`;
});
return font;
}
private getFont() {
if (this.fallbacks.length === 0) {
return this.build();
} else {
const usedFont = new Set<Font>();
return this.getFallbackFont(usedFont);
}
}
/**
* 获取字体的 CSS 字符串
*/
string() {
return this.fontString;
}
private static parseOne(str: string) {
if (!str) return new Font();
let italic = this.defaultItalic;
let weight = this.defaultWeight;
let size = this.defaultSize;
let unit = this.defaultSizeUnit;
let family = this.defaultFamily;
const tokens = str.split(/\s+/);
let lastIndex = 0;
tokens.forEach((v, i) => {
// font-italic
if (v === 'italic') {
italic = true;
lastIndex = i;
return;
}
// font-weight
const num = Number(v);
if (!isNaN(num)) {
weight = num;
lastIndex = i;
return;
}
// font-size
const parse = parseFloat(v);
if (!isNaN(parse)) {
size = parse;
unit = v.slice(parse.toString().length);
lastIndex = i;
return;
}
});
family = tokens.slice(lastIndex + 1).join(' ') ?? 'Verdana';
return new Font(family, size, unit, weight, italic);
}
/**
* 从 CSS 字体字符串解析出 Font 实例,不支持的属性将被忽略
*/
static parse(str: string) {
const fonts = str.split(',');
const main = this.parseOne(fonts[0]);
for (let i = 1; i < fonts.length; i++) {
main.addFallback(this.parseOne(fonts[i]));
}
}
/**
* 设置默认字体
*/
static setDefaults(font: Font) {
this.defaultFamily = font.family;
this.defaultItalic = font.italic;
this.defaultSize = font.size;
this.defaultSizeUnit = font.sizeUnit;
this.defaultWeight = font.weight;
}
/**
* 获取默认字体
*/
static defaults(config?: Partial<IFontConfig>) {
if (!config) {
return new Font();
} else {
return new Font(
config.family,
config.size,
config.sizeUnit,
config.weight,
config.italic
);
}
}
/**
* 复制一个字体,同时修改字体的一部分属性
* @param font 要复制的字体
*/
static clone(
font: Font,
{
family = font.family,
size = font.size,
sizeUnit = font.sizeUnit,
weight = font.weight,
italic = font.italic
}: Partial<IFontConfig>
) {
return new Font(family, size, sizeUnit, weight, italic);
}
}
+1
View File
@@ -0,0 +1 @@
export * from './font';
+6
View File
@@ -0,0 +1,6 @@
export type SizedCanvasImageSource = Exclude<
CanvasImageSource,
VideoFrame | SVGElement
>;
export type CanvasStyle = string | CanvasGradient | CanvasPattern;