From a10625013d4741fe0671e490c8369ffda64b1eec Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sun, 19 Oct 2025 20:19:43 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=B4=A0=E6=9D=90=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + packages-user/client-modules/package.json | 1 + .../src/render/elements/cache.ts | 6 +- .../src/render/elements/hero.ts | 3 +- .../src/render/elements/misc.ts | 4 +- .../src/render/elements/props.ts | 3 +- .../src/render/weather/presets/cloud.ts | 2 +- .../src/render/weather/presets/cloudLike.ts | 2 +- .../src/render/weather/presets/fog.ts | 2 +- packages/common/src/logger.json | 8 + packages/render-assets/package.json | 3 + packages/render-assets/src/animater.ts | 189 +++++++ packages/render-assets/src/composer.ts | 488 ++++++++++++++++++ packages/render-assets/src/index.ts | 7 + packages/render-assets/src/shader/pack.frag | 12 + packages/render-assets/src/shader/pack.vert | 12 + packages/render-assets/src/splitter.ts | 65 +++ packages/render-assets/src/store.ts | 102 ++++ packages/render-assets/src/texture.ts | 90 ++++ packages/render-assets/src/types.ts | 201 ++++++++ packages/render-assets/src/utils.ts | 46 ++ packages/render-core/package.json | 5 +- packages/render-core/src/gl2.ts | 2 +- packages/render-core/src/index.ts | 1 - packages/render-core/src/types.ts | 6 - packages/render-elements/src/graphics.ts | 4 +- packages/render-elements/src/misc.ts | 3 +- packages/render-vue/src/props.ts | 4 +- packages/render/src/index.ts | 1 + pnpm-lock.yaml | 13 + 30 files changed, 1259 insertions(+), 27 deletions(-) create mode 100644 packages/render-assets/package.json create mode 100644 packages/render-assets/src/animater.ts create mode 100644 packages/render-assets/src/composer.ts create mode 100644 packages/render-assets/src/index.ts create mode 100644 packages/render-assets/src/shader/pack.frag create mode 100644 packages/render-assets/src/shader/pack.vert create mode 100644 packages/render-assets/src/splitter.ts create mode 100644 packages/render-assets/src/store.ts create mode 100644 packages/render-assets/src/texture.ts create mode 100644 packages/render-assets/src/types.ts create mode 100644 packages/render-assets/src/utils.ts delete mode 100644 packages/render-core/src/types.ts diff --git a/package.json b/package.json index d340f69..ff7281f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "jszip": "^3.10.1", "lodash-es": "^4.17.21", "lz-string": "^1.5.0", + "maxrects-packer": "^2.7.3", "mutate-animate": "^1.4.2", "ogg-opus-decoder": "^1.6.14", "opus-decoder": "^0.7.7", diff --git a/packages-user/client-modules/package.json b/packages-user/client-modules/package.json index 3a9c7b5..ffd1f88 100644 --- a/packages-user/client-modules/package.json +++ b/packages-user/client-modules/package.json @@ -4,6 +4,7 @@ "@motajs/client-base": "workspace:*", "@motajs/common": "workspace:*", "@motajs/render": "workspace:*", + "@motajs/render-assets": "workspace:*", "@motajs/render-core": "workspace:*", "@motajs/legacy-common": "workspace:*", "@motajs/legacy-ui": "workspace:*", diff --git a/packages-user/client-modules/src/render/elements/cache.ts b/packages-user/client-modules/src/render/elements/cache.ts index d8ab5d7..d0a51b4 100644 --- a/packages-user/client-modules/src/render/elements/cache.ts +++ b/packages-user/client-modules/src/render/elements/cache.ts @@ -1,8 +1,6 @@ import { logger } from '@motajs/common'; -import { - MotaOffscreenCanvas2D, - SizedCanvasImageSource -} from '@motajs/render-core'; +import { MotaOffscreenCanvas2D } from '@motajs/render-core'; +import { SizedCanvasImageSource } from '@motajs/render-assets'; // 经过测试(https://www.measurethat.net/Benchmarks/Show/30741/1/drawimage-img-vs-canvas-vs-bitmap-cropping-fix-loading) // 得出结论,ImageBitmap和Canvas的绘制性能不如Image,于是直接画Image就行,所以缓存基本上就是存Image diff --git a/packages-user/client-modules/src/render/elements/hero.ts b/packages-user/client-modules/src/render/elements/hero.ts index 28c2b4a..32c4fa2 100644 --- a/packages-user/client-modules/src/render/elements/hero.ts +++ b/packages-user/client-modules/src/render/elements/hero.ts @@ -1,4 +1,5 @@ -import { RenderAdapter, SizedCanvasImageSource } from '@motajs/render-core'; +import { RenderAdapter } from '@motajs/render-core'; +import { SizedCanvasImageSource } from '@motajs/render-assets'; import { logger } from '@motajs/common'; import { ILayerRenderExtends, Layer, LayerMovingRenderable } from './layer'; import EventEmitter from 'eventemitter3'; diff --git a/packages-user/client-modules/src/render/elements/misc.ts b/packages-user/client-modules/src/render/elements/misc.ts index 811537d..d8ef9b6 100644 --- a/packages-user/client-modules/src/render/elements/misc.ts +++ b/packages-user/client-modules/src/render/elements/misc.ts @@ -4,9 +4,9 @@ import { RenderItem, RenderItemPosition, MotaOffscreenCanvas2D, - Transform, - SizedCanvasImageSource + Transform } from '@motajs/render-core'; +import { SizedCanvasImageSource } from '@motajs/render-assets'; import { isNil } from 'lodash-es'; import { RenderableData, AutotileRenderable, texture } from './cache'; import { IAnimateFrame, renderEmits } from './frame'; diff --git a/packages-user/client-modules/src/render/elements/props.ts b/packages-user/client-modules/src/render/elements/props.ts index 5bfe370..1525d79 100644 --- a/packages-user/client-modules/src/render/elements/props.ts +++ b/packages-user/client-modules/src/render/elements/props.ts @@ -1,5 +1,6 @@ import { BaseProps, TagDefine } from '@motajs/render-vue'; -import { CanvasStyle, Transform } from '@motajs/render-core'; +import { Transform } from '@motajs/render-core'; +import { CanvasStyle } from '@motajs/render-assets'; import { ILayerGroupRenderExtends, FloorLayer, diff --git a/packages-user/client-modules/src/render/weather/presets/cloud.ts b/packages-user/client-modules/src/render/weather/presets/cloud.ts index 6f7004c..b76b3cc 100644 --- a/packages-user/client-modules/src/render/weather/presets/cloud.ts +++ b/packages-user/client-modules/src/render/weather/presets/cloud.ts @@ -1,5 +1,5 @@ import { CloudLike } from './cloudLike'; -import { SizedCanvasImageSource } from '@motajs/render-core'; +import { SizedCanvasImageSource } from '@motajs/render-assets'; export class CloudWeather extends CloudLike { getImage(): SizedCanvasImageSource | null { diff --git a/packages-user/client-modules/src/render/weather/presets/cloudLike.ts b/packages-user/client-modules/src/render/weather/presets/cloudLike.ts index fcde800..237819b 100644 --- a/packages-user/client-modules/src/render/weather/presets/cloudLike.ts +++ b/packages-user/client-modules/src/render/weather/presets/cloudLike.ts @@ -1,6 +1,6 @@ import { MotaOffscreenCanvas2D, Sprite } from '@motajs/render-core'; import { Weather } from '../weather'; -import { SizedCanvasImageSource } from '@motajs/render-core'; +import { SizedCanvasImageSource } from '@motajs/render-assets'; export abstract class CloudLike extends Weather { /** 不透明度 */ diff --git a/packages-user/client-modules/src/render/weather/presets/fog.ts b/packages-user/client-modules/src/render/weather/presets/fog.ts index 0ad39fb..30692f8 100644 --- a/packages-user/client-modules/src/render/weather/presets/fog.ts +++ b/packages-user/client-modules/src/render/weather/presets/fog.ts @@ -1,6 +1,6 @@ import { MotaOffscreenCanvas2D } from '@motajs/render-core'; import { CloudLike } from './cloudLike'; -import { SizedCanvasImageSource } from '@motajs/render-core'; +import { SizedCanvasImageSource } from '@motajs/render-assets'; export class FogWeather extends CloudLike { /** 雾天气的图像比较小,因此将四个进行合并 */ diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index a45a561..80d4ced 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -97,6 +97,14 @@ "63": "Uncaught promise error in waiting box component. Error reason: $1", "64": "Text node type and block type mismatch: '$1' vs '$2'", "65": "Cannot bind a weather controller twice.", + "66": "Texture identifier repeated: $1", + "67": "Cannot set alias '$1' for texture $2, since there is already an alias '$3' for it.", + "68": "Cannot set alias '$1' for texture $2, since '$1' is already an alias for $3.", + "69": "Clip totally exceeds texture's dimensions, no operation will be alppied.", + "70": "An TextureAnimater can only be created once.", + "71": "Cannot start animate when animater not created on texture.", + "72": "Frame count delivered to frame-based animater need to exactly divide texture's height.", + "73": "Consistent size is needed when using WebGL2 composer.", "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." } diff --git a/packages/render-assets/package.json b/packages/render-assets/package.json new file mode 100644 index 0000000..a7e71e8 --- /dev/null +++ b/packages/render-assets/package.json @@ -0,0 +1,3 @@ +{ + "name": "@motajs/render-assets" +} diff --git a/packages/render-assets/src/animater.ts b/packages/render-assets/src/animater.ts new file mode 100644 index 0000000..4f71beb --- /dev/null +++ b/packages/render-assets/src/animater.ts @@ -0,0 +1,189 @@ +import { logger } from '@motajs/common'; +import { + IRect, + ITexture, + ITextureAnimater, + ITextureListedRenderable +} from './types'; + +/** + * 基于帧的动画控制器,创建时传入的参数代表帧数,生成动画时传入的参数自定义 + */ +export abstract class FrameBasedAnimater + implements ITextureAnimater +{ + texture: ITexture | 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 | null; + + abstract cycled(init: T): Generator | null; +} + +/** + * 行动画控制器,将贴图按照从上到下的顺序依次组成帧动画,创建时传入的参数代表帧数 + */ +export class TextureRowAnimater extends FrameBasedAnimater { + *open(): Generator | null { + if (!this.check()) return null; + const renderable = this.texture!.static(); + 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 renderable: ITextureListedRenderable = { + source: this.texture!.source, + rect: [{ x: ox, y: i * h + oy, w, h }] + }; + yield renderable; + } + } + + *cycled(): Generator | null { + if (!this.check()) return null; + const renderable = this.texture!.static(); + const { x: ox, y: oy } = renderable.rect; + const { width: w, height } = this.texture!; + const h = height / this.frames; + let i = 0; + while (true) { + if (i === this.frames) i = 0; + const renderable: ITextureListedRenderable = { + source: this.texture!.source, + rect: [{ x: ox, y: i * h + oy, w, h }] + }; + yield renderable; + } + } +} + +/** + * 列动画控制器,将贴图按照从左到右的顺序依次组成帧动画,创建时传入的参数代表帧数 + */ +export class TextureColumnAnimater extends FrameBasedAnimater { + *open(): Generator | null { + if (!this.check()) return null; + const renderable = this.texture!.static(); + 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 renderable: ITextureListedRenderable = { + source: this.texture!.source, + rect: [{ x: i * width + ox, y: oy, w, h }] + }; + yield renderable; + } + } + + *cycled(): Generator | null { + if (!this.check()) return null; + const renderable = this.texture!.static(); + const { x: ox, y: oy } = renderable.rect; + const { width, height: h } = this.texture!; + const w = width / this.frames; + let i = 0; + while (true) { + if (i === this.frames) i = 0; + const renderable: ITextureListedRenderable = { + source: this.texture!.source, + rect: [{ x: i * w + ox, y: oy, w, h }] + }; + yield renderable; + } + } +} + +export interface IAnimaterTranslatedInit { + /** 以此矩形作为参考矩形 */ + readonly rect: Readonly; + /** 传递给原先的动画控制器的参数 */ + readonly data: T; + /** 原先的动画控制器 */ + readonly animate: ITextureAnimater; +} + +type AdderImplements = ITextureAnimater>; + +type AdderTexture = ITexture>; + +/** + * 对一个动画控制器执行偏移操作的控制器,一般用于图集上。 + * 创建时传入的参数代表要执行偏移操作的动画控制器,动画参数包含两部分,一个是参考矩形,一个是传递给原先动画控制器的参数 + */ +export class TextureAnimaterTranslated implements AdderImplements { + texture: AdderTexture | null = null; + + create(texture: ITexture): void { + this.texture = texture; + } + + *output( + ani: Generator, + origin: Readonly, + rect: Readonly + ) { + const { x: ox, y: oy } = origin; + const { x: nx, y: ny } = rect; + const source = this.texture!.source; + + while (true) { + const next = ani.next(); + if (next.done) break; + const renderable = next.value; + const list: IRect[] = []; + renderable.rect.forEach(({ x, y, w, h }) => { + list.push({ x: x - ox + nx, y: y - oy + ny, w, h }); + }); + const res: ITextureListedRenderable = { + source, + rect: list + }; + yield res; + } + } + + open( + init: IAnimaterTranslatedInit + ): Generator | null { + const ani = init.animate.open(init.data); + const origin = init.animate.texture?.static().rect; + if (!ani || !origin) return null; + return this.output(ani, origin, init.rect); + } + + cycled( + init: IAnimaterTranslatedInit + ): Generator | null { + const ani = init.animate.cycled(init.data); + const origin = init.animate.texture?.static().rect; + if (!ani || !origin) return null; + return this.output(ani, origin, init.rect); + } +} diff --git a/packages/render-assets/src/composer.ts b/packages/render-assets/src/composer.ts new file mode 100644 index 0000000..5087207 --- /dev/null +++ b/packages/render-assets/src/composer.ts @@ -0,0 +1,488 @@ +import { + IOption, + IRectangle, + MaxRectsPacker, + Rectangle +} from 'maxrects-packer'; +import { IAnimaterTranslatedInit, TextureAnimaterTranslated } from './animater'; +import { Texture } from './texture'; +import { + IRect, + ITexture, + ITextureComposedData, + ITextureComposer, + SizedCanvasImageSource +} from './types'; +import vert from './shader/pack.vert?raw'; +import frag from './shader/pack.frag?raw'; +import { compileGLWith } from './utils'; +import { logger } from '@motajs/common'; +import { isNil } from 'lodash-es'; + +interface IndexMarkedComposedData { + /** 组合数据 */ + readonly data: ITextureComposedData; + /** 组合时最后一个用到的贴图的索引 */ + readonly index: number; +} + +type TranslatedComposer = ITextureComposer< + D, + void, + IAnimaterTranslatedInit +>; + +export interface IGridComposerData { + /** 单张图集的最大宽度 */ + readonly maxWidth: number; + /** 单张图集的最大高度 */ + readonly maxHeight: number; + /** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */ + readonly width: number; + /** 单个贴图的宽度,与之不同的贴图将会被剔除并警告 */ + readonly height: number; +} + +interface GridNeededSize { + /** 图集宽度 */ + readonly width: number; + /** 图集高度 */ + readonly height: number; + /** 一共有多少行 */ + readonly rows: number; + /** 一共有多少列 */ + readonly cols: number; +} + +/** + * 网格组合器,将等大小的贴图组合成图集,要求每个贴图的尺寸一致 + */ +export class TextureGridComposer + implements TranslatedComposer +{ + private getNeededSize( + tex: ITexture[], + start: number, + data: IGridComposerData + ): GridNeededSize { + const maxRows = Math.floor(data.maxWidth / data.width); + const maxCols = Math.floor(data.maxHeight / data.height); + const rest = tex.length - start; + if (rest >= maxRows * maxCols) { + const size: GridNeededSize = { + width: data.maxWidth, + height: data.maxHeight, + rows: maxRows, + cols: maxCols + }; + return size; + } else { + const aspect = data.height / data.width; + const cols = Math.ceil(Math.sqrt(rest * aspect)); + const rows = Math.ceil(rest / cols); + const size: GridNeededSize = { + width: cols * data.width, + height: rows * data.height, + rows, + cols + }; + return size; + } + } + + private nextAsset( + tex: ITexture[], + start: number, + data: IGridComposerData + ): IndexMarkedComposedData { + const size = this.getNeededSize(tex, start, data); + const { width, height, rows, cols } = size; + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d')!; + canvas.width = width; + canvas.height = height; + + const count = Math.min(rows * cols, tex.length - start); + const map = new Map(); + + 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.static(); + 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); + texture.animated(new TextureAnimaterTranslated(), void 0); + + const composed: ITextureComposedData = { + texture, + assetMap: map + }; + + return { data: composed, index: start + count }; + } + + *compose( + input: Iterable, + data: IGridComposerData + ): Generator { + const arr = [...input]; + + let i = 0; + + while (i < arr.length) { + const { data: asset, index } = this.nextAsset(arr, i, data); + i = index + 1; + yield asset; + } + } +} + +export interface IMaxRectsComposerData extends IOption { + /** 贴图之间的间距 */ + readonly padding: number; +} + +interface MaxRectsRectangle extends IRectangle { + /** 这个矩形对应的贴图对象 */ + readonly data: ITexture; +} + +/** + * 使用 Max Rects 算法执行贴图整合,输入数据参考 {@link IMaxRectsComposerData}, + * 输出的纹理的图像源将会是不同的画布,注意与 {@link TextureMaxRectsWebGL2Composer} 区分 + */ +export class TextureMaxRectsComposer + implements TranslatedComposer +{ + constructor( + public readonly maxWidth: number, + public readonly maxHeight: number + ) {} + + *compose( + input: Iterable, + data: IMaxRectsComposerData + ): Generator>> { + const packer = new MaxRectsPacker( + this.maxWidth, + this.maxHeight, + data.padding, + data + ); + const arr = [...input]; + const rects = arr.map(v => { + const rect = v.static().rect; + const toPack = new Rectangle(rect.w, rect.h); + toPack.data = v; + return toPack; + }); + packer.addArray(rects); + + for (const bin of packer.bins) { + const map = new Map(); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d')!; + canvas.width = bin.width; + canvas.height = bin.height; + 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.static(); + 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 + ); + texture.animated(new TextureAnimaterTranslated(), void 0); + const data: ITextureComposedData = { + texture, + assetMap: map + }; + yield data; + } + } +} + +interface RectProcessed { + /** 贴图位置映射 */ + readonly texMap: Map>; + /** 顶点数组 */ + readonly attrib: Float32Array; +} + +/** + * 使用 Max Rects 算法执行贴图整合,输入数据参考 {@link IMaxRectsComposerData}, + * 要求每个贴图的图像源尺寸一致,贴图大小可以不同。 + * 注意,本组合器同时只能处理一个组合操作,上一个没执行完的时候再次调用 `compose` 会出现问题。 + * 所有输出的内容中,贴图对象的图像源都是同一个画布,因此获取后要么直接使用,要么立刻调用 `toBitmap` + */ +export class TextureMaxRectsWebGL2Composer + implements TranslatedComposer +{ + /** 使用的画布 */ + 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; + + 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 = compileGLWith(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 { + const map = new Map(); + 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) { + 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 + ): RectProcessed { + const { width: ow, height: oh } = this.canvas; + const map = new Map(); + 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.static(); + 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, + data: IMaxRectsComposerData + ): Generator>> { + this.opWidth = 0; + this.opHeight = 0; + + const packer = new MaxRectsPacker( + this.maxWidth, + this.maxHeight, + data.padding, + data + ); + const arr = [...input]; + const rects = arr.map(v => { + const rect = v.static().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); + + for (const bin of packer.bins) { + const { texMap, attrib } = this.processRects(bin.rects, indexMap); + this.renderAtlas(attrib); + const texture = new Texture>( + this.canvas + ); + texture.animated(new TextureAnimaterTranslated(), void 0); + const data: ITextureComposedData = { + texture, + assetMap: texMap + }; + + yield data; + } + + this.gl.disableVertexAttribArray(this.posPos); + this.gl.disableVertexAttribArray(this.texPos); + } +} diff --git a/packages/render-assets/src/index.ts b/packages/render-assets/src/index.ts new file mode 100644 index 0000000..bd8f344 --- /dev/null +++ b/packages/render-assets/src/index.ts @@ -0,0 +1,7 @@ +export * from './animater'; +export * from './composer'; +export * from './splitter'; +export * from './store'; +export * from './texture'; +export * from './types'; +export * from './utils'; diff --git a/packages/render-assets/src/shader/pack.frag b/packages/render-assets/src/shader/pack.frag new file mode 100644 index 0000000..f00195d --- /dev/null +++ b/packages/render-assets/src/shader/pack.frag @@ -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); +} diff --git a/packages/render-assets/src/shader/pack.vert b/packages/render-assets/src/shader/pack.vert new file mode 100644 index 0000000..e626a84 --- /dev/null +++ b/packages/render-assets/src/shader/pack.vert @@ -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); +} diff --git a/packages/render-assets/src/splitter.ts b/packages/render-assets/src/splitter.ts new file mode 100644 index 0000000..8fdb4b6 --- /dev/null +++ b/packages/render-assets/src/splitter.ts @@ -0,0 +1,65 @@ +import { Texture } from './texture'; +import { ITexture, ITextureSplitter, IRect } from './types'; + +/** + * 按行分割贴图,即分割成一行行的贴图,按从上到下的顺序输出 + * 输入参数代表每一行的高度 + */ +export class TextureRowSplitter implements ITextureSplitter { + *split(texture: ITexture, data: number): Generator { + const lines = Math.ceil(texture.height / data); + for (let i = 0; i < lines; i++) { + const tex = new Texture(texture.source); + tex.clip(0, i * data, texture.width, data); + yield tex; + } + } +} + +/** + * 按列分割贴图,即分割成一列列的贴图,按从左到右的顺序输出 + * 输入参数代表每一列的宽度 + */ +export class TextureColumnSplitter implements ITextureSplitter { + *split(texture: ITexture, data: number): Generator { + const lines = Math.ceil(texture.width / data); + for (let i = 0; i < lines; i++) { + const tex = new Texture(texture.source); + tex.clip(i * data, 0, data, texture.height); + yield tex; + } + } +} + +/** + * 按照网格分割贴图,按照先从左到右,再从上到下的顺序输出 + * 输入参数代表每一列的宽度和高度 + */ +export class TextureGridSplitter implements ITextureSplitter<[number, number]> { + *split(texture: ITexture, data: [number, number]): Generator { + const [w, h] = data; + const rows = Math.ceil(texture.width / w); + const lines = Math.ceil(texture.height / h); + for (let y = 0; y < lines; y++) { + for (let x = 0; x < rows; x++) { + const tex = new Texture(texture.source); + tex.clip(x * w, y * h, w, h); + yield tex; + } + } + } +} + +/** + * 根据图集信息分割贴图,按照传入的矩形数组的顺序输出 + * 输入参数代表每个贴图对应到图集上的矩形位置 + */ +export class TextureAssetSplitter implements ITextureSplitter { + *split(texture: ITexture, data: IRect[]): Generator { + for (const { x, y, w, h } of data) { + const tex = new Texture(texture.source); + tex.clip(x, y, w, h); + yield tex; + } + } +} diff --git a/packages/render-assets/src/store.ts b/packages/render-assets/src/store.ts new file mode 100644 index 0000000..125f302 --- /dev/null +++ b/packages/render-assets/src/store.ts @@ -0,0 +1,102 @@ +import { isNil } from 'lodash-es'; +import { Texture } from './texture'; +import { ITexture, ITextureStore, SizedCanvasImageSource } from './types'; +import { logger } from '@motajs/common'; + +export class TextureStore implements ITextureStore { + private readonly texMap: Map = new Map(); + private readonly invMap: Map = new Map(); + private readonly aliasMap: Map = new Map(); + private readonly aliasInvMap: Map = new Map(); + + [Symbol.iterator](): Iterator<[key: number, tex: ITexture]> { + return this.texMap.entries(); + } + + entries(): Iterable<[key: number, tex: ITexture]> { + return this.texMap.entries(); + } + + keys(): Iterable { + return this.texMap.keys(); + } + + values(): Iterable { + return this.texMap.values(); + } + + createTexture(source: SizedCanvasImageSource): ITexture { + return new Texture(source); + } + + addTexture(identifier: number, texture: ITexture): 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: ITexture, alias?: string) { + this.texMap.delete(id); + this.invMap.delete(tex); + if (alias) { + this.aliasMap.delete(alias); + this.aliasInvMap.delete(id); + } + } + + removeTexture(identifier: number | string | ITexture): 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); + } + } + + getTexture(identifier: number): ITexture | 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: ITexture): number | undefined { + return this.invMap.get(texture); + } + + aliasOf(identifier: number): string | undefined { + return this.aliasInvMap.get(identifier); + } +} diff --git a/packages/render-assets/src/texture.ts b/packages/render-assets/src/texture.ts new file mode 100644 index 0000000..ffd8e28 --- /dev/null +++ b/packages/render-assets/src/texture.ts @@ -0,0 +1,90 @@ +import { logger } from '@motajs/common'; +import { + ITexture, + ITextureAnimater, + ITextureListedRenderable, + ITextureSingleRenderable, + ITextureSplitter, + SizedCanvasImageSource +} from './types'; + +export class Texture implements ITexture { + source: SizedCanvasImageSource; + animater: ITextureAnimater | null = null; + width: number; + height: number; + + private cx: number; + private cy: number; + + constructor(source: SizedCanvasImageSource) { + this.source = source; + this.width = source.width; + this.height = source.height; + this.cx = 0; + this.cy = 0; + } + + /** + * 对纹理进行裁剪操作,不会改变图像源 + * @param x 裁剪左上角横坐标 + * @param y 裁剪左上角纵坐标 + * @param w 裁剪宽度 + * @param h 裁剪高度 + */ + clip(x: number, y: number, w: number, h: number) { + const r = x + w; + const b = y + h; + if (x > this.width || y > this.height || r < 0 || b < 0) { + logger.warn(69); + return; + } + const left = Math.max(0, x); + const top = Math.max(0, y); + const right = Math.min(this.width, r); + const bottom = Math.min(this.height, b); + this.cx = left; + this.cy = top; + this.width = right - left; + this.height = bottom - top; + } + + async toBitmap(): Promise { + if (this.source instanceof ImageBitmap) return; + this.source = await createImageBitmap(this.source as any); + } + + split(splitter: ITextureSplitter, data: U): Generator { + return splitter.split(this, data); + } + + animated(animater: ITextureAnimater, data: T): void { + this.animater = animater; + animater.create(this, data); + } + + static(): ITextureSingleRenderable { + const renderable: ITextureSingleRenderable = { + source: this.source, + rect: { x: this.cx, y: this.cy, w: this.width, h: this.height } + }; + return renderable; + } + + dynamic(data: A): Generator | null { + if (!this.animater) return null; + return this.animater.open(data); + } + + cycled(data: A): Generator | null { + if (!this.animater) return null; + return this.animater.cycled(data); + } + + dispose(): void { + if (this.source instanceof ImageBitmap) { + this.source.close(); + } + this.animater = null; + } +} diff --git a/packages/render-assets/src/types.ts b/packages/render-assets/src/types.ts new file mode 100644 index 0000000..2e23933 --- /dev/null +++ b/packages/render-assets/src/types.ts @@ -0,0 +1,201 @@ +export type SizedCanvasImageSource = Exclude< + CanvasImageSource, + VideoFrame | SVGElement +>; + +export type CanvasStyle = string | CanvasGradient | CanvasPattern; + +export interface IRect { + x: number; + y: number; + w: number; + h: number; +} + +export interface ITextureSingleRenderable { + /** 可渲染贴图对象的图像源 */ + readonly source: SizedCanvasImageSource; + /** 贴图裁剪区域 */ + readonly rect: Readonly; +} + +export interface ITextureListedRenderable { + /** 可渲染贴图对象的图像源 */ + readonly source: SizedCanvasImageSource; + /** 贴图裁剪区域 */ + readonly rect: Readonly[]; +} + +export interface ITextureComposedData { + /** 这个纹理图集的贴图对象 */ + readonly texture: ITexture; + /** 每个参与组合的贴图对应到图集对象的矩形范围 */ + readonly assetMap: Map>; +} + +export interface ITextureComposer { + /** + * 将一系列纹理组合成为一系列纹理图集 + * @param input 输入纹理 + * @param data 输入给组合器的参数 + */ + compose( + input: Iterable, + data: T + ): Generator>; +} + +export interface ITextureSplitter { + /** + * 对一个贴图对象执行拆分操作 + * @param texture 要拆分的贴图 + * @param data 传给拆分器的参数 + */ + split(texture: ITexture, data: T): Generator; +} + +export interface ITextureAnimater { + /** 此动画控制器所控制的贴图 */ + readonly texture: ITexture | null; + + /** + * 对一个贴图对象创建动画控制器 + * @param texture 要绑定的贴图对象 + * @param data 传递给动画控制器的参数 + */ + create(texture: ITexture, data: T): void; + + /** + * 开始动画序列 + * @param init 动画初始化参数 + */ + open(init: I): Generator | null; + + /** + * 开始循环动画序列 + * @param init 动画初始化参数 + */ + cycled(init: I): Generator | null; +} + +export interface ITexture { + /** 贴图的图像源 */ + readonly source: SizedCanvasImageSource; + /** 此贴图使用的动画控制器 */ + readonly animater: ITextureAnimater | null; + /** 贴图宽度 */ + readonly width: number; + /** 贴图高度 */ + readonly height: number; + + /** + * 将此贴图转换为 bitmap 图像,图像源也会转变成 ImageBitmap + */ + toBitmap(): Promise; + + /** + * 使用指定贴图切分器切分贴图 + * @param splitter 贴图切分器 + * @param data 传递给切分器的参数 + * @returns 切分出的贴图所组成的可迭代对象 + */ + split(splitter: ITextureSplitter, data: T): Generator; + + /** + * 将此贴图标记为可动画贴图,使用传入的动画控制器描述动画。每个贴图只能绑定一个动画控制器,反之同理 + * @param animater 动画控制器 + * @param data 传递给动画控制器的参数 + */ + animated(animater: ITextureAnimater, data: T): void; + + /** + * 获取整张图的可渲染对象 + */ + static(): ITextureSingleRenderable; + + /** + * 获取一系列动画可渲染对象,不循环,按帧数依次排列 + * @param data 传递给动画控制器的初始化参数 + */ + dynamic(data: A): Generator | null; + + /** + * 获取无限循环的动画可渲染对象 + * @param data 传递给动画控制器的初始化参数 + */ + cycled(data: A): Generator | null; + + /** + * 释放此贴图的资源,将不能再被使用 + */ + dispose(): void; +} + +export interface ITextureStore { + [Symbol.iterator](): Iterator<[key: number, tex: ITexture]>; + + /** + * 获取纹理对象键值对的可迭代对象 + */ + entries(): Iterable<[key: number, tex: ITexture]>; + + /** + * 获取纹理对象的键的可迭代对象 + */ + keys(): Iterable; + + /** + * 获取纹理对象的值的可迭代对象 + */ + values(): Iterable; + + /** + * 通过图像源创建贴图对象 + * @param source 贴图使用的图像源 + */ + createTexture(source: SizedCanvasImageSource): ITexture; + + /** + * 添加一个贴图 + * @param identifier 贴图 id + * @param texture 贴图对象 + */ + addTexture(identifier: number, texture: ITexture): void; + + /** + * 移除一个贴图 + * @param identifier 要移除的贴图对象 id 或 别名 或 贴图对象 + */ + removeTexture(identifier: number | string | ITexture): void; + + /** + * 根据贴图对象 id 获取贴图 + * @param identifier 贴图对象 id + */ + getTexture(identifier: number): ITexture | null; + + /** + * 给贴图对象命名一个别名 + * @param identifier 贴图对象 id + * @param alias 要命名的别名 + */ + alias(identifier: number, alias: string): void; + + /** + * 根据贴图对象别名获取贴图 + * @param alias 贴图对象别名 + */ + fromAlias(alias: string): ITexture | null; + + /** + * 根据贴图对象获取此贴图对象在此控制器中的 id,如果贴图不在此控制器,返回 `undefined` + * @param texture 贴图对象 + */ + idOf(texture: ITexture): number | undefined; + + /** + * 根据贴图对象获取此贴图对象在此控制器中的别名,如果不存在此 id,返回 `undefined` + * @param identifier 贴图 id + */ + aliasOf(identifier: number): string | undefined; +} diff --git a/packages/render-assets/src/utils.ts b/packages/render-assets/src/utils.ts new file mode 100644 index 0000000..a95631c --- /dev/null +++ b/packages/render-assets/src/utils.ts @@ -0,0 +1,46 @@ +import { logger } from '@motajs/common'; + +export function compileGLWith( + gl: WebGL2RenderingContext, + vert: string, + frag: string +): WebGLProgram | null { + const vsShader = compileShader(gl, gl.VERTEX_SHADER, vert); + const fsShader = compileShader(gl, gl.FRAGMENT_SHADER, frag); + + if (!vsShader || !fsShader) return null; + + const program = gl.createProgram(); + gl.attachShader(program, vsShader); + gl.attachShader(program, fsShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + const info = gl.getProgramInfoLog(program); + logger.error(9, info ?? ''); + return null; + } + + return program; +} + +function compileShader( + gl: WebGL2RenderingContext, + type: number, + source: string +): WebGLShader | null { + const shader = gl.createShader(type); + if (!shader) return null; + gl.shaderSource(shader, source); + gl.compileShader(shader); + + // 如果编译失败 + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + const info = gl.getShaderInfoLog(shader); + const typeStr = type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'; + logger.error(10, typeStr, info ?? ''); + return null; + } + + return shader; +} diff --git a/packages/render-core/package.json b/packages/render-core/package.json index c98b2bf..e05f46c 100644 --- a/packages/render-core/package.json +++ b/packages/render-core/package.json @@ -1,6 +1,7 @@ { "name": "@motajs/render-core", "dependencies": { - "@motajs/common": "workspace:*" + "@motajs/common": "workspace:*", + "@motajs/render-assets": "workspace:*" } -} \ No newline at end of file +} diff --git a/packages/render-core/src/gl2.ts b/packages/render-core/src/gl2.ts index 1292be8..bf1e94d 100644 --- a/packages/render-core/src/gl2.ts +++ b/packages/render-core/src/gl2.ts @@ -4,7 +4,7 @@ import { MotaOffscreenCanvas2D } from './canvas2d'; import { ERenderItemEvent, RenderItem, RenderItemPosition } from './item'; import { Transform } from './transform'; import { isWebGL2Supported } from './utils'; -import { SizedCanvasImageSource } from './types'; +import { SizedCanvasImageSource } from '@motajs/render-assets'; export interface IGL2ProgramPrefix { readonly VERTEX: string; diff --git a/packages/render-core/src/index.ts b/packages/render-core/src/index.ts index 6b2f23b..02de53c 100644 --- a/packages/render-core/src/index.ts +++ b/packages/render-core/src/index.ts @@ -8,5 +8,4 @@ export * from './render'; export * from './shader'; export * from './sprite'; export * from './transform'; -export * from './types'; export * from './utils'; diff --git a/packages/render-core/src/types.ts b/packages/render-core/src/types.ts deleted file mode 100644 index 133dde7..0000000 --- a/packages/render-core/src/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type SizedCanvasImageSource = Exclude< - CanvasImageSource, - VideoFrame | SVGElement ->; - -export type CanvasStyle = string | CanvasGradient | CanvasPattern; diff --git a/packages/render-elements/src/graphics.ts b/packages/render-elements/src/graphics.ts index a3c7405..f6064fe 100644 --- a/packages/render-elements/src/graphics.ts +++ b/packages/render-elements/src/graphics.ts @@ -2,9 +2,9 @@ import { Transform, ERenderItemEvent, RenderItem, - MotaOffscreenCanvas2D, - CanvasStyle + MotaOffscreenCanvas2D } from '@motajs/render-core'; +import { CanvasStyle } from '@motajs/render-assets'; import { logger } from '@motajs/common'; import { clamp, isEqual, isNil } from 'lodash-es'; diff --git a/packages/render-elements/src/misc.ts b/packages/render-elements/src/misc.ts index a6e596e..a2b88b6 100644 --- a/packages/render-elements/src/misc.ts +++ b/packages/render-elements/src/misc.ts @@ -5,13 +5,12 @@ import { Transform, MotaOffscreenCanvas2D } from '@motajs/render-core'; +import { CanvasStyle } from '@motajs/render-assets'; import { Font } from '@motajs/render-style'; /** 文字的安全填充,会填充在文字的上侧和下侧,防止削顶和削底 */ const SAFE_PAD = 1; -type CanvasStyle = string | CanvasGradient | CanvasPattern; - export interface ETextEvent extends ERenderItemEvent { setText: [text: string, width: number, height: number]; } diff --git a/packages/render-vue/src/props.ts b/packages/render-vue/src/props.ts index 3a2e39d..57620ff 100644 --- a/packages/render-vue/src/props.ts +++ b/packages/render-vue/src/props.ts @@ -7,9 +7,9 @@ import { ElementLocator, ElementScale, CustomContainerRenderFn, - CustomContainerPropagateFn, - CanvasStyle + CustomContainerPropagateFn } from '@motajs/render-core'; +import { CanvasStyle } from '@motajs/render-assets'; import { BezierParams, CircleParams, diff --git a/packages/render/src/index.ts b/packages/render/src/index.ts index e929fe4..738e7ca 100644 --- a/packages/render/src/index.ts +++ b/packages/render/src/index.ts @@ -1,3 +1,4 @@ +export * from '@motajs/render-assets'; export * from '@motajs/render-core'; export * from '@motajs/render-elements'; export * from '@motajs/render-style'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5de67fd..a158f03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: lz-string: specifier: ^1.5.0 version: 1.5.0 + maxrects-packer: + specifier: ^2.7.3 + version: 2.7.3 mutate-animate: specifier: ^1.4.2 version: 1.4.2 @@ -480,11 +483,16 @@ importers: specifier: ^3.5.13 version: 3.5.20(typescript@5.9.2) + packages/render-assets: {} + packages/render-core: dependencies: '@motajs/common': specifier: workspace:* version: link:../common + '@motajs/render-assets': + specifier: workspace:* + version: link:../render-assets packages/render-elements: dependencies: @@ -4513,6 +4521,9 @@ packages: mathjax-full@3.2.2: resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} + maxrects-packer@2.7.3: + resolution: {integrity: sha512-bG6qXujJ1QgttZVIH4WDanhoJtvbud/xP/XPyf6A69C9RdA61BM4TomFALCq2nrTa+tARRIBB4LuIFsnUQU2wA==} + mdast-util-to-hast@13.2.0: resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} @@ -10722,6 +10733,8 @@ snapshots: mj-context-menu: 0.6.1 speech-rule-engine: 4.1.2 + maxrects-packer@2.7.3: {} + mdast-util-to-hast@13.2.0: dependencies: '@types/hast': 3.0.4